Running log. Errors, dead ends, fixes, and things that surprised me. Dated entries, newest at the bottom. This file is the proof the lab was real.
### YYYY-MM-DD, what I was trying to do
**Expected:**
**Got:**
**Cause:**
**Fix:**
Secrets Manager has to be allowed to call the function. Without
aws_lambda_permission.allow_secretsmanager, rotate-secret returns an error
that talks about the secret, not the missing Lambda permission, so it sends you
looking in the wrong file. Already wired in rotation.tf, noted because it's
the most common first-run failure.
Leaves the secret in a state where the console shows rotation on and every
rotation attempt fails silently. Rotation is only enabled here because the
function exists and depends_on forces the ordering.
Rotation changes the value outside Terraform, so the next plan sees drift and
offers to "fix" it, by writing the stale seed value back over a live credential.
ignore_secret_changes = true prevents it. Without it this eventually causes an
outage during an unrelated apply.
The advanced event selector bills per event. Fine for a lab. Worth measuring before enabling account-wide, put the number in the comparison doc when you have it.
- How long does a full four-step rotation actually take end to end?
- Does
prove-deniedstill fail if the caller is the account root? - What's the real per-event cost of data events over a week?
- Does the
kms:ViaServicecondition block aGenerateDataKeypath I missed? - Measure: reads recorded in CloudTrail vs. reads actually made. Any gap?
Expected: terraform validate to pass once the rotation Lambda existed.
Got:
Error: Cycle: module.db_secret.var.name (expand), module.db_secret.var.replica (expand),
module.db_secret.var.description (expand), module.db_secret.var.create (expand),
module.db_secret.var.kms_key_id (expand), ... module.db_secret.output.secret_arn (expand),
aws_iam_role_policy.rotation, aws_lambda_function.rotation,
aws_lambda_permission.allow_secretsmanager, module.db_secret (expand)
Cause: I scoped the rotation role's policy to module.db_secret.secret_arn, which
felt like good practice. But the secret needs the function's ARN to enable rotation,
the function needs its role policy, and the policy was reaching back for the secret.
Three-way cycle.
Fix: Built the ARN by hand from account + region + secret name, with a trailing
-* for the six random characters AWS appends. Same scoping, no edge back into the
module. Worth noting the scoping is not looser: the wildcard only covers the random
suffix, not other secrets.
Ran the configuration against LocalStack community, which implements Secrets Manager, KMS and IAM locally. The secret, the customer managed key, its key policy and both roles all created, and retrieval works end to end:
secret: lab05/database/app
KmsKeyId: arn:aws:kms:us-east-1:000000000000:key/8c3d268f-...
RotationEnabled: true
get-secret-value -> {'engine':'postgres','host':'lab-placeholder.local',
'password':'fP5A...','port':5432,'username':'lab_app_user'}
The kms:ViaService condition is present on the deployed key policy:
AllowConsumerDecryptViaSecretsManagerOnly ->
{"StringEquals": {"kms:ViaService": "secretsmanager.us-east-1.amazonaws.com"}}
That is the control that stops the consumer role carrying ciphertext somewhere else and unwrapping it, and it has no clean CyberArk equivalent. Worth pointing at directly in the comparison doc rather than describing.
What would not run, and it is the important half. A full apply hangs:
CloudTrail is a LocalStack Pro feature. So terraform/audit.tf is unexercised,
and that file carries this lab's central finding, that secret reads are
invisible until you turn on data events and pay per event. It stays unverified
until a real AWS run. Applied the rest with -target.
Rotation is enabled on the secret but I did not trigger one. LocalStack community's Lambda execution is not reliable enough for the four step protocol, and a green result there would not mean anything.
LocalStack also does not evaluate resource policy at request time, so
prove-denied remains a real AWS test. The honest position: the access model is
built and deployed, the retrieval path is proven, the audit and enforcement
claims are not yet.
Full output in findings/localstack-apply-run.txt.
Expected: three statements (allow read, allow rotate, deny everyone else) via the
module's policy_statements.
Got:
Error: Invalid value for input variable
on main.tf line 141, in module "db_secret":
The given value is not suitable for module.db_secret.var.policy_statements
declared at .terraform\modules\db_secret\variables.tf:81,1-29: all map
elements must have the same type.
Cause: The module types policy_statements as a map, so every statement needs an
identical attribute set. A Deny with not_principals can't match the shape of a
conditional Allow.
Fix: Set create_policy = false and wrote the policy directly with
aws_iam_policy_document + aws_secretsmanager_secret_policy (see policy.tf).
Ended up clearer anyway: the whole access model is one readable block instead of a
typed map fighting the schema.
Takeaway: a module that makes the common case easy can make the correct case impossible. Dropping to the resource was the right call, not a workaround.