CloudFormation’s ability to manage IAM resource-based policies is surprisingly difficult to get right because the policy document itself is treated as a simple JSON string, hiding the complex validation and dependency management that actually occurs.
Let’s watch this in action. Imagine you want to grant an S3 bucket permission to be read by a specific Lambda function. You’d define your bucket and then attach a BucketPolicy resource.
Resources:
MyS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-unique-bucket-name-12345
MyBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref MyS3Bucket
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: AllowLambdaRead
Effect: Allow
Principal:
AWS: !GetAtt MyLambdaFunction.Arn
Action:
- s3:GetObject
Resource: !Join ['', ['arn:aws:s3:::', !Ref MyS3Bucket, '/*']]
Here, MyLambdaFunction is another resource defined elsewhere in the template. CloudFormation needs to resolve the ARN of MyLambdaFunction before it can construct the PolicyDocument string, and then it needs to ensure the S3 bucket exists before applying the policy. This looks simple, but the dependency chain is crucial.
The core problem CloudFormation is solving here is enabling you to declare what you want (an S3 bucket with specific access) and how it should be configured, without dictating the exact order of operations. It figures out that the Lambda function ARN must be known, the bucket must exist, and then the policy can be applied. It’s a declarative system that infers execution order from resource dependencies. The PolicyDocument is a JSON object, but CloudFormation serializes it into a string for the BucketPolicy resource. This string serialization is where many subtle bugs hide.
The Bucket property in AWS::S3::BucketPolicy is not just a string name; it’s a reference to the bucket resource. CloudFormation resolves this reference, ensuring the bucket exists before trying to attach a policy to it. Similarly, !GetAtt MyLambdaFunction.Arn ensures the Lambda function is provisioned and its ARN is available before the policy is created. This dependency management is what prevents the "resource not found" errors you’d get if you tried to apply a policy before the target resource.
When you define a resource-based policy, like an S3 bucket policy or an SNS topic policy, you’re essentially embedding an IAM policy directly onto the resource itself. This is distinct from IAM policies attached to users, groups, or roles. Resource-based policies grant cross-account or AWS service access. For example, allowing another AWS account to access your S3 bucket or letting an EC2 instance assume a role.
The PolicyDocument itself follows the standard IAM policy structure: Version, Statement (an array of individual policy statements). Each statement has Sid (optional identifier), Effect (Allow/Deny), Principal (who is granted or denied access), Action (what operations are allowed/denied), and Resource (which resources the action applies to). The Principal can be an AWS account (AWS: arn:aws:iam::123456789012:root), a specific IAM user or role (AWS: arn:aws:iam::123456789012:user/MyUser), or a service principal (Service: lambda.amazonaws.com).
The most common pitfall is how CloudFormation handles the PolicyDocument. If the PolicyDocument contains invalid JSON, or if any of the ARNs or principals within it are malformed or refer to resources that don’t exist (and aren’t defined in the same stack or a dependency stack), CloudFormation will fail the stack update. It’s not just about valid JSON; it’s about valid references within that JSON.
The surprising part is how CloudFormation handles the serialization of the PolicyDocument. It doesn’t just copy the JSON object. It serializes it and then passes that string to the underlying AWS API. If your PolicyDocument uses intrinsic functions like !Ref or !GetAtt, CloudFormation resolves these before serializing the JSON string. This means the resolved JSON string is what gets sent to AWS. Any error in the resolution of these intrinsic functions will manifest as a policy application failure. For instance, if !GetAtt MyLambdaFunction.Arn fails because MyLambdaFunction is misspelled or not in the stack, the PolicyDocument string sent to S3 will be invalid, and the AWS::S3::BucketPolicy resource will fail.
If your AWS::S3::BucketPolicy resource fails with a message like Policy is not valid, it’s often because the PolicyDocument string generated by CloudFormation contains an invalid ARN or a principal that doesn’t resolve correctly. The fix is to meticulously check every !Ref and !GetAtt within your PolicyDocument against the actual resource names and attributes.
The next concept to grapple with is managing multiple resource-based policies on a single resource, which often requires careful ordering or using custom resources if direct CloudFormation support is insufficient.