In an AWS CDK app, what does calling bucket.grantRead(myFunction) actually put into the synthesized CloudFormation template, and why is that preferred over hand-writing the policy document?
answer
- the helper writes the IAM for you
- statement lands on the grantee's role
- bucket ARN and objects are separate
- the key permission rides along
- imported resources know no key
basics
~20 sCalling grantRead adds a policy statement to the Lambda function's execution role in the synthesized template, scoped to that bucket's ARN and its objects, and it also grants decrypt on the bucket's encryption key when the CDK app knows about one.
solid answer
~50 s`grantRead` is a method on the L2 bucket construct that writes IAM for you at synth time. It resolves the grantee to its principal — for a `lambda.Function`, the execution role the construct created — and attaches a statement carrying the bucket read actions (the `s3:GetObject*`, `s3:GetBucket*` and `s3:List*` families) with two resources: the bucket ARN and the bucket ARN plus `/*`, because listing and object access are separate resource shapes in S3. If the bucket has an encryption key the app knows about, the same call adds the matching decrypt permission on that key, which is the part people forget when hand-writing. It returns an `iam.Grant` object, so you can make other constructs depend on the policy existing. The win is not typing less JSON: it is that the scoping, the two ARN forms and the key permission are decided by the construct that owns the resource, rather than by whoever is editing the policy today.
code
typescript · 25 linesimport * as cdk from 'aws-cdk-lib';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { Construct } from 'constructs';
export class GrantStack extends cdk.Stack {
constructor(scope: Construct, id: string) {
super(scope, id);
// BucketEncryption.KMS makes the CDK create the key, so it knows about it
const bucket = new s3.Bucket(this, 'Reports', {
encryption: s3.BucketEncryption.KMS,
});
const fn = new lambda.Function(this, 'Reader', {
runtime: lambda.Runtime.NODEJS_20_X,
handler: 'index.handler',
code: lambda.Code.fromInline('exports.handler = async () => {};'),
environment: { BUCKET: bucket.bucketName },
});
// writes the S3 read actions AND the key decrypt permission
bucket.grantRead(fn);
}
}go deeper
Know that grant methods exist on L2 constructs and save you from writing IAM JSON, and be able to say that the permission ends up on the calling function's execution role.
Explain what the synthesized statement contains: the read action family, both the bucket and object ARN forms, and the key permission when the app owns the encryption key. That is the mechanics tier of this question.
Bring the failure mode. Show that grants on imported resources silently skip the key permission and cannot touch a resource policy the app does not own, and say how you would spot that before production rather than from a runtime AccessDenied.
Take a position on where grant helpers are allowed. Decide whether the estate accepts action wildcards for speed and consistency or requires explicit statements on regulated resources, and say who reviews the synthesized policy either way.
## What a grant call is Grant methods are the clearest reason to prefer an L2 construct over the generated L1 underneath it. They are ordinary methods on the resource construct — `bucket.grantRead`, `bucket.grantPut`, `queue.grantConsumeMessages`, `table.grantReadWriteData` — and they take a grantee: anything implementing `iam.IGrantable`, which in practice means a Lambda function, an ECS task definition's task role, a role, or a user. The call happens at synth time, in your program, and its only effect is on the template that gets emitted. ## What lands in the template For `bucket.grantRead(fn)` where both live in the same stack, the statement lands on the **grantee's** identity policy — the inline policy of the function's execution role — not on the bucket. Two details matter and both are things hand-written policies get wrong. First, the action set is a family, not a single action: the read grant covers the `s3:GetObject*`, `s3:GetBucket*` and `s3:List*` shapes. That is broader than `s3:GetObject` alone and narrower than `s3:*`, and it is deliberately the set a reader actually needs. Second, the resource is expressed twice. Object-level actions apply to `arn:aws:s3:::bucket-name/*` while bucket-level actions apply to `arn:aws:s3:::bucket-name`. A policy that lists only one of the two produces the classic "I can get objects but cannot list the bucket" ticket. The grant emits both, and the ARNs are generated references to the bucket construct rather than strings you typed, so they stay correct when the physical name is auto-generated. ## Encryption keys ride along If the bucket construct in your app owns a KMS key — for example because you asked for `BucketEncryption.KMS`, so the CDK created the key — then the read grant also adds the decrypt permission on that key. This is the single most valuable thing the helper does, because a hand-written S3 policy that looks perfectly correct still yields `AccessDenied` at runtime when the object is encrypted with a customer managed key and the caller has no key permission. ## Same stack, other stack, other account Grants are not blindly "add to the role". For resources that support a resource policy, the CDK decides where the statement belongs: for a principal it can reach, the identity policy is enough; where the grantee is an account or a principal outside the app's control, it can add the statement to the resource's own policy instead. This is why the same one-line call behaves sensibly in more than one topology — and why you should read the diff rather than assume. ## Imported resources are the sharp edge The common production surprise is granting on an imported resource. `s3.Bucket.fromBucketName(this, 'Existing', 'my-bucket')` gives you a bucket object whose only knowledge is a name. The CDK will still write the S3 actions onto the grantee's role, but it does not know the bucket is encrypted with a customer managed key, so no decrypt permission is added — and the deploy succeeds while the function fails at runtime. Similarly, the CDK does not own that bucket's policy, so it cannot modify it; a cross-account grant on an imported bucket is a no-op on the bucket side. The fix is to import with the attributes the grant needs, using `fromBucketAttributes` and passing the `encryptionKey`, so the grant has something to grant on. ```ts const bucket = s3.Bucket.fromBucketAttributes(this, 'Existing', { bucketArn: 'arn:aws:s3:::my-bucket', encryptionKey: kms.Key.fromKeyArn(this, 'Key', keyArn), }); bucket.grantRead(fn); ``` ## What grants do not do They are not a compliance control. A grant produces the permissions the construct author considered appropriate for that verb, which is usually tighter than what a human would paste but is not minimal — the action wildcards mean a `grantRead` covers more than one API call. They do not replace reviewing the synthesized policy, and on a security-sensitive resource it is entirely reasonable to skip the helper and write the statement explicitly with `addToRolePolicy`. The argument for the helper is consistency and the parts you would forget, not perfection. ## Why an interviewer asks this Because the answer separates people who have used CDK from people who have read about it. Anyone can say "it grants read". The signal is in knowing the statement goes on the grantee, that two ARN forms are emitted, that the key permission comes along only when the app knows about the key, and that imported resources are where all three of those quietly stop being true.
- Why does a grant emit both the bucket ARN and the bucket ARN with a trailing /*?Because S3 splits actions across two resource shapes. Object actions such as GetObject apply to `arn:aws:s3:::bucket/*`, while bucket-level actions such as ListBucket apply to the bucket ARN itself. A policy naming only one of the two produces the classic symptom where objects can be fetched by key but the bucket cannot be listed.
- A team imports an existing encrypted bucket with fromBucketName and calls grantRead. The deploy is green and the function still gets AccessDenied. What happened?The imported construct knows only a name, so the CDK has no encryption key to grant on and emits S3 actions alone. The object is encrypted with a customer managed key the role cannot use. Import with `fromBucketAttributes` and pass the `encryptionKey`, or grant the key explicitly, so the decrypt permission is emitted too.
- Would you ever skip the grant helper and write the statement yourself?Yes, on a resource where the exact action set matters to an auditor. Grant helpers use action wildcards such as `s3:GetObject*`, which is a family rather than a single call. Where you need to justify each action, use `addToRolePolicy` with the explicit statement and accept that you now own keeping it correct, including any key permission.
saying these in an interview costs you the question
- Thinks grantRead edits the bucket policy in the same-stack case
- Believes grant helpers always produce truly minimal permissions
- Forgets the KMS key permission when the bucket is encrypted
- Assumes grants work identically on imported resources
- Grants a wildcard action set because the helper name sounded narrow