Hedronite · Ops Lesson · 01-Earth-DevOps / Kubernetes · Sun 2026-09-06

EKS IRSA — OIDC, ServiceAccount annotation, IAM trust

A Pod needs S3 read. IRSA binds one IAM role to one Kubernetes ServiceAccount through the cluster OIDC issuer.

Lesson Class: Ops (DevOps + Kubernetes + EKS IRSA)
Cloud Referent: EKS OIDC issuer + eks.amazonaws.com/role-arn + IAM trust sub
Paired Dev: Python ServiceAccount IRSA annotation census
Paired Cert: CKS projected SA tokens + IRSA least privilege
Paired Go: client-go informer inventory of IRSA ServiceAccounts
Grounding: KUR Ch.14 SA Management · Kubestronaut CKS Hardening · Bootcamp EKS.md
OIDC
Cluster issuer associated as IAM OIDC provider.
Annotation
eks.amazonaws.com/role-arn on the workload ServiceAccount.
Trust
StringEquals on system:serviceaccount:ns:name.
Three strings must agree: issuer, annotation, and trust sub.

<!-- hal:authoritative:yaml -->

A Pod needs S3 read. The node role can do that, but so can every other Pod on the node. IRSA binds one IAM role to one Kubernetes ServiceAccount through the cluster OIDC issuer.

§I — Frame

Wednesday named Application Gateway paths on AKS. The visit before that hardened GKE nodes with COS and AppArmor. The visit before that tuned EKS Deployment surge. None of those shirts is today's job.

Today the track is Kubernetes on Amazon EKS. The concrete cloud returns to AWS after an AKS and a GKE stretch. The facet is identity at the Pod boundary: IAM Roles for Service Accounts, called IRSA in the EKS docs.

Kubernetes Up and Running, Chapter 14, Service Account Management, places the ServiceAccount as the Pod's identity for the API server. Kubestronaut Cluster Hardening adds the caution: disable default automount, minimize permissions, issue tokens deliberately. The Bootcamp EKS notes list IRSA (and the newer EKS Pod Identity) under Best Practices beside Pod Security Standards and managed node groups.

08-04 already spent GKE Workload Identity: GSA, KSA, and the federation binding. Do not redo the Google story. Today's question is the AWS path: cluster OIDC provider, eks.amazonaws.com/role-arn on the ServiceAccount, and an IAM trust policy that names one system:serviceaccount:namespace:name subject.

§II — Foundations: five facts about IRSA

Fact one. The node instance profile is a shared floor, not a per-Pod ceiling.

Every Pod scheduled onto an EC2 worker can reach the IMDS hop unless you block it. If the node role can s3:GetObject on the invoice bucket, a compromised sidecar on that node can too. IRSA exists so the Pod assumes a role scoped to its ServiceAccount. The node role keeps node-agent duties (CNI, logging agents you intentionally grant). Application data plane permissions move to IRSA roles.

Fact two. EKS publishes an OIDC issuer URL for the cluster.

aws eks describe-cluster --name prod --query cluster.identity.oidc.issuer returns a URL of the form https://oidc.eks.region.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE. That issuer signs the projected ServiceAccount JWT the kubelet mounts into the Pod. AWS IAM can trust that issuer after you associate an IAM OIDC identity provider with the cluster (console "Associate," eksctl utils associate-iam-oidc-provider, or the Terraform aws_iam_openid_connect_provider pattern the AWS docs show).

Without the IAM OIDC provider association, sts:AssumeRoleWithWebIdentity has nothing to validate against. The annotation alone does not grant AWS credentials.

Fact three. The ServiceAccount annotation is the K8s-side pointer.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: invoice-reader
  namespace: billing
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/invoice-reader-irsa

The EKS Pod Identity webhook (or the older mutating path documented for IRSA) injects environment variables and a projected token volume so the AWS SDKs find credentials through the default credential chain. Kubernetes Up and Running's Service Account Management section is the in-cluster half: the Pod must use this ServiceAccount (serviceAccountName: invoice-reader), not the namespace default.

Fact four. The IAM trust policy is the AWS-side gate.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:sub": "system:serviceaccount:billing:invoice-reader",
        "oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
      }
    }
  }]
}

sub must match exactly. A wildcard system:serviceaccount:billing:* widens the blast radius to every SA in the namespace. aud must match what the projected token carries for STS (commonly sts.amazonaws.com for IRSA). CKS projected-token labs train the same audience discipline on the Kubernetes side: Bootcamp question 30 sets audience and expirationSeconds on a serviceAccountToken projection.

Fact five. Permission policies on the role stay least-privilege and resource-scoped.

Trust answers "who may assume." The permission policy answers "what the assumed session may do." Prefer Resource ARNs for the invoice prefix, not *. Prefer condition keys (s3:prefix, KMS via kms:ViaService) where the data model allows. 08-19 spent EKS secrets encryption with KMS; an IRSA role that reads Secrets Manager still needs kms:Decrypt on the right key when the secret uses a CMK.

§III — Worked path: wire IRSA for one reader Pod

Step A. Confirm OIDC association.

aws eks describe-cluster --name prod --query cluster.identity.oidc.issuer --output text
aws iam list-open-id-connect-providers

If the issuer host/path is missing from IAM OIDC providers, associate it before debugging Pod env. The AWS EKS User Guide section "IAM roles for service accounts" leads with this prerequisite.

Step B. Create the IAM role with trust + permissions.

Name the role after the workload (invoice-reader-irsa). Attach an inline or managed policy that allows only the S3 prefix the reader needs. Put the trust document from Fact four on the role.

Step C. Create the ServiceAccount and annotate it.

kubectl -n billing create serviceaccount invoice-reader
kubectl -n billing annotate serviceaccount invoice-reader \
  eks.amazonaws.com/role-arn=arn:aws:iam::111122223333:role/invoice-reader-irsa

Kubestronaut's Minimize Permissions guidance still applies: bind RBAC separately if the Pod also talks to the Kubernetes API. IRSA does not replace RoleBindings. It replaces node-role AWS calls.

Step D. Run the Pod on that ServiceAccount.

apiVersion: v1
kind: Pod
metadata:
  name: invoice-job
  namespace: billing
spec:
  serviceAccountName: invoice-reader
  containers:
  - name: reader
    image: public.ecr.aws/aws-cli/aws-cli:2.17.0
    command: ["aws", "s3", "ls", "s3://acme-invoices/2026/"]

Exec and call aws sts get-caller-identity. The Arn should show assumed-role/invoice-reader-irsa/..., not the node instance role. If you still see the node role, check: annotation spelling, webhook/mutating injection, OIDC association, and whether the Pod was created before the annotation (recreate the Pod).

Step E. Block the easy downgrade.

Prefer blocking instance-metadata access from Pods that use IRSA (httpPutResponseHopLimit / network policy / IMDSv2 hop settings per current EKS node guidance) so a process cannot silently fall back to the node role. The Bootcamp EKS Best Practices line pairs IRSA with Pod Security Standards for a reason: identity and admission travel together.

§IV — Failure modes operators actually hit

**Wrong sub in the trust policy.** Namespace renamed in Git, ServiceAccount renamed in the chart, trust policy left on the old string. STS returns AccessDenied with an AssumeRoleWithWebIdentity message. Diff the live SA name against the trust StringEquals value. Do not widen to * to "unblock."

OIDC provider thumbprint or URL drift. Recreated cluster, new issuer ID, old aws_iam_openid_connect_provider still pointing at the previous EXAMPLED id. Describe-cluster issuer and IAM OIDC provider URL must match.

**Pod uses default.** Chart forgot serviceAccountName. Annotation sits on invoice-reader while the Pod mounts default. Caller identity shows the node role or fails AWS calls. Census the live Pod's spec.serviceAccountName (Dev lesson today).

Multiple annotations, one role shared across teams. Several ServiceAccounts point at one powerful role. Convenient until one namespace is compromised. Prefer one role per SA (or a tight family with identical blast radius).

Confusing IRSA with EKS Pod Identity. Bootcamp EKS.md names both. Pod Identity is the newer association API. This lesson stays on IRSA annotations because the trust-policy-and-OIDC story is still the dominant production pattern and maps cleanly to projected tokens. When you migrate, treat Pod Identity as a separate cutover with its own association objects.

RBAC too wide on the same SA. IRSA limits AWS. A ClusterRoleBinding that grants * on secrets still burns you inside the cluster. Kubestronaut's caution sections are CKS-domain work; keep them on the same SA you annotate.

§IV.B — Chart and GitOps discipline

Store the role ARN in values, not as a silent cluster mutate after apply:

serviceAccount:
  create: true
  name: invoice-reader
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/invoice-reader-irsa

Terraform (or CloudFormation) owns the IAM role and the OIDC provider. Helm owns the ServiceAccount annotation that consumes the role ARN output. Crossing that boundary with a one-off kubectl annotate creates drift the next chart sync will erase or fight.

When two environments share a chart, parameterize the ARN. Staging and production must not share one IAM role "for convenience." Separate trust policies, separate role ARNs, same annotation key.

Admission policy (OPA Gatekeeper / Kyverno) can require the annotation key on ServiceAccounts in billing and deny Pods that use default in that namespace. That enforcement is CKS-flavored cluster hardening sitting on top of the IRSA wire-up.

Compare GKE's Workload Identity annotation key (iam.gke.io/gcp-service-account) from 08-04: different key, same idea that the Kubernetes object carries a pointer the cloud identity system honors only when the cloud-side binding agrees.

§V — Relations to recent arc lessons

08-04 taught GKE Workload Identity as GSA-KSA federation. The shape rhymes: Kubernetes identity maps to cloud identity. The AWS mechanics differ: OIDC issuer on the EKS control plane, IAM federated principal, eks.amazonaws.com/role-arn.

08-19 taught envelope encryption for Secrets at rest on EKS. IRSA is how a Pod decrypts or fetches the secret's plaintext from AWS APIs without borrowing the node role.

09-03 taught Ingress path routing on AKS. Identity is orthogonal to path matching. A correctly routed Ingress still serves a Pod that can steal the node role if IRSA is absent.

08-31 taught node OS and LSM hardening on GKE. Node hardening reduces host escape damage. IRSA reduces lateral AWS damage when the container is already the attacker.

§VI — Operator checklist

  1. Issuer from describe-cluster matches an IAM OIDC provider.
  2. ServiceAccount carries eks.amazonaws.com/role-arn for exactly one role.
  3. IAM trust sub equals system:serviceaccount:{ns}:{sa} and aud matches STS.
  4. Permission policy is resource-scoped; no unused admin managed policies.
  5. Pods set serviceAccountName; default stays automount-false per Kubestronaut.
  6. sts get-caller-identity inside the Pod shows the IRSA role, not the node instance role.
  7. Document the role ARN next to the chart values so drift is reviewable in PR.

§VII — Close

IRSA is three objects agreeing on one string: the OIDC issuer, the ServiceAccount annotation, and the IAM trust sub. Miss any one and the Pod falls back to the shared node floor. Wire them once per workload, verify caller identity in-cluster, and keep RBAC on the same ServiceAccount as tight as the IAM policy.

Examine the trust policy beside the live ServiceAccount name before you widen either side.

Related