Podmin AWS IAM Roles
This guide covers the recommended permissions and roles to assume when a user or CI build invokes the Podmin CLI.
Users should assume a role through an AWS profile.
CI systems should use short-lived federated credentials instead of stored access keys.
Recommended Role Boundaries
Use these roles for a production cluster:
| Role | Podmin commands | Recommendation |
|---|---|---|
| Cluster Operator | setup, teardown |
Setup is authoritative and can replace or remove NodeGroups; teardown removes cluster compute and Podmin-managed networking. |
| Super Admin | destroy |
Recommended to be seperate from the Cluster operation and outside routine automation e.g. only for use by an administrator or break-glass role with an explicit approval step. |
| Deployer | push, deploy, delete, list |
Preferrably one role per application or deployment pipeline - scope image and immutable workload writes where practical. |
Most teams should start with these three roles. A single role gives every user and CI build infrastructure and permanent-destruction privileges. Separate push and deploy only when builds and deployments have different owners.
Configure IAM OIDC Trust Policy to Assume a Role
The following example uses GitHub Actions.
Add GitHub's OIDC provider to AWS with audience sts.amazonaws.com, then add this trust policy to the role. Replace the angle-bracketed values:
{
"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",
"token.actions.githubusercontent.com:sub": "repo:<owner>/<repository>:environment:production"
}
}
}
]
}
This example requires environment: production. For a branch-bound role, use repo:<owner>/<repository>:ref:refs/heads/main instead. Add each allowed subject explicitly; do not use an organization-wide wildcard.
This trust policy allows only the selected GitHub workflows to exchange their OIDC tokens for temporary credentials for the role.
Create a Deployer Role & Policy
A deployer needs access only to an existing cluster bucket. This example allows it to:
- connect to
<bucket>; - push to
apps/<image-repository>/; - deploy to NodeGroup
<nodegroup>; and - publish Service
<namespace>/<service>.
Create a role with the following inline policy, replacing each angle-bracketed value and adding resource entries for any additional image repositories, NodeGroups, or Services:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ConnectToClusterBucket",
"Effect": "Allow",
"Action": [
"s3:GetBucketPublicAccessBlock",
"s3:GetBucketLocation",
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::<bucket>"
},
{
"Sid": "VerifyClusterBucketAccess",
"Effect": "Allow",
"Action": [
"s3:DeleteObject",
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::<bucket>/.podmin-access-check-*"
},
{
"Sid": "PublishApplicationImages",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::<bucket>/apps/<image-repository>/*"
},
{
"Sid": "PublishImmutableWorkloads",
"Effect": "Allow",
"Action": "s3:PutObject",
"Resource": [
"arn:aws:s3:::<bucket>/nodegroups/<nodegroup>/pods/*",
"arn:aws:s3:::<bucket>/services/<namespace>/<service>/*"
]
},
{
"Sid": "ReadDeploymentInventory",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<bucket>/nodegroups/*/pods/*"
},
{
"Sid": "CommitDesiredState",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::<bucket>/deployments/index.json"
}
]
}
The first two statements support podmin connect. Connect reads the public-access block and changes it only when protection is missing. Because the deployer cannot make that change, an unsafe bucket causes connect to fail until a Cluster Operator repairs it.
Use an explicit push destination so the S3 path is predictable:
podmin push "web:v1" "web:v1"
This writes beneath apps/web/. Remove ReadDeploymentInventory if the role does not run podmin list; listing requires every referenced Pod payload and cannot be read-scoped to one NodeGroup.
Understand the Deployment Scoping Limit
Image, Pod, and Service writes can be prefix-scoped. Deploy and delete cannot: both replace the cluster-wide deployments/index.json, and IAM cannot restrict changes within its JSON body. Separate deployer roles therefore do not provide strict application isolation; use separate clusters when required.
Add Secret Management Permissions
Add this policy only if the role runs podmin secret commands. Replace the angle-bracketed values, then remove statements for providers or operations it does not use:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ManageParameterStoreSecrets",
"Effect": "Allow",
"Action": [
"ssm:DeleteParameter",
"ssm:GetParametersByPath",
"ssm:PutParameter"
],
"Resource": "arn:aws:ssm:<region>:<account-id>:parameter/<cluster>/<namespace>/<app>/*"
},
{
"Sid": "ManageSecretsManagerSecrets",
"Effect": "Allow",
"Action": [
"secretsmanager:CreateSecret",
"secretsmanager:DeleteSecret",
"secretsmanager:PutSecretValue",
"secretsmanager:RestoreSecret"
],
"Resource": "arn:aws:secretsmanager:<region>:<account-id>:secret:/<cluster>/<namespace>/<app>/*"
},
{
"Sid": "ListSecretsManagerSecrets",
"Effect": "Allow",
"Action": "secretsmanager:ListSecrets",
"Resource": "*"
}
]
}
Customer-managed KMS keys require separately scoped KMS access. Prefer a separate secret-management role.
When setup uses --workload-ca-publish s3://BUCKET/KEY, the generated EC2 instance role and S3 VPC endpoint policy additionally receive s3:GetObject and s3:PutObject for only arn:aws:s3:::BUCKET/KEY. The destination bucket must already exist. Its bucket policy and any customer-managed KMS key policy must permit the generated role, including when the bucket is in another account; Podmin does not create those policies or delete the object.
When --otel-logs uses ca=s3://BUCKET/KEY, the generated role and endpoint policy additionally receive s3:GetObject for arn:aws:s3:::BUCKET/KEY. In the same account that permission is sufficient unless the bucket or a customer-managed KMS key has a restrictive policy; cross-account access also requires the destination policies to permit the generated role.
Create a Cluster Operator Role & Policy
The Cluster Operator runs Podmin's AWS SDK calls and embedded OpenTofu/Terraform. Replace the angle-bracketed values in this starting policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ManageClusterBucket",
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:GetBucketLocation",
"s3:GetBucketPublicAccessBlock",
"s3:ListBucket",
"s3:PutBucketPublicAccessBlock"
],
"Resource": "arn:aws:s3:::<bucket>"
},
{
"Sid": "VerifyClusterBucketAccess",
"Effect": "Allow",
"Action": [
"s3:DeleteObject",
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::<bucket>/.podmin-access-check-*"
},
{
"Sid": "ManageSetupAndStateObjects",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": [
"arn:aws:s3:::<bucket>/dependencies/*",
"arn:aws:s3:::<bucket>/mirror/*",
"arn:aws:s3:::<bucket>/tfstate/*"
]
},
{
"Sid": "DeleteExpiredDependenciesAndStateLocks",
"Effect": "Allow",
"Action": "s3:DeleteObject",
"Resource": [
"arn:aws:s3:::<bucket>/dependencies/*",
"arn:aws:s3:::<bucket>/tfstate/*"
]
},
{
"Sid": "CreateClusterCertificateAuthorities",
"Effect": "Allow",
"Action": "ssm:PutParameter",
"Resource": [
"arn:aws:ssm:<region>:<account-id>:parameter/<cluster>/_system/workload-ca-key",
"arn:aws:ssm:<region>:<account-id>:parameter/<cluster>/_system/cluster-ca"
]
},
{
"Sid": "ReadEC2State",
"Effect": "Allow",
"Action": [
"ec2:Describe*",
"ec2:GetInstanceUefiData"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "<region>"
}
}
},
{
"Sid": "ManageEC2Infrastructure",
"Effect": "Allow",
"Action": [
"ec2:AssociateRouteTable",
"ec2:AssociateSubnetCidrBlock",
"ec2:AssociateVpcCidrBlock",
"ec2:AttachInternetGateway",
"ec2:AuthorizeSecurityGroupEgress",
"ec2:AuthorizeSecurityGroupIngress",
"ec2:CreateInternetGateway",
"ec2:CreateLaunchTemplate",
"ec2:CreateLaunchTemplateVersion",
"ec2:CreateRoute",
"ec2:CreateRouteTable",
"ec2:CreateSecurityGroup",
"ec2:CreateSubnet",
"ec2:CreateTags",
"ec2:CreateVpc",
"ec2:CreateVpcEndpoint",
"ec2:DeleteInternetGateway",
"ec2:DeleteLaunchTemplate",
"ec2:DeleteLaunchTemplateVersions",
"ec2:DeleteRoute",
"ec2:DeleteRouteTable",
"ec2:DeleteSecurityGroup",
"ec2:DeleteSubnet",
"ec2:DeleteTags",
"ec2:DeleteVpc",
"ec2:DeleteVpcEndpoints",
"ec2:DetachInternetGateway",
"ec2:DisassociateRouteTable",
"ec2:DisassociateSubnetCidrBlock",
"ec2:DisassociateVpcCidrBlock",
"ec2:ModifyLaunchTemplate",
"ec2:ModifySecurityGroupRules",
"ec2:ModifySubnetAttribute",
"ec2:ModifyVpcAttribute",
"ec2:ModifyVpcEndpoint",
"ec2:ReplaceRoute",
"ec2:ReplaceRouteTableAssociation",
"ec2:RevokeSecurityGroupEgress",
"ec2:RevokeSecurityGroupIngress"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "<region>"
}
}
},
{
"Sid": "ManageAutoScalingGroups",
"Effect": "Allow",
"Action": [
"autoscaling:CancelInstanceRefresh",
"autoscaling:CreateAutoScalingGroup",
"autoscaling:CreateOrUpdateTags",
"autoscaling:DeleteAutoScalingGroup",
"autoscaling:DeleteTags",
"autoscaling:Describe*",
"autoscaling:StartInstanceRefresh",
"autoscaling:UpdateAutoScalingGroup"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:RequestedRegion": "<region>"
}
}
},
{
"Sid": "ManageClusterInstanceIdentity",
"Effect": "Allow",
"Action": [
"iam:AddRoleToInstanceProfile",
"iam:CreateInstanceProfile",
"iam:CreateRole",
"iam:DeleteInstanceProfile",
"iam:DeleteRole",
"iam:DeleteRolePolicy",
"iam:GetInstanceProfile",
"iam:GetRole",
"iam:GetRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListInstanceProfileTags",
"iam:ListInstanceProfilesForRole",
"iam:ListRolePolicies",
"iam:ListRoleTags",
"iam:PutRolePolicy",
"iam:RemoveRoleFromInstanceProfile",
"iam:TagInstanceProfile",
"iam:TagRole",
"iam:UntagInstanceProfile",
"iam:UntagRole",
"iam:UpdateAssumeRolePolicy",
"iam:UpdateRole",
"iam:UpdateRoleDescription"
],
"Resource": [
"arn:aws:iam::<account-id>:role/<cluster>-*",
"arn:aws:iam::<account-id>:instance-profile/<cluster>-*"
]
},
{
"Sid": "PassClusterInstanceRole",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": "arn:aws:iam::<account-id>:role/<cluster>-*",
"Condition": {
"StringEquals": {
"iam:PassedToService": "ec2.amazonaws.com"
}
}
},
{
"Sid": "CreateAutoScalingServiceLinkedRole",
"Effect": "Allow",
"Action": "iam:CreateServiceLinkedRole",
"Resource": "arn:aws:iam::<account-id>:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling",
"Condition": {
"StringEquals": {
"iam:AWSServiceName": "autoscaling.amazonaws.com"
}
}
}
]
}
The regional EC2 and Auto Scaling actions use Resource: "*" because several required APIs cannot be resource-scoped. Test this policy in a non-production account, then tighten it with CloudTrail or IAM Access Analyzer. Recheck it after Podmin or AWS provider upgrades.
Create a Super Admin Policy
podmin teardown stays with the cluster operator because it preserves the bucket and certificate authorities. For podmin destroy, start with teardown permissions and add:
ssm:DeleteParameterfor the exact/<cluster>/_system/workload-ca-keyand/<cluster>/_system/cluster-caparameter ARNs;s3:ListBucketands3:ListBucketVersionsfor the cluster bucket;s3:DeleteObjectands3:DeleteObjectVersionfor every object in the cluster bucket; ands3:DeleteBucketfor the cluster bucket.
Destroy must access the entire bucket, including versions and delete markers. Keep these permissions out of cluster operator and deployer roles. If GitHub Actions must run destroy, use a dedicated workflow_dispatch workflow and a protected environment with required reviewers.
Next Steps
- See GitHub Actions for complete workflow examples.