From 721348472011a98054c5fb3ff05b3e053f09d9b7 Mon Sep 17 00:00:00 2001 From: yook11 <246658998+yook11@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:45:20 +0900 Subject: [PATCH] =?UTF-8?q?fix(infra):=20secret=20=E8=AA=AD=E3=81=BF?= =?UTF-8?q?=E5=8F=96=E3=82=8A=20Deny=20=E3=82=92=E8=87=AA=E3=82=A2?= =?UTF-8?q?=E3=82=AB=E3=82=A6=E3=83=B3=E3=83=88=E3=81=AE=20parameter=20?= =?UTF-8?q?=E3=81=AB=E7=B5=9E=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NoSecretValues が Resource "*" だったため、AWS が公開する /aws/service/ami-amazon-linux-latest/* まで巻き込んでいた。account 部が空の ARN なので `*` に含まれ、bastion の AMI 参照 (data.aws_ssm_parameter) が terraform-apply で AccessDenied になる。 これまで踏み台は admin で生やしていたため露見しなかった。人間の経路を CI ロールへ 寄せたことで表に出たもので、CI が bastion を apply する場合も同じ箇所で落ちる。 SSM だけ Resource を arn:aws:ssm:*::parameter/* に絞る。秘密は全て 自アカウントの parameter にあるので射程は変わらない。secretsmanager と kms は "*" のまま。4 policy で重複していた Deny 文を locals へ括り出す。 Co-Authored-By: Claude Opus 5 --- infra/aws/bootstrap/oidc.tf | 74 +++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/infra/aws/bootstrap/oidc.tf b/infra/aws/bootstrap/oidc.tf index 1e94deba..a1de4cef 100644 --- a/infra/aws/bootstrap/oidc.tf +++ b/infra/aws/bootstrap/oidc.tf @@ -73,13 +73,32 @@ locals { # secret の実体を CI が読めないようにする。 # 「Terraform から SSM の値を読まない」を運用の約束ではなく IAM の Deny にする。 - secret_read_actions = [ - "ssm:GetParameter", - "ssm:GetParameters", - "ssm:GetParameterHistory", - "ssm:GetParametersByPath", - "secretsmanager:GetSecretValue", - "kms:Decrypt", + # + # SSM だけ Resource を自アカウント所有の parameter に絞る。AWS が公開する + # /aws/service/* (最新 AMI の ID など) は **account 部が空の ARN** で、`*` で + # 巻き込むと bastion の AMI 参照が Deny で落ちる。秘密は全て自アカウントの + # parameter にあるので、絞っても射程は変わらない。 + secret_read_statements = [ + { + Sid = "NoOwnParameterValues" + Effect = "Deny" + Action = [ + "ssm:GetParameter", + "ssm:GetParameters", + "ssm:GetParameterHistory", + "ssm:GetParametersByPath", + ] + Resource = "arn:aws:ssm:*:${local.account_id}:parameter/*" + }, + { + Sid = "NoSecretValues" + Effect = "Deny" + Action = [ + "secretsmanager:GetSecretValue", + "kms:Decrypt", + ] + Resource = "*" + }, ] } @@ -168,15 +187,8 @@ resource "aws_iam_role_policy" "plan_deny_secret_read" { role = aws_iam_role.ci["plan"].id policy = jsonencode({ - Version = "2012-10-17" - Statement = [ - { - Sid = "NoSecretValues" - Effect = "Deny" - Action = local.secret_read_actions - Resource = "*" - }, - ] + Version = "2012-10-17" + Statement = local.secret_read_statements }) } @@ -188,7 +200,7 @@ resource "aws_iam_role_policy" "apply" { policy = jsonencode({ Version = "2012-10-17" - Statement = [ + Statement = concat([ # 本体スタックが触るリージョナルサービス。 # # ここを action 単位で列挙しないのは意図的。greenfield のスタックで @@ -336,13 +348,7 @@ resource "aws_iam_role_policy" "apply" { # computed なので、ignore_changes を付けても refresh で state に載る)。 # 「値を読まない」を Deny にすることで、data source をうっかり足しても # apply が失敗して気づく。 - { - Sid = "NoSecretValues" - Effect = "Deny" - Action = local.secret_read_actions - Resource = "*" - }, - ] + ], local.secret_read_statements) }) } @@ -357,7 +363,7 @@ resource "aws_iam_role_policy" "push" { policy = jsonencode({ Version = "2012-10-17" - Statement = [ + Statement = concat([ { Sid = "EcrAuthToken" Effect = "Allow" @@ -378,13 +384,7 @@ resource "aws_iam_role_policy" "push" { ] Resource = "arn:aws:ecr:${var.region}:${local.account_id}:repository/${var.name_prefix}/*" }, - { - Sid = "NoSecretValues" - Effect = "Deny" - Action = local.secret_read_actions - Resource = "*" - }, - ] + ], local.secret_read_statements) }) } @@ -400,7 +400,7 @@ resource "aws_iam_role_policy" "rollout" { policy = jsonencode({ Version = "2012-10-17" - Statement = [ + Statement = concat([ { Sid = "EcsRollout" Effect = "Allow" @@ -429,12 +429,6 @@ resource "aws_iam_role_policy" "rollout" { StringEquals = { "iam:PassedToService" = "ecs-tasks.amazonaws.com" } } }, - { - Sid = "NoSecretValues" - Effect = "Deny" - Action = local.secret_read_actions - Resource = "*" - }, - ] + ], local.secret_read_statements) }) }