IAM permissions
Least-privilege sketch for what's shipped today. Tighten Resource in production — these examples use "*" so the wedge works out of the box.
S3 (create / list / share / destroy)
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:DeleteBucket",
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:DeleteObjects",
"s3:GetBucketTagging",
"s3:GetObject",
"s3:HeadBucket",
"s3:ListAllMyBuckets",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:PutBucketEncryption",
"s3:PutBucketTagging",
"s3:PutBucketVersioning",
"s3:PutPublicAccessBlock"
],
"Resource": "*"
}
]
}| Action | Why |
|---|---|
s3:DeleteObjects | Bulk version/marker purge during destroy |
s3:GetObject | Presign for bucket share |
s3:ListBucketVersions | Versioned empty before DeleteBucket |
CloudWatch Logs
{
"Effect": "Allow",
"Action": [
"logs:DescribeLogGroups",
"logs:FilterLogEvents"
],
"Resource": "*"
}SSM Parameter Store (secrets)
{
"Effect": "Allow",
"Action": [
"ssm:PutParameter",
"ssm:GetParameter",
"ssm:GetParametersByPath",
"ssm:DeleteParameter",
"ssm:AddTagsToResource",
"ssm:ListTagsForResource"
],
"Resource": "*"
}Prefer scoping Resource to arn:aws:ssm:*:*:parameter/utopia/* in production.
Lambda + IAM (functions)
Needed for create fn / destroy fn / list (and for cron to grant invoke):
{
"Effect": "Allow",
"Action": [
"lambda:CreateFunction",
"lambda:UpdateFunctionCode",
"lambda:GetFunction",
"lambda:DeleteFunction",
"lambda:ListFunctions",
"lambda:ListTags",
"lambda:TagResource",
"lambda:CreateFunctionUrlConfig",
"lambda:GetFunctionUrlConfig",
"lambda:AddPermission",
"iam:CreateRole",
"iam:GetRole",
"iam:AttachRolePolicy",
"iam:PassRole",
"iam:TagRole"
],
"Resource": "*"
}create fn auto-creates utopia-{project}-{env}-fn-role with AWSLambdaBasicExecutionRole unless you pass --role.
EventBridge (cron)
{
"Effect": "Allow",
"Action": [
"events:PutRule",
"events:PutTargets",
"events:DeleteRule",
"events:RemoveTargets",
"events:DescribeRule",
"events:ListRules",
"events:ListTargetsByRule",
"events:TagResource",
"events:ListTagsForResource"
],
"Resource": "*"
}CloudFront + S3 origin (sites)
Needed for create site / destroy site / list (sites):
{
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:DeleteBucket",
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:DeleteObjects",
"s3:GetBucketTagging",
"s3:HeadBucket",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:PutBucketEncryption",
"s3:PutBucketPolicy",
"s3:PutBucketTagging",
"s3:PutBucketVersioning",
"s3:PutObject",
"s3:PutPublicAccessBlock",
"cloudfront:CreateDistribution",
"cloudfront:GetDistribution",
"cloudfront:GetDistributionConfig",
"cloudfront:UpdateDistribution",
"cloudfront:DeleteDistribution",
"cloudfront:ListDistributions",
"cloudfront:TagResource",
"cloudfront:ListTagsForResource",
"cloudfront:CreateOriginAccessControl",
"cloudfront:DeleteOriginAccessControl",
"sts:GetCallerIdentity"
],
"Resource": "*"
}Site origins stay private; CloudFront reads via OAC + bucket policy.
ACM + Route53 (custom domains)
Needed for create domain (and best-effort cert cleanup on destroy site):
{
"Effect": "Allow",
"Action": [
"acm:RequestCertificate",
"acm:DescribeCertificate",
"acm:DeleteCertificate",
"acm:ListCertificates",
"acm:ListTagsForCertificate",
"acm:AddTagsToCertificate",
"route53:ListHostedZonesByName",
"route53:ChangeResourceRecordSets"
],
"Resource": "*"
}ACM for CloudFront custom certs must be in us-east-1 — the CLI pins that client region automatically.
EC2 + SSM (servers)
Needed for create server / destroy server / list / connect:
{
"Effect": "Allow",
"Action": [
"ec2:DescribeVpcs",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:CreateSecurityGroup",
"ec2:DeleteSecurityGroup",
"ec2:CreateTags",
"ec2:RunInstances",
"ec2:DescribeInstances",
"ec2:TerminateInstances",
"iam:CreateRole",
"iam:GetRole",
"iam:AttachRolePolicy",
"iam:PassRole",
"iam:TagRole",
"iam:CreateInstanceProfile",
"iam:GetInstanceProfile",
"iam:AddRoleToInstanceProfile",
"iam:TagInstanceProfile",
"ssm:GetParameter",
"ssm:DescribeInstanceInformation",
"ssm:StartSession"
],
"Resource": "*"
}create server attaches AmazonSSMManagedInstanceCore to utopia-{project}-{env}-server-role. Prefer scoping iam:PassRole to that role pattern. Your laptop also needs the Session Manager plugin for connect.
RDS (databases)
Needed for create db / destroy db / list / db connect:
{
"Effect": "Allow",
"Action": [
"rds:CreateDBInstance",
"rds:DeleteDBInstance",
"rds:DescribeDBInstances",
"rds:CreateDBSubnetGroup",
"rds:DescribeDBSubnetGroups",
"rds:AddTagsToResource",
"rds:ListTagsForResource",
"ec2:DescribeVpcs",
"ec2:DescribeSubnets",
"ec2:DescribeSecurityGroups",
"ec2:CreateSecurityGroup",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:CreateTags",
"ssm:PutParameter",
"ssm:GetParameter",
"ssm:AddTagsToResource"
],
"Resource": "*"
}Instances are private (PubliclyAccessible=false). Connection URLs go to SSM — never echo passwords in logs.
DynamoDB (kv)
{
"Effect": "Allow",
"Action": [
"dynamodb:CreateTable",
"dynamodb:DeleteTable",
"dynamodb:DescribeTable",
"dynamodb:ListTables",
"dynamodb:TagResource",
"dynamodb:ListTagsOfResource"
],
"Resource": "*"
}SQS (queue)
Needed for create queue / destroy queue / list:
{
"Effect": "Allow",
"Action": [
"sqs:CreateQueue",
"sqs:DeleteQueue",
"sqs:GetQueueUrl",
"sqs:GetQueueAttributes",
"sqs:ListQueues",
"sqs:ListQueueTags",
"sqs:TagQueue"
],
"Resource": "*"
}(sqs:* on utopia queues is fine if you prefer a broader allow.)
Cost Explorer (list / costs)
Resource-level costs call us-east-1 Cost Explorer. Without these, inventory still works — rows show COST_UNAVAILABLE.
{
"Effect": "Allow",
"Action": [
"ce:GetCostAndUsage",
"ce:GetCostAndUsageWithResources",
"ce:GetTags",
"ce:GetDimensionValues"
],
"Resource": "*"
}Notes:
- Prefer
ce:GetCostAndUsageWithResources(what the CLI calls today). - Resource-level billing data can lag / require CE enablement in the account.
- Denied CE must never block
list.
STS
Comes along with the default credential chain (GetCallerIdentity during login).