Skip to content

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)

json
{
  "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": "*"
    }
  ]
}
ActionWhy
s3:DeleteObjectsBulk version/marker purge during destroy
s3:GetObjectPresign for bucket share
s3:ListBucketVersionsVersioned empty before DeleteBucket

CloudWatch Logs

json
{
  "Effect": "Allow",
  "Action": [
    "logs:DescribeLogGroups",
    "logs:FilterLogEvents"
  ],
  "Resource": "*"
}

SSM Parameter Store (secrets)

json
{
  "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):

json
{
  "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)

json
{
  "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):

json
{
  "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):

json
{
  "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:

json
{
  "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:

json
{
  "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)

json
{
  "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:

json
{
  "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.

json
{
  "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).

Working name — your AWS account, your bill.