Sync from Git needs an IAM role that CloudFormation assumes to create resources in your stacks. This role gets its own repo and its own stack. You create it once, complete, and never come back to add permissions as the series grows.
Every permission the certificate and site stacks need is here from the start. The permissions live in three customer-managed policies attached to the role, not one inline block, for a reason covered below.
Trust Policy
The AssumeRolePolicyDocument is a trust policy. It says who may assume the role, not what the role can do. The permissions come from the managed policies in ManagedPolicyArns.
AWSTemplateFormatVersion: '2010-09-09'
Description: Git Sync IAM role that CloudFormation assumes to deploy the certificate and site stacks.
Resources:
CloudFormationGitSyncRole:
Type: 'AWS::IAM::Role'
Properties:
RoleName: CloudFormationGitSyncRole
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- cloudformation.sync.codeconnections.amazonaws.com
- cloudformation.amazonaws.com
Action: 'sts:AssumeRole'
Condition:
StringEquals:
'aws:SourceAccount': !Sub '${AWS::AccountId}'
ManagedPolicyArns:
- !Ref GitSyncPlatformPolicy
- !Ref CertificateStackPolicy
- !Ref SiteStackPolicy
cloudformation.sync.codeconnections.amazonaws.comis the Git Sync service that starts a deployment when your repo changes.cloudformation.amazonaws.comis CloudFormation itself, which assumes the role to create, update, and delete resources.aws:SourceAccountlocks both to your account, closing the confused deputy hole.
Why Managed Policies, Not Inline
Putting every permission in a single inline policy hits a wall. A role’s aggregate inline policy size caps at 10,240 characters (IAM does not count whitespace). This role touches CloudFormation, ACM, Route 53, S3, CloudFront, CodePipeline, CodeBuild, Lambda, IAM, and more, and once you scope statements to real ARNs the character count climbs fast.
Splitting into several inline policies does not help, because the 10,240 limit is the sum of all inline policies on the role.
Customer-managed policies have a separate budget: each can be up to 6,144 characters, and you can attach up to 10 to one role by default, raising to 20 with a quota increase. Splitting by concern across three of them takes the ceiling from 10,240 to roughly 18 KB now, expandable to around 120 KB, all on the same role. As a bonus, a managed policy is a standalone resource, so another role in the account could attach the same SiteStackPolicy by ARN instead of redefining it.
The split, by concern:
GitSyncPlatformPolicy: CloudFormation, EventBridge, and the role’s self-management.CertificateStackPolicy: ACM and Route 53.SiteStackPolicy: S3, CloudFront, CodePipeline, CodeBuild, Lambda, the site’s IAM roles, PassConnection, and Logs.
Platform Policy
CloudFormation change sets and EventBridge are what Git Sync needs regardless of what it deploys.
EventBridge is AWS’s event bus. Services emit events when something changes, and rules route those events to targets. Git Sync uses it to react to your repo: it creates and manages a few EventBridge rules so a push can trigger a deployment automatically. The events:ManagedBy condition fences the write actions to only the rules the Git Sync service owns, and DescribeRule lets it read them back. These statements come straight from the Git Sync prerequisites.
The last two statements are self-management: they let Git Sync update this role and push permission edits as new policy versions. The managed policies sit under the IAM path /gitsync/, so the self-management scopes to policy/gitsync/*.
GitSyncPlatformPolicy:
Type: 'AWS::IAM::ManagedPolicy'
Properties:
Path: /gitsync/
Description: CloudFormation, EventBridge, and self-management for the Git Sync role.
PolicyDocument:
Version: '2012-10-17'
Statement:
# Plan and apply stack changes through change sets.
- Effect: Allow
Action:
- 'cloudformation:CreateChangeSet'
- 'cloudformation:DeleteChangeSet'
- 'cloudformation:DescribeChangeSet'
- 'cloudformation:DescribeStackEvents'
- 'cloudformation:DescribeStacks'
- 'cloudformation:ExecuteChangeSet'
- 'cloudformation:GetTemplate'
- 'cloudformation:ListChangeSets'
- 'cloudformation:ListStacks'
- 'cloudformation:ValidateTemplate'
Resource: '*'
# Manage the EventBridge rules Git Sync owns for its sync triggers.
- Effect: Allow
Action:
- 'events:PutRule'
- 'events:PutTargets'
- 'events:DeleteRule'
- 'events:RemoveTargets'
Resource: '*'
Condition:
StringEquals:
'events:ManagedBy': 'cloudformation.sync.codeconnections.amazonaws.com'
# Read those EventBridge rules.
- Effect: Allow
Action:
- 'events:DescribeRule'
Resource: '*'
# Update this role itself: trust policy, tags, and policy attachments.
- Effect: Allow
Action:
- 'iam:GetRole'
- 'iam:UpdateAssumeRolePolicy'
- 'iam:AttachRolePolicy'
- 'iam:DetachRolePolicy'
- 'iam:ListAttachedRolePolicies'
- 'iam:ListRolePolicies'
- 'iam:TagRole'
- 'iam:UntagRole'
- 'iam:ListRoleTags'
Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/CloudFormationGitSyncRole'
# Version this role's own managed policies so Git Sync can edit them.
- Effect: Allow
Action:
- 'iam:CreatePolicy'
- 'iam:DeletePolicy'
- 'iam:GetPolicy'
- 'iam:GetPolicyVersion'
- 'iam:ListPolicyVersions'
- 'iam:CreatePolicyVersion'
- 'iam:DeletePolicyVersion'
- 'iam:SetDefaultPolicyVersion'
Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:policy/gitsync/*'
Certificate and Site Policies
The certificate policy is small: ACM, plus the Route 53 actions it validates against (the site stack reuses Route 53 for its alias records).
CertificateStackPolicy:
Type: 'AWS::IAM::ManagedPolicy'
Properties:
Path: /gitsync/
Description: ACM and Route 53 for the certificate stack and DNS records.
PolicyDocument:
Version: '2012-10-17'
Statement:
# Issue, tag, and delete the site's ACM certificate.
- Effect: Allow
Action:
- 'acm:RequestCertificate'
- 'acm:DescribeCertificate'
- 'acm:DeleteCertificate'
- 'acm:AddTagsToCertificate'
- 'acm:RemoveTagsFromCertificate'
- 'acm:ListTagsForCertificate'
- 'acm:ListCertificates'
Resource: '*'
# Write DNS validation and alias records in the hosted zone.
- Effect: Allow
Action:
- 'route53:ChangeResourceRecordSets'
- 'route53:GetHostedZone'
- 'route53:GetChange'
- 'route53:ListResourceRecordSets'
Resource: '*'
The site policy is the large one. It carries S3, CloudFront, the pipeline, the empty-bucket Lambda, the site’s own service roles, PassConnection, and Logs. The IAM and PassRole statements are scoped to the /site/ path (see the next section).
SiteStackPolicy:
Type: 'AWS::IAM::ManagedPolicy'
Properties:
Path: /gitsync/
Description: S3, CloudFront, CodePipeline, CodeBuild, Lambda, site IAM roles, and logs for the site stack.
PolicyDocument:
Version: '2012-10-17'
Statement:
# Create and configure the website and artifact buckets.
- Effect: Allow
Action:
- 's3:CreateBucket'
- 's3:DeleteBucket'
- 's3:PutBucketPublicAccessBlock'
- 's3:GetBucketPublicAccessBlock'
- 's3:PutBucketVersioning'
- 's3:GetBucketVersioning'
- 's3:PutEncryptionConfiguration'
- 's3:GetEncryptionConfiguration'
- 's3:PutLifecycleConfiguration'
- 's3:GetLifecycleConfiguration'
- 's3:PutBucketTagging'
- 's3:GetBucketTagging'
- 's3:PutBucketPolicy'
- 's3:GetBucketPolicy'
- 's3:DeleteBucketPolicy'
Resource: '*'
# Manage the distribution, origin access control, headers policy, and function.
- Effect: Allow
Action:
- 'cloudfront:CreateDistribution'
- 'cloudfront:UpdateDistribution'
- 'cloudfront:DeleteDistribution'
- 'cloudfront:GetDistribution'
- 'cloudfront:TagResource'
- 'cloudfront:UntagResource'
- 'cloudfront:ListTagsForResource'
- 'cloudfront:CreateOriginAccessControl'
- 'cloudfront:DeleteOriginAccessControl'
- 'cloudfront:GetOriginAccessControl'
- 'cloudfront:UpdateOriginAccessControl'
- 'cloudfront:CreateResponseHeadersPolicy'
- 'cloudfront:GetResponseHeadersPolicy'
- 'cloudfront:UpdateResponseHeadersPolicy'
- 'cloudfront:DeleteResponseHeadersPolicy'
- 'cloudfront:CreateFunction'
- 'cloudfront:GetFunction'
- 'cloudfront:UpdateFunction'
- 'cloudfront:DeleteFunction'
- 'cloudfront:PublishFunction'
- 'cloudfront:DescribeFunction'
Resource: '*'
# Manage the CI/CD pipeline.
- Effect: Allow
Action:
- 'codepipeline:CreatePipeline'
- 'codepipeline:UpdatePipeline'
- 'codepipeline:DeletePipeline'
- 'codepipeline:GetPipeline'
- 'codepipeline:GetPipelineState'
- 'codepipeline:TagResource'
- 'codepipeline:UntagResource'
- 'codepipeline:ListTagsForResource'
Resource: '*'
# Manage the build and deploy projects.
- Effect: Allow
Action:
- 'codebuild:CreateProject'
- 'codebuild:UpdateProject'
- 'codebuild:DeleteProject'
- 'codebuild:BatchGetProjects'
Resource: '*'
# Manage the empty-bucket custom-resource function.
- Effect: Allow
Action:
- 'lambda:CreateFunction'
- 'lambda:DeleteFunction'
- 'lambda:GetFunction'
- 'lambda:GetFunctionConfiguration'
- 'lambda:UpdateFunctionCode'
- 'lambda:UpdateFunctionConfiguration'
- 'lambda:InvokeFunction'
- 'lambda:TagResource'
- 'lambda:UntagResource'
- 'lambda:ListTags'
Resource: !Sub 'arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:*EmptyBucket'
# Create and manage the site stack's service roles under /site/.
- Effect: Allow
Action:
- 'iam:CreateRole'
- 'iam:GetRole'
- 'iam:DeleteRole'
- 'iam:UpdateRole'
- 'iam:PutRolePolicy'
- 'iam:GetRolePolicy'
- 'iam:DeleteRolePolicy'
- 'iam:ListRolePolicies'
- 'iam:ListAttachedRolePolicies'
- 'iam:ListRoleTags'
- 'iam:TagRole'
- 'iam:UntagRole'
- 'iam:UpdateAssumeRolePolicy'
Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/site/*'
# Hand those roles to CodePipeline, CodeBuild, and Lambda only.
- Effect: Allow
Action:
- 'iam:PassRole'
Resource: !Sub 'arn:aws:iam::${AWS::AccountId}:role/site/*'
Condition:
StringEquals:
'iam:PassedToService':
- codepipeline.amazonaws.com
- codebuild.amazonaws.com
- lambda.amazonaws.com
# Pass the shared GitHub connection to the pipeline (legacy prefix).
- Effect: Allow
Action:
- 'codestar-connections:PassConnection'
Resource: !ImportValue 'Example-GitHubConnectionArn'
Condition:
StringEquals:
'codestar-connections:PassedToService': codepipeline.amazonaws.com
# Pass the shared GitHub connection to the pipeline (current prefix).
- Effect: Allow
Action:
- 'codeconnections:PassConnection'
Resource: !ImportValue 'Example-GitHubConnectionArn'
Condition:
StringEquals:
'codeconnections:PassedToService': codepipeline.amazonaws.com
# Manage log groups for the build, deploy, and Lambda.
- Effect: Allow
Action:
- 'logs:CreateLogGroup'
- 'logs:DeleteLogGroup'
- 'logs:PutRetentionPolicy'
Resource: '*'
Scoping by Path, Not Stack Name
When CloudFormation deploys the site stack as this role, the role has to create that stack’s service roles and pass them to AWS services. Granted on everything, iam:CreateRole and iam:PassRole are close to account admin, so they need to point at the site stack’s roles and nothing else.
You can’t point at them by stack name: ${AWS::StackName} only ever means the current stack, and this role lives in a different one. Three options:
- A
SiteStackNameparameter you pass in and keep in sync. - A
your-site-stack-*placeholder you hand-edit to the real name. - An IAM path, which this uses: the site stack puts every service role under
/site/, and the policy scopes torole/site/*. No name to share, nothing to edit.
A path, unlike a custom role name, keeps the site stack on plain CAPABILITY_IAM, the acknowledgement you give to let a template create IAM resources, rather than the stricter CAPABILITY_NAMED_IAM.
Keep it scoped: on '*', these actions let anyone who can push mint an admin role. The role/site/* scope, the PassedToService condition, and branch protection are the guardrails.
Deploying the Role
One prerequisite: deploy the connection stack from post 2 first. SiteStackPolicy imports its Example-GitHubConnectionArn export, so the role will not deploy until that export exists.
The role cannot bootstrap itself, so the first create is manual.
- Open CloudFormation > Create stack > With new resources, choose Upload a template file, and upload
infra.yaml. - Name the stack, acknowledge the IAM capability (this role has an explicit
RoleName, so the console asks forCAPABILITY_NAMED_IAM), and create it.
Then connect Git Sync to this repo so every later edit deploys on push: open the stack’s Git sync tab, Connect, pick the CodeConnection from post 2, this repo and branch, and deployment-file.yaml. Select CloudFormationGitSyncRole itself as the role. It can update itself because GitSyncPlatformPolicy lets it version its own managed policies (iam:CreatePolicyVersion on policy/gitsync/*) and attach them to the role.
One caution: this role manages the stack that defines it. Protect main and keep push access as tight as IAM access in the console.