- 1 §1 なぜ Security Vol3 か — Zero Trust と Defense in Depth の予防系深化
- 2 §2 IAM Access Analyzer 本番運用 — External / Unused / Custom Policy Check
- 3 §3 IAM Identity Center 本番運用 — Permission Sets × SCIM × Multi-Account SSO ★山場1
- 4 §4 KMS Multi-Region 本番運用 — Replica Key × Grants × 暗号境界設計 ★山場2
- 5 §5 Secrets Manager × Certificate Manager 本番運用 — Auto Rotation × Resource Policy × Lambda連携
- 6 §6 Verified Access 本番運用 — ZTNA × Trust Provider × Policy × VPC Endpoint
- 7 §7 詰まりポイント7選 図解
- 7.1 詰まり1: IAM Identity Center Permission Set の Customer Managed Policy 連携失敗
- 7.2 詰まり2: KMS Multi-Region Replica 作成後の Decrypt 遅延
- 7.3 詰まり3: Secrets Manager Auto Rotation の冪等性破綻
- 7.4 詰まり4: Verified Access Cedar Policy の Permit 範囲誤り
- 7.5 詰まり5: Access Analyzer External Findings のノイズ過多
- 7.6 詰まり6: IAM Identity Center External IdP の属性マッピング不一致
- 7.7 詰まり7: Multi-Region 暗号化境界の Cross-Account Grants 失効
- 8 §8 アンチパターン → 正解パターン変換演習 + シリーズ繋ぎ
§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縦深・暗号化・ZTNA | Access 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 境界を維持するサイクルは以下の通りだ。
- Identity Center で Permission Sets を最小権限で設計する(§3 で詳解)
- Access Analyzer の Unused Access Findings で未使用権限を継続検出する(§2 で詳解)
- Custom Policy Check を CI/CD に組み込み、新規ポリシーの過剰権限を事前遮断する(§2 で詳解)
- 一定期間使用されない権限を定期棚卸しで削除するプロセスを確立する
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 Analyzer | Identity Center の最小権限設計 |
| 認証統合 | 認証情報の漏洩・不正使用 | IAM Identity Center | Verified Access のデバイス認証 |
| 暗号化境界 | 静止データ・転送データの露出 | KMS Multi-Region | Secrets Manager のローテーション |
| シークレット管理 | 認証情報の長期固定・露出 | Secrets Manager + ACM | KMS による暗号化保護 |
| ネットワーク認証 | 未認証・未認可のネットワークアクセス | Verified Access | Identity 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領域ロードマップ
| § | 対象領域 | 主な実装内容 | 達成する予防効果 |
|---|---|---|---|
| §2 | IAM Access Analyzer | External / Unused Findings + Custom Policy Check + CI/CD 統合 | 権限の過剰付与・放置・外部公開を継続検出 |
| §3 | IAM Identity Center | Permission Sets + SCIM + Multi-Account SSO + External IdP 連携 | SSO 統合と最小権限を組織全体に適用 |
| §4 | KMS Multi-Region | Replica Key + Key Policy + Grants + Auto Rotation | 暗号化境界の明示的設計とマルチリージョン対応 |
| §5 | Secrets Manager + ACM | Auto Rotation + Lambda + Resource Policy + 証明書管理 | 認証情報の動的管理と証明書の自動更新 |
| §6 | Verified Access | Trust Provider + Cedar Policy + VPC Endpoint + ZTNA設計 | ZTNA によるゼロトラストネットワークアクセス実装 |
§7 では本番設計でよく詰まる7パターンを体系化し、§8 ではアンチパターンから正解パターンへの変換演習を5問収録する。理論だけでなく「本番で直面する落とし穴と正しい解決策」まで含めて習得できる構成だ。
各§はそれぞれ独立した本番実装ガイドとして機能する。§3 の IAM Identity Center のみを読んでも完全な実装が可能であり、§4 の KMS のみを読んでも暗号化境界の設計が完結する。ただし全体を通して読むことで、5つのサービスが Zero Trust の4境界にどう対応するかという全体像が見えてくる。
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 Check | CI/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 許可 |

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 として扱われないため、クロスアカウントロールの誤検知が大幅に減少する。
パターン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
}
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 Policy | AWS管理 | ○ | 読み取り専用 (ReadOnlyAccess) など汎用ロール |
| Inline Policy | Permission 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/"
}
}
}
鉄則① 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 コンソール → 設定 → 自動プロビジョニング → 有効化:
- SCIM endpoint URL をコピー (例:
https://scim.us-east-1.amazonaws.com/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/scim/v2/) - アクセストークンを生成・コピー (トークンは一度しか表示されない)
生成したトークンは 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 設定手順
- Okta Admin Console → Applications → Browse App Catalog → “AWS IAM Identity Center” を追加
- Provisioning タブ → Configure API Integration → SCIM endpoint と Token を入力
- To App: Create Users / Update User Attributes / Deactivate Users を有効化
- Push Groups: 同期対象グループを選択
- Assignments: プロビジョニング対象ユーザーを割り当て
属性マッピングの必須設定 (Okta):
| Okta 属性 | IAM Identity Center 属性 | 備考 |
|---|---|---|
user.login | userName | メールアドレス形式必須 |
user.firstName | givenName | 必須 |
user.lastName | familyName | 必須 |
user.email | emails[primary].value | 必須 |
user.displayName | displayName | 省略時は givenName+familyName |
Azure AD 設定手順
- Azure Portal → エンタープライズアプリケーション → 新しいアプリケーション → “AWS IAM Identity Center” を検索して追加
- プロビジョニング → 自動 → 管理者資格情報: テナントURL (SCIM endpoint) とシークレットトークンを入力
- 接続のテストをクリックして疎通確認
- マッピング → ユーザーのプロビジョニング → 属性マッピングを確認
- スコープを「割り当てられたユーザーとグループのみ同期」に設定
Azure AD で必要な属性マッピング:
| Azure AD 属性 | IAM Identity Center 属性 | 備考 |
|---|---|---|
userPrincipalName | userName | @ 含むメールアドレス形式 |
givenName | givenName | |
surname | familyName | |
mail | emails[primary].value | userPrincipalName と異なるドメインに注意 |
Google Workspace 設定手順
- Google Admin Console → アプリ → ウェブアプリとモバイルアプリ → アプリを追加 → アプリカタログから追加 → “AWS IAM Identity Center” を検索
- 自動プロビジョニングを有効化 → SCIM URL とトークンを入力
- 属性マッピングを確認 (
primaryEmail→userNameは必須) - サービスのアクセス権: 同期対象 OU またはグループを指定
グループ同期 vs ユーザー同期の使い分け
本番ではグループ同期を優先する。直接ユーザー同期は Account Assignment 設計が複雑になり、IdP 側でのグループ管理が失われる。
| 方式 | メリット | デメリット | 推奨 |
|---|---|---|---|
| グループ同期 | IdP 側でグループ管理、Account Assignment がシンプル | グループ設計が必要 | ◎ 本番推奨 |
| ユーザー直接同期 | 細粒度制御可能 | Account Assignment が N × Permission Set の組み合わせで爆発 | △ 小規模のみ |
パターン①: 同期遅延 — SCIM はプッシュ型だが IdP 側の同期間隔が15〜40分の場合がある。即時反映が必要な場合は IdP 管理コンソールから「今すぐ同期」を手動実行するか、SCIM API を直接呼び出す。
パターン②: 属性マッピング不一致 — IAM Identity Center の SCIM は
userName がメールアドレス形式 (user@example.com) である必要がある。Okta の login 属性が短縮形の場合、変換式で補正する。Azure AD は userPrincipalName と mail が異なるドメインになるケースに注意。パターン③: Group → Permission Set 紐付け失敗 — グループが同期されているのに Account Assignment が機能しない場合、グループ名の大文字小文字・スペースの違いが原因なことが多い。Identity Store コンソールでグループ名を目視確認し、Terraform の
data.aws_identitystore_group の filter 値と完全一致させる。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 を作成する前に以下が完了していなければならない:
- グループが Identity Store に同期済み — SCIM 同期または手動作成後、
aws_identitystore_groupデータソースで参照できることを確認 - Permission Set に Customer Managed Policy が添付済み — 対象アカウントに同名ポリシーが存在し、
aws_ssoadmin_customer_managed_policy_attachmentが Apply 済み - 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 設定手順
- IAM Identity Center コンソール → 設定 → ID ソース → 外部 ID プロバイダー → SAML メタデータをダウンロード
- Okta Admin Console → Applications (上で追加済み) → Sign On タブ → SAML 2.0 設定
- Okta の IdP メタデータ XML をダウンロードし、IAM Identity Center の「IdP SAML メタデータ」にアップロード
- Okta 側の ACS URL と Entity ID が IAM Identity Center の値と一致することを確認
| 設定項目 | IAM Identity Center 値 |
|---|---|
| ACS URL | https://us-east-1.signin.aws.amazon.com/platform/saml/acs/xxxxxxxx |
| Entity ID | https://us-east-1.signin.aws.amazon.com/platform/saml/d-xxxxxxxxxx |
| NameID format | emailAddress |
Azure AD の SAML 設定手順
- Azure Portal → エンタープライズアプリ → シングルサインオン → SAML を選択
- 基本的な SAML 構成: 識別子 (Entity ID) と 応答 URL (ACS URL) に IAM Identity Center の値を入力
- フェデレーションメタデータ XML をダウンロード → IAM Identity Center にアップロード
- ユーザー属性とクレーム:
user.mailをNameIdentifierとしてマッピング
Just-In-Time (JIT) プロビジョニング
JIT プロビジョニングを有効にすると、SCIM 同期なしでも SAML 認証時に自動でユーザーを作成できる。ただし本番での使用は非推奨だ。
| 方式 | メリット | デメリット | 本番推奨 |
|---|---|---|---|
| SCIM + SAML | グループ同期で Account Assignment 自動適用 | 設定複雑 | ◎ |
| JIT + SAML | 設定シンプル | グループ情報が伝搬されず Permission Set 自動付与不可 | ✕ |
JIT はグループメンバーシップを SAML Assertion から取得できないため、Permission Set の自動割り当てが機能しない。10アカウント以上の環境では SCIM を必ず構成する。

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 設計で必須の理解となります。

- Replica は同一 Key ID を維持する — Primary で暗号化したデータは、同一 mrk- Key ID を持つ Replica でそのまま復号できる。Key Material は同一だが、Replica は Primary とは独立したリソースとして管理される
- Key Policy は Primary / Replica で独立管理する — Primary の Key Policy を変更しても Replica に自動反映されない。Cross-Account Decrypt を許可する場合は Replica 側の Key Policy にも明示的に記載が必要
- 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 Policy | KMS Key 単位の権限定義 | 必須ステートメント必要。Key Policy なしでは IAM Policy 完全無効 |
| IAM Policy | IAM 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 Policy | kms: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 + 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 の constraints | IAM 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 — Built-in と Lambda Custom
Auto Rotation にはBuilt-inとLambda 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 Manager | Parameter 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_policy に ELBSecurityPolicy-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 インスタンスも旧認証情報で接続継続できる。
- createSecret: AWSPENDING に新値を書き込む。既存 AWSPENDING を検出したらスキップ(冪等)
- setSecret: バックエンド(DB など)に新パスワードを適用する
- testSecret: AWSPENDING の認証情報でバックエンドへの接続テストを実行。失敗時は例外を raise して finishSecret をブロック
- finishSecret: AWSPENDING → AWSCURRENT に昇格。旧 AWSCURRENT → AWSPREVIOUS に降格
鉄則: 各ステップの先頭で「同一 ClientRequestToken の処理済みチェック」を入れること。finishSecret が未実行のまま Lambda が終了すると AWSCURRENT と AWSPENDING が乖離し、接続障害になる。
- 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 は「アクセスのたびに検証」する。
| 観点 | 従来 VPN | Verified Access (ZTNA) |
|---|---|---|
| 信頼モデル | ネットワーク境界信頼 | Never trust, always verify |
| アクセス粒度 | ネットワークレベル(全リソース) | アプリケーション単位 |
| ユーザー認証 | VPN ログイン後は内部信頼 | アクセスごとにポリシー評価 |
| デバイス検証 | ほぼなし | Device Trust 統合可能 |
| ラテラルムーブメント | 可能 | ポリシーで明示的に制御 |
| スケール | クライアントソフトウェア必須 | クライアントレス(ブラウザのみ) |
Verified Access の4コンポーネント
- Trust Provider: ユーザー / デバイスの信頼評価ソース(IAM Identity Center / OIDC / Device Trust)
- Group: Trust Provider を紐付けてアクセスポリシーを定義するグループ単位
- Endpoint: 保護対象のアプリケーションエンドポイント(ALB / EC2 / Lambda URL)
- 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 の評価順
- Forbid ルール (最高優先度): いずれか1つでも forbid にマッチすればアクセス拒否
- Permit ルール: forbid がない場合、いずれか1つでも permit にマッチすればアクセス許可
- デフォルト拒否: どのルールにもマッチしなければアクセス拒否
デバッグ時に一時的に投入した全許可ポリシーをそのまま本番に残す事故が多い。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 SG | 443 |
| Verified Access → 内部 ALB | VA Endpoint SG | ALB SG | 443 |
| 内部 ALB → アプリ | ALB SG | App SG | 8080 |
VA Endpoint の Security Group は内部 ALB に向けて HTTPS 送信のみ許可する。クライアント側の受信はマネージドのため Security Group での追加設定は不要。
- Trust Provider 選定: AWS 中心環境 → IAM Identity Center / 外部 IdP 既存 → OIDC / デバイス検証必須 → Device Trust (Jamf / CrowdStrike) を組み合わせる
- Policy 階層化: Forbid を先に定義(管理対象外デバイス・組織外メール)→ Permit で部門・グループ単位に許可。デバッグ用全許可ポリシーを本番に残さない
- VPC Endpoint 経路: Verified Access → 内部 ALB のルートのみ Security Group で開ける。バックエンドへの直接アクセスパスをネットワーク設計で排除する
- Logging: S3 + CloudWatch の両方を有効化。アクセスログには Cedar Policy の評価結果(permit / forbid)が含まれるため、ポリシーデバッグに必須
- 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 で ProvisionPermissionSet に FAILURE ステータスが記録され、対象アカウントでロールを引き受けても期待した権限が付与されない。
原因: 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]
}
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
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.groups や context.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"]}}'
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"]
}
}
詰まり6: IAM Identity Center External IdP の属性マッピング不一致
症状: Okta との SCIM 同期後、AWS コンソールのユーザー一覧で displayName が空文字になる。aws identitystore list-users で DisplayName フィールドが空になっている。
原因: Okta の SCIM Provisioning では属性マッピングを明示的に設定しないと displayName が連携されない。Okta の User schema では firstName と lastName が分離されており、IAM Identity Center が期待する displayName に自動マップされない。
対処手順 (Okta 設定):
- Okta Admin > Applications > AWS IAM Identity Center > Provisioning > To App
- Attribute Mappings で
displayNameを追加: - Okta attribute:
String.join(" ", user.firstName, user.lastName) - IAM Identity Center attribute:
displayName - Force Sync を実行して既存ユーザーに反映
aws identitystore list-users \
--identity-store-id d-xxxxxxxxxx \
--query 'Users[*].{Name:UserName,Display:DisplayName}' \
--output table
aws identitystore list-users で全フィールドを確認する。displayName・email・userName の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 を実行し、グラントを継続的にリフレッシュする。
§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
全軸クロスリンク
- セキュリティシリーズ:
- IAM シリーズ:
- 復旧・運用編シリーズ:
- AI/ML シリーズ:
- EKS/Container シリーズ:
- Observability シリーズ:
- Network シリーズ:
- DevOps シリーズ:
- Database シリーズ:
- Serverless シリーズ:
- Storage シリーズ:
- Edge: Vol1: CloudFront × Lambda@Edge × Route 53 本番ガイド
- Analytics: Vol1: Data Lake 本番入門 — Glue × Athena × Redshift
- Migration: Vol1: Application Migration 本番ガイド — DMS × MGN × Snow Family
本記事公開で AWS本番運用シリーズは 41記事化 を達成。セキュリティ三部作が完成した。
Vol1 (Security Hub × GuardDuty × Audit Manager — 脅威検出基盤) →
Vol2 (SOC × Detective — 検知系強化) →
Vol3 (IAM Access Analyzer × KMS × Verified Access — 予防系完成) の積み上げで、
検知から予防まで一貫したセキュリティ設計が完成する。
セキュリティ Vol1 を読む
セキュリティ Vol2 を読む