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設定直後は「緊急用の回避策」を常に用意してる。具体的には、こんなステップで進めてる。

  1. Week 1-2:SCPを記述モード(ログ記録のみ)で実行

    • AWS Access Analyzerで権限不足を事前検出
    • CloudTrailで「このSCPが適用されたらどの操作が失敗するか」を可視化
  2. Week 3-4:小規模アカウント(開発用)で実適用

    • 影響範囲を最小化
    • 問題が起きても大ごとにならない
  3. Week 5-6:本番アカウントの一部に段階的適用

    • OU分割で、Dev → Prod の順に適用
    • 各段階で1週間の待機期間を設ける
  4. Month 2 以降:モニタリングと改善

    • CloudTrailで「DeniedOperation」を定期的にレビュー
    • 正当なユースケースが見つかればSCP修正

地味だけど、この段階的なアプローチで本当に失敗が減った。最初から「完璧に」はできないんだと割り切ることが大事。

チームで共有して気づいたこと

セキュリティチームは「SCPで統制したい」という気持ちがわかる。でも、開発チームはそこまで強い制限を嫌がるんだ。その葛藤を上手くバランスするのが、本当のSCP設計なんだと感じた。

「完璧なセキュリティ設定」と「実運用の利便性」のトレードオフを、本当に理解してる人は少ない。だから、今はセキュリティチームと開発チームで定期的に「SCP Review会」やってる。月1回、実際のDeniedOperationログを見ながら、「この制限、本当に必要か?」って一緒に考えるんだ。

実際、3ヶ月前に「東京リージョンのみ」という制限をプロダクション環境に入れたんだけど、キャパシティの問題で一部をシンガポールに移す必要が出てきた。SCPを「柔軟に」修正する仕組みがあったから、1日で対応できた。この柔軟性がなかったら、緊急対応時に本当に困るんだ。

まとめ

  1. SCPはIAMの上に乗る制限層——二重チェックを常に意識する。IAMで許可されても、SCPで拒否されれば使えない。この相互作用を理解することが全ての始まり。

  2. 最初から完璧を目指さない——段階的にルールを追加する。GuardRailレベルの基本禁止から始めて、段階的に制限を強める方が、失敗が少なくて修正も容易だ。

  3. 緊急時の回避経路を用意する——ルートアカウント保護は必須だが、絶対ロックアウトするな。Management アカウントには別ルールを適用するなど、運用リスク対策が重要。

  4. テスト環境での検証は必須——IAM シミュレータを本番反映前に必ず実行する。複雑な権限判定は、事前検証でほぼ9割のバグを潰せる。

  5. チーム間で継続的にレビューする——セキュリティ要件と実務のバランスを月1で確認。ログを見ながら「この制限、本当に必要か」という対話が、長期運用のカギになる。

SCPは導入して終わりじゃなく、本番で3〜6ヶ月かけて「本当の形」が見えてくるんだ。その過程を楽しめるチームなら、セキュアで運用しやすい基盤が作れる。正直、セキュリティとUXのバランスを取るのは本当に大変なんだけど、その試行錯誤がチーム全体の成長につながる。

U

Untanbaby

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

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

関連記事