Repository navigation
fix(iac/aws): fail closed when the Lambda DB secret ARN is empty - #1117
Conversation
The Lambda role's GetSecretValue resource list appended an unguarded
"${database_password_secret_arn}*". With an empty ARN that element was
the bare "*", which compact() keeps, so the role could read every
Secrets Manager secret in the account. The sibling optional secrets
were already guarded.
Validate database_password_secret_arn as a concrete Secrets Manager
secret ARN (no empty value, no wildcards) so a bad input fails the
plan, and guard the "${arn}*" entry like its siblings so the policy
stays fail-closed if the input is ever made optional.
Adds tests/secrets_policy.tftest.hcl (mock provider) pinning the
rendered resource list and the rejected inputs.
Closes LeanerCloud#291
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing: 3
✨ Finishing Touches
Comment |
|
Independent review (claude-cc-platform reviewer, cold brief): APPROVE No blocking findings. Fail-closed check works. Extra Callers are safe. The only caller is
Runtime: no platform change is needed. Tests:
Base drift: clean merge onto current main (tree Nits:
|
|
Independent review (claude-fleet): APPROVE 6d951bf Findings (none blocking):
Attacked:
Verified locally, with Terraform 1.10.5 (the version CI pins), a mock provider, and no cloud credentials. No plan or apply was run.
Reviewed by an independent agent with no author context. Review only: nothing was pushed, and the GitHub review state is unchanged. |
|
Local verification (macOS, exact head Gate status: two independent reviews approve this SHA (posted above), local verification passes, and CI is green. Merge waits on @IAmJSD marking the PR ready (see #974). |
|
Local verification at
|
|
Marked ready and merged by the owner's agent after 2 independent reviews and local verification (see the review and verification comments above, all at |
Root cause
aws_iam_role_policy.secrets_accessinterraform/modules/compute/aws/lambda/main.tfbuilds itssecretsmanager:GetSecretValueresource list withcompact([...]). The admin, credential-encryption, scheduled-task and Archera entries are written asX != "" ? "${X}*" : "", but the database entry was a bare"${var.database_password_secret_arn}*". With an empty ARN that element renders as the literal"*",compact()keeps it, and the Lambda role can read every secret in the account.database_password_secret_arnhad no validation, so nothing stopped the empty value.The only in-tree caller passes
module.database.password_secret_arn, so this was a latent fail-open, not a live over-grant (as the issue says).Fix
database_password_secret_arn(lambda/variables.tf): the value must be a concrete Secrets Manager secret ARN,^arn:aws(-[a-z]+)*:secretsmanager:[a-z0-9-]+:[0-9]{12}:secret:[A-Za-z0-9/_+=.@-]+$. That rejects an empty value, null, a bare*, and wildcards in the region, account or name, any of which would widen the grant. Partitionsaws,aws-us-govandaws-cnare accepted. The name charset is the one Secrets Manager allows (/_+=.@-plus alphanumerics), so it covers the random-AbCdEfsuffix on real ARNs. During plan the caller's value is unknown (the secret is created in the same apply), so the check is deferred and doesn't break the environment root.lambda/main.tf: the"${arn}*"entry is nowarn != "" ? "${arn}*" : "", the same as its siblings. While the validation holds, this branch can't be reached through the variable. It is there for the issue's failure scenario: if a later refactor makes the input optional and drops the validation, the policy still fails closed (see mutation M2 below).Verification
All evidence comes from unit tests and
terraform testwithmock_provider "aws"(plan only). Nothing ran against a real AWS account, no plan used a real backend, and nothing was applied.New test:
terraform/modules/compute/aws/lambda/tests/secrets_policy.tftest.hcl(8 runs). It pins the exact rendered GetSecretValue resource list, checks that no statement grants"*", accepts a GovCloud ARN, and rejects empty,*, a wildcard name, a wildcard account and a non-Secrets-Manager ARN.Fail-before (main's
main.tfandvariables.tfwith the new test file):I also ran a throwaway probe on main (
database_password_secret_arn = "", assert that the resource list does not contain"*"; not committed). It reproduces the issue directly:Pass-after: Terraform 1.10.5 (the CI pin) and 1.15.8 both give
Success! 15 passed, 0 failed.for the whole module (new file plus the existingsweepsandtimeouttests). On the fixed code, the same probe fails at plan with the new validation message.Mutation probes:
rejects_empty_db_secret_arnfails (caught). The probe with an empty ARN now renders no"*", so the guard alone fails closed.^arn:aws[^:]*:secretsmanager:.+$:rejects_wildcard_secret_namefails (caught).Other checks run locally:
terraform fmt -check -recursive terraform/passes.terraform validatepasses on the lambda module and onterraform/environments/aws(init -backend=false) with 1.10.5.AWS_CONFIG_FILE=/dev/null AWS_SHARED_CREDENTIALS_FILE=/dev/null go test ./terraform/modules/compute/... ./terraform/environments/aws/... -count=1all pass. The/dev/nulloverrides are needed only because this machine's~/.aws/configdefault profile is invalid.scripts/check-aws-iam-parity.shpasses, andscripts/test-aws-iam-parity.shgives 12 passed, 0 failed.trivy config --severity HIGH,CRITICALon the lambda module: 0 misconfigurations.tflint0.58.1 with the repo.tflint.hclon the lambda module: clean.--register-aws, the custom patterns fromscripts/setup-git-secrets.shand.gitallowed, run in a fresh temp repo: clean.scripts/test-git-secrets-allowlist.sh: 42 PASS, 0 FAIL.Out of scope (noted, not changed)
terraform/modules/compute/aws/fargate/main.tf:108-109,:153-154): it writes"${arn}-*"with no guard, and itsdatabase_password_secret_arnhas no validation. An empty value renders-*, which fails closed (it matches nothing) rather than open, so I left it alone. The same validation could be added there for symmetry if wanted.PutSecretValuestatement: ifadmin_password_secret_arnis empty, it renders"Resource":[](seen in the probe output above). That is an empty resource list, not a wildcard, but IAM may reject it as malformed. This predates the PR and isn't touched.Closes #291
Maintainer: please mirror the issue's triage labels onto this PR:
priority/p2,severity/medium,urgency/this-quarter,impact/internal,effort/xs,type/security,triaged.🤖 Generated with Claude Code