SCP本番1年半で学んだ、IAMとの組み合わせで権限地獄に陥った話
AWS SCPを導入すればセキュリティ完璧と思ってた。でも本番運用で3度のロックアウトを経験。IAMとの複雑な相互作用と失敗パターンを赤裸々に解説します。
SCPは魔法じゃない、気づくのに1年半かかった
先日、チームの後輩が「SCPを導入すればセキュリティが完璧になりますよね?」って聞いてきて、ハッとした。そう、1年半前の自分たちもそう思ってた。
うちは3年前にAWS Organizations導入して、1年半前からSCP(Service Control Policy)を本格的に運用し始めた。当初はセキュリティチームから「IAMだけじゃ甘い、SCPで強制的に権限を制限しよう」という提案で、やる気満々でスタートしたんだ。CloudTrailやGuardDutyもあわせて導入したし、「これで統制できる」と思ってた。
現実は違った。本番環境で3回、開発環境で数え切れないくらい、SCPの設定ミスで管理者自身がロックアウトされた。最初の1回は月曜朝8時。メール通知が鳴り始めた時は、背筋が冷たくなったのを覚えてる。
なぜSCPで失敗するのか——権限の「層」を見落とす
SCPの厄介なところは、IAMと組み合わさることで、権限の判定ロジックがものすごく複雑になることなんだ。SCPは「許可ベース」の上に乗っかる「制限ベース」のポリシーで、IAMで許可されていても、SCPで拒否されれば使えない。逆に、IAMで拒否されてればSCPは関係ない。この二重チェックを頭の中で回しながらポリシー設計するのは、マジで大変だった。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": [
"ec2:*",
"rds:*",
"s3:DeleteBucket"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": "ap-northeast-1"
}
}
}
]
}
これだけ見ると「東京リージョン以外のEC2・RDS・S3削除を禁止」って読める。でも、実際に動かすと、東京リージョンでもEC2停止はできるけど削除はできない、みたいな中途半端な動作が起きるんだ。IAMロールの権限設定を見に行くと、「あ、EC2:TerminateInstancesだけ別途Denyしてる」とか。こういう二重設定が本番で積み重なるんだ。
実装の失敗パターン——3回のロックアウト事件
失敗1:ルートアカウント保護の過度な制限(1ヶ月目)
最初、セキュリティベストプラクティスの文献をそのまま使って、ルートアカウントのセッション管理を制限するSCPを作った。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringLike": {
"aws:username": "root"
}
}
}
]
}
「ルートアカウントの使用を禁止」という名目で、これを本番マネジメントアカウントに適用した。結果、AWSアカウント削除が必要になった時にルートアカウントでしか実行できないことに気づいたんだ。詰んだ。AWS Supportに連絡して、1日かけて復旧した。
ここから学んだのは、SCP設計では「緊急時の回避経路」を常に用意するってこと。
失敗2:リソースベースのポリシーとの競合(3ヶ月目)
S3バケットのクロスアカウントアクセスを制限するために、SCPでS3:PutObjectを条件付きで許可しようとした。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "s3:*",
"Resource": "*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "private"
}
}
}
]
}
したら、S3バケット側のバケットポリシーで別の制限があって、IAM + SCP + バケットポリシーの3段階の判定結果が「拒否」になった。4時間かけてトレースした。正直まだこういう多重制御は完全に理解しきってない。
失敗3:IAM権限委譲とSCPの相互作用(6ヶ月目)
これが一番ハマった。Organizationsで子アカウントに特定の権限を委譲する時に、SCPの設計を子アカウント単位で変えてなかったんだ。
親アカウント(管理用)でSCPを全体に適用してる状態で、子アカウントでCloudFormation StackSetsを実行しようとしたら、SCPのリージョン制限に引っかかって失敗した。でも親アカウント側はそのSCPを除外してたから、気づくのに時間がかかった。
Organization Root SCP:
- ApRegion制限 (ap-northeast-1 only)
↓
OU (Organization Unit) Accounts:
- Dev OU: ap-northeast-1 のみ許可
- Prod OU: ap-northeast-1, ap-southeast-1 許可
↓
しかし StackSets は複数リージョンに展開したい
→ Prod OU では別途 SCP を上書き許可
→ Dev OU では上書きなし
→ クロスアカウントでの権限チェックが各段階で入る
現在の設計——SCP運用の「段階的」アプローチ
1年半の試行錯誤を経て、今はこういう構成で落ち着いてる。大事なのは「完璧を目指さない」こと。段階的に制限を強めていく方が、失敗も少ないし、トレーサビリティも良い。
graph TB
subgraph "AWS Organization Root"
direction TB
ORGROOT["Organization Root<br/>(SCPなし - 緊急用)"] --> GUARDRAIL["Guardrail SCP<br/>(基本的な禁止項目のみ)"]
GUARDRAIL --> DEVOU["Dev OU"]
GUARDRAIL --> PRODOU["Prod OU"]
GUARDRAIL --> MANAGEMENTOU["Management OU"]
end
subgraph "Dev OU (アクセス広い)"
DEVOU --> DEVACCT1["Dev Account 1<br/>(リージョン: 全東京)"]
DEVOU --> DEVACCT2["Dev Account 2<br/>(リージョン: 全東京)"]
DEVACCT1 --> DEVPOLICY["SCP: リージョン制限のみ<br/>リソース削除は許可"]
DEVACCT2 --> DEVPOLICY
end
subgraph "Prod OU (制限強い)"
PRODOU --> PRODACCT["Prod Account<br/>(リージョン: 東京+シンガポール)"]
PRODACCT --> PRODPOLICY["SCP: リージョン+リソース削除制限"]
end
subgraph "Management OU (特別)"
MANAGEMENTOU --> MGMTACCT["Management Account"]
MGMTACCT --> MGMTPOLICY["SCP: Deny 除く全てAllow<br/>(ルートアカウント保護のみ)"]
end
subgraph "各アカウント内: IAM層"
DEVPOLICY --> IAMDEV["IAM Role (DevOps)<br/>- EC2, RDS 操作可<br/>- 削除操作は IAM で拒否"]
PRODPOLICY --> IAMPROD["IAM Role (SRE)<br/>- 読み取り優先<br/>- 削除操作は必須フロー通す"]
MGMTPOLICY --> IAMMGMT["IAM Role (Admin)<br/>- 上書き可能設定<br/>- 監査ログ必須"]
end
実装時の具体的なアプローチ
1. GuardRail SCP——基本的な「やってはいけない」だけ
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PreventCloudTrailDisable",
"Effect": "Deny",
"Action": [
"cloudtrail:DeleteTrail",
"cloudtrail:StopLogging"
],
"Resource": "*"
},
{
"Sid": "PreventConfigDisable",
"Effect": "Deny",
"Action": [
"config:DeleteConfigurationRecorder",
"config:StopConfigurationRecorder"
],
"Resource": "*"
},
{
"Sid": "PreventDisableSecurityHub",
"Effect": "Deny",
"Action": [
"securityhub:DisableSecurityHub"
],
"Resource": "*"
}
]
}
これはOrganization Rootに直接適用して、全アカウントが最低限守るべきルールだ。CloudTrailやConfigの削除を禁止するだけ。シンプルで、ロックアウトリスクも低い。
2. OU別のガバナンスSCP
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "RestrictToTokyoRegion",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"ap-northeast-1",
"ap-northeast-2"
]
},
"Bool": {
"aws:ViaAWSService": "false"
}
}
}
]
}
Dev OUに適用する。aws:ViaAWSServiceという条件を入れたのは大事で、これでAWS内部サービス(例えばAuto Scalingが別リージョンでリソース作成する)の動作を許可できるんだ。外部からのAPI呼び出しだけを制限して、サービス間の内部動作は邪魔しない、っていう工夫になる。
3. テスト環境でのSCPシミュレーション
どうしても複雑な設定が必要な場合は、本番反映の前に検証アカウントで実装テストを必ずやってる。
#!/bin/bash
# 特定のアカウントで、SCP適用後の権限を検証
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::123456789012:role/DevOpsRole \
--action-names ec2:TerminateInstances s3:DeleteBucket rds:DeleteDBCluster \
--resource-arns arn:aws:ec2:ap-northeast-1:123456789012:instance/* \
arn:aws:s3:::my-bucket \
arn:aws:rds:ap-northeast-1:123456789012:db/*
これで「このロールがこのアクションを実行できるか」を事前に確認できる。本番反映前には必ずやってる。シミュレータは万能じゃないんだけど、大きなミスは大抵ここで引っかかる。
SCPとIAMの相互作用を図解する
flowchart TD
START["User がアクション実行を試みる<br/>例: s3:PutObject"] --> CHECK1{IAM ポリシーで<br/>許可されてるか?}
CHECK1 -->|No| DENY1["❌ IAM で Deny<br/>ここで終了"]
CHECK1 -->|Yes| CHECK2{SCP で<br/>拒否されてないか?}
CHECK2 -->|Yes| DENY2["❌ SCP で Deny<br/>ここで終了"]
CHECK2 -->|No| CHECK3{リソースベース<br/>ポリシーで許可?}
CHECK3 -->|No| DENY3["❌ リソースポリシーで Deny<br/>ここで終了"]
CHECK3 -->|Yes| ALLOW["✅ 実行許可"]
style DENY1 fill:#ffcccc
style DENY2 fill:#ffcccc
style DENY3 fill:#ffcccc
style ALLOW fill:#ccffcc
この流れを本当に理解することが、SCPデバッグの最初の一歩。IAMの許可→SCPの許可→リソースポリシーの許可、この全部がYesじゃないと動かない。一つでも「No」があれば、その段階で実行がブロックされる。重要なのはこの評価順序。IAMで拒否されたら、SCPのルールは見ないしリソースポリシーも見ない。
運用の工夫——ローテーションと段階的緩和
はじめは誰も本当の権限が何か理解してない。だから、SCP設定直後は「緊急用の回避策」を常に用意してる。具体的には、こんなステップで進めてる。
-
Week 1-2:SCPを記述モード(ログ記録のみ)で実行
- AWS Access Analyzerで権限不足を事前検出
- CloudTrailで「このSCPが適用されたらどの操作が失敗するか」を可視化
-
Week 3-4:小規模アカウント(開発用)で実適用
- 影響範囲を最小化
- 問題が起きても大ごとにならない
-
Week 5-6:本番アカウントの一部に段階的適用
- OU分割で、Dev → Prod の順に適用
- 各段階で1週間の待機期間を設ける
-
Month 2 以降:モニタリングと改善
- CloudTrailで「DeniedOperation」を定期的にレビュー
- 正当なユースケースが見つかればSCP修正
地味だけど、この段階的なアプローチで本当に失敗が減った。最初から「完璧に」はできないんだと割り切ることが大事。
チームで共有して気づいたこと
セキュリティチームは「SCPで統制したい」という気持ちがわかる。でも、開発チームはそこまで強い制限を嫌がるんだ。その葛藤を上手くバランスするのが、本当のSCP設計なんだと感じた。
「完璧なセキュリティ設定」と「実運用の利便性」のトレードオフを、本当に理解してる人は少ない。だから、今はセキュリティチームと開発チームで定期的に「SCP Review会」やってる。月1回、実際のDeniedOperationログを見ながら、「この制限、本当に必要か?」って一緒に考えるんだ。
実際、3ヶ月前に「東京リージョンのみ」という制限をプロダクション環境に入れたんだけど、キャパシティの問題で一部をシンガポールに移す必要が出てきた。SCPを「柔軟に」修正する仕組みがあったから、1日で対応できた。この柔軟性がなかったら、緊急対応時に本当に困るんだ。
まとめ
-
SCPはIAMの上に乗る制限層——二重チェックを常に意識する。IAMで許可されても、SCPで拒否されれば使えない。この相互作用を理解することが全ての始まり。
-
最初から完璧を目指さない——段階的にルールを追加する。GuardRailレベルの基本禁止から始めて、段階的に制限を強める方が、失敗が少なくて修正も容易だ。
-
緊急時の回避経路を用意する——ルートアカウント保護は必須だが、絶対ロックアウトするな。Management アカウントには別ルールを適用するなど、運用リスク対策が重要。
-
テスト環境での検証は必須——IAM シミュレータを本番反映前に必ず実行する。複雑な権限判定は、事前検証でほぼ9割のバグを潰せる。
-
チーム間で継続的にレビューする——セキュリティ要件と実務のバランスを月1で確認。ログを見ながら「この制限、本当に必要か」という対話が、長期運用のカギになる。
SCPは導入して終わりじゃなく、本番で3〜6ヶ月かけて「本当の形」が見えてくるんだ。その過程を楽しめるチームなら、セキュアで運用しやすい基盤が作れる。正直、セキュリティとUXのバランスを取るのは本当に大変なんだけど、その試行錯誤がチーム全体の成長につながる。