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

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

Reveal the solution

Deploy the complete reference template without editing anything:

SRC=solution ./scripts/deploy.sh && ./scripts/test.sh

What you just learned

Next

The rest of the SCS lab track is listed in labs/README-scs.md.