Skip to main content

Executive summary

To run LILT in your own AWS account, LILT’s deployment automation provisions a standard set of AWS resources — a managed Amazon EKS cluster with EKS-managed node groups, an S3 bucket, an RDS MySQL database, an optional SQS queue, the supporting network, and the encryption keys that protect them — and then runs the LILT platform on that cluster. AWS manages the Kubernetes control plane and the worker-node instance lifecycle; this is not a self-managed cluster running on hand-operated virtual machines. This document is the access request: it lists exactly what your security team needs to grant, why each item is needed, how it is scoped, and how long it is required. There are two grants:

1 · Deployment role

One IAM role, used only while creating or updating infrastructure. Grant it the deployment permissions below. It can be disabled or removed between deployments — it is not needed for day-to-day operation.

2 · Runtime roles

A set of narrowly-scoped roles the deployment creates for you and that the cluster’s workloads assume at runtime. Your team’s role here is to review and approve them — see runtime roles.

What you are granting, at a glance

GrantPurposeScopeLifetime
Deployment roleCreate/update/delete the EKS cluster, S3, SQS, RDS, networking, IAM roles, and KMS keys for LILTAWS account (services listed below); tightenable to a naming prefixOnly during install/upgrade — can be removed afterward
Runtime — EKS service rolesLet the control plane and worker nodes operate (standard AWS-managed policies)The LILT clusterLife of the cluster
Runtime — application & add-on rolesLet LILT pods use their own S3 bucket, KMS key, DNS zone, and (when configured) SQS queueThe specific bucket / queue / key / hosted zone LILT ownsLife of the cluster

Security posture

What your team can rely on when granting this:
  • Least privilege. Every policy lists only the actions the component needs. Runtime roles are additionally scoped to the specific resource LILT owns (its bucket, queue, key, or DNS zone) — they cannot touch anything else in the account.
  • No long-lived keys. Access is brokered by short-lived, federated credentials (EKS IRSA / Pod Identity for workloads; your choice of federation for the deployment role). LILT does not require static IAM access keys.
  • Time-boxed deployment access. The broad create/delete permissions live on the deployment role, which is only needed during install or upgrade and can be detached the rest of the time.
  • Encryption by default. S3 and RDS are encrypted with dedicated KMS keys; the RDS database is private (no public endpoint) and reachable only from within the VPC.
  • Auditable. The runtime policies are fixed and enumerated in this document, so a granted deployment produces a known, reviewable set of standing roles.
Placeholders. The policies below use <ACCOUNT_ID>, <REGION>, <CLUSTER_NAME>, <PREFIX> (an environment name prefix, e.g. prod), and the <*_ARN> / <HOSTED_ZONE_ID> / <OIDC_PROVIDER> values — substitute the ones for your environment. This request assumes a single-region deployment; multi-region follows the same model per region.

Grant 1 · Deployment role

Create one IAM role for LILT’s deployment identity and attach the permissions below. This is the complete set of create, describe, modify, tag, and delete actions required to stand up and later tear down the resources LILT manages. It is needed only while provisioning; you may detach it between changes. Why each service is needed:
ServiceWhat it is used for
EKSCreate and manage the Kubernetes cluster, node groups, add-ons, and admin access entries
EC2 / VPCCreate the private subnets, NAT, route tables, security groups, and node launch templates the cluster runs in
S3Create the object-storage bucket (application assets and the container registry backend)
SQSCreate the queues used for S3 event notifications and node-lifecycle handling
RDSCreate and configure the MySQL database
KMSCreate the encryption keys that protect S3 and RDS
IAMCreate the runtime roles in Grant 2 and the cluster’s OIDC identity provider
Route 53Create the DNS zone and records for the application endpoints
Secrets Manager, CloudWatch Logs, EventBridge, SSMStore the DB master secret, cluster log groups, and node-lifecycle event rules
The statements are split into four documents purely to stay under the AWS 6,144-character managed-policy size limit — attach all four to the same role.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "STSAuth",
      "Effect": "Allow",
      "Action": "sts:GetCallerIdentity",
      "Resource": "*"
    },
    {
      "Sid": "ECRManagement",
      "Effect": "Allow",
      "Action": [
        "ecr:CreateRepository", "ecr:DeleteRepository", "ecr:DescribeRepositories",
        "ecr:GetRepositoryPolicy", "ecr:SetRepositoryPolicy", "ecr:DeleteRepositoryPolicy",
        "ecr:TagResource", "ecr:UntagResource", "ecr:ListTagsForResource",
        "ecr:CreateLifecyclePolicy", "ecr:DeleteLifecyclePolicy", "ecr:GetLifecyclePolicy",
        "ecr:PutLifecyclePolicy", "ecr:GetAuthorizationToken"
      ],
      "Resource": "*"
    },
    {
      "Sid": "SecretsManager",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:CreateSecret", "secretsmanager:DeleteSecret", "secretsmanager:UpdateSecret",
        "secretsmanager:PutSecretValue", "secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret",
        "secretsmanager:TagResource", "secretsmanager:UntagResource",
        "secretsmanager:ListSecretVersionIds", "secretsmanager:RestoreSecret"
      ],
      "Resource": "arn:aws:secretsmanager:*:<ACCOUNT_ID>:secret:*"
    },
    {
      "Sid": "S3",
      "Effect": "Allow",
      "Action": [
        "s3:CreateBucket", "s3:DeleteBucket", "s3:ListBucket", "s3:ListAllMyBuckets",
        "s3:GetBucketLocation", "s3:GetBucketAcl", "s3:PutBucketAcl",
        "s3:GetBucketCORS", "s3:PutBucketCORS", "s3:GetBucketLogging", "s3:PutBucketLogging",
        "s3:GetBucketNotification", "s3:PutBucketNotification",
        "s3:GetBucketPolicy", "s3:PutBucketPolicy", "s3:DeleteBucketPolicy",
        "s3:GetBucketPublicAccessBlock", "s3:PutBucketPublicAccessBlock",
        "s3:GetBucketTagging", "s3:PutBucketTagging",
        "s3:GetBucketVersioning", "s3:PutBucketVersioning",
        "s3:GetEncryptionConfiguration", "s3:PutEncryptionConfiguration",
        "s3:GetLifecycleConfiguration", "s3:PutLifecycleConfiguration",
        "s3:GetReplicationConfiguration", "s3:PutReplicationConfiguration",
        "s3:GetBucketObjectLockConfiguration", "s3:PutBucketObjectLockConfiguration",
        "s3:GetBucketRequestPayment", "s3:PutBucketRequestPayment",
        "s3:GetIntelligentTieringConfiguration", "s3:PutIntelligentTieringConfiguration"
      ],
      "Resource": "*"
    }
  ]
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SQS",
      "Effect": "Allow",
      "Action": [
        "sqs:CreateQueue", "sqs:DeleteQueue", "sqs:GetQueueAttributes", "sqs:SetQueueAttributes",
        "sqs:GetQueueUrl", "sqs:ListQueues", "sqs:TagQueue", "sqs:UntagQueue", "sqs:ListQueueTags"
      ],
      "Resource": "*"
    },
    {
      "Sid": "RDS",
      "Effect": "Allow",
      "Action": [
        "rds:CreateDBCluster", "rds:DeleteDBCluster", "rds:DescribeDBClusters", "rds:ModifyDBCluster",
        "rds:CreateDBInstance", "rds:DeleteDBInstance", "rds:DescribeDBInstances", "rds:ModifyDBInstance",
        "rds:RebootDBInstance",
        "rds:CreateDBSubnetGroup", "rds:DeleteDBSubnetGroup", "rds:DescribeDBSubnetGroups", "rds:ModifyDBSubnetGroup",
        "rds:CreateDBParameterGroup", "rds:DeleteDBParameterGroup", "rds:DescribeDBParameterGroups", "rds:ModifyDBParameterGroup",
        "rds:CreateDBClusterParameterGroup", "rds:DeleteDBClusterParameterGroup", "rds:DescribeDBClusterParameterGroups", "rds:ModifyDBClusterParameterGroup",
        "rds:ListTagsForResource", "rds:AddTagsToResource", "rds:RemoveTagsFromResource",
        "rds:DescribeDBEngineVersions", "rds:DescribeOrderableDBInstanceOptions",
        "rds:CreateDBSnapshot", "rds:DeleteDBSnapshot", "rds:DescribeDBSnapshots",
        "rds:CreateDBClusterSnapshot", "rds:DeleteDBClusterSnapshot", "rds:DescribeDBClusterSnapshots",
        "rds:DescribeCertificates"
      ],
      "Resource": "*"
    },
    {
      "Sid": "EKS",
      "Effect": "Allow",
      "Action": [
        "eks:CreateCluster", "eks:DeleteCluster", "eks:DescribeCluster", "eks:ListClusters",
        "eks:UpdateClusterConfig", "eks:UpdateClusterVersion",
        "eks:CreateNodegroup", "eks:DeleteNodegroup", "eks:DescribeNodegroup", "eks:ListNodegroups",
        "eks:UpdateNodegroupConfig", "eks:UpdateNodegroupVersion",
        "eks:CreateAddon", "eks:DeleteAddon", "eks:DescribeAddon", "eks:UpdateAddon",
        "eks:ListAddons", "eks:DescribeAddonVersions",
        "eks:CreateAccessEntry", "eks:DeleteAccessEntry", "eks:DescribeAccessEntry",
        "eks:AssociateAccessPolicy", "eks:DisassociateAccessPolicy",
        "eks:ListAccessEntries", "eks:ListAccessPolicies", "eks:ListAssociatedAccessPolicies",
        "eks:TagResource", "eks:UntagResource"
      ],
      "Resource": "*"
    }
  ]
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EC2ForEKSAndRDS",
      "Effect": "Allow",
      "Action": [
        "ec2:CreateVpc", "ec2:DeleteVpc", "ec2:DescribeVpcs", "ec2:ModifyVpcAttribute",
        "ec2:CreateSubnet", "ec2:DeleteSubnet", "ec2:DescribeSubnets", "ec2:ModifySubnetAttribute",
        "ec2:CreateInternetGateway", "ec2:DeleteInternetGateway", "ec2:DescribeInternetGateways",
        "ec2:AttachInternetGateway", "ec2:DetachInternetGateway",
        "ec2:CreateNatGateway", "ec2:DeleteNatGateway", "ec2:DescribeNatGateways",
        "ec2:AllocateAddress", "ec2:ReleaseAddress", "ec2:DescribeAddresses",
        "ec2:CreateRouteTable", "ec2:DeleteRouteTable", "ec2:DescribeRouteTables",
        "ec2:CreateRoute", "ec2:DeleteRoute", "ec2:AssociateRouteTable", "ec2:DisassociateRouteTable",
        "ec2:CreateSecurityGroup", "ec2:DeleteSecurityGroup", "ec2:DescribeSecurityGroups",
        "ec2:AuthorizeSecurityGroupIngress", "ec2:RevokeSecurityGroupIngress",
        "ec2:AuthorizeSecurityGroupEgress", "ec2:RevokeSecurityGroupEgress",
        "ec2:CreateLaunchTemplate", "ec2:DeleteLaunchTemplate", "ec2:DescribeLaunchTemplates",
        "ec2:DescribeLaunchTemplateVersions", "ec2:CreateLaunchTemplateVersion", "ec2:DeleteLaunchTemplateVersions",
        "ec2:DescribeAvailabilityZones", "ec2:DescribeKeyPairs", "ec2:DescribeInstances",
        "ec2:DescribeInstanceTypes", "ec2:DescribeImages", "ec2:DescribeNetworkInterfaces",
        "ec2:CreateTags", "ec2:DeleteTags", "ec2:DescribeTags"
      ],
      "Resource": "*"
    },
    {
      "Sid": "IAM",
      "Effect": "Allow",
      "Action": [
        "iam:CreatePolicy", "iam:DeletePolicy", "iam:GetPolicy", "iam:GetPolicyVersion",
        "iam:ListPolicyVersions", "iam:CreatePolicyVersion", "iam:DeletePolicyVersion",
        "iam:TagPolicy", "iam:UntagPolicy", "iam:ListEntitiesForPolicy",
        "iam:CreateRole", "iam:DeleteRole", "iam:GetRole", "iam:UpdateRole",
        "iam:UpdateAssumeRolePolicy", "iam:AttachRolePolicy", "iam:DetachRolePolicy",
        "iam:ListAttachedRolePolicies", "iam:ListRolePolicies", "iam:TagRole", "iam:UntagRole",
        "iam:PassRole",
        "iam:CreateInstanceProfile", "iam:DeleteInstanceProfile", "iam:GetInstanceProfile",
        "iam:AddRoleToInstanceProfile", "iam:RemoveRoleFromInstanceProfile",
        "iam:CreateOpenIDConnectProvider", "iam:DeleteOpenIDConnectProvider", "iam:GetOpenIDConnectProvider",
        "iam:UpdateOpenIDConnectProviderThumbprint", "iam:TagOpenIDConnectProvider",
        "iam:UntagOpenIDConnectProvider", "iam:ListOpenIDConnectProviders"
      ],
      "Resource": "*"
    },
    {
      "Sid": "KMS",
      "Effect": "Allow",
      "Action": [
        "kms:CreateKey", "kms:DescribeKey",
        "kms:GetKeyPolicy", "kms:PutKeyPolicy", "kms:GetKeyRotationStatus",
        "kms:EnableKeyRotation", "kms:DisableKeyRotation",
        "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion",
        "kms:ListKeys", "kms:ListAliases", "kms:CreateAlias", "kms:DeleteAlias", "kms:UpdateAlias",
        "kms:TagResource", "kms:UntagResource",
        "kms:CreateGrant", "kms:ListGrants", "kms:RevokeGrant"
      ],
      "Resource": "*"
    }
  ]
}
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Route53",
      "Effect": "Allow",
      "Action": [
        "route53:CreateHostedZone", "route53:DeleteHostedZone", "route53:GetHostedZone",
        "route53:ListHostedZones", "route53:ListHostedZonesByName",
        "route53:ChangeResourceRecordSets", "route53:ListResourceRecordSets", "route53:GetChange",
        "route53:ChangeTagsForResource", "route53:ListTagsForResource"
      ],
      "Resource": "*"
    },
    {
      "Sid": "EC2Extended",
      "Effect": "Allow",
      "Action": [
        "ec2:RunInstances", "ec2:TerminateInstances", "ec2:StopInstances", "ec2:StartInstances",
        "ec2:CreateNetworkAcl", "ec2:DeleteNetworkAcl", "ec2:CreateNetworkAclEntry",
        "ec2:DeleteNetworkAclEntry", "ec2:ReplaceNetworkAclAssociation", "ec2:DescribeNetworkAcls",
        "ec2:CreateNetworkInterface", "ec2:DeleteNetworkInterface",
        "ec2:DescribeNetworkInterfaceAttribute", "ec2:ModifyNetworkInterfaceAttribute",
        "ec2:DescribeInstanceAttribute", "ec2:DescribeInstanceCreditSpecifications",
        "ec2:ModifyInstanceAttribute", "ec2:DescribeVolumes"
      ],
      "Resource": "*"
    },
    {
      "Sid": "CloudWatchLogs",
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup", "logs:DeleteLogGroup", "logs:DescribeLogGroups",
        "logs:PutRetentionPolicy", "logs:DeleteRetentionPolicy",
        "logs:TagResource", "logs:UntagResource", "logs:ListTagsForResource"
      ],
      "Resource": "*"
    },
    {
      "Sid": "EKSPodIdentity",
      "Effect": "Allow",
      "Action": [
        "eks:CreatePodIdentityAssociation", "eks:DeletePodIdentityAssociation",
        "eks:DescribePodIdentityAssociation", "eks:ListPodIdentityAssociations"
      ],
      "Resource": "*"
    },
    {
      "Sid": "EventBridgeRules",
      "Effect": "Allow",
      "Action": [
        "events:PutRule", "events:DeleteRule", "events:DescribeRule",
        "events:EnableRule", "events:DisableRule",
        "events:PutTargets", "events:RemoveTargets", "events:ListTargetsByRule",
        "events:TagResource", "events:UntagResource", "events:ListTagsForResource"
      ],
      "Resource": "arn:aws:events:*:<ACCOUNT_ID>:rule/*"
    },
    {
      "Sid": "SSMReadServiceParams",
      "Effect": "Allow",
      "Action": ["ssm:GetParameter", "ssm:GetParameters"],
      "Resource": "arn:aws:ssm:*::parameter/aws/service/eks/*"
    }
  ]
}
For an EKS managed node group cluster like LILT’s reference deployment:
  • The eks:*PodIdentityAssociation actions (EKSPodIdentity) are required — LILT binds every workload to its IAM role via EKS Pod Identity, so the deployment creates these associations. Do not drop them.
  • EventBridgeRules and the EKS-service ssm:GetParameter read are only needed for node auto-provisioning (Karpenter); they can be dropped here.
  • The instance-lifecycle actions in EC2Extendedec2:RunInstances, ec2:TerminateInstances, ec2:StopInstances, ec2:StartInstances — are only needed for self-managed nodes or Karpenter. With EKS-managed node groups the EKS service manages instance lifecycle, so the deployment role does not use them; the remaining EC2Extended network-interface / ACL / volume actions are still required.

Grant 2 · Runtime roles

These roles are created for you by the deployment (using Grant 1) and are assumed by the cluster and its workloads at runtime. You do not hand-craft them; they are enumerated here so your team can review the standing access that will exist in the account after install.

EKS service roles (AWS-managed policies)

The cluster and its worker nodes use roles built entirely from AWS’s own managed policies — AWS’s least-privilege definitions for these service roles. No custom permissions are involved.
RoleAssumed byAWS-managed policies attached
Cluster rolethe EKS control planeAmazonEKSClusterPolicy, AmazonEKSVPCResourceController
Node roleworker EC2 instancesAmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, AmazonEC2ContainerRegistryReadOnly, AmazonEKSVPCResourceController, CloudWatchAgentServerPolicy
Node role (auto-provisioning)as aboveadditionally AmazonSSMManagedInstanceCore
EBS CSI driverthe storage driver (EKS Pod Identity)service-role/AmazonEBSCSIDriverPolicy
LILT’s reference cluster also attaches the legacy AmazonEKSServicePolicy to the cluster role. On EKS 1.24+ its permissions are folded into AmazonEKSClusterPolicy, so it is optional on new clusters.
LILT runs on EKS-managed node groups: the worker and GPU nodes are EC2 instances whose lifecycle Amazon EKS manages for you (creation, replacement, and scaling via the node group’s min/max/desired config). There are no self-managed EC2 instances or Auto Scaling groups to operate. As a result the Cluster Autoscaler and Karpenter roles below, and the “auto-provisioning” node-role variant above (AmazonSSMManagedInstanceCore), are not used in this model — they are documented only for deployments that opt into node auto-provisioning.

Application & add-on roles (custom, least-privilege)

Each of these is a role with a small inline policy, scoped to the specific resource LILT owns and assumed by one named Kubernetes workload via short-lived federated credentials (EKS IRSA or Pod Identity). None of them can access resources outside the ones LILT created.
The core LILT platform. Reads/writes objects in the LILT bucket, uses that bucket’s KMS key, and consumes/produces on the LILT SQS queue — nothing else.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowS3Access",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject", "s3:ListBucket", "s3:GetBucketLocation"],
      "Resource": ["<S3_BUCKET_ARN>", "<S3_BUCKET_ARN>/*"]
    },
    {
      "Sid": "AllowKMSForS3",
      "Effect": "Allow",
      "Action": ["kms:Decrypt", "kms:Encrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:DescribeKey"],
      "Resource": "<S3_KMS_KEY_ARN>"
    },
    {
      "Sid": "AllowSQSAccess",
      "Effect": "Allow",
      "Action": ["sqs:SendMessage", "sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes", "sqs:GetQueueUrl"],
      "Resource": "<SQS_QUEUE_ARN>"
    }
  ]
}
In LILT’s reference terraform this single Pod Identity role (bound to the cluster service account) also grants the same S3 object/list and KMS access to a second bucket used by Argo Workflows (<CLUSTER_NAME>-bucket-argo), encrypted with the same S3 KMS key — so Resource lists both <S3_BUCKET_ARN> and <ARGO_S3_BUCKET_ARN> (and their /*). SQS is only present when the deployment provisions an SQS queue; the reference module does not, so the AllowSQSAccess statement is omitted there.
LILT’s in-cluster image registry stores layers under the harbor/ prefix of the LILT bucket. Object access and bucket listing are both scoped to that prefix — it cannot see the rest of the bucket.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowObjectAccessUnderHarborPrefix",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject", "s3:PutObject", "s3:DeleteObject",
        "s3:AbortMultipartUpload", "s3:ListMultipartUploadParts"
      ],
      "Resource": "<S3_BUCKET_ARN>/harbor/*"
    },
    {
      "Sid": "AllowListBucketScopedToHarborPrefix",
      "Effect": "Allow",
      "Action": ["s3:ListBucket", "s3:ListBucketMultipartUploads"],
      "Resource": "<S3_BUCKET_ARN>",
      "Condition": { "StringLike": { "s3:prefix": ["harbor/*"] } }
    },
    {
      "Sid": "AllowBucketLocation",
      "Effect": "Allow",
      "Action": ["s3:GetBucketLocation"],
      "Resource": "<S3_BUCKET_ARN>"
    },
    {
      "Sid": "AllowKMSForS3",
      "Effect": "Allow",
      "Action": ["kms:Decrypt", "kms:Encrypt", "kms:ReEncrypt*", "kms:GenerateDataKey*", "kms:DescribeKey"],
      "Resource": "<S3_KMS_KEY_ARN>"
    }
  ]
}
Keeps DNS records in sync for the application endpoints, scoped to LILT’s hosted zone.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowChangeRecords",
      "Effect": "Allow",
      "Action": ["route53:ChangeResourceRecordSets"],
      "Resource": "arn:aws:route53:::hostedzone/<HOSTED_ZONE_ID>"
    },
    {
      "Sid": "AllowListZones",
      "Effect": "Allow",
      "Action": ["route53:ListHostedZones", "route53:ListResourceRecordSets", "route53:ListTagsForResource"],
      "Resource": "*"
    }
  ]
}
Proves domain control for automated TLS certificates, scoped to LILT’s hosted zone.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowGetChange",
      "Effect": "Allow",
      "Action": ["route53:GetChange"],
      "Resource": "arn:aws:route53:::change/*"
    },
    {
      "Sid": "AllowDNS01Challenge",
      "Effect": "Allow",
      "Action": ["route53:ChangeResourceRecordSets", "route53:ListResourceRecordSets"],
      "Resource": "arn:aws:route53:::hostedzone/<HOSTED_ZONE_ID>"
    },
    {
      "Sid": "AllowListZones",
      "Effect": "Allow",
      "Action": ["route53:ListHostedZonesByName"],
      "Resource": "*"
    }
  ]
}
Only present if the cluster scales via managed node groups. Scaling actions are gated on the cluster-ownership tag.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowDescribe",
      "Effect": "Allow",
      "Action": [
        "autoscaling:DescribeAutoScalingGroups", "autoscaling:DescribeAutoScalingInstances",
        "autoscaling:DescribeLaunchConfigurations", "autoscaling:DescribeScalingActivities",
        "autoscaling:DescribeTags",
        "ec2:DescribeImages", "ec2:DescribeInstanceTypes", "ec2:DescribeLaunchTemplateVersions",
        "ec2:GetInstanceTypesFromInstanceRequirements", "eks:DescribeNodegroup"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AllowScale",
      "Effect": "Allow",
      "Action": ["autoscaling:SetDesiredCapacity", "autoscaling:TerminateInstanceInAutoScalingGroup"],
      "Resource": "*",
      "Condition": { "StringEquals": { "aws:ResourceTag/kubernetes.io/cluster/<CLUSTER_NAME>": "owned" } }
    }
  ]
}
Only present if node auto-provisioning is enabled. Launches and terminates nodes for the LILT cluster; every mutating action is gated on the cluster tag and region. This is the upstream Karpenter controller policy.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowScopedEC2InstanceAccessActions",
      "Effect": "Allow",
      "Resource": [
        "arn:aws:ec2:<REGION>::image/*", "arn:aws:ec2:<REGION>::snapshot/*",
        "arn:aws:ec2:<REGION>:*:security-group/*", "arn:aws:ec2:<REGION>:*:subnet/*",
        "arn:aws:ec2:<REGION>:*:capacity-reservation/*"
      ],
      "Action": ["ec2:RunInstances", "ec2:CreateFleet"]
    },
    {
      "Sid": "AllowRegionalReadActions",
      "Effect": "Allow",
      "Resource": "*",
      "Action": [
        "ec2:DescribeCapacityReservations", "ec2:DescribeImages", "ec2:DescribeInstances",
        "ec2:DescribeInstanceStatus", "ec2:DescribeInstanceTypeOfferings", "ec2:DescribeInstanceTypes",
        "ec2:DescribeLaunchTemplates", "ec2:DescribePlacementGroups", "ec2:DescribeSecurityGroups",
        "ec2:DescribeSpotPriceHistory", "ec2:DescribeSubnets"
      ],
      "Condition": { "StringEquals": { "aws:RequestedRegion": "<REGION>" } }
    },
    {
      "Sid": "AllowSSMReadActions",
      "Effect": "Allow",
      "Resource": "arn:aws:ssm:<REGION>::parameter/aws/service/*",
      "Action": "ssm:GetParameter"
    },
    {
      "Sid": "AllowPricingReadActions",
      "Effect": "Allow",
      "Resource": "*",
      "Action": "pricing:GetProducts"
    },
    {
      "Sid": "AllowInterruptionQueueActions",
      "Effect": "Allow",
      "Resource": "<KARPENTER_SQS_QUEUE_ARN>",
      "Action": ["sqs:DeleteMessage", "sqs:GetQueueUrl", "sqs:ReceiveMessage"]
    },
    {
      "Sid": "AllowPassingInstanceRole",
      "Effect": "Allow",
      "Resource": "<NODE_ROLE_ARN>",
      "Action": "iam:PassRole",
      "Condition": { "StringEquals": { "iam:PassedToService": ["ec2.amazonaws.com"] } }
    },
    {
      "Sid": "AllowAPIServerEndpointDiscovery",
      "Effect": "Allow",
      "Resource": "arn:aws:eks:<REGION>:<ACCOUNT_ID>:cluster/<CLUSTER_NAME>",
      "Action": "eks:DescribeCluster"
    }
  ]
}
Abbreviated to the core statements. The full policy additionally scopes ec2:CreateFleet / RunInstances / CreateLaunchTemplate / CreateTags / TerminateInstances and the iam:*InstanceProfile actions by the kubernetes.io/cluster/<CLUSTER_NAME>: owned and karpenter.sh/nodepool tags — use the current published Karpenter controller policy for your Karpenter version.
Provisions the ALB/NLB load balancers for the application’s public endpoints. Management actions are gated on the controller’s ownership tag.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCreateServiceLinkedRole",
      "Effect": "Allow",
      "Action": ["iam:CreateServiceLinkedRole"],
      "Resource": "*",
      "Condition": { "StringEquals": { "iam:AWSServiceName": "elasticloadbalancing.amazonaws.com" } }
    },
    {
      "Sid": "AllowDescribe",
      "Effect": "Allow",
      "Action": [
        "ec2:DescribeAccountAttributes", "ec2:DescribeAddresses", "ec2:DescribeAvailabilityZones",
        "ec2:DescribeInternetGateways", "ec2:DescribeVpcs", "ec2:DescribeVpcPeeringConnections",
        "ec2:DescribeSubnets", "ec2:DescribeSecurityGroups", "ec2:DescribeInstances",
        "ec2:DescribeNetworkInterfaces", "ec2:DescribeTags", "ec2:GetCoipPoolUsage",
        "ec2:DescribeCoipPools", "ec2:GetSecurityGroupsForVpc", "ec2:DescribeIpamPools",
        "elasticloadbalancing:Describe*"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AllowManageOwnedResources",
      "Effect": "Allow",
      "Action": [
        "elasticloadbalancing:CreateLoadBalancer", "elasticloadbalancing:CreateTargetGroup",
        "elasticloadbalancing:CreateListener", "elasticloadbalancing:DeleteListener",
        "elasticloadbalancing:CreateRule", "elasticloadbalancing:DeleteRule",
        "elasticloadbalancing:ModifyLoadBalancerAttributes", "elasticloadbalancing:SetSubnets",
        "elasticloadbalancing:SetSecurityGroups", "elasticloadbalancing:DeleteLoadBalancer",
        "elasticloadbalancing:ModifyTargetGroup", "elasticloadbalancing:ModifyTargetGroupAttributes",
        "elasticloadbalancing:DeleteTargetGroup", "elasticloadbalancing:RegisterTargets",
        "elasticloadbalancing:DeregisterTargets", "elasticloadbalancing:ModifyListener",
        "elasticloadbalancing:AddTags", "elasticloadbalancing:RemoveTags",
        "ec2:CreateSecurityGroup", "ec2:AuthorizeSecurityGroupIngress",
        "ec2:RevokeSecurityGroupIngress", "ec2:CreateTags", "ec2:DeleteTags"
      ],
      "Resource": "*",
      "Condition": { "Null": { "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" } }
    }
  ]
}
Condensed for readability. Deploy the exact, tag-condition-complete policy from the AWS Load Balancer Controller install guide matching your controller version.
Only present when the deployment uses the External Secrets Operator to sync AWS Secrets Manager into the cluster; some deployments do not, in which case this role is absent. When used, it syncs a fixed set of secrets (the RDS credentials, the ingress TLS secret, and the SMTP secret): read-only and scoped to exactly those secret ARNs — it cannot enumerate or read any other secret in the account. Assumed by the external-secrets-sa service account in the external-secrets namespace via EKS Pod Identity.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowReadLiltManagedSecrets",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetResourcePolicy", "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret", "secretsmanager:ListSecretVersionIds"
      ],
      "Resource": ["<RDS_SECRET_ARN>", "<SSL_SECRET_ARN>", "<SMTP_SECRET_ARN>"]
    }
  ]
}
Only present when a bastion is enabled (enable_bastion). An EC2 instance role that lets operators run aws eks describe-cluster / update-kubeconfig against this one cluster and reach the private bastion over AWS Systems Manager Session Manager (no public IP or open SSH port required). It attaches the AWS-managed AmazonSSMManagedInstanceCore policy plus the inline statement below.
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": ["eks:DescribeCluster"],
      "Resource": "arn:aws:eks:<REGION>:<ACCOUNT_ID>:cluster/<CLUSTER_NAME>"
    }
  ]
}

Cluster service account & Pod Identity wiring

The runtime roles above are not attached to pods directly. Each is bound to a named Kubernetes service account (SA), and an EKS Pod Identity association maps that SA to the IAM role. This section describes how LILT’s terraform sets that up so your team can see exactly which in-cluster identity receives which AWS permissions.

The LILT cluster service account

LILT’s application does not spread its AWS access across many service accounts. A single service account — configured via the cluster_service_account variable and created in the deployment namespace (namespace, default lilt) — is the identity used for all of the application’s AWS access (S3, and any other application-level AWS resources). It is a plain service account with no IRSA eks.amazonaws.com/role-arn annotation: the role binding is done outside the SA object, by the Pod Identity association.
# The single SA the LILT application runs under
resource "kubernetes_service_account" "cluster_sa" {
  metadata {
    name      = var.cluster_service_account   # e.g. "lilt-sa"
    namespace = var.namespace                 # default "lilt"
  }
}

# Bind that SA to the S3/application IAM role via EKS Pod Identity
resource "aws_eks_pod_identity_association" "s3_attach_to_sa" {
  cluster_name    = <cluster>
  namespace       = var.namespace
  service_account = var.cluster_service_account
  role_arn        = <s3_pod_role ARN>          # the LILT application role
}

How the pieces fit together

Three objects have to agree for a pod to receive AWS permissions:
  1. The Kubernetes service account — the identity the pod runs as (namespace + name).
  2. The IAM role — carries the least-privilege policy (e.g. the S3 + KMS permissions) and has a trust policy that allows pods.eks.amazonaws.com to assume it (sts:AssumeRole + sts:TagSession). This is what makes it a Pod Identity role rather than an IRSA/OIDC role.
  3. The Pod Identity association — the AWS-side record that says “SA namespace/name on this cluster may assume this role.”
At runtime the EKS Pod Identity Agent (installed as a cluster add-on) sees a pod running under that service account, matches it to the association, and hands the pod short-lived, automatically-rotated credentials for the mapped role. No static keys and no OIDC provider are involved — the SA name/namespace pair is the binding key.
Because the association is keyed on namespace + service account name, those must match on both sides. If the SA is renamed, moved to another namespace, or recreated in a different namespace without updating the association, the pod loses its AWS access. Some add-on service accounts additionally have fixed names required by their controllers — e.g. the EBS CSI driver expects ebs-csi-controller-sa and the External Secrets Operator expects external-secrets-sa — and cannot be renamed.

Service-account-to-role map

Kubernetes service accountNamespaceBound IAM role (Grant 2)Grants
cluster_service_account (e.g. lilt-sa)namespace (default lilt)LILT application roleS3 (LILT + Argo buckets) and its KMS key
aws-load-balancer-controller-sakube-systemAWS Load Balancer Controller roleManage the cluster’s ALBs/NLBs
ebs-csi-controller-sakube-systemEBS CSI driver roleAmazonEBSCSIDriverPolicy
external-secrets-sa (if used)external-secretsExternal Secrets roleRead the RDS / SSL / SMTP secrets
The LILT cluster service account also carries in-cluster RBAC (Kubernetes Role/RoleBindings for the application and Argo Workflows components). That RBAC governs access to the Kubernetes API inside the cluster and is entirely separate from the AWS IAM permissions granted through the Pod Identity association described here — the same SA simply serves both roles.

How access is brokered

No component uses a long-lived IAM access key. Each principal obtains short-lived credentials via a federated trust:
PrincipalMechanismHow it is trusted
App, registry, ExternalDNS, cert-manager, autoscaler, Karpenter, LB controllerEKS IRSA (OIDC) or EKS Pod IdentityWith IRSA, scoped to one Kubernetes service account (<OIDC_PROVIDER>:sub = system:serviceaccount:<namespace>:<sa-name>); with Pod Identity, the same SA is bound via an association. LILT’s reference terraform uses Pod Identity for the app and LB controller — see the note below.
External Secrets Operator (if used)EKS Pod IdentityThe external-secrets-sa service account in the external-secrets namespace
EBS CSI storage driverEKS Pod IdentityThe EKS Pod Identity service on your cluster
Worker nodesEC2 instance profileThe node role, assumed by EC2
Bastion host (if enabled)EC2 instance profileThe bastion role, assumed by EC2
Control planeEKS service roleAssumed by the EKS service
Deployment role (Grant 1)Your choiceYou decide who may assume it — a federated CI identity or a named administrator, per your own access standards
LILT’s reference terraform implements all of its workload roles (the LILT application/S3, the AWS Load Balancer Controller, the EBS CSI driver, and — when used — the External Secrets Operator) via EKS Pod Identity rather than IRSA, and therefore provisions no OIDC identity provider. Either mechanism is supported; if your deployment uses Pod Identity throughout, the OIDC provider and its IAM permissions are not required, and each workload role trusts pods.eks.amazonaws.com instead of a web-identity subject.
The deployment role’s trust policy is yours to define — this document does not prescribe who may assume it. Grant the Grant 1 permissions to whichever identity your team authorizes to run the deployment, following your federation and access-control standards.