> ## Documentation Index
> Fetch the complete documentation index at: https://support.lilt.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AWS Access Requirements

> Executive summary of the AWS IAM permissions and roles to grant LILT before a managed Amazon EKS deployment — written for review by your cloud security team.

## 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:

<CardGroup cols={2}>
  <Card title="1 · Deployment role" icon="key">
    One IAM role, used **only while creating or updating infrastructure**. Grant
    it the [deployment permissions](#grant-1-deployment-role) below. It can be
    disabled or removed between deployments — it is not needed for day-to-day
    operation.
  </Card>

  <Card title="2 · Runtime roles" icon="gears">
    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](#grant-2-runtime-roles).
  </Card>
</CardGroup>

### What you are granting, at a glance

| Grant | Purpose | Scope | Lifetime |
| - | - | - | - |
| **Deployment role** | Create/update/delete the EKS cluster, S3, SQS, RDS, networking, IAM roles, and KMS keys for LILT | AWS account (services listed below); tightenable to a naming prefix | Only during install/upgrade — can be removed afterward |
| **Runtime — EKS service roles** | Let the control plane and worker nodes operate (standard AWS-managed policies) | The LILT cluster | Life of the cluster |
| **Runtime — application & add-on roles** | Let LILT pods use their own S3 bucket, SQS queue, KMS key, and DNS zone | The specific bucket / queue / key / hosted zone LILT owns | Life 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 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. The DB master password is managed by AWS Secrets Manager
  (`manage_master_user_password`) — no password is stored in Terraform state.
  Application workloads retrieve it via the External Secrets Operator
  or directly from Secrets Manager.
* **Auditable.** The runtime policies are fixed and enumerated in this document,
  so a granted deployment produces a known, reviewable set of standing roles.

<Note>
  **Placeholders.** The policies below use <code>\<ACCOUNT\_ID></code>, <code>\<REGION></code>,
  <code>\<CLUSTER\_NAME></code>, <code>\<PREFIX></code> (an environment name prefix, e.g. `prod`), and the
  <code>\<\*\_ARN></code> / <code>\<HOSTED\_ZONE\_ID></code> values — substitute the ones for your environment.
  This request assumes a single-region deployment; multi-region follows the same
  model per region.
</Note>

***

## 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:**

| Service | What it is used for |
| - | - |
| **EKS** | Create and manage the Kubernetes cluster, node groups, add-ons, and admin access entries |
| **EC2 / VPC** | Create the private subnets, NAT, route tables, security groups, and node launch templates the cluster runs in |
| **S3** | Create the object-storage bucket (application assets and the container registry backend) |
| **SQS** | Create the queue used for S3 event notifications; the app workload role has send/receive/delete access at runtime |
| **RDS** | Create and configure the MySQL 8.0 database (private endpoint, KMS-encrypted, AWS-managed master password stored in Secrets Manager) |
| **KMS** | Create the encryption keys that protect S3 and RDS |
| **IAM** | Create the runtime roles in Grant 2 and their Pod Identity associations |
| **Route 53** | Create the DNS zone and records for the application endpoints |
| **Secrets Manager, CloudWatch Logs, EventBridge, SSM** | Store the DB master secret, cluster log groups, and node-lifecycle event rules |

<Tip>
  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.
</Tip>

<AccordionGroup>
  <Accordion title="Policy 1 — STS, ECR, Secrets Manager, S3">
    ```json theme={null}
    {
      "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": "*"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 2 — SQS, RDS, EKS">
    ```json theme={null}
    {
      "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": "*"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 3 — EC2 / VPC, IAM, KMS">
    ```json theme={null}
    {
      "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": "*"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Policy 4 — Route 53, extended EC2, CloudWatch Logs, EKS Pod Identity, EventBridge, SSM">
    ```json theme={null}
    {
      "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/*"
        }
      ]
    }
    ```

    <Note>
      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 `EC2Extended` — `ec2: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.
    </Note>
  </Accordion>
</AccordionGroup>

***

## 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.

| Role | Assumed by | AWS-managed policies attached |
| - | - | - |
| **Cluster role** | the EKS control plane | `AmazonEKSClusterPolicy`, `AmazonEKSVPCResourceController` |
| **Node role** | worker EC2 instances | `AmazonEKSWorkerNodePolicy`, `AmazonEKS_CNI_Policy`, `AmazonEC2ContainerRegistryReadOnly`, `AmazonEKSVPCResourceController`, `CloudWatchAgentServerPolicy` |
| **Node role (auto-provisioning)** | as above | additionally `AmazonSSMManagedInstanceCore` |
| **EBS CSI driver** | the storage driver (EKS Pod Identity) | `service-role/AmazonEBSCSIDriverPolicy` |

<Note>
  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.
</Note>

<Note>
  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 enabled by
  default** — Karpenter is a future release; they are documented here for
  deployments that opt into node auto-provisioning.
</Note>

### 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.

<AccordionGroup>
  <Accordion title="LILT application — its own S3 bucket, SQS queue, KMS key" icon="star">
    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.

    ```json theme={null}
    {
      "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>"
        }
      ]
    }
    ```

    <Note>
      The SQS queue is always provisioned by the module. The `AllowSQSAccess`
      statement shown above is always present in the app workload role — it is not
      optional. The queue ARN is surfaced as the `queue_arn` output.
    </Note>
  </Accordion>

  <Accordion title="ExternalDNS — the LILT DNS zone only">
    Keeps DNS records in sync for the application endpoints, scoped to LILT's
    hosted zone.

    ```json theme={null}
    {
      "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": "*"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="cert-manager — TLS certificate issuance (DNS-01)">
    Proves domain control for automated TLS certificates, scoped to LILT's
    hosted zone.

    ```json theme={null}
    {
      "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": "*"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Cluster Autoscaler — scaling the LILT cluster's node groups">
    Only present if the cluster scales via managed node groups. Scaling actions
    are gated on the cluster-ownership tag.

    ```json theme={null}
    {
      "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" } }
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Node auto-provisioning (Karpenter) — cluster-tagged EC2 only — future release, disabled by default">
    Optional — disabled by default; available in a future release. 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.

    ```json theme={null}
    {
      "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"
        }
      ]
    }
    ```

    <Note>
      Abbreviated to the core statements. The full policy additionally scopes
      `ec2:CreateFleet` / `RunInstances` / `CreateLaunchTemplate` / `CreateTags` /
      `TerminateInstances` and the `iam:*InstanceProfile` actions by the
      <code>kubernetes.io/cluster/\<CLUSTER\_NAME>: owned</code> and `karpenter.sh/nodepool`
      tags — use the current published
      [Karpenter controller policy](https://karpenter.sh/docs/reference/cloudformation/)
      for your Karpenter version.
    </Note>
  </Accordion>

  <Accordion title="AWS Load Balancer Controller — load balancers it creates">
    Provisions the ALB/NLB load balancers for the application's public
    endpoints. Management actions are gated on the controller's ownership tag.

    ```json theme={null}
    {
      "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" } }
        }
      ]
    }
    ```

    <Note>
      Condensed for readability. Deploy the exact, tag-condition-complete policy
      from the
      [AWS Load Balancer Controller install guide](https://kubernetes-sigs.github.io/aws-load-balancer-controller/latest/deploy/installation/)
      matching your controller version.
    </Note>
  </Accordion>

  <Accordion title="External Secrets Operator (optional) — read LILT-managed secrets">
    Optional — when the External Secrets Operator is enabled, this role
    allows it to pull any secret whose name begins with
    the environment prefix — scoped to `<PREFIX>/*` in Secrets Manager. It cannot
    read secrets outside that prefix. Assumed by the `external-secrets-sa` service
    account in the `external-secrets` namespace via EKS Pod Identity.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Sid": "AllowReadLiltManagedSecrets",
          "Effect": "Allow",
          "Action": [
            "secretsmanager:GetSecretValue",
            "secretsmanager:DescribeSecret"
          ],
          "Resource": "arn:aws:secretsmanager:<REGION>:<ACCOUNT_ID>:secret:<PREFIX>/*"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Harbor container registry (optional, future release) — S3 backend access">
    Optional — disabled by default. Harbor requires
    access to the S3 bucket that backs its image registry and the KMS key used to
    encrypt that bucket. The module provisions Pod Identity associations for three
    Harbor service accounts (`harbor-core`, `harbor-registry`, `harbor-jobservice`)
    in the `harbor` namespace — all three assume the same role.

    <Note>
      Harbor support is included in the module for organizations that run a
      self-hosted registry. For most deployments, **ECR is
      the recommended path** — no additional infrastructure is needed and images
      are pulled directly by node groups that already hold `AmazonEC2ContainerRegistryReadOnly`.
    </Note>

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": [
            "s3:PutObject", "s3:GetObject", "s3:DeleteObject",
            "s3:ListBucket", "s3:GetBucketLocation"
          ],
          "Resource": [
            "<S3_BUCKET_ARN>",
            "<S3_BUCKET_ARN>/harbor/*"
          ]
        },
        {
          "Effect": "Allow",
          "Action": ["kms:GenerateDataKey", "kms:Decrypt"],
          "Resource": "<S3_KMS_KEY_ARN>"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="ECR private registry (optional) — push/pull access for app workloads">
    Optional — disabled by default. Creates a private
    ECR repository and grants the `app_workload` role push and pull permissions
    scoped to that repository. Node groups always receive
    `AmazonEC2ContainerRegistryReadOnly` (pull-only) regardless of this toggle.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["ecr:GetAuthorizationToken"],
          "Resource": "*"
        },
        {
          "Effect": "Allow",
          "Action": [
            "ecr:BatchCheckLayerAvailability", "ecr:GetDownloadUrlForLayer",
            "ecr:BatchGetImage", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart",
            "ecr:CompleteLayerUpload", "ecr:PutImage",
            "ecr:DescribeImages", "ecr:ListImages"
          ],
          "Resource": "<ECR_REPOSITORY_ARN>"
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="Bastion host (optional) — describe the cluster + Session Manager">
    Optional — present when the bastion is enabled. 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.

    ```json theme={null}
    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["eks:DescribeCluster"],
          "Resource": "arn:aws:eks:<REGION>:<ACCOUNT_ID>:cluster/<CLUSTER_NAME>"
        }
      ]
    }
    ```
  </Accordion>
</AccordionGroup>

***

## 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 application service accounts

LILT's application runs under one or more service accounts in the `lilt`
namespace. The module variable `app_workload_service_accounts` (default:
`["app-workload-sa"]`) controls which service accounts receive Pod Identity
associations to the app workload IAM role. Each named SA gets its own
`aws_eks_pod_identity_association`. The service accounts themselves are created
by your cluster's application deployment (e.g. Helm chart), **not** by this
Terraform module — the Pod Identity association is the AWS-side binding only.

```hcl theme={null}
# Each SA in app_workload_service_accounts gets its own association
resource "aws_eks_pod_identity_association" "app_workload" {
  for_each = toset(var.app_workload_service_accounts)

  cluster_name    = aws_eks_cluster.main.name
  namespace       = "lilt"
  service_account = each.value
  role_arn        = aws_iam_role.app_workload.arn
}
```

### 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 (`sts:AssumeRole` + `sts:TagSession` from `pods.eks.amazonaws.com`). Under IRSA (`iam_auth_method = "irsa"`), the trust policy uses `sts:AssumeRoleWithWebIdentity` with a service-account-scoped condition against the cluster OIDC provider instead.
3. **The Pod Identity association** (Pod Identity mode only) — the AWS-side record that says "SA *namespace/name* on *this cluster* may assume *this role*." Under IRSA, the Kubernetes SA carries an `eks.amazonaws.com/role-arn` annotation in place of this association.

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 are involved in either mode.

<Warning>
  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.
</Warning>

### Service-account-to-role map

| Kubernetes service account | Namespace | Bound IAM role (Grant 2) | Grants |
| - | - | - | - |
| `app-workload-sa` (configurable via `app_workload_service_accounts`) | `lilt` | App workload role | S3 bucket + KMS key + SQS queue (module-owned resources only) |
| `aws-load-balancer-controller-sa` | `kube-system` | AWS Load Balancer Controller role | Manage the cluster's ALBs/NLBs |
| `cluster-autoscaler-sa` | `kube-system` | Cluster Autoscaler role | ASG describe + scale (cluster-scoped tag) |
| `external-dns-sa` | `kube-system` | ExternalDNS role | Route 53 zone change + list (optional) |
| `cert-manager-sa` | `cert-manager` | cert-manager role | Route 53 DNS-01 challenge (optional) |
| `ebs-csi-controller-sa` | `kube-system` | EBS CSI driver role | `AmazonEBSCSIDriverPolicy` |
| `external-secrets-sa` | `external-secrets` | External Secrets role | Read `<PREFIX>/*` Secrets Manager secrets (optional) |
| `karpenter` | `kube-system` | Karpenter controller role | EC2 fleet + SQS interruption queue (optional, future release) |
| `harbor-core`, `harbor-registry`, `harbor-jobservice` | `harbor` | Harbor role | S3 bucket + KMS key (optional, future release) |

<Note>
  The LILT cluster service account also carries in-cluster **RBAC** (Kubernetes
  `Role`/`RoleBinding`s 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.
</Note>

***

## How access is brokered

No component uses a long-lived IAM access key. Each principal obtains
short-lived credentials via a federated trust:

| Principal | Mechanism | How it is trusted |
| - | - | - |
| App workload, ExternalDNS, cert-manager, Cluster Autoscaler, LB controller, ESO, Harbor (optional), Karpenter (optional) | **EKS Pod Identity** (default) or **IRSA** | Pod Identity: each SA bound via `aws_eks_pod_identity_association`, no OIDC provider. IRSA: OIDC provider provisioned, each role uses `sts:AssumeRoleWithWebIdentity`. Controlled by `iam_auth_method`. |
| External Secrets Operator (if used) | **EKS Pod Identity** | The `external-secrets-sa` service account in the `external-secrets` namespace |
| EBS CSI storage driver | **EKS Pod Identity** | The EKS Pod Identity service on your cluster |
| Worker nodes | **EC2 instance profile** | The node role, assumed by EC2 |
| Bastion host (if enabled) | **EC2 instance profile** | The bastion role, assumed by EC2 |
| Control plane | **EKS service role** | Assumed by the EKS service |
| **Deployment role (Grant 1)** | **Your choice** | You decide who may assume it — a federated CI identity or a named administrator, per your own access standards |

<Note>
  LILT's reference Terraform module supports two IAM authentication modes, selected via `iam_auth_method`:

  * **`pod_identity`** (default, recommended) — all workload roles use EKS Pod Identity. No OIDC provider is provisioned. Every role trusts `pods.eks.amazonaws.com` via `sts:AssumeRole` + `sts:TagSession`. Pod Identity associations wire each Kubernetes service account to its role on the AWS side.
  * **`irsa`** (backward compat) — an OIDC identity provider is provisioned for the cluster, and each role uses `sts:AssumeRoleWithWebIdentity` with a service-account-scoped condition. Use this for environments already running IRSA that are not yet ready to migrate.
</Note>

<Warning>
  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.
</Warning>

***

## Helm chart sourcing for constrained environments

By default, LILT's deployment pulls community Helm charts from their upstream
HTTPS repositories (Artifact Hub, `kubernetes-sigs`, `aws.github.io`, etc.).
In environments with restricted egress, two variables let you redirect chart
sources without changing any other module configuration.

### Option A — OCI registry mirror (recommended)

Mirror all charts to a private OCI registry (e.g. Amazon ECR) and point the
module at the base URL. All add-ons pull from that registry; no public internet
access is required. The ECR VPC endpoint is included when VPC endpoints are enabled.

```hcl theme={null}
# Mirror charts first:
#   helm pull cluster-autoscaler --repo https://kubernetes.github.io/autoscaler --version 9.46.0
#   helm push cluster-autoscaler-9.46.0.tgz oci://123456789.dkr.ecr.us-east-1.amazonaws.com/helm-charts

helm_chart_registry = "oci://123456789.dkr.ecr.us-east-1.amazonaws.com/helm-charts"
```

### Option B — per-chart OCI override

Override individual charts while leaving others at their defaults (or at the
`helm_chart_registry` base). Useful when only a subset of charts has been
mirrored.

```hcl theme={null}
helm_chart_sources = {
  "cert-manager" = {
    repository = "oci://123456789.dkr.ecr.us-east-1.amazonaws.com/helm-charts"
    chart      = "cert-manager"
  }
}
```

Valid keys: `cluster-autoscaler`, `metrics-server`, `prometheus-node-exporter`,
`external-dns`, `cert-manager`, `kube-state-metrics`, `datadog-operator`,
`aws-load-balancer-controller`, `external-secrets`.

### Option C — break-the-glass (local `.tgz`, no network)

For fully isolated environments where even ECR is unreachable. Pre-fetch chart
tarballs onto the machine running `terraform apply`, then reference them by
absolute path. Setting `repository = null` tells the Helm provider to skip all
registry lookups.

```hcl theme={null}
helm_chart_sources = {
  "cluster-autoscaler" = { repository = null, chart = "/mnt/charts/cluster-autoscaler-9.46.0.tgz" }
  "cert-manager"       = { repository = null, chart = "/mnt/charts/cert-manager-1.17.2.tgz" }
  # … one entry per enabled chart
}
```

<Warning>
  Local `.tgz` paths must exist on the machine running `terraform apply`. This
  option is not portable across runners unless all paths are pre-populated.
  Options A and B are preferred for reproducible deployments.
</Warning>
