AWS Security Vol3 IAM×KMS×Secrets×Verified Access

目次

§1 なぜ Security Vol3 か — Zero Trust と Defense in Depth の予防系深化

AWS セキュリティを本番環境で真に機能させるには、脅威を検知して対処するだけでは不十分だ。攻撃者がシステムに侵入した後に検知して対応するリアクティブな設計は、侵入自体を許した時点でリスクが顕在化している。本番セキュリティの到達点は「そもそも侵入させない」「侵入されても被害を最小化する」という予防的な設計にある。

本記事はその予防系設計の完全実装を扱う AWS Security シリーズの第3巻だ。

シリーズ三部作の全体像

本記事を読む前に、AWS Security シリーズ3部作の位置付けを整理する。

Vol1: Security 運用3本柱 基礎

AWS セキュリティの出発点として、証跡収集・CloudTrail 設定・基本的なアラート実装を扱う。セキュリティ運用の「基盤」を構築するフェーズだ。何をログに残すか、どこにアラートを飛ばすか、という基本設計がここで完成する。

Vol2: SOC 統合運用 — 脅威検知系深化

Vol1 で構築した基盤の上で、脅威検知系を高度化するフェーズ。複数の検知サービスを統合し、攻撃パターンの検出精度を高め、自動対応パイプラインを構築する。「検知系」の深化がここにある。

Vol3(本記事): 予防系深化

Vol1/Vol2 で「起きてから対処する」仕組みを確立した読者が、「起きる前に防ぐ」設計を実装するフェーズ。IAM の権限縦深化・暗号化境界の厳密な設計・Zero Trust ネットワークの実装が本巻の主題だ。

フェーズ設計軸主要サービス
Vol1基盤構築証跡収集・基本検知設計CloudTrail / Config
Vol2検知深化脅威検出・自動対応パイプライン検知系統合サービス群
Vol3予防深化IAM縦深・暗号化・ZTNAAccess Analyzer / Identity Center / KMS / Verified Access

Vol1→Vol2→Vol3 の順で読むことで AWS セキュリティの全サイクル(基盤 → 検知 → 予防)が体系化される。ただし Vol3 は各章が独立しているため、特定領域のみをピンポイントで読むことも可能だ。

Zero Trust モデル — 4境界での予防設計

Zero Trust とはネットワーク境界への信頼を前提にしない設計思想だ。「社内ネットワークだから安全」「VPN 接続しているから信頼できる」という前提を捨て、すべてのリクエストをアイデンティティ・デバイス・ネットワーク・データの4軸で継続的に検証する。

AWS が提供する Zero Trust アーキテクチャは以下の4境界で構成される。

Identity 境界 — 誰がアクセスするか

最小権限の IAM ポリシーと、それを継続的に検証する仕組みが Identity 境界の核心だ。IAM Identity Center で SSO と Permission Sets を統合し、IAM Access Analyzer で「使われていない権限」と「意図せず外部公開された権限」を継続検出する。静的な権限設計だけでは不十分で、運用中の権限ドリフトを自動的に発見するループが必要だ。

Identity 境界を維持するサイクルは以下の通りだ。

  1. Identity Center で Permission Sets を最小権限で設計する(§3 で詳解)
  2. Access Analyzer の Unused Access Findings で未使用権限を継続検出する(§2 で詳解)
  3. Custom Policy Check を CI/CD に組み込み、新規ポリシーの過剰権限を事前遮断する(§2 で詳解)
  4. 一定期間使用されない権限を定期棚卸しで削除するプロセスを確立する

Device 境界 — どの端末・エンドポイントからアクセスするか

デバイスの信頼性を検証せずにネットワークアクセスを許可することは、認証情報が漏洩した時点で防御が崩れることを意味する。Verified Access の Trust Provider でデバイス証明書・IdP 認証情報を統合し、Cedar Policy でデバイスポスチャー(OS バージョン・パッチ適用状況・MDM 登録有無)を評価する。

デバイス境界が機能することで「盗まれた認証情報だけでは侵入できない」状態を実現できる。

Network 境界 — どの経路でアクセスするか

インターネット経由のアクセスを減らし、VPC Endpoint 経由のプライベートアクセスを増やすことがネットワーク境界の基本戦略だ。Verified Access はアプリケーションへのアクセスをゼロトラストの原則でフィルタリングし、VPC 内部リソースへの直接露出を排除する。

IAM Access Analyzer の External Access Findings は、S3 バケット・IAM ロール・KMS キー・Lambda 関数などのリソースポリシーを常時スキャンし、組織外への意図しない公開を即時通知する。

Data 境界 — 何を保護するか

データ境界は「暗号化の範囲」と「シークレットの有効期間」の2軸で設計する。KMS Multi-Region Replica Key を使うことで「東京で暗号化、大阪でも復号できる」設計が可能になり、リージョン障害時のデータ可用性を確保しながら暗号化境界を維持できる。

Secrets Manager Auto Rotation は認証情報を動的に更新し、漏洩した認証情報が永続的に悪用されるリスクを排除する。Certificate Manager の自動更新と組み合わせることで、証明書失効によるサービス断を防ぐ。

4境界を横断的に設計することで「ネットワーク内だから安全」という古典的な前提を完全に排除する。Zero Trust は特定のサービスを使えば完成するものではなく、4境界すべてを継続的に検証し続けるアーキテクチャ全体を指す。

Defense in Depth — 予防系レイヤの積層設計

多層防御の本質は「1つの制御が破られても、次の制御が攻撃を止める」設計にある。Vol3 の5つのサービスはそれぞれ独立したレイヤとして機能しながら、相互に補完し合う。

防御レイヤ防御対象対応サービス当レイヤが破られた場合の次の防御
アクセス制御権限の過剰付与・放置IAM Access AnalyzerIdentity Center の最小権限設計
認証統合認証情報の漏洩・不正使用IAM Identity CenterVerified Access のデバイス認証
暗号化境界静止データ・転送データの露出KMS Multi-RegionSecrets Manager のローテーション
シークレット管理認証情報の長期固定・露出Secrets Manager + ACMKMS による暗号化保護
ネットワーク認証未認証・未認可のネットワークアクセスVerified AccessIdentity Center の MFA 強制

重要なのは「どれか1つで十分」ではなく、すべてのレイヤを同時に実装することだ。Access Analyzer で未使用権限を検出しても、KMS の Key Policy が過剰に許可されていればデータは保護されない。逆に KMS が完璧でも、Secrets Manager に静的なアクセスキーが放置されれば暗号化を迂回した攻撃が成立してしまう。

各レイヤは独立して動作するが、相互に検証し合う関係でもある。IAM Access Analyzer の Findings が権限設計の見直しトリガーとなり、KMS の Key Policy が Verified Access の認可前提を支え、Secrets Manager の自動ローテーションが長期的な認証情報リスクを排除する。このように連携させることで Defense in Depth の効果が最大化される。

本記事で扱う5領域ロードマップ

§対象領域主な実装内容達成する予防効果
§2IAM Access AnalyzerExternal / Unused Findings + Custom Policy Check + CI/CD 統合権限の過剰付与・放置・外部公開を継続検出
§3IAM Identity CenterPermission Sets + SCIM + Multi-Account SSO + External IdP 連携SSO 統合と最小権限を組織全体に適用
§4KMS Multi-RegionReplica Key + Key Policy + Grants + Auto Rotation暗号化境界の明示的設計とマルチリージョン対応
§5Secrets Manager + ACMAuto Rotation + Lambda + Resource Policy + 証明書管理認証情報の動的管理と証明書の自動更新
§6Verified AccessTrust Provider + Cedar Policy + VPC Endpoint + ZTNA設計ZTNA によるゼロトラストネットワークアクセス実装

§7 では本番設計でよく詰まる7パターンを体系化し、§8 ではアンチパターンから正解パターンへの変換演習を5問収録する。理論だけでなく「本番で直面する落とし穴と正しい解決策」まで含めて習得できる構成だ。

各§はそれぞれ独立した本番実装ガイドとして機能する。§3 の IAM Identity Center のみを読んでも完全な実装が可能であり、§4 の KMS のみを読んでも暗号化境界の設計が完結する。ただし全体を通して読むことで、5つのサービスが Zero Trust の4境界にどう対応するかという全体像が見えてくる。

📍 AWS Security シリーズ — 全軸ナビ

Vol1: Security 運用3本柱 基礎
証跡収集・CloudTrail 設計・基本アラート実装。AWS セキュリティ実装の出発点となる基盤巻。

Vol2: SOC 統合運用 — 脅威検知系深化
脅威検知サービスの統合運用と自動対応パイプライン構築。Vol1 読了後に読む検知系の深化巻。
AWS Security Vol2 を読む

Vol3(本記事): 予防系深化 — IAM × KMS × Secrets × Verified Access
Vol1/Vol2 読了者向け予防系深化巻。Zero Trust 設計・暗号化境界・ZTNA の完全実装を扱う。

前提知識: Vol1 の CloudTrail 設定と、Vol2 の検知統合を完了していること。3巻を順番に読むと AWS セキュリティ全サイクルが体系化される。本記事は §2〜§6 が独立して読めるため、特定領域からの参照も可能だ。


§2 IAM Access Analyzer 本番運用 — External / Unused / Custom Policy Check

IAM Access Analyzer は IAM ポリシー・リソースポリシーを継続的に分析し、3種類の Findings を生成する。それぞれが異なる問題を検出し、組み合わせることで「権限の安全性」を多角的に保証する。

Findings タイプ検出する問題対象リソース
External Access Findings組織外への意図しないアクセス公開S3 / IAM Role / KMS / Lambda / SQS / Secrets Manager
Unused Access Findings一定期間使用されていない権限・キー・パスワードIAM ユーザー / ロール / アクセスキー
Custom Policy CheckCI/CD 段階での過剰権限ポリシーの事前検出IAM ポリシー文書(デプロイ前)

Analyzer の種類と設定

Access Analyzer には「アカウントレベル Analyzer」と「組織レベル Analyzer」の2種類がある。

アカウントレベル Analyzer

単一の AWS アカウント内のリソースを対象とし、アカウント外部(他の AWS アカウント・インターネット)へのアクセス公開を検出する。単一アカウントで利用している場合はこちらから始める。

組織レベル Analyzer

AWS Organizations と統合し、組織全体をスキャンする。検出対象は「組織外部」への公開のみとなるため、組織内のアカウント間クロスアカウントアクセスは Findings として検出されない(正常なアクセスとして扱われる)。マルチアカウント構成では組織レベル Analyzer の設定が必須だ。

# 組織レベル Analyzer(Delegated Admin アカウントで設定)
resource "aws_accessanalyzer_analyzer" "org_analyzer" {
  analyzer_name = "org-level-analyzer"
  type = "ORGANIZATION"

  tags = {
 Environment = "production"
 ManagedBy= "terraform"
  }
}

# アカウントレベル Analyzer(単一アカウント / 各メンバーアカウント)
resource "aws_accessanalyzer_analyzer" "account_analyzer" {
  analyzer_name = "account-level-analyzer"
  type = "ACCOUNT"

  tags = {
 Environment = "production"
 ManagedBy= "terraform"
  }
}

組織レベル Analyzer を使う場合、設定は Organizations の Delegated Admin アカウント(通常はセキュリティ専用アカウント)から行う。Organizations の管理アカウントで Access Analyzer サービスアクセスを有効化してから Delegated Admin を登録する。

# Delegated Admin の登録(Organizations 管理アカウントから実行)
aws organizations register-delegated-administrator \
  --account-id 123456789012 \
  --service-principal access-analyzer.amazonaws.com

# 登録確認
aws organizations list-delegated-administrators \
  --service-principal access-analyzer.amazonaws.com

External Access Findings — 組織外公開の継続検出

External Access Findings は、リソースポリシー・IAM ロールの Trust Policy を常時スキャンし、組織外エンティティ(他の AWS アカウント・AWS サービス・インターネット)へのアクセスを検出する。

検出対象リソースタイプ

リソースタイプ検出される設定の例
S3 バケットPrincipal: "*" / バケットポリシーの外部アカウント Allow
IAM ロールTrust Policy で外部アカウント・外部プリンシパルの AssumeRole 許可
KMS キーKey Policy で外部アカウントへの Decrypt / DescribeKey 許可
Lambda 関数Resource-based Policy で外部からの invoke 許可
SQS キューキューポリシーで外部アカウントの SendMessage / ReceiveMessage 許可
Secrets Managerシークレットポリシーで外部アカウントの GetSecretValue 許可
IAM Access Analyzer 全体アーキテクチャ
図: IAM Access Analyzer の External / Unused / Custom Policy Check 統合構成

Trust Policy の Principal 判定ロジック

IAM ロールの Trust Policy を分析する際、Access Analyzer は Principal の値を以下のルールで評価する。

  • 同一アカウント ID を持つ Principal → 同一アカウント(Findings 生成なし)
  • 別アカウント ID を持つ Principal → 外部アカウント(Findings 生成)
  • "AWS": "*" を含む Principal → インターネット公開(CRITICAL 判定)
  • "Service": "lambda.amazonaws.com" 等の AWS サービス → 通常は Findings 生成なし
  • SAML / OIDC フェデレーション → 分析対象(External として判定される場合あり)

組織レベル Analyzer では、同じ Organizations 内のアカウントへの Trust Policy は Findings として扱われないため、クロスアカウントロールの誤検知が大幅に減少する。

⚠️ External Access Findings の検出ロジック — 落とし穴パターン

パターン1: 既知のクロスアカウントアクセスが大量に検出される
アカウントレベル Analyzer を使用している場合、正規の Delegated Admin アカウントや共有サービスアカウントへの Trust Policy も Findings として表示される。対処: 組織レベル Analyzer に切り替えるか、Archive Rule で既知の信頼済みアカウントを登録する。

パターン2: Condition 付き外部許可が正確に評価されない
aws:PrincipalOrgID の Condition が付いていても、Analyzer はポリシー文書を静的に解析するため一部の Condition は正確に評価されない場合がある。Condition による制限は Analyzer の判定に頼らず、手動での定期レビューと組み合わせること。

パターン3: Unused Access Analyzer は有料サービス
External Access Analyzer は無料だが、Unused Access Findings の生成には IAM Access Analyzer コスト(IAM エンティティ数に応じた課金)が発生する。有効化前にコスト試算を行うこと。

Archive Rule による既知アクセスの管理

Findings が大量に生成されると、実際に問題のある Findings が埋もれてしまう。Archive Rule を設定することで、既知の意図したクロスアカウントアクセスを自動的にアーカイブし、残った Findings を真に対処が必要なものに絞り込む。

# Archive Rule: 既知の外部アカウントへのアクセスを自動アーカイブ
resource "aws_accessanalyzer_archive_rule" "trusted_accounts" {
  analyzer_name = aws_accessanalyzer_analyzer.org_analyzer.analyzer_name
  rule_name  = "trusted-cross-account-access"

  filter {
 criteria = "principal.AWS"
 eq = ["arn:aws:iam::TRUSTED_ACCOUNT_ID:root"]
  }
}

# Archive Rule: AWS サービスへの Lambda 関数公開をアーカイブ
resource "aws_accessanalyzer_archive_rule" "aws_service_access" {
  analyzer_name = aws_accessanalyzer_analyzer.org_analyzer.analyzer_name
  rule_name  = "aws-service-lambda-access"

  filter {
 criteria = "resourceType"
 eq = ["AWS::Lambda::Function"]
  }

  filter {
 criteria = "principal.Service"
 contains = ["amazonaws.com"]
  }
}

Unused Access Findings — 未使用権限の継続検出

Unused Access Findings は、IAM ユーザー・ロール・アクセスキー・パスワードのうち、一定期間(1〜180日で設定可能)使用されていないエンティティを検出する。

検出される Findings タイプ

タイプ検出条件推奨対処
UnusedPermissionポリシーに含まれるが設定期間内に使用されていないアクション権限の削除 / 絞り込み
UnusedIAMRole設定期間内に AssumeRole されていないロールロールの削除または無効化
UnusedIAMUserAccessKey設定期間内に使用されていないアクセスキーアクセスキーの無効化・削除
UnusedIAMUserPassword設定期間内にコンソールログインのないユーザーユーザーの無効化・削除

Unused Access Analyzer を組織レベルで有効化するには、Delegated Admin アカウントから ORGANIZATION_UNUSED_ACCESS タイプの Analyzer を作成する。

resource "aws_accessanalyzer_analyzer" "unused_access" {
  analyzer_name = "unused-access-analyzer"
  type = "ORGANIZATION_UNUSED_ACCESS"

  configuration {
 unused_access {
unused_access_age = 90  # 90日間未使用の場合に検出
 }
  }

  tags = {
 Environment = "production"
 ManagedBy= "terraform"
  }
}

Unused Access Findings は権限の棚卸しプロセスを自動化する。月次または四半期ごとに Findings を集約し、権限の削除・絞り込みサイクルを継続することで、IAM の最小権限原則を長期的に維持できる。

Custom Policy Check — CI/CD 段階での過剰権限遮断

Custom Policy Check は既存の Findings とは異なり、デプロイ前のポリシー文書を API ベースで検証する機能だ。CI/CD パイプラインに組み込むことで「過剰権限ポリシーが本番にデプロイされる前」に検出できる。

check-no-new-access

既存のポリシーと比較して、新しいアクセス許可が追加されているかを検証する。Pull Request でのポリシー変更審査に使用する。

# 既存ポリシーと新ポリシーを比較(新規アクセス権限の追加検出)
aws accessanalyzer check-no-new-access \
  --existing-policy-document file://existing-policy.json \
  --new-policy-document file://new-policy.json \
  --policy-type IDENTITY_POLICY

戻り値が PASS の場合は新規アクセス権限なし、FAIL の場合は新規権限が追加されている。FAIL 時は追加された権限のリストも返るため、レビュー担当者が差分を確認できる。

check-access-not-granted

特定のアクションがポリシーで許可されていないことを検証する。「s3:DeleteBucket を許可するポリシーは本番に入れない」などの制約を CI で強制する。

# 特定アクションの許可がないことを確認
aws accessanalyzer check-access-not-granted \
  --policy-document file://policy.json \
  --access '[{"actions": ["s3:DeleteBucket", "iam:CreateUser"]}]' \
  --policy-type IDENTITY_POLICY

GitHub Actions での CI/CD 統合例

name: IAM Policy Check
on:
  pull_request:
 paths:
- 'terraform/**/*.tf'
- 'iam-policies/**/*.json'

jobs:
  policy-check:
 runs-on: ubuntu-latest
 steps:
- uses: actions/checkout@v4

- name: Configure AWS credentials
  uses: aws-actions/configure-aws-credentials@v4
  with:
 role-to-assume: arn:aws:iam::ACCOUNT_ID:role/ci-policy-checker
 aws-region: ap-northeast-1

- name: Check for prohibited access permissions
  run: |
 CHANGED_POLICIES=$(git diff --name-only origin/main...HEAD | grep '\.json$')
 for policy_file in $CHANGED_POLICIES; do
echo "Checking: $policy_file"
RESULT=$(aws accessanalyzer check-access-not-granted \
  --policy-document "file://${policy_file}" \
  --access '[{"actions": ["iam:CreateUser", "iam:AttachRolePolicy", "s3:DeleteBucket"]}]' \
  --policy-type IDENTITY_POLICY \
  --query 'result' --output text)
if [ "$RESULT" = "FAIL" ]; then
  echo "ERROR: Prohibited permissions found in ${policy_file}"
  exit 1
fi
 done

CI/CD 専用ロールの Terraform 設定

resource "aws_iam_role" "policy_checker" {
  name = "ci-policy-checker"

  assume_role_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect = "Allow"
Principal = { Federated = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:oidc-provider/token.actions.githubusercontent.com" }
Action = "sts:AssumeRoleWithWebIdentity"
Condition = {
  StringEquals = {
 "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
  }
  StringLike = {
 "token.actions.githubusercontent.com:sub" = "repo:YOUR_ORG/YOUR_REPO:*"
  }
}
 }]
  })
}

resource "aws_iam_role_policy" "policy_checker" {
  name = "policy-checker-permissions"
  role = aws_iam_role.policy_checker.id

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect = "Allow"
Action = [
  "access-analyzer:CheckNoNewAccess",
  "access-analyzer:CheckAccessNotGranted",
  "access-analyzer:ValidatePolicy"
]
Resource = "*"
 }]
  })
}

組織レベル Analyzer の設計 — Delegated Admin パターン

マルチアカウント構成では、組織全体の Access Analyzer を一元管理する Delegated Admin 設計が標準パターンだ。

Organizations 管理アカウントから Delegated Admin アカウント(セキュリティ専用)を登録し、そこで組織レベル Analyzer を集約管理する。

Organizations 管理アカウント
  └── Delegated Admin アカウント(セキュリティ専用)
  ├── ORGANIZATION Analyzer(External Access Findings)
  ├── ORGANIZATION_UNUSED_ACCESS Analyzer(Unused Access Findings)
  └── EventBridge Rule → SNS → Security 通知システム

Delegated Admin アカウントは Organizations の管理アカウントとは別のセキュリティ専用アカウントを使用する。管理アカウントへの操作権限を分離することでリスクを低減する設計だ。

EventBridge 連携による自動通知

Access Analyzer は新しい Findings が生成されると EventBridge にイベントを送出する。EventBridge ルールを設定することで、Finding の重要度に応じた自動通知やチケット起票が可能だ。

resource "aws_cloudwatch_event_rule" "access_analyzer_findings" {
  name  = "access-analyzer-high-severity-findings"
  description = "Capture Access Analyzer findings with HIGH severity"

  event_pattern = jsonencode({
 source  = ["aws.access-analyzer"]
 "detail-type" = ["Access Analyzer Finding"]
 detail = {
status= ["ACTIVE"]
severity = ["HIGH", "CRITICAL"]
 }
  })
}

resource "aws_cloudwatch_event_target" "sns_target" {
  rule= aws_cloudwatch_event_rule.access_analyzer_findings.name
  target_id = "SendToSNS"
  arn = aws_sns_topic.security_alerts.arn
}
✅ Access Analyzer 組織レベル設計チェックリスト

Analyzer 設定
□ Delegated Admin アカウントを Organizations で登録済みか
□ ORGANIZATION タイプの External Access Analyzer を作成済みか
□ ORGANIZATION_UNUSED_ACCESS タイプの Unused Access Analyzer を作成済みか(有料・コスト試算必須)
□ Unused Access Age を組織ポリシーに合わせて設定済みか(推奨: 90日)

Archive Rule 設定
□ 既知の信頼済みクロスアカウントアクセスを Archive Rule で登録済みか
□ AWS サービスからの Lambda 呼び出しなど正規アクセスをアーカイブ済みか
□ Archive Rule の対象範囲を過剰に広げていないか(全件アーカイブは禁止)

CI/CD 統合
□ Pull Request 時に check-no-new-access を実行する CI ジョブを設定済みか
□ 本番禁止アクション(iam:CreateUser / s3:DeleteBucket 等)の check-access-not-granted を設定済みか
□ CI 専用ロールに Access Analyzer 権限のみを最小権限で付与済みか

通知・運用
□ HIGH / CRITICAL 重要度の Findings を EventBridge で自動通知済みか
□ Unused Access Findings の月次棚卸しプロセスが確立済みか
□ Findings の解決期限(SLA)が定義され、チームに共有済みか


§3 IAM Identity Center 本番運用 — Permission Sets × SCIM × Multi-Account SSO ★山場1

IAM Identity Center は Organizations 配下の複数 AWS アカウントへのシングルサインオン (SSO) を担う中核サービスだ。Permission Sets による権限モデル、SCIM による IdP との自動同期、Account Assignment による権限割り当てを組み合わせることで、数十〜数百アカウントを一元管理できる。本番運用の要はこの三角形の設計精度にかかっている。

3-1 Permission Sets 設計 — 4タイプの使い分け

Permission Set は IAM Identity Center における「役割テンプレート」だ。1つの Permission Set が1つの IAM Role に対応し、Account Assignment でアカウント × グループ × Permission Set の組み合わせを定義する。

タイプ別使い分け判断基準

タイプ管理場所変更即時反映推奨ユースケース
AWS Managed PolicyAWS管理読み取り専用 (ReadOnlyAccess) など汎用ロール
Inline PolicyPermission Set内○ (再同期要)例外的な1回限りの細粒度制御
Customer Managed Policy各アカウントのIAM✕ (アカウント側先行デプロイ必須)本番推奨。ポリシーをコードで管理
Permission Boundary各アカウントのIAM✕ (アカウント側先行デプロイ必須)最大権限の天井を設定し昇格攻撃を防ぐ

Customer Managed Policy の attach/detach タイミング

Customer Managed Policy を Permission Set に紐付けるには、対象アカウント側に同名・同パスの IAM Managed Policy が事前に存在していなければならない。順序を誤ると Account Assignment 時に同期エラーが発生する。

デプロイ順序:

① 各アカウントに IAM Managed Policy を Terraform で作成
② Permission Set に customer_managed_policy_attachment を追加
③ Account Assignment を作成 (または再同期)

Terraform でこの順序を保証するには depends_on を使う:

resource "aws_ssoadmin_permission_set" "developer" {
  name = "Developer"
  instance_arn  = data.aws_ssoadmin_instances.main.arns[0]
  session_duration = "PT8H"
}

resource "aws_ssoadmin_customer_managed_policy_attachment" "developer_cmp" {
  instance_arn = data.aws_ssoadmin_instances.main.arns[0]
  permission_set_arn = aws_ssoadmin_permission_set.developer.arn

  customer_managed_policy_reference {
 name = "DeveloperPolicy"
 path = "/sso/"
  }

  depends_on = [aws_iam_policy.developer_policy]
}

Permission Boundary の本番設定例

Permission Boundary は Permission Set が付与できる最大権限の上限を定義する。開発者ロールが誤って管理者権限を自己付与することを防ぐ。

resource "aws_ssoadmin_permissions_boundary_attachment" "developer_boundary" {
  instance_arn = data.aws_ssoadmin_instances.main.arns[0]
  permission_set_arn = aws_ssoadmin_permission_set.developer.arn

  permissions_boundary {
 customer_managed_policy_reference {
name = "DeveloperBoundaryPolicy"
path = "/sso/"
 }
  }
}
🔥 Permission Sets 設計 3鉄則
鉄則① Customer Managed Policy を使え — Inline Policy は再同期が必要で変更検知が困難。本番では必ず Customer Managed Policy + Terraform 管理に統一する。
鉄則② Permission Boundary を全 Permission Set に付与せよ — Permission Boundary なしの Permission Set は権限昇格の起点になる。最低でも「自 Permission Set 以上の権限を付与できない」ポリシーを設定する。
鉄則③ アカウント側ポリシーを先にデプロイせよ — Customer Managed Policy / Permission Boundary は Account Assignment より前に対象アカウント側で作成されていなければならない。CI/CD パイプラインでフェーズを分離し、Terraform depends_on で順序を保証する。

3-2 SCIM 同期 — IdP 別設定と落とし穴

IAM Identity Center は SCIM (System for Cross-domain Identity Management) 2.0 を使って外部 IdP からユーザー・グループを自動同期する。手動プロビジョニングは運用不能なため、本番では必須の設定だ。

SCIM Endpoint URL と Token 取得手順

IAM Identity Center コンソール → 設定 → 自動プロビジョニング → 有効化:

  1. SCIM endpoint URL をコピー (例: https://scim.us-east-1.amazonaws.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scim/v2/)
  2. アクセストークンを生成・コピー (トークンは一度しか表示されない)

生成したトークンは Secrets Manager に保存し、Terraform で参照する:

resource "aws_secretsmanager_secret" "scim_token" {
  name = "/identity-center/scim-token"
}

resource "aws_secretsmanager_secret_version" "scim_token_value" {
  secret_id  = aws_secretsmanager_secret.scim_token.id
  secret_string = var.scim_token  # CI/CD で入力
}

Okta 設定手順

  1. Okta Admin Console → Applications → Browse App Catalog → “AWS IAM Identity Center” を追加
  2. Provisioning タブ → Configure API Integration → SCIM endpoint と Token を入力
  3. To App: Create Users / Update User Attributes / Deactivate Users を有効化
  4. Push Groups: 同期対象グループを選択
  5. Assignments: プロビジョニング対象ユーザーを割り当て

属性マッピングの必須設定 (Okta):

Okta 属性IAM Identity Center 属性備考
user.loginuserNameメールアドレス形式必須
user.firstNamegivenName必須
user.lastNamefamilyName必須
user.emailemails[primary].value必須
user.displayNamedisplayName省略時は givenName+familyName

Azure AD 設定手順

  1. Azure Portal → エンタープライズアプリケーション → 新しいアプリケーション → “AWS IAM Identity Center” を検索して追加
  2. プロビジョニング → 自動 → 管理者資格情報: テナントURL (SCIM endpoint) とシークレットトークンを入力
  3. 接続のテストをクリックして疎通確認
  4. マッピング → ユーザーのプロビジョニング → 属性マッピングを確認
  5. スコープを「割り当てられたユーザーとグループのみ同期」に設定

Azure AD で必要な属性マッピング:

Azure AD 属性IAM Identity Center 属性備考
userPrincipalNameuserName@ 含むメールアドレス形式
givenNamegivenName
surnamefamilyName
mailemails[primary].valueuserPrincipalName と異なるドメインに注意

Google Workspace 設定手順

  1. Google Admin Console → アプリ → ウェブアプリとモバイルアプリ → アプリを追加 → アプリカタログから追加 → “AWS IAM Identity Center” を検索
  2. 自動プロビジョニングを有効化 → SCIM URL とトークンを入力
  3. 属性マッピングを確認 (primaryEmailuserName は必須)
  4. サービスのアクセス権: 同期対象 OU またはグループを指定

グループ同期 vs ユーザー同期の使い分け

本番ではグループ同期を優先する。直接ユーザー同期は Account Assignment 設計が複雑になり、IdP 側でのグループ管理が失われる。

方式メリットデメリット推奨
グループ同期IdP 側でグループ管理、Account Assignment がシンプルグループ設計が必要◎ 本番推奨
ユーザー直接同期細粒度制御可能Account Assignment が N × Permission Set の組み合わせで爆発△ 小規模のみ
✅ SCIM 同期トラブル対処法
パターン①: 同期遅延 — SCIM はプッシュ型だが IdP 側の同期間隔が15〜40分の場合がある。即時反映が必要な場合は IdP 管理コンソールから「今すぐ同期」を手動実行するか、SCIM API を直接呼び出す。
パターン②: 属性マッピング不一致 — IAM Identity Center の SCIM は userName がメールアドレス形式 (user@example.com) である必要がある。Okta の login 属性が短縮形の場合、変換式で補正する。Azure AD は userPrincipalNamemail が異なるドメインになるケースに注意。
パターン③: Group → Permission Set 紐付け失敗 — グループが同期されているのに Account Assignment が機能しない場合、グループ名の大文字小文字・スペースの違いが原因なことが多い。Identity Store コンソールでグループ名を目視確認し、Terraform の data.aws_identitystore_groupfilter 値と完全一致させる。

3-3 Multi-Account SSO — Account Assignment の設計

Account Assignment は「どのグループが、どのアカウントで、どの Permission Set を使えるか」を定義する。Organizations と連携することで新規アカウント追加時の自動同期も可能だ。

Identitystore: グループとユーザーの Terraform 参照

data "aws_ssoadmin_instances" "main" {}

data "aws_identitystore_group" "developers" {
  identity_store_id = tolist(data.aws_ssoadmin_instances.main.identity_store_ids)[0]

  alternate_identifier {
 unique_attribute {
attribute_path  = "DisplayName"
attribute_value = "Developers"
 }
  }
}

data "aws_identitystore_user" "alice" {
  identity_store_id = tolist(data.aws_ssoadmin_instances.main.identity_store_ids)[0]

  alternate_identifier {
 unique_attribute {
attribute_path  = "UserName"
attribute_value = "alice@example.com"
 }
  }
}

Account Assignment の Terraform 完全例

resource "aws_ssoadmin_account_assignment" "developers_staging" {
  instance_arn = tolist(data.aws_ssoadmin_instances.main.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.developer.arn

  principal_id= data.aws_identitystore_group.developers.group_id
  principal_type = "GROUP"

  target_id= var.staging_account_id
  target_type = "AWS_ACCOUNT"
}

resource "aws_ssoadmin_account_assignment" "developers_prod" {
  instance_arn = tolist(data.aws_ssoadmin_instances.main.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.developer_readonly.arn

  principal_id= data.aws_identitystore_group.developers.group_id
  principal_type = "GROUP"

  target_id= var.prod_account_id
  target_type = "AWS_ACCOUNT"
}

Account Assignment の順序依存性

Account Assignment を作成する前に以下が完了していなければならない:

  1. グループが Identity Store に同期済み — SCIM 同期または手動作成後、aws_identitystore_group データソースで参照できることを確認
  2. Permission Set に Customer Managed Policy が添付済み — 対象アカウントに同名ポリシーが存在し、aws_ssoadmin_customer_managed_policy_attachment が Apply 済み
  3. Permission Boundary が添付済みaws_ssoadmin_permissions_boundary_attachment が Apply 済み

CI/CD パイプラインでは以下のフェーズ分割を推奨する:

Phase A: IAM Managed Policy を各アカウントにデプロイ (全アカウント対象)
Phase B: Permission Set + CMP Attachment + Boundary を Identity Center にデプロイ
Phase C: Account Assignment をデプロイ

terraform apply -target で明示的に順序制御するか、別の Terraform State に分割して管理する。


3-4 External IdP 統合 — SAML 2.0 / OIDC / JIT プロビジョニング

SCIM によるプロビジョニングと SAML/OIDC による認証は独立した設定だ。SCIM はユーザー・グループの「同期」、SAML/OIDC は「認証トークン発行」を担う。

Okta の SAML 設定手順

  1. IAM Identity Center コンソール → 設定 → ID ソース → 外部 ID プロバイダー → SAML メタデータをダウンロード
  2. Okta Admin Console → Applications (上で追加済み) → Sign On タブ → SAML 2.0 設定
  3. Okta の IdP メタデータ XML をダウンロードし、IAM Identity Center の「IdP SAML メタデータ」にアップロード
  4. Okta 側の ACS URL と Entity ID が IAM Identity Center の値と一致することを確認
設定項目IAM Identity Center 値
ACS URLhttps://us-east-1.signin.aws.amazon.com/platform/saml/acs/xxxxxxxx
Entity IDhttps://us-east-1.signin.aws.amazon.com/platform/saml/d-xxxxxxxxxx
NameID formatemailAddress

Azure AD の SAML 設定手順

  1. Azure Portal → エンタープライズアプリ → シングルサインオン → SAML を選択
  2. 基本的な SAML 構成: 識別子 (Entity ID) と 応答 URL (ACS URL) に IAM Identity Center の値を入力
  3. フェデレーションメタデータ XML をダウンロード → IAM Identity Center にアップロード
  4. ユーザー属性とクレーム: user.mailNameIdentifier としてマッピング

Just-In-Time (JIT) プロビジョニング

JIT プロビジョニングを有効にすると、SCIM 同期なしでも SAML 認証時に自動でユーザーを作成できる。ただし本番での使用は非推奨だ。

方式メリットデメリット本番推奨
SCIM + SAMLグループ同期で Account Assignment 自動適用設定複雑
JIT + SAML設定シンプルグループ情報が伝搬されず Permission Set 自動付与不可

JIT はグループメンバーシップを SAML Assertion から取得できないため、Permission Set の自動割り当てが機能しない。10アカウント以上の環境では SCIM を必ず構成する。


IAM Identity Center Multi-Account SSO 全体図
図02: IAM Identity Center Permission Sets × SCIM × External IdP の Multi-Account SSO 全体構成
sequenceDiagram
 participant User as ユーザー
 participant IdP as External IdP (Okta/Azure AD)
 participant IC as IAM Identity Center
 participant STS as AWS STS
 participant Account as AWS Account (Target)
 User->>IdP: ① SSO ポータルへアクセス (SAML Request)
 IdP-->>User: ② 認証 (MFA 含む)
 User->>IC: ③ SAML Response (Assertion) を送信
 IC->>IC: ④ Assertion 検証 + Permission Set 解決
 IC->>STS: ⑤ AssumeRoleWithSAML
 STS-->>IC: ⑥ 一時クレデンシャル (最大8h)
 IC-->>User: ⑦ AWS マネコン or CLI クレデンシャルを返却
 User->>Account: ⑧ API コール (一時クレデンシャルで認可)

§4 KMS Multi-Region 本番運用 — Replica Key × Grants × 暗号境界設計 ★山場2

AWS KMS Multi-Region Key は、複数リージョンに同一 Key ID を持つ対称暗号鍵を展開する機能です。プレフィックスが mrk- で始まる Key ID が全リージョンで共有されるため、Primary Region で暗号化したデータを再暗号化なしに Replica Region で復号できます。マルチリージョン Active-Active 構成や DR 設計で必須の理解となります。

KMS Multi-Region 暗号境界設計
図03: KMS Multi-Region Primary / Replica / Grants / Auto Rotation の暗号境界設計
🔥 KMS Multi-Region 暗号境界 3鉄則

  1. Replica は同一 Key ID を維持する — Primary で暗号化したデータは、同一 mrk- Key ID を持つ Replica でそのまま復号できる。Key Material は同一だが、Replica は Primary とは独立したリソースとして管理される
  2. Key Policy は Primary / Replica で独立管理する — Primary の Key Policy を変更しても Replica に自動反映されない。Cross-Account Decrypt を許可する場合は Replica 側の Key Policy にも明示的に記載が必要
  3. Cross-Account 短命アクセスは Grants で付与する — Key Policy への外部 Account の Principal 恒久記載は管理が複雑化する。短命・使い捨てアクセスは aws_kms_grant + RetiringPrincipal で制御し、不要になったら Retire する

§4.1 Multi-Region Key の仕組み — Primary / Replica Key ID 同一性

Multi-Region Key は mrk- プレフィックスで識別されます。通常の KMS Key がリージョンごとに異なる Key ID を持つのに対し、Multi-Region Key は Key ID 部分が全リージョンで同一 のため、アプリケーションコード変更なしにリージョン間の暗号化・復号を実現できます。

Primary Key 作成 → Replica への複製手順

# ap-northeast-1 に Primary Key 作成
aws kms create-key \
  --region ap-northeast-1 \
  --multi-region true \
  --description "Multi-Region Primary Key" \
  --origin AWS_KMS

# us-east-1 に Replica 複製
aws kms replicate-key \
  --region ap-northeast-1 \
  --key-id mrk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  --replica-region us-east-1 \
  --description "Multi-Region Replica Key (us-east-1)"

Replica Key は Primary 作成後いつでも追加できます。Replica 作成時点で Key Material が複製されますが、以後のライフサイクルは独立です。Replica を無効化・削除しても Primary には影響しません。

Replica Key の独立ライフサイクル

操作Primary への影響他 Replica への影響
Replica を無効化なしなし
Replica を削除スケジュールなしなし
UpdatePrimaryRegion で別 Region に Primary 移動元 Primary は Replica に降格Key ID はそのまま維持
Primary の Key Policy 変更Primary のみ更新Replica は独立管理

§4.2 Auto Rotation と Replica 連動

enable_key_rotation = true を設定すると、KMS は年次(デフォルト 365 日)またはカスタム頻度でキーマテリアルを自動ローテーションします。Multi-Region Key の場合、Primary で発生した Auto Rotation は全 Replica に自動伝播します。

ローテーション伝播の挙動

Primary: mrk-XXXX  → Auto Rotation (v1 → v2)
  ├─ Replica us-east-1: mrk-XXXX (v2 自動伝播)
  └─ Replica eu-west-1: mrk-XXXX (v2 自動伝播)

旧キーマテリアル (v1) は KMS 内部に保持されるため、v1 で暗号化したデータは引き続き復号できます。アプリケーション側での再暗号化は不要です。

# カスタムローテーション頻度 (90日) を設定
aws kms enable-key-rotation \
  --region ap-northeast-1 \
  --key-id mrk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  --rotation-period-in-days 90

重要: Auto Rotation はキーマテリアルのみを更新します。Key ID・Key ARN・Alias は変わらないため、アプリケーション設定の変更は不要です。

§4.3 Key Policy vs IAM Policy vs Grants — 3層モデル

KMS の権限制御は3層で構成されます。この3層を正しく理解せずに設計すると、意図しない権限付与・意図しない復号拒否が同時に発生します。

制御対象特徴
Key PolicyKMS Key 単位の権限定義必須ステートメント必要。Key Policy なしでは IAM Policy 完全無効
IAM PolicyIAM Principal の権限付与Key Policy で許可された範囲内で細粒度制御
Grants特定 Principal への一時委譲短命・使い捨て・RetiringPrincipal で自動廃棄可能

Key Policy: 必須ステートメント (root 許可) の落とし穴

{
  "Sid": "Enable IAM User Permissions",
  "Effect": "Allow",
  "Principal": { "AWS": "arn:aws:iam::123456789012:root" },
  "Action": "kms:*",
  "Resource": "*"
}

このステートメントがないと IAM Policy が完全に無視されます。root とはアカウント全体を指し、「このアカウントの IAM Policy による制御を許可する」委譲が行われます。削除するとアカウント管理者でもアクセス不能になり、AWS サポートでも復旧不可です。

IAM Policy: kms:Decrypt / kms:GenerateDataKey の付与タイミング

Key Policy で root 許可を設定した後、個々の IAM Role には IAM Policy でアクション単位に付与します。

{
  "Version": "2012-10-17",
  "Statement": [{
 "Effect": "Allow",
 "Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:GenerateDataKeyWithoutPlaintext",
"kms:DescribeKey"
 ],
 "Resource": "arn:aws:kms:ap-northeast-1:123456789012:key/mrk-xxxxxxxx"
  }]
}

Grants: 短命権限 / Service-Linked 連携 / RetiringPrincipal 設計

Grants は Key Policy・IAM Policy を変更せずに特定の Principal に一時的な KMS 操作権限を委譲します。S3 SSE-KMS・EBS 暗号化などは Service-Linked Grant をバックグラウンドで自動作成・廃棄します。

# 短命 Grant の作成
aws kms create-grant \
  --key-id mrk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  --grantee-principal arn:aws:iam::123456789012:role/LambdaExecRole \
  --retiring-principal arn:aws:iam::123456789012:role/KeyAdminRole \
  --operations Decrypt DescribeKey \
  --name "lambda-decrypt-temp-grant"

# Grant の廃棄 (RetiringPrincipal が実行)
aws kms retire-grant \
  --key-id mrk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
  --grant-id <grant-id>

RetiringPrincipal を設定すると、Grantee 以外の管理者 Role が Grant を廃棄できます。Lambda 実行完了後にすぐ Retire する運用設計が重要です。

§4.4 暗号境界設計 — Region / Account / Service 3軸

Region 軸: Multi-Region Key による境界解除

同一 mrk- Key ID を使用することで、ap-northeast-1 で暗号化した S3 オブジェクトを us-east-1 の Lambda からそのまま復号できます。DR シナリオで「復号に必要なキーが別リージョンにない」という問題を根本解決します。

Account 軸: Cross-Account KMS Decrypt のための Key Policy 設計

Cross-Account アクセスには Key Policy への明示的な許可が必要かつ十分です(外部アカウントの IAM Policy との AND 条件)。

{
  "Sid": "Allow Cross-Account Decrypt",
  "Effect": "Allow",
  "Principal": {
 "AWS": "arn:aws:iam::987654321098:role/ExternalAppRole"
  },
  "Action": ["kms:Decrypt", "kms:DescribeKey"],
  "Resource": "*"
}

外部アカウントの Principal を長期間 Key Policy に記載したくない場合は Grants を活用します。

Service 軸: S3 / RDS / EBS / Secrets Manager との統合暗号化パターン

サービスKMS 連携方式Key Policy で必要なアクション
S3 SSE-KMSバケットポリシー + Key Policykms:GenerateDataKey kms:Decrypt
RDS作成時に KMS Key 指定kms:CreateGrant kms:DescribeKey
EBSボリューム作成時に KMS Key 指定kms:CreateGrant kms:GenerateDataKey
Secrets Managerシークレット作成時に KMS Key 指定kms:GenerateDataKey kms:Decrypt

RDS・EBS は KMS Grant を自動作成します。Key Policy に kms:CreateGrant を許可しないと暗号化ボリュームの作成が失敗するため注意してください。

§4.5 Terraform 完全例 — aws_kms_key / aws_kms_replica_key / aws_kms_grant

# providers.tf — Multi-Region 構成
terraform {
  required_providers {
 aws = {
source  = "hashicorp/aws"
version = "~> 5.0"
 }
  }
}

provider "aws" {
  alias  = "primary"
  region = "ap-northeast-1"
}

provider "aws" {
  alias  = "replica"
  region = "us-east-1"
}
# kms_primary.tf — Primary Key (Multi-Region)
resource "aws_kms_key" "primary" {
  provider = aws.primary
  description = "Multi-Region Primary Key"
  multi_region= true
  enable_key_rotation  = true
  rotation_period_in_days = 90
  deletion_window_in_days = 30
  is_enabled  = true

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [
{
  Sid = "Enable IAM User Permissions"
  Effect = "Allow"
  Principal = {
 AWS = "arn:aws:iam::${var.account_id}:root"
  }
  Action= "kms:*"
  Resource = "*"
},
{
  Sid = "Allow Key Administrators"
  Effect = "Allow"
  Principal = { AWS = var.admin_role_arn }
  Action = [
 "kms:Create*", "kms:Describe*", "kms:Enable*",
 "kms:List*", "kms:Put*", "kms:Update*",
 "kms:Revoke*", "kms:Disable*", "kms:Get*",
 "kms:Delete*", "kms:TagResource", "kms:UntagResource",
 "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion",
 "kms:ReplicateKey", "kms:UpdatePrimaryRegion"
  ]
  Resource = "*"
},
{
  Sid = "Allow Application Use"
  Effect = "Allow"
  Principal = { AWS = var.app_role_arn }
  Action = [
 "kms:Decrypt",
 "kms:GenerateDataKey",
 "kms:GenerateDataKeyWithoutPlaintext",
 "kms:DescribeKey",
 "kms:CreateGrant"
  ]
  Resource = "*"
}
 ]
  })

  tags = {
 Environment = var.environment
 ManagedBy= "terraform"
  }
}

resource "aws_kms_alias" "primary" {
  provider= aws.primary
  name = "alias/${var.key_alias}-primary"
  target_key_id = aws_kms_key.primary.key_id
}
# kms_replica.tf — Replica Key (us-east-1)
resource "aws_kms_replica_key" "replica" {
  provider = aws.replica
  description = "Multi-Region Replica Key (us-east-1)"
  primary_key_arn= aws_kms_key.primary.arn
  deletion_window_in_days = 30
  enabled  = true

  policy = jsonencode({
 Version = "2012-10-17"
 Statement = [
{
  Sid = "Enable IAM User Permissions"
  Effect = "Allow"
  Principal = {
 AWS = "arn:aws:iam::${var.account_id}:root"
  }
  Action= "kms:*"
  Resource = "*"
},
{
  Sid = "Allow Replica Application Use"
  Effect = "Allow"
  Principal = { AWS = var.app_role_arn_replica }
  Action= ["kms:Decrypt", "kms:DescribeKey"]
  Resource = "*"
}
 ]
  })

  tags = {
 Environment = var.environment
 ManagedBy= "terraform"
 ReplicaOf= "ap-northeast-1"
  }
}

resource "aws_kms_alias" "replica" {
  provider= aws.replica
  name = "alias/${var.key_alias}-replica"
  target_key_id = aws_kms_replica_key.replica.key_id
}
# kms_grant.tf — Lambda 短命 Decrypt Grant
resource "aws_kms_grant" "lambda_decrypt" {
  provider  = aws.primary
  name= "${var.environment}-lambda-decrypt-grant"
  key_id = aws_kms_key.primary.key_id
  grantee_principal  = aws_iam_role.lambda_exec.arn
  retiring_principal = aws_iam_role.key_admin.arn

  operations = ["Decrypt", "DescribeKey"]

  constraints {
 encryption_context_equals = {
service  = "lambda"
environment = var.environment
 }
  }
}
sequenceDiagram
 participant App as Application (ap-northeast-1)
 participant Primary as KMS Primary (ap-northeast-1)
 participant Replica as KMS Replica (us-east-1)
 participant DR as DR App (us-east-1)

 App->>Primary: GenerateDataKey(mrk-XXXX)
 Primary-->>App: PlaintextKey + EncryptedKey
 App->>App: Encrypt data with PlaintextKey
 App->>S3: Store EncryptedData + EncryptedKey

 Note over Primary,Replica: Auto Rotation (90日周期)<br/>Key Material を全 Replica に自動伝播

 DR->>S3: Fetch EncryptedData + EncryptedKey
 DR->>Replica: Decrypt(mrk-XXXX, EncryptedKey)
 Replica-->>DR: PlaintextKey (同一 Key ID で復号)
 DR->>DR: Decrypt data with PlaintextKey
✅ Key Policy / Grants 使い分けチェックリスト

権限の種類推奨手段理由
永続的なサービスアカウント権限Key Policy + IAM Policy監査証跡が明確・変更に承認フロー適用可
短命・使い捨て権限 (Lambda / バッチ)Grants + RetiringPrincipal使用後に Retire → Key Policy を汚染しない
AWS サービス連携 (S3/RDS/EBS)Service-Linked Grant (自動)サービスが自動作成・廃棄・手動管理不要
Cross-Account 一時委譲Grants (grantee=外部 Role)Key Policy に外部 ARN を恒久記載せずに済む
Cross-Account 恒久委譲Key Policy の Principal 追加継続的な委譲は Key Policy で明示管理
条件付き復号 (暗号化コンテキスト)Grants の constraintsIAM Policy 条件キーより表現力が高い

設計チェック項目:
☑ Key Policy に root 許可ステートメントが存在するか
☑ Replica Key の Key Policy を Primary とは独立して管理しているか
☑ Cross-Account Principal は Grants で短命付与しているか
☑ RetiringPrincipal を設定して Grant 廃棄の責任者を明示しているか
☑ Auto Rotation 有効化 (enable_key_rotation = true) を確認したか
☑ deletion_window_in_days を 7 日未満にしていないか (最短 7 日)


§5 Secrets Manager × Certificate Manager 本番運用 — Auto Rotation × Resource Policy × Lambda連携

Secrets Manager Auto Rotation 全体構成
図04: Secrets Manager + Certificate Manager の Auto Rotation × Lambda × KMS 統合構成

Secrets Manager Auto Rotation — Built-in と Lambda Custom

Auto Rotation にはBuilt-inLambda Customの2方式がある。

Built-in Rotation は RDS・Aurora・DocumentDB・Redshift など AWS マネージドサービス向けのネイティブ統合。Secrets Manager が提供する AWS マネージド Lambda 関数を内部利用するため、Lambda 実装不要で有効化できる。

resource "aws_secretsmanager_secret" "db_credentials" {
  name  = "prod/rds/app-credentials"
  kms_key_id  = aws_kms_key.secrets_key.key_id
  recovery_window_in_days = 7

  tags = {
 Env = "production"
  }
}

resource "aws_secretsmanager_secret_rotation" "db_rotation" {
  secret_id  = aws_secretsmanager_secret.db_credentials.id
  rotation_lambda_arn = "arn:aws:lambda:ap-northeast-1:${data.aws_caller_identity.current.account_id}:function:SecretsManagerRDSMySQLRotationSingleUser"

  rotation_rules {
 automatically_after_days = 30
  }
}

Lambda Custom Rotation はサードパーティ DB や独自認証システム向け。Rotation Lambda は4ステップを冪等に実装することが必須。

import boto3
import json

def lambda_handler(event, context):
 client = boto3.client('secretsmanager')
 step= event['Step']
 sid = event['SecretId']
 token  = event['ClientRequestToken']
 dispatch = {
  'createSecret': _create,
  'setSecret': _set,
  'testSecret':_test,
  'finishSecret': _finish,
 }
 dispatch[step](client, sid, token)

def _create(client, sid, token):
 try:
  client.get_secret_value(
SecretId=sid, VersionId=token, VersionStage='AWSPENDING'
  )
  return  # AWSPENDING 既存 → スキップ(冪等)
 except client.exceptions.ResourceNotFoundException:
  pass
 current = json.loads(
  client.get_secret_value(SecretId=sid, VersionStage='AWSCURRENT')['SecretString']
 )
 current['password'] = _generate_password()
 client.put_secret_value(
  SecretId=sid,
  ClientRequestToken=token,
  SecretString=json.dumps(current),
  VersionStages=['AWSPENDING'],
 )

def _set(client, sid, token):
 pending = json.loads(
  client.get_secret_value(
SecretId=sid, VersionId=token, VersionStage='AWSPENDING'
  )['SecretString']
 )
 _apply_to_backend(pending)  # DB にパスワードを適用

def _test(client, sid, token):
 pending = json.loads(
  client.get_secret_value(
SecretId=sid, VersionId=token, VersionStage='AWSPENDING'
  )['SecretString']
 )
 _assert_connection(pending)  # AWSPENDING で接続テスト

def _finish(client, sid, token):
 meta = client.describe_secret(SecretId=sid)
 current_id = next(
  v for v, stages in meta['VersionIdsToStages'].items()
  if 'AWSCURRENT' in stages
 )
 client.update_secret_version_stage(
  SecretId=sid,
  VersionStage='AWSCURRENT',
  MoveToVersionId=token,
  RemoveFromVersionId=current_id,
 )

Rotation Lambda の VPC 配置と ENI 設計

プライベートサブネット内の RDS にアクセスする Rotation Lambda は、同一 VPC のプライベートサブネットに配置する。Secrets Manager API への疎通にはVPC Endpoint (com.amazonaws.{region}.secretsmanager) が必要。VPC Endpoint なしではプライベートサブネットの Lambda がエンドポイントを呼び出せず、ローテーションが失敗する。

resource "aws_lambda_function" "rotation" {
  function_name = "secrets-rotation"
  role = aws_iam_role.rotation_role.arn
  runtime = "python3.12"
  handler = "rotation.lambda_handler"
  filename= data.archive_file.rotation.output_path

  vpc_config {
 subnet_ids= aws_subnet.private[*].id
 security_group_ids = [aws_security_group.rotation_lambda.id]
  }
}

resource "aws_security_group_rule" "rotation_to_db" {
  type= "egress"
  security_group_id  = aws_security_group.rotation_lambda.id
  source_security_group_id = aws_security_group.rds.id
  protocol  = "tcp"
  from_port = 5432
  to_port= 5432
}

resource "aws_security_group_rule" "rotation_to_sm_endpoint" {
  type= "egress"
  security_group_id  = aws_security_group.rotation_lambda.id
  source_security_group_id = aws_security_group.sm_vpc_endpoint.id
  protocol  = "tcp"
  from_port = 443
  to_port= 443
}

Resource Policy × KMS Encryption × Cross-Account 共有

Secrets Manager のResource Policyで他アカウントへのシークレット共有が可能。aws:PrincipalOrgID 条件で Organization 内に限定することが推奨。

{
  "Version": "2012-10-17",
  "Statement": [{
 "Sid": "CrossAccountRead",
 "Effect": "Allow",
 "Principal": { "AWS": "arn:aws:iam::CONSUMER_ACCOUNT_ID:root" },
 "Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
 ],
 "Resource": "*",
 "Condition": {
"StringEquals": { "aws:PrincipalOrgID": "o-xxxxxxxxxxxx" }
 }
  }]
}

クロスアカウント共有ではシークレットを暗号化したKMS Customer Managed Key の Key Policyにもコンシューマアカウントの kms:Decrypt / kms:GenerateDataKey 権限が必要。aws/secretsmanager マネージドキーはクロスアカウント共有不可のため、必ず CMK を指定する。

Secrets Manager × Parameter Store 使い分け基準

観点Secrets ManagerParameter Store
自動ローテーションネイティブサポートなし
料金$0.40/シークレット/月 + API 課金Standard 無料
バージョン管理AWSCURRENT / AWSPENDING / AWSPREVIOUSラベル付き手動管理
最大値サイズ65,536 バイトStandard: 4KB / Advanced: 8KB
推奨ユースケースDB パスワード / API キー / OAuth Token環境変数 / 機能フラグ / 設定値

判断基準: 「自動ローテーションが必要か」で分岐する。必要なら Secrets Manager、不要な設定値なら Parameter Store が原則。

Certificate Manager — Public / Private CA × Auto Renewal

DNS 検証 vs Email 検証の判断基準

検証方式推奨ケース自動更新
DNS 検証Route 53 管理ドメイン / 本番環境CNAME 維持で永続自動更新
Email 検証DNS 権限のない第三者ドメイン手動承認が毎回必要

本番では DNS 検証 + Route 53 の組み合わせで Terraform による完全自動化が可能。

resource "aws_acm_certificate" "main" {
  domain_name= "example.com"
  validation_method= "DNS"
  subject_alternative_names = ["*.example.com"]

  lifecycle {
 create_before_destroy = true
  }
}

resource "aws_route53_record" "cert_validation" {
  for_each = {
 for dvo in aws_acm_certificate.main.domain_validation_options :
 dvo.domain_name => dvo
  }
  zone_id = aws_route53_zone.main.zone_id
  name = each.value.resource_record_name
  type = each.value.resource_record_type
  ttl  = 60
  records = [each.value.resource_record_value]
}

resource "aws_acm_certificate_validation" "main" {
  certificate_arn= aws_acm_certificate.main.arn
  validation_record_fqdns = [for r in aws_route53_record.cert_validation : r.fqdn]
}

ACM Private CA の Subordinate CA 構成

社内向けサービスや mTLS が必要なマイクロサービス間通信は ACM Private CA を使用する。本番では Root CA を組織 PKI 管理下に置き、Subordinate CA を AWS 側で発行する2層構成が推奨。

resource "aws_acmpca_certificate_authority" "subordinate" {
  type = "SUBORDINATE"
  certificate_authority_configuration {
 key_algorithm  = "RSA_2048"
 signing_algorithm = "SHA256WITHRSA"
 subject {
common_name  = "Internal Subordinate CA"
organization = "Example Corp"
country= "JP"
 }
  }
  revocation_configuration {
 crl_configuration {
enabled= true
s3_bucket_name  = aws_s3_bucket.crl.id
expiration_in_days = 7
 }
  }
}

ALB / CloudFront への証明書アタッチ

ALB には ssl_policyELBSecurityPolicy-TLS13-1-2-2021-06 (TLS 1.3 推奨) を指定する。CloudFront は証明書をus-east-1 リージョンのみで発行する制約がある。Terraform で CloudFront 用証明書を発行する際は provider = aws.us_east_1 の alias プロバイダを使う。

resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.main.arn
  port  = "443"
  protocol = "HTTPS"
  ssl_policy  = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn= aws_acm_certificate_validation.main.certificate_arn

  default_action {
 type = "forward"
 target_group_arn = aws_lb_target_group.app.arn
  }
}

Lambda 連携 Cache 戦略

Lambda から Secrets Manager を毎リクエスト呼び出すとレイテンシと API コストが増大する。aws-secretsmanager-caching ライブラリ(公式 SDK 拡張)で TTL キャッシュを実装する。

from aws_secretsmanager_caching import SecretCache, SecretCacheConfig
import boto3

cache = SecretCache(
 config=SecretCacheConfig(secret_refresh_interval=3600),
 client=boto3.client('secretsmanager'),
)

def lambda_handler(event, context):
 secret = cache.get_secret_string('prod/rds/app-credentials')
 # TTL 内なら API 呼び出しなし

ローテーション直後はキャッシュが古いまま残る場合がある。finishSecret 後も旧 AWSPREVIOUS の認証情報を一定期間(1時間以上)保持することで、キャッシュ TTL 切れ前の Lambda インスタンスも旧認証情報で接続継続できる。


Secrets Manager 公式ドキュメント

✅ Auto Rotation Lambda 設計パターン

  1. createSecret: AWSPENDING に新値を書き込む。既存 AWSPENDING を検出したらスキップ(冪等)
  2. setSecret: バックエンド(DB など)に新パスワードを適用する
  3. testSecret: AWSPENDING の認証情報でバックエンドへの接続テストを実行。失敗時は例外を raise して finishSecret をブロック
  4. finishSecret: AWSPENDING → AWSCURRENT に昇格。旧 AWSCURRENT → AWSPREVIOUS に降格

鉄則: 各ステップの先頭で「同一 ClientRequestToken の処理済みチェック」を入れること。finishSecret が未実行のまま Lambda が終了すると AWSCURRENT と AWSPENDING が乖離し、接続障害になる。

⚠️ Certificate Manager 期限管理の落とし穴

  • DNS 検証 CNAME 削除: Route 53 の CNAME レコードを削除すると自動更新が停止し、証明書が期限切れになる。Terraform state 外での手動削除が最多原因。CNAME レコードは恒久的に維持すること
  • Email 検証の承認タイムアウト: Email 検証は更新のたびに承認メールが届く。72時間以内に承認しないと証明書が失効する。本番では DNS 検証へ移行を推奨
  • Private CA 証明書の有効期限見落とし: ACM Private CA 自体の有効期限と発行した証明書の有効期限は別管理。CA 証明書の更新を忘れると配下の全証明書が失効する
  • CloudFront us-east-1 制約: CloudFront 用証明書は us-east-1 のみ有効。リージョン移行時は証明書の再発行が必要

§6 Verified Access 本番運用 — ZTNA × Trust Provider × Policy × VPC Endpoint

ZTNA モデルの設計思想 — 従来 VPN との比較

Verified Access は Zero Trust Network Access (ZTNA) モデルを実装した AWS サービス。従来の VPN が「ネットワーク境界に入れば信頼」するのに対し、ZTNA は「アクセスのたびに検証」する。

観点従来 VPNVerified Access (ZTNA)
信頼モデルネットワーク境界信頼Never trust, always verify
アクセス粒度ネットワークレベル(全リソース)アプリケーション単位
ユーザー認証VPN ログイン後は内部信頼アクセスごとにポリシー評価
デバイス検証ほぼなしDevice Trust 統合可能
ラテラルムーブメント可能ポリシーで明示的に制御
スケールクライアントソフトウェア必須クライアントレス(ブラウザのみ)

Verified Access の4コンポーネント

  1. Trust Provider: ユーザー / デバイスの信頼評価ソース(IAM Identity Center / OIDC / Device Trust)
  2. Group: Trust Provider を紐付けてアクセスポリシーを定義するグループ単位
  3. Endpoint: 保護対象のアプリケーションエンドポイント(ALB / EC2 / Lambda URL)
  4. Policy: Cedar 言語で記述するアクセス制御ルール

Trust Provider — IAM Identity Center / OIDC / Device Trust

Trust Provider は3種類に分類される。

IAM Identity Center 統合 (SSO 連携)

IAM Identity Center を Trust Provider として使用すると、context.idc 属性(email / groups / department)が Cedar Policy 内で利用可能になる。既存の AWS SSO 基盤をそのまま活用できるため、AWS 中心の環境では最もシンプルな選択。

resource "aws_verifiedaccess_trust_provider" "idc" {
  trust_provider_type= "user"
  user_trust_provider_type = "iam-identity-center"
  policy_reference_name = "idc"
  description  = "IAM Identity Center SSO provider"

  tags = {
 Env = "production"
  }
}

OIDC Provider 統合 (Okta / Azure AD)

既存の外部 IdP (Okta / Azure AD) を直接統合する場合は OIDC Trust Provider を使用。context.oidc 属性でカスタムクレームを参照できる。

resource "aws_verifiedaccess_trust_provider" "okta" {
  trust_provider_type= "user"
  user_trust_provider_type = "oidc"
  policy_reference_name = "okta"

  oidc_options {
 issuer  = "https://example.okta.com"
 authorization_endpoint = "https://example.okta.com/oauth2/v1/authorize"
 token_endpoint= "https://example.okta.com/oauth2/v1/token"
 user_info_endpoint  = "https://example.okta.com/oauth2/v1/userinfo"
 client_id  = var.okta_client_id
 client_secret = var.okta_client_secret
 scope= "openid email profile groups"
  }
}

Device Trust 連携 (Jamf / CrowdStrike)

MDM (Jamf) やエンドポイント保護ツール (CrowdStrike) と連携してデバイスのポスチャーを検証する。context.device 属性で OS バージョン / ディスク暗号化 / MDM 登録状態を確認可能。社内端末以外からのアクセスを Cedar Policy でブロックできる。

Cedar Policy Language

Cedar は Amazon が開発したポリシー言語。permit / forbid の2エフェクトで制御し、forbid は permit より常に優先される。

基本構文 — IAM Identity Center 属性ベース

// engineering グループのメンバーに内部ダッシュボードへのアクセスを許可
permit (
  principal,
  action == Action::"connect",
  resource == VerifiedAccessEndpoint::"internal-dashboard-endpoint-id"
)
when {
  context.idc.groups.contains("engineering") &&
  context.idc.email.endsWith("@example.com")
};

ABAC パターン — 部門属性 + デバイスポスチャー

// セキュリティ部門かつ MDM 登録済みデバイスのみ許可
permit (
  principal,
  action == Action::"connect",
  resource == VerifiedAccessEndpoint::"security-dashboard-endpoint-id"
)
when {
  context.idc.department == "Security" &&
  context.device.managed == true &&
  context.device.disk_encrypted == true
};

Forbid — 明示的拒否 (最高優先度)

// 管理対象外デバイスからの全アクセスを明示的拒否
forbid (
  principal,
  action,
  resource
)
unless {
  context.device.managed == true
};

階層化 Policy の評価順

  1. Forbid ルール (最高優先度): いずれか1つでも forbid にマッチすればアクセス拒否
  2. Permit ルール: forbid がない場合、いずれか1つでも permit にマッチすればアクセス許可
  3. デフォルト拒否: どのルールにもマッチしなければアクセス拒否

デバッグ時に一時的に投入した全許可ポリシーをそのまま本番に残す事故が多い。Cedar の評価構造を理解した上で、本番ポリシーには必ず明示的な条件を付ける。

VPC Endpoint 統合 — ALB との配線と Security Group 設計

Verified Access Endpoint はバックエンドアプリケーションへのアクセスを VPC 内で終端するため、内部 ALB との組み合わせが基本構成。

resource "aws_verifiedaccess_instance" "main" {
  description  = "Production Verified Access instance"
  fips_enabled = false

  tags = {
 Name = "prod-verified-access"
 Env  = "production"
  }
}

resource "aws_verifiedaccess_instance_trust_provider_attachment" "idc" {
  verifiedaccess_instance_id = aws_verifiedaccess_instance.main.id
  verifiedaccess_trust_provider_id = aws_verifiedaccess_trust_provider.idc.id
}

resource "aws_verifiedaccess_group" "internal_apps" {
  verifiedaccess_instance_id = aws_verifiedaccess_instance.main.id
  description = "Internal applications group"

  policy_document = <<-EOT
 permit(principal, action, resource)
 when {
context.idc.groups.contains("internal-users")
 };
  EOT
}

resource "aws_verifiedaccess_endpoint" "app" {
  application_domain  = "app.internal.example.com"
  attachment_type  = "vpc"
  domain_certificate_arn = aws_acm_certificate_validation.main.certificate_arn
  endpoint_domain_prefix = "app"
  endpoint_type = "load-balancer"
  verifiedaccess_group_id = aws_verifiedaccess_group.internal_apps.id
  security_group_ids  = [aws_security_group.verified_access.id]

  load_balancer_options {
 load_balancer_arn = aws_lb.internal.arn
 port  = 443
 protocol = "https"
 subnet_ids  = aws_subnet.private[*].id
  }
}

Logging 設定 (アクセスログを S3 / CloudWatch に保存):

resource "aws_verifiedaccess_instance_logging_configuration" "main" {
  verifiedaccess_instance_id = aws_verifiedaccess_instance.main.id

  access_logs {
 cloudwatch_logs {
enabled= true
log_group = aws_cloudwatch_log_group.verified_access.name
 }
 s3 {
enabled  = true
bucket_name = aws_s3_bucket.va_logs.id
prefix= "verified-access"
 }
  }
}

Security Group 設計

トラフィックソースデスティネーションポート
クライアント → Verified Accessインターネット (0.0.0.0/0)VA Endpoint SG443
Verified Access → 内部 ALBVA Endpoint SGALB SG443
内部 ALB → アプリALB SGApp SG8080

VA Endpoint の Security Group は内部 ALB に向けて HTTPS 送信のみ許可する。クライアント側の受信はマネージドのため Security Group での追加設定は不要。

📘 Verified Access × ZTNA 設計チェックリスト

  1. Trust Provider 選定: AWS 中心環境 → IAM Identity Center / 外部 IdP 既存 → OIDC / デバイス検証必須 → Device Trust (Jamf / CrowdStrike) を組み合わせる
  2. Policy 階層化: Forbid を先に定義(管理対象外デバイス・組織外メール)→ Permit で部門・グループ単位に許可。デバッグ用全許可ポリシーを本番に残さない
  3. VPC Endpoint 経路: Verified Access → 内部 ALB のルートのみ Security Group で開ける。バックエンドへの直接アクセスパスをネットワーク設計で排除する
  4. Logging: S3 + CloudWatch の両方を有効化。アクセスログには Cedar Policy の評価結果(permit / forbid)が含まれるため、ポリシーデバッグに必須
  5. Failure Mode: Trust Provider が応答しない場合はデフォルト拒否になる。可用性が必要なケースでは Trust Provider の冗長化(IAM Identity Center は AWS マネージドのため考慮不要)と Circuit Breaker パターンを検討する

§7 詰まりポイント7選 図解

本番導入で頻出する7パターンを症状・原因・対処の3層で解説する。

詰まり1: IAM Identity Center Permission Set の Customer Managed Policy 連携失敗

症状: Terraform の aws_ssoadmin_customer_managed_policy_attachment を実行後、Permission Set のプロビジョニングが失敗する。CloudTrail で ProvisionPermissionSetFAILURE ステータスが記録され、対象アカウントでロールを引き受けても期待した権限が付与されない。

原因: IAM Identity Center は Permission Set に指定した Customer Managed Policy (CMP) 名が対象アカウントに「既に存在する」ことを前提にプロビジョニングする。Terraform が Permission Set を先に apply してもアカウント側に同名ポリシーがなければ紐付けに失敗する。

対処手順:

# Step1: 先に CMP を全アカウントにデプロイ
resource "aws_iam_policy" "sso_readonly" {
  name= "SSOReadOnlyPolicy"
  policy = data.aws_iam_policy_document.readonly.json
}

# Step2: Permission Set に CMP を紐付け
resource "aws_ssoadmin_customer_managed_policy_attachment" "readonly" {
  instance_arn = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.readonly.arn
  customer_managed_policy_reference {
 name = aws_iam_policy.sso_readonly.name
 path = "/"
  }
  depends_on = [aws_iam_policy.sso_readonly]
}

# Step3: 全アカウントへのプロビジョニング
resource "aws_ssoadmin_permission_set_provisioning" "readonly" {
  instance_arn = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.readonly.arn
  target_type  = "ALL_PROVISIONED_ACCOUNTS"
  depends_on= [aws_ssoadmin_customer_managed_policy_attachment.readonly]
}
対処ポイント: Customer Managed Policy は Permission Set より先に対象アカウントへデプロイすること。depends_on で Terraform の実行順序を明示し、プロビジョニング前に CMP 存在を保証する。

詰まり2: KMS Multi-Region Replica 作成後の Decrypt 遅延

症状: aws_kms_replica_key の Terraform apply 完了直後に kms:Decrypt を呼び出すと KMSInvalidStateException: key state is not valid for this operation (Pending) が返る。数分後には正常に動作する。

原因: Multi-Region Replica は API レスポンス 200 を返した時点でキーマテリアルの伝播が完了していない。Terraform は API の成功を apply 完了と判定するため、実際の使用可能状態になる前に後続リソースが実行される。

対処手順:

resource "aws_kms_replica_key" "replica_us_west_2" {
  description = "Multi-Region Replica Key"
  primary_key_arn= aws_kms_key.primary.arn
  deletion_window_in_days = 7

  provisioner "local-exec" {
 command = "sleep 60"
  }
}

Lambda 側での実装:

import boto3, time
from botocore.exceptions import ClientError

kms = boto3.client("kms", region_name="us-west-2")

def decrypt_with_retry(ciphertext_blob, max_retries=3):
 for attempt in range(max_retries):
  try:
return kms.decrypt(CiphertextBlob=ciphertext_blob)
  except ClientError as e:
code = e.response["Error"]["Code"]
if code == "KMSInvalidStateException" and attempt < max_retries - 1:
 time.sleep(2 ** attempt)
 continue
raise
対処ポイント: Replica Key 作成後は 60 秒以上の待機を挟む。Lambda では KMSInvalidStateException のみを対象とした exponential backoff (最大3回) を実装し、伝播遅延を吸収する。

詰まり3: Secrets Manager Auto Rotation の冪等性破綻

症状: Rotation Lambda の2回目以降の実行で createSecret フェーズが ResourceExistsException (409 Conflict) で失敗し、finishSecret が実行されない。AWSCURRENT と AWSPENDING の両ステージが同時に存在する矛盾状態が継続する。

原因: 前回の Rotation が testSecret フェーズで失敗し、finishSecret が実行されず AWSPENDING ステージのシークレットが残留している。次回の Rotation トリガー時に createSecret が残留 AWSPENDING と衝突する。

対処実装:

def create_secret(service_client, arn, token):
 # 冪等性チェック: AWSPENDING が既存か確認
 try:
  service_client.get_secret_value(
SecretId=arn,
VersionStage="AWSPENDING"
  )
  return  # AWSPENDING が存在する場合はスキップ
 except service_client.exceptions.ResourceNotFoundException:
  pass

 passwd = generate_password()
 service_client.put_secret_value(
  SecretId=arn,
  ClientRequestToken=token,
  SecretString=passwd,
  VersionStages=["AWSPENDING"]
 )
対処ポイント: createSecret の先頭で get_secret_value(VersionStage="AWSPENDING") を実行し、既存なら作成をスキップする。冪等性パターンを Lambda の全4ステップ (create/set/test/finish) に適用することで途中失敗からの再実行を安全にする。

詰まり4: Verified Access Cedar Policy の Permit 範囲誤り

症状: IAM Identity Center で認証済みのユーザーが Verified Access 経由でアプリケーションにアクセスすると 403 が返る。Verified Access Access Logs で decision: DENY が記録される。

原因: Cedar Policy では全ての許可を明示的に記述する必要がある。when 句内でのコンテキスト変数の参照ミスが多い。IAM Identity Center の場合、ユーザー属性は context.idc.groupscontext.idc.attributes 経由で参照する。principal.groups のように誤った名前空間を使うと評価が false になり implicit deny が適用される。

デバッグと対処:

// NG: コンテキスト変数の参照ミス
permit(
  principal,
  action == AWS::VerifiedAccess::HTTP::Action::"GET",
  resource
) when {
  principal.groups.contains("engineering")
};

// OK: IAM Identity Center の場合は context.idc を参照
permit(
  principal,
  action,
  resource
) when {
  context.idc.groups.contains("grp-engineering-id")
};

Verified Access の Test Access 機能でポリシーを事前検証する:

aws verifiedaccess test-verified-access-custom-policy \
  --policy 'permit(principal, action, resource) when { context.idc.groups.contains("grp-xxxx") };' \
  --context '{"idc": {"groups": ["grp-xxxx"]}}'
対処ポイント: Cedar Policy のコンテキスト変数は Trust Provider の種類により異なる。IAM Identity Center の場合は context.idc.groups / context.idc.attributes、OIDC の場合は context.oidc.claims。Test Access 機能でデプロイ前に必ず検証する。

詰まり5: Access Analyzer External Findings のノイズ過多

症状: Access Analyzer External Access Findings が毎日数百件発生し、実際に調査すべき事象が埋もれる。大半は CloudFront OAC、Cross-Account S3 共有、意図的な外部公開リソースの誤検知だった。

原因: Access Analyzer は Organizations の Zone of Trust 外から参照可能なリソースを全件報告する。Archive Rule なしでは運用不能なノイズ量になる。

対処: Archive Rule の設計と適用:

resource "aws_accessanalyzer_archive_rule" "trusted_account" {
  analyzer_name = aws_accessanalyzer_analyzer.org.analyzer_name
  rule_name  = "TrustedAccountAccess"

  filter {
 criteria = "principal.AWS"
 starts_with = [
"arn:aws:iam::111122223333:",
"arn:aws:iam::444455556666:",
 ]
  }
}

resource "aws_accessanalyzer_archive_rule" "cloudfront_oac" {
  analyzer_name = aws_accessanalyzer_analyzer.org.analyzer_name
  rule_name  = "CloudFrontOACAccess"

  filter {
 criteria = "principal.Service"
 eq = ["cloudfront.amazonaws.com"]
  }
}
対処ポイント: Archive Rule を「Trusted Account」「サービスプリンシパル」「既知の外部共有」の3カテゴリで設計する。四半期ごとに Archive Rule を見直し、不要な除外パターンが溜まらないよう管理する。新規 Finding は24時間以内にアーカイブまたはエスカレーションする SLA を設定する。

詰まり6: IAM Identity Center External IdP の属性マッピング不一致

症状: Okta との SCIM 同期後、AWS コンソールのユーザー一覧で displayName が空文字になる。aws identitystore list-usersDisplayName フィールドが空になっている。

原因: Okta の SCIM Provisioning では属性マッピングを明示的に設定しないと displayName が連携されない。Okta の User schema では firstNamelastName が分離されており、IAM Identity Center が期待する displayName に自動マップされない。

対処手順 (Okta 設定):

  1. Okta Admin > Applications > AWS IAM Identity Center > Provisioning > To App
  2. Attribute MappingsdisplayName を追加:
  3. Okta attribute: String.join(" ", user.firstName, user.lastName)
  4. IAM Identity Center attribute: displayName
  5. Force Sync を実行して既存ユーザーに反映
aws identitystore list-users \
  --identity-store-id d-xxxxxxxxxx \
  --query 'Users[*].{Name:UserName,Display:DisplayName}' \
  --output table
対処ポイント: SCIM 同期直後に aws identitystore list-users で全フィールドを確認する。displayNameemailuserName の3フィールドが必須。Okta では String.join を使用した結合式でマッピングを定義する。

詰まり7: Multi-Region 暗号化境界の Cross-Account Grants 失効

症状: Cross-Account の S3 レプリケーションで定期的に AccessDenied: User is not authorized to use CMK エラーが発生する。CloudTrail で RetireGrant が記録された直後から Decrypt 失敗が始まる。

原因: KMS Grants は RetiringPrincipal を指定するとそのプリンシパルによる RetireGrant 操作で失効する。Multi-Region Replica では Primary Key と Replica Key の Grants が独立管理され、レプリケーションサービス側がグラントを retire した後に新規グラントが発行されないと Decrypt が失敗し続ける。

対処: Grant ローリング更新 Lambda の実装:

import boto3

kms = boto3.client("kms")

def refresh_replication_grant(key_id: str, grantee_principal: str):
 existing_grants = kms.list_grants(KeyId=key_id)["Grants"]
 target_grants = [
  g for g in existing_grants
  if g["GranteePrincipal"] == grantee_principal
 ]

 # Step1: 新規 Grant を先に作成
 new_grant = kms.create_grant(
  KeyId=key_id,
  GranteePrincipal=grantee_principal,
  Operations=["Decrypt", "DescribeKey", "GenerateDataKey"],
  RetiringPrincipal=grantee_principal,
 )

 # Step2: 旧 Grant を retire (新規が有効になってから削除)
 for grant in target_grants:
  kms.retire_grant(
KeyId=key_id,
GrantId=grant["GrantId"],
  )

 return new_grant["GrantToken"]

CloudWatch Events で 6時間ごとに Lambda を実行し、グラントを継続的にリフレッシュする。

対処ポイント: Grant のローリング更新は「新規作成 → 旧 Grant 削除」の順序で行う。逆順にすると Decrypt 失敗の瞬間が生じる。Primary Key と全 Replica Key 両方の Grant を定期リフレッシュする Lambda を CloudWatch Events でスケジューリングする。

§8 アンチパターン → 正解パターン変換演習 + シリーズ繋ぎ

本番設計で繰り返されるアンチパターンを Before/After 形式で体得する5問演習。

アンチパターン演習 (5問)

Q1: Permission Sets — Inline Policy を全員に付与

Before (アンチパターン):

resource "aws_ssoadmin_permission_set" "admin" {
  name= "AdminAccess"
  instance_arn = local.sso_instance_arn

  inline_policy = jsonencode({
 Version = "2012-10-17"
 Statement = [{
Effect= "Allow"
Action= "*"
Resource = "*"
 }]
  })
}

問題点: Inline Policy は Permission Set 固有で管理できず、変更時に全ユーザーへの影響を追跡できない。全許可が全員に付与される。

After (正解パターン):

resource "aws_ssoadmin_managed_policy_attachment" "readonly" {
  instance_arn = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.readonly.arn
  managed_policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}

resource "aws_ssoadmin_customer_managed_policy_attachment" "custom" {
  instance_arn = local.sso_instance_arn
  permission_set_arn = aws_ssoadmin_permission_set.readonly.arn
  customer_managed_policy_reference {
 name = aws_iam_policy.sso_custom.name
 path = "/"
  }
}

Q2: KMS Multi-Region Replica — Key ARN をハードコード

Before (アンチパターン):

resource "aws_s3_bucket_server_side_encryption_configuration" "replica" {
  bucket = aws_s3_bucket.replica.id

  rule {
 apply_server_side_encryption_by_default {
kms_master_key_id = "arn:aws:kms:us-west-2:123456789012:key/mrk-abc123"
sse_algorithm  = "aws:kms"
 }
  }
}

After (正解パターン):

resource "aws_kms_alias" "replica" {
  provider= aws.us_west_2
  name = "alias/my-app-replica-key"
  target_key_id = aws_kms_replica_key.replica_us_west_2.key_id
}

resource "aws_s3_bucket_server_side_encryption_configuration" "replica" {
  bucket = aws_s3_bucket.replica.id

  rule {
 apply_server_side_encryption_by_default {
kms_master_key_id = aws_kms_alias.replica.arn
sse_algorithm  = "aws:kms"
 }
  }
}

Q3: Secrets Manager Rotation Lambda — VPC 外デプロイ

Before (アンチパターン):

resource "aws_lambda_function" "rotation" {
  function_name = "SecretsManagerRotation"
  handler = "rotation.handler"
  runtime = "python3.12"
  role = aws_iam_role.rotation.arn
  filename= "rotation.zip"
  # VPC 設定なし
}

After (正解パターン):

resource "aws_lambda_function" "rotation" {
  function_name = "SecretsManagerRotation"
  handler = "rotation.handler"
  runtime = "python3.12"
  role = aws_iam_role.rotation.arn
  filename= "rotation.zip"

  vpc_config {
 subnet_ids= var.private_subnet_ids
 security_group_ids = [aws_security_group.rotation_lambda.id]
  }
}

resource "aws_vpc_endpoint" "secretsmanager" {
  vpc_id  = var.vpc_id
  service_name  = "com.amazonaws.${var.region}.secretsmanager"
  vpc_endpoint_type= "Interface"
  subnet_ids = var.private_subnet_ids
  security_group_ids  = [aws_security_group.endpoint.id]
  private_dns_enabled = true
}

Q4: Verified Access Cedar Policy — 過剰許可の全許可ポリシー

Before (アンチパターン):

permit(principal, action, resource);

After (正解パターン):

permit(
  principal,
  action in [
 AWS::VerifiedAccess::HTTP::Action::"GET",
 AWS::VerifiedAccess::HTTP::Action::"POST"
  ],
  resource
) when {
  context.idc.groups.contains("grp-app-readers") &&
  context.http.request.path.contains("/api/")
};

permit(
  principal,
  action,
  resource
) when {
  context.idc.groups.contains("grp-app-admins") &&
  context.http.request.path.contains("/admin/")
};

Q5: Access Analyzer Custom Policy Check — CI/CD 未連携

Before (アンチパターン): 手動でコンソールから IAM ポリシーを確認し、問題があれば口頭でフィードバックする。Terraform apply 後に問題が発覚してロールバックが必要になる。

After (正解パターン):

name: Security Policy Check

on:
  pull_request:
 paths:
- "**/*.tf"

jobs:
  policy-check:
 runs-on: ubuntu-latest
 steps:
- uses: actions/checkout@v4

- name: Terraform Plan
  run: |
 terraform init
 terraform plan -out=tfplan.binary
 terraform show -json tfplan.binary > tfplan.json

- name: Access Analyzer Custom Policy Check
  run: |
 aws accessanalyzer check-no-new-access \
--existing-policy-document file://baseline_policy.json \
--new-policy-document file://new_policy.json \
--policy-type IDENTITY_POLICY
演習のまとめ: 5問に共通する設計原則は「最小権限」「VPC 閉域化」「明示的 Allow」「コード管理」「CI/CD 自動検証」の5軸。アンチパターンは「楽な初期設定がそのまま本番化する」ことで発生する。CI/CD ゲートで自動検知することで人的ミスを防ぐ。

全軸クロスリンク

AWS本番運用シリーズ 41記事化達成
本記事公開で AWS本番運用シリーズは 41記事化 を達成。セキュリティ三部作が完成した。
Vol1 (Security Hub × GuardDuty × Audit Manager — 脅威検出基盤) →
Vol2 (SOC × Detective — 検知系強化) →
Vol3 (IAM Access Analyzer × KMS × Verified Access — 予防系完成) の積み上げで、
検知から予防まで一貫したセキュリティ設計が完成する。
セキュリティ Vol1 を読む
セキュリティ Vol2 を読む