SCS Lab 01 - Grant KMS access with a key policy, not just IAM
Scaffold: 3/5. The customer-managed KMS key, the Lambda function, and its execution role are all built. The handler is complete and works. What is missing is one statement in the key policy: the grant that lets the Lambda role use the key. You add it, redeploy, and the round-trip goes from AccessDenied to a clean encrypt-and-decrypt.
The scenario
A Lambda function needs to encrypt and decrypt some text with a customer-managed
KMS key. The obvious instinct is to give the function’s execution role the KMS
permissions in its IAM policy: kms:Encrypt, kms:Decrypt,
kms:GenerateDataKey. Attach those, and on an AWS-managed key or an S3 SSE-S3
bucket you would be done.
For a customer-managed key it is not enough. A CMK carries its own resource
policy, the key policy, and that policy is authoritative. A principal reaches
the key only when the key policy allows it, either directly or by delegating to
IAM with the account-root statement. A role can hold every KMS action in its IAM
policy and still get AccessDenied if the key’s policy never opened the door.
This lab makes that concrete by removing the IAM half entirely. The Lambda role here has no kms permissions at all in its IAM policy, only basic logging. So the key policy is the only place access can come from, which is exactly the point the scenario is trying to teach.
What’s provided
src/template.yaml— the KMS key, the Lambda function, and its execution role (logging only, no kms permissions). The key policy has the standard root statement, and a clearly-markedTODOwhere the grant to the Lambda role goes.src/handler.py— the round-trip handler. It is complete: it encrypts the input with the key, decrypts it back, and reports whether the two match. The gap is the key policy, not the code, so this ships working in bothsrc/andsolution/.scripts/— deploy, test, and teardown.solution/template.yaml— the complete, correct template.
Deploying src/template.yaml as shipped succeeds: the key, the role, and the
function all come up. But the first kms:Encrypt the handler attempts fails with
AccessDenied, because nothing grants the Lambda role on the key.
Your task
Open src/template.yaml and fill in the TODO in the key policy with a second
statement. It names the Lambda’s execution role as a principal and allows the
three data actions the handler needs:
- Sid: AllowLambdaRoleToUseTheKey
Effect: Allow
Principal:
AWS: !GetAtt FunctionRole.Arn
Action:
- kms:Encrypt
- kms:Decrypt
- kms:GenerateDataKey
Resource: '*'
The root statement stays; you are adding to the policy, not replacing it.
Referencing !GetAtt FunctionRole.Arn in the key policy is fine: the role is
created before the key, so its ARN resolves. (Resource: '*' in a key policy
means “this key”, since a key policy only ever governs its own key.)
Run it
# Defaults: stack scs-lab-01, region ap-southeast-2.
./scripts/deploy.sh # deploys src/template.yaml, then uploads the handler
./scripts/test.sh # invokes the Lambda and checks the round-trip
./scripts/teardown.sh # deletes everything
With the unedited src/template.yaml, test.sh invokes the function and the
handler reports AccessDenied on the key: the key exists, the role exists, but
the role was never granted. Add the statement, redeploy, and run the test again.
What success looks like
./scripts/test.sh prints the function’s JSON with "matched": true, then:
PASS: the string was encrypted and decrypted back to itself. The key
policy grants the Lambda role, so KMS let the function use the key.
If it fails
- AccessDenied on the key. The grant is missing. The Lambda role has no kms permissions in its IAM policy, so until the key policy names it as a principal, the role cannot touch the key. This is the whole lesson: an IAM allow alone does not reach a customer-managed key.
- AccessDenied even after you added the statement. Check the principal. It
has to be the execution role’s ARN (
!GetAtt FunctionRole.Arn), and the three actions have to includekms:Encryptandkms:Decryptat minimum. - The stack fails to update the key policy. A key policy must keep a statement that lets the account manage the key. Leave the root statement in place; add your grant alongside it, do not overwrite it.
Reveal the solution
Deploy the complete reference template without editing anything:
SRC=solution ./scripts/deploy.sh && ./scripts/test.sh
What you just learned
- For a customer-managed key, the key policy is authoritative. A principal reaches the key only when the key policy allows it, directly or by delegating to IAM. IAM permissions on a role are not sufficient on their own.
- The default root statement (
Principal: root,Action: kms:*) is what lets IAM policies grant access to the key at all. Remove it and even an admin can lock themselves out; keep it and IAM allows on roles start working, because the key now delegates to the account. - There are three ways to hand out KMS access: a statement in the key policy, an IAM policy on the principal (which only works because the root statement delegates), or a grant for narrow, programmatic, temporary access. This lab uses the first directly, with no IAM half, so the key policy is doing all the work.
Next
The rest of the SCS lab track is listed in labs/README-scs.md.