AWS Organizations運用1年半で学んだSCP・RCP設計の失敗と教訓

SCPを書きすぎてEC2が起動できなくなった経験はありませんか?Organizations実運用で踏んだ地雷と、RCP・Declarative Policiesを使った2026年時点の統制設計をまとめました。

Organizations運用1年半で見えてきた「統制の難しさ」

うちのチームがAWS Organizationsを本格運用し始めてから1年半が経つ。最初は「アカウントをまとめて統一ポリシーを当てれば安全になる」くらいの認識だったけど、実際に運用してみると全然そんな甘いものじゃなかった。

SCPを書きすぎてDenyが重なって誰もEC2を起動できなくなったり、Control TowerのLanding Zoneをアップグレードしたら既存SCPと競合してパイプラインが止まったり。正直ここ半年は事故対応ログみたいな状態だった。

でも2026年に入ってAWSのOrganizations関連機能がかなり成熟してきて、特にResource Control Policies(RCP)とSCPv2(Declarative Policies)が組み合わさることで「防御の設計思想」自体が変わってきた。今回はその辺の実務知見をまとめておく。

ちなみにマルチアカウント構成全般の話はマルチクラウド戦略2026の記事でも触れてるので、クラウド全体の戦略から考えたい方はそちらも参考に。


2026年現在のOrganizations統制レイヤー全体像

2026年時点でのOrganizations統制は、ざっくり5層で考えるのが整理しやすい。

レイヤー役割
SCP(Service Control Policies)アカウントレベルのDeny境界
RCP(Resource Control Policies)リソースレベルのアクセス制御(2024〜正式GA)
Declarative Policies(SCPv2相当)設定強制型ポリシー
Security Hub Aggregator全アカウントの検知統合
Config Aggregator + Conformance Packコンプライアンス自動評価

RCPはリソースポリシーを上書きせずに「組織外からのアクセスをDenyする」用途に特化していて、SCPとは補完関係にある。SCPは「IAMプリンシパルが何をできるか」を制限するのに対し、RCPは「リソースが誰から操作されるか」を制限する。この使い分けを最初に理解しておかないと設計がぐちゃぐちゃになる。

実際のアカウント構成図はこんな感じ。

graph TB
    subgraph org["AWS Organizations"]
        mgmt["Management Account\n(payer)"]
        
        subgraph sec_ou["Security OU"]
            audit["Audit Account\n(CloudTrail集約・Security Hub)"]
            logarch["Log Archive Account\n(S3・CloudWatch Logs)"]
        end
        
        subgraph shared_ou["Shared Services OU"]
            network["Network Account\n(Transit Gateway・DNS)"]
            toolchain["Toolchain Account\n(CodePipeline・ECR)"]
        end
        
        subgraph prod_ou["Production OU"]
            prod_app1["Prod App1 Account"]
            prod_app2["Prod App2 Account"]
        end
        
        subgraph staging_ou["Staging OU"]
            stg_app1["Staging App1 Account"]
            stg_app2["Staging App2 Account"]
        end
        
        subgraph dev_ou["Development OU"]
            dev1["Dev Account A"]
            dev2["Dev Account B"]
        end
    end
    
    subgraph scp_layer["SCP 適用レイヤー"]
        scp_root["Root SCP\n(ガードレール最低限)"]
        scp_prod["Prod OU SCP\n(厳格Deny)"]
        scp_dev["Dev OU SCP\n(緩め・コスト上限)"]
    end
    
    subgraph detection["検知・監査"]
        securityhub["Security Hub\n(Aggregator)"]
        guardduty_master["GuardDuty\nAdministrator"]
        config_agg["Config\nAggregator"]
    end
    
    mgmt --> sec_ou
    mgmt --> shared_ou
    mgmt --> prod_ou
    mgmt --> staging_ou
    mgmt --> dev_ou
    
    scp_root --> mgmt
    scp_prod --> prod_ou
    scp_dev --> dev_ou
    
    audit --> securityhub
    audit --> guardduty_master
    audit --> config_agg

Management AccountにはWorkloadを絶対に置かない。これだけは何があっても守ってほしい。うちのチームも最初に「一時的に」と言ってLambdaを置いたら2年間消せなかったという苦い経験がある。


SCPの書き方で失敗しないための実践パターン

SCPで一番やりがちなミスが「Deny All → 必要なものだけAllow」という設計で全部書いてしまうこと。OrganizationsのSCPはAWS管理ポリシー(FullAccess等)と組み合わせて機能するので、SCPでDenyしなければIAMポリシーが効いてる部分はそのまま使える。

実際にうちで使ってるRoot SCP(全OU共通のガードレール)の骨格はこれ。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyLeavingOrg",
      "Effect": "Deny",
      "Action": [
        "organizations:LeaveOrganization"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyRootUserActions",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:root"
        }
      }
    },
    {
      "Sid": "DenyDisableSecurityServices",
      "Effect": "Deny",
      "Action": [
        "guardduty:DeleteDetector",
        "guardduty:DisassociateFromMasterAccount",
        "securityhub:DisableSecurityHub",
        "config:DeleteConfigurationRecorder",
        "cloudtrail:DeleteTrail",
        "cloudtrail:StopLogging",
        "macie2:DisableMacie"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotLike": {
          "aws:PrincipalArn": [
            "arn:aws:iam::*:role/OrganizationAccountAccessRole",
            "arn:aws:iam::*:role/AWSControlTowerExecution"
          ]
        }
      }
    },
    {
      "Sid": "DenyNonApprovedRegions",
      "Effect": "Deny",
      "NotAction": [
        "iam:*",
        "organizations:*",
        "sts:*",
        "cloudfront:*",
        "route53:*",
        "support:*",
        "trustedadvisor:*",
        "waf:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": [
            "ap-northeast-1",
            "ap-northeast-3",
            "us-east-1"
          ]
        }
      }
    }
  ]
}

DenyRootUserActionsはかなり重要で、個人的にはこれが全SCPの中で最優先だと思ってる。Rootアカウントの鍵を持っていても何もできない状態にしておくことで、フィッシングやクレデンシャル漏洩時の被害を激減させられる。SOC2審査のときにこのSCPが評価されたのは地味にうれしかった(SOC2審査でのCloudTrail・Config設計の話も読んでもらえると設計の背景がわかるはず)。

Production OU 専用の追加SCP

本番環境だけに当てるSCPはもう少し厳格にしてる。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "RequireIMDSv2",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringNotEquals": {
          "ec2:MetadataHttpTokens": "required"
        }
      }
    },
    {
      "Sid": "DenyUnencryptedS3",
      "Effect": "Deny",
      "Action": "s3:PutObject",
      "Resource": "*",
      "Condition": {
        "Null": {
          "s3:x-amz-server-side-encryption": true
        }
      }
    },
    {
      "Sid": "RequireKMSForRDS",
      "Effect": "Deny",
      "Action": "rds:CreateDBInstance",
      "Resource": "*",
      "Condition": {
        "Bool": {
          "rds:StorageEncrypted": false
        }
      }
    },
    {
      "Sid": "DenyPublicS3ACL",
      "Effect": "Deny",
      "Action": [
        "s3:PutBucketAcl",
        "s3:PutObjectAcl"
      ],
      "Resource": "*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-acl": [
            "public-read",
            "public-read-write",
            "authenticated-read"
          ]
        }
      }
    }
  ]
}

RequireIMDSv2は2025年後半あたりからAWS側でもデフォルト強制の動きが強まってきたけど、SCPで明示的にDenyしておくことで「誰かがコンソールから古いインスタンスを間違えて作る」事故を防げる。これ実際にあって、IMDSv1経由でEC2メタデータが読まれる手前で止められた経験があるので正直マジで助かってる。


Resource Control Policies(RCP)で「外部アクセス」を組織レベルで封じる

2024年末にGAになったRCPは、2026年現在でうちのチームが最も「導入して良かった」と感じてる機能のひとつだ。

RCPの本質は「組織外のプリンシパルからのアクセスをリソース側でDenyする」こと。S3バケットポリシーやKMSキーポリシーを各アカウントで適切に設定してくれると理想的だけど、100個のアカウントを管理してたら現実的に無理。その穴をRCPで組織レベルで埋められる。

実際に入れてるRCPの例。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyExternalS3Access",
      "Effect": "Deny",
      "Principal": "*",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject",
        "s3:ListBucket"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEqualsIfExists": {
          "aws:PrincipalOrgID": "o-xxxxxxxxxx"
        },
        "BoolIfExists": {
          "aws:PrincipalIsAWSService": false
        }
      }
    },
    {
      "Sid": "DenyExternalSecretsManagerAccess",
      "Effect": "Deny",
      "Principal": "*",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEqualsIfExists": {
          "aws:PrincipalOrgID": "o-xxxxxxxxxx"
        }
      }
    }
  ]
}

S3のクロスアカウントアクセスでAWSサービス(Athena、GlueなどのAWSマネージドサービス)が引っかかることがあって最初は相当ハマった。BoolIfExists: aws:PrincipalIsAWSService: falseを条件に加えることでAWSサービスを除外できる。これに気づくまで2日溶かした。

SCPとRCPの役割の違いを整理するとこんな感じ。

機能制御対象評価タイミング主な用途
SCPIAMプリンシパルのアクションIAM評価前操作権限の制限
RCPリソースへのアクセスリソースポリシー評価時外部アクセスの遮断
Declarative Policyサービス設定値サービス設定時設定の強制(例:EBS暗号化デフォルトON)
Permissions BoundaryIAMエンティティの最大権限IAM評価時権限の上限設定

Declarative Policies(いわゆるSCPv2)も2025年に正式に使えるようになって、特にEC2のEBSデフォルト暗号化やS3のブロックパブリックアクセスを組織全体に強制できるようになったのは大きい。「設定すれば終わり、例外を作りにくい」という点が地味に便利で、個人的にはSCP・RCPと並んで一番先に有効化しておいてほしい機能だと思ってる。

セキュリティ検知の統合フロー

flowchart TD
    subgraph accounts["各ワークロードアカウント"]
        gd_member["GuardDuty Member"]
        sh_member["Security Hub Member"]
        ct_member["CloudTrail (個別Trail)"]
        cfg_member["AWS Config"]
    end
    
    subgraph audit_account["Audit Account"]
        gd_admin["GuardDuty Administrator"]
        sh_admin["Security Hub Aggregator"]
        ct_org["CloudTrail Org Trail"]
        cfg_agg["Config Aggregator"]
        eventbridge["EventBridge"]
        lambda_triage["Lambda Triage"]
    end
    
    subgraph log_archive["Log Archive Account"]
        s3_ct["S3 CloudTrail Bucket"]
        s3_cfg["S3 Config Bucket"]
        s3_flow["S3 VPC Flow Logs"]
    end
    
    subgraph notify["通知・対応"]
        slack["Slack Security Channel"]
        pagerduty["PagerDuty"]
        jira["Jira Ticket"]
    end
    
    gd_member -->|"Findings集約"| gd_admin
    sh_member -->|"Findings集約"| sh_admin
    ct_member -->|"ログ転送"| s3_ct
    cfg_member -->|"設定変更記録"| cfg_agg
    
    ct_org --> s3_ct
    gd_admin --> sh_admin
    sh_admin --> eventbridge
    cfg_agg --> eventbridge
    
    eventbridge --> lambda_triage
    lambda_triage -->|"CRITICAL"| pagerduty
    lambda_triage -->|"HIGH"| slack
    lambda_triage -->|"MEDIUM以下"| jira

この構成でFindingsのトリアージをLambdaで自動化してる。GuardDutyのFalse Positiveとの格闘話はGuardDuty導入3ヶ月の地獄と脱出戦略で詳しく書いたので参考に。あれは本当に辛かった。


Conformance PackとConfig Rulesでコンプライアンスを自動評価する

SCPは「違反操作をそもそもできなくする」予防的制御だけど、設定ドリフトを検知する検出的制御も必要だ。そこでAWS ConfigのConformance Packを組み合わせてる。

OrganizationsレベルでConformance Packを展開する場合、CloudFormation StackSetsと組み合わせるのが基本。

# Conformance Pack の組織展開
aws configservice put-organization-conformance-pack \
  --organization-conformance-pack-name "nist-csf-conformance-pack" \
  --template-s3-uri "s3://my-conformance-templates/nist-csf-2024.yaml" \
  --delivery-s3-bucket "org-conformance-pack-results" \
  --excluded-accounts "111111111111" # Management Account除外

Conformance Packで使ってるカスタムルールの一部。Lambdaで書くカスタムルールは地味に便利で、「S3バケット名が命名規則に従ってるか」とか「タグが全部ついてるか」みたいな独自要件も評価できる。

import json
import boto3

def lambda_handler(event, context):
    invoking_event = json.loads(event['invokingEvent'])
    configuration_item = invoking_event.get('configurationItem')
    
    if not configuration_item:
        return build_evaluation('NOT_APPLICABLE', event)
    
    # S3バケットのタグ必須チェック
    if configuration_item['resourceType'] != 'AWS::S3::Bucket':
        return build_evaluation('NOT_APPLICABLE', event)
    
    required_tags = ['Environment', 'Owner', 'CostCenter', 'DataClassification']
    tags = configuration_item.get('tags', {})
    
    missing_tags = [tag for tag in required_tags if tag not in tags]
    
    if missing_tags:
        annotation = f"Missing required tags: {', '.join(missing_tags)}"
        return build_evaluation('NON_COMPLIANT', event, annotation)
    
    return build_evaluation('COMPLIANT', event)

def build_evaluation(compliance_type, event, annotation=''):
    config = boto3.client('config')
    
    evaluation = {
        'ComplianceResourceType': json.loads(event['invokingEvent'])['configurationItem']['resourceType'],
        'ComplianceResourceId': json.loads(event['invokingEvent'])['configurationItem']['resourceId'],
        'ComplianceType': compliance_type,
        'OrderingTimestamp': json.loads(event['invokingEvent'])['configurationItem']['configurationItemCaptureTime']
    }
    
    if annotation:
        evaluation['Annotation'] = annotation
    
    config.put_evaluations(
        Evaluations=[evaluation],
        ResultToken=event['resultToken']
    )

コンプライアンスの達成率推移をダッシュボードで見てるけど、最初(2025年1月)は全体で60%台だったのが今は90%超えてる。最初の数ヶ月は既存リソースへの対処が大変で、NON_COMPLIANTを全部潰すまでに相当時間かかった。

xychart-beta
    title "Conformance Pack 準拠率推移(全アカウント平均)"
    x-axis ["2025-01", "2025-04", "2025-07", "2025-10", "2026-01", "2026-04"]
    y-axis "準拠率 (%)" 0 --> 100
    line [62, 71, 79, 85, 91, 94]

ここは好みが分かれるかもしれないけど、個人的には「新規リソースはSCPで強制、既存はConfig+修復アクションで段階的に対処」というアプローチが現実的だと思ってる。一気に自動修復を有効化しようとして障害を起こした話は後述する。

Config自動修復アクションの例

# CloudFormation - Config Remediation Configuration
Resources:
  S3BlockPublicAccessRemediation:
    Type: AWS::Config::RemediationConfiguration
    Properties:
      ConfigRuleName: s3-bucket-public-read-prohibited
      TargetType: SSM_DOCUMENT
      TargetId: AWS-DisableS3BucketPublicReadWrite
      Automatic: true
      MaximumAutomaticAttempts: 3
      RetryAttemptSeconds: 60
      Parameters:
        BucketName:
          ResourceValue:
            Value: RESOURCE_ID
        AutomationAssumeRole:
          StaticValue:
            Values:
              - !GetAtt ConfigRemediationRole.Arn

自動修復は強力だけど、アプリケーションが公開S3バケット前提で動いてる場合に修復が走ると障害になる。うちは最初にDryRunで動作確認してから自動修復を有効化する運用にしてる。正直まだ完全には信頼できてないので、CRITICALなものだけ自動修復にして、その他はJiraチケット起票にとどめてる。


CDKでSCPとRCPをコード管理する

最後にSCPのコード管理の話。最初はAWS Consoleでポチポチ書いてたんだけど、これが本当に地獄だった。変更履歴が追えないし、複数人でレビューもできない。

今はCDKで管理してる。CDKのL2 ConstructでOrganizationsを直接扱うにはカスタムリソースが必要だけど、2026年時点ではcdklabs/cdk-organizationsが使い物になるレベルまで成熟してきた。

import * as cdk from 'aws-cdk-lib';
import { Organization, OrganizationalUnit, Policy, PolicyType, PolicyAttachment } from '@aws-cdk/aws-organizations-alpha';

export class OrganizationsSecurityStack extends cdk.Stack {
  constructor(scope: cdk.App, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // Root SCP - 全OU共通ガードレール
    const rootGuardrailScp = new Policy(this, 'RootGuardrailScp', {
      policyType: PolicyType.SERVICE_CONTROL_POLICY,
      policyName: 'root-guardrail-scp',
      policyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Sid: 'DenyLeavingOrg',
            Effect: 'Deny',
            Action: ['organizations:LeaveOrganization'],
            Resource: '*',
          },
          {
            Sid: 'DenyRootUserActions',
            Effect: 'Deny',
            Action: '*',
            Resource: '*',
            Condition: {
              StringLike: {
                'aws:PrincipalArn': 'arn:aws:iam::*:root',
              },
            },
          },
        ],
      },
    });

    // Production OU SCP
    const prodHardeningScpDoc = {
      Version: '2012-10-17',
      Statement: [
        {
          Sid: 'RequireIMDSv2',
          Effect: 'Deny',
          Action: 'ec2:RunInstances',
          Resource: 'arn:aws:ec2:*:*:instance/*',
          Condition: {
            StringNotEquals: {
              'ec2:MetadataHttpTokens': 'required',
            },
          },
        },
      ],
    };

    const prodHardeningScp = new Policy(this, 'ProdHardeningScp', {
      policyType: PolicyType.SERVICE_CONTROL_POLICY,
      policyName: 'prod-hardening-scp',
      policyDocument: prodHardeningScpDoc,
    });

    // OUへのアタッチ
    const prodOuId = cdk.Fn.importValue('ProdOuId');
    new PolicyAttachment(this, 'ProdScpAttachment', {
      policy: prodHardeningScp,
      target: { id: prodOuId },
    });
  }
}

GitHubでPRベースで管理するようになってから、「誰かが気づかないうちにSCPを変更してた」という事故がゼロになった。地味だけど相当効いてる。CDKでのSCP管理についてはCDK Aspects・Nagと組み合わせた自動検証の話も絡むので、CDK Aspects・Nag導入の実体験も読んでみてほしい。


まとめ

1年半のOrganizations運用でわかったことをまとめておく。

  • SCP・RCP・Declarative Policiesの使い分けが重要。 SCPで「何をできるか」を制限し、RCPで「外部アクセス」を封じ、Declarative Policiesで「設定値を強制」する。この3層を組み合わせることで防御の網羅性が上がる
  • Root SCPに「Rootユーザーのアクション全Deny」を必ず入れる。 これだけで認証情報漏洩時のリスクが大幅に下がる
  • 既存リソースへの強制適用は段階的に。 Config Conformance PackでNON_COMPLIANTを可視化してから、自動修復は慎重に有効化する
  • SCPはCDKでコード管理する。 コンソールで管理する時代はもう終わり。変更履歴とPRレビューは必須
  • セキュリティサービスの無効化をSCPでDenyする。 GuardDuty、Security Hub、CloudTrailが誤って無効化される事故を防ぐための最初の一手

最初はSCPを書き過ぎて自分たちが詰まるし、RCPはAWSサービスの挙動との相性でハマるし、で正直しんどい時期が続いた。それでも今の構成に落ち着いてからは「知らないうちに何かが壊れてた」という類の事故がほぼなくなった。設計に迷ったときは「防ぐ(SCP)・封じる(RCP)・強制する(Declarative)・検知する(Security Hub/Config)」の4軸で考えると整理しやすいかもしれない。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事