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の役割の違いを整理するとこんな感じ。
| 機能 | 制御対象 | 評価タイミング | 主な用途 |
|---|---|---|---|
| SCP | IAMプリンシパルのアクション | IAM評価前 | 操作権限の制限 |
| RCP | リソースへのアクセス | リソースポリシー評価時 | 外部アクセスの遮断 |
| Declarative Policy | サービス設定値 | サービス設定時 | 設定の強制(例:EBS暗号化デフォルトON) |
| Permissions Boundary | IAMエンティティの最大権限 | 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軸で考えると整理しやすいかもしれない。