Host your own email and enhance your privacy
You must sign up for an Amazon Web Services account. You may use an existing account, but I recommend creating a dedicated account for this workload.
After signing up, perform the following steps:
Create an IAM policy called “cicd” with the following permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Vicious",
"Effect": "Allow",
"Action": [
"acm:AddTagsToCertificate",
"acm:DeleteCertificate",
"acm:DescribeCertificate",
"acm:ImportCertificate",
"acm:ListTagsForCertificate",
"acm:RenewCertificate",
"acm:RequestCertificate",
"apigateway:*",
"application-autoscaling:*",
"autoscaling:*",
"backup:*",
"backup-storage:*",
"cloudfront:*",
"cloudwatch:DeleteAlarms",
"cloudwatch:DescribeAlarms",
"cloudwatch:ListTagsForResource",
"cloudwatch:PutMetricAlarm",
"cloudwatch:TagResource",
"cloudwatch:UntagResource",
"cognito-identity:*",
"cognito-idp:*",
"dynamodb:*",
"ec2:*",
"ecr:*",
"ecs:*",
"elasticfilesystem:*",
"elasticloadbalancing:*",
"iam:*",
"imagebuilder:*",
"kms:*",
"lambda:*",
"logs:CreateLogGroup",
"logs:DescribeLogGroups",
"logs:ListTagsForResource",
"logs:ListTagsLogGroup",
"logs:PutRetentionPolicy",
"logs:TagResource",
"route53:ActivateKeySigningKey",
"route53:ChangeResourceRecordSets",
"route53:ChangeTagsForResource",
"route53:CreateHostedZone",
"route53:CreateKeySigningKey",
"route53:DeactivateKeySigningKey",
"route53:DeleteHostedZone",
"route53:DeleteKeySigningKey",
"route53:DisableHostedZoneDNSSEC",
"route53:EnableHostedZoneDNSSEC",
"route53:GetChange",
"route53:GetDNSSEC",
"route53:GetHostedZone",
"route53:ListHostedZonesByName",
"route53:ListResourceRecordSets",
"route53:ListTagsForResource",
"s3:*",
"s3-object-lambda:*",
"scheduler:*",
"servicediscovery:*",
"sms-voice:DescribePhoneNumbers",
"sms-voice:ListTagsForResource",
"sms-voice:ReleasePhoneNumber",
"sms-voice:RequestPhoneNumber",
"sms-voice:TagResource",
"sns:*",
"sqs:*",
"ssm:*",
"sts:GetCallerIdentity"
],
"Resource": "*"
}
]
}
(If you don’t intend to use this repo to configure AWS Backup, then you may omit the backup:* and backup-storage:* lines. If you do enable backups (TF_VAR_BACKUP), one additional one-time CLI step - the advanced DynamoDB backup-features opt-in - is required for cross-region copy; see Disaster recovery.)
(kms:* exists for DNSSEC signing (TF_VAR_DNSSEC_ENABLED, off by default). An enumerated grant was tried and abandoned: Terraform’s key lifecycle touched new kms: actions on every apply stage - more than twenty in all - and chasing them one AccessDenied at a time is not worth it for a policy that already carries iam:*. If you don’t intend to enable DNSSEC, you may shrink it to kms:CreateGrant and kms:DescribeKey, and omit the six route53: lines that mention KeySigningKey or DNSSEC - but not route53:GetDNSSEC, which Terraform reads unconditionally. See DNSSEC.)
(If you don’t intend to enable SMS verification through AWS End User Messaging (TF_VAR_USE_EUM_SMS, off by default), you may omit the five sms-voice: lines. If you do enable it, completing the toll-free verification registration requires an additional one-time policy; see SMS toll-free verification setup.)
(imagebuilder:* covers the EC2 Image Builder pipeline that bakes the custom NAT instance AMI. scheduler:* covers the EventBridge Scheduler schedules for certificate renewal and DMARC report processing.)
(Terraform state access is covered by s3:*. If your state bucket lives in a different AWS account than the one you are setting up here, that bucket’s policy must also grant this user access; no extra statements are needed in this policy.)
https://token.actions.githubusercontent.com, audience sts.amazonaws.com. (In the console “Add provider” flow, click “Get thumbprint”; AWS validates GitHub’s OIDC against its own certificate authority, so the thumbprint value is no longer load-bearing.)Create an IAM role for that web identity called “cicd”, attach the “cicd” policy from step 5, and set its trust policy to the following. Substitute your account ID, your fork’s owner/repo, and the environment that maps to this account – development, stage, or prod. GitHub Actions presents a short-lived OIDC token that this role trusts; no static access key is created or stored, and there is nothing to rotate.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:<OWNER>/<REPO>:environment:<ENVIRONMENT>"
}
}
}
]
}
The sub condition scopes role assumption to GitHub Actions jobs bound to that GitHub Environment in your fork (the deploy workflows all bind one). If you ever run more than one environment from a single account (not recommended), add one sub line per environment. Note the role ARN – arn:aws:iam::<ACCOUNT_ID>:role/cicd – you will need it when you set up GitHub.
If you have followed the recommendation to create a dedicated account, then the above steps should be the only manual steps required in this account. Everything else should be managed by Terraform.