Session 048 — EKS: IRSA and Pod Identity — Workload-Level IAM Without Static Credentials
Prerequisite: session-047 (EKS node groups, Fargate, upgrades)
Session Objectives
- Understand the OIDC flow that underpins IRSA (ProjectedServiceAccountToken → STS AssumeRoleWithWebIdentity)
- Create an IAM OIDC Provider for the cluster and configure a ServiceAccount with an IAM Role via IRSA
- Verify that a pod uses the correct credentials without access keys in the manifest
- Configure EKS Pod Identity (add-on + association) and compare with IRSA
- Decide when to use Pod Identity vs IRSA
1. Why Not Use Static Credentials
[FACT] Before IRSA and Pod Identity, the common approaches to grant AWS access to pods were:
PROBLEMÁTICAS — não usar em produção:
1. Access keys como env vars no manifesto/Secret
kubectl create secret generic aws-creds --from-literal=AWS_ACCESS_KEY_ID=xxx
→ Credenciais não rotacionam, expostas em ETCD, visíveis no git
2. Node IAM Role (kiam/kube2iam)
→ Todos os pods no node têm acesso às credenciais do node role
→ IMDS não restrito = qualquer pod pode assumir o role do node
→ Viola princípio do least privilege
CORRETAS — usar em produção:
3. IRSA (IAM Roles for Service Accounts) — via OIDC
4. EKS Pod Identity — via EKS Auth service (mais recente)
2. IRSA — How It Works (OIDC Flow)
2.1 Technical Fundamentals
[FACT] IRSA uses the OpenID Connect (OIDC) standard, added to IAM in 2014. The flow:
┌─────────────────────────────────────────────────────────────────────┐
│ Fluxo de obtenção de credenciais via IRSA │
│ │
│ K8s API Server │
│ ┌──────────────┐ │
│ │ Projected │ 1. Kubelet monta token projetado no pod │
│ │ Service │ como volume em /var/run/secrets/eks.amazonaws │
│ │ Account │ .com/serviceaccount/token │
│ │ Token (JWT) │ │
│ └──────┬───────┘ │
│ │ Token contém: │
│ │ - iss: https://oidc.eks.us-east-1.amazonaws.com/id/XXXX│
│ │ - sub: system:serviceaccount:ns:sa-name │
│ │ - aud: sts.amazonaws.com │
│ │ - exp: agora + 1h │
│ ▼ │
│ AWS SDK (dentro do pod) │
│ ┌──────────────┐ │
│ │ Credential │ 2. SDK lê env vars injetadas pelo mutating │
│ │ Provider │ webhook: │
│ │ Chain │ AWS_ROLE_ARN=arn:aws:iam::123:role/MyRole │
│ │ │ AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/ │
│ └──────┬───────┘ eks.amazonaws.com/serviceaccount/token │
│ │ │
│ │ 3. SDK chama STS AssumeRoleWithWebIdentity │
│ ▼ │
│ AWS STS │
│ ┌──────────────┐ │
│ │ Assume Role │ 4. STS valida o JWT contra o OIDC provider do │
│ │ With Web │ cluster (endpoint público do EKS) │
│ │ Identity │ Verifica: iss, sub, aud, assinatura │
│ └──────┬───────┘ │
│ │ 5. Retorna credenciais temporárias (AccessKey, SecretKey, │
│ │ SessionToken) — válidas por 1h por padrão │
│ ▼ │
│ Pod recebe credenciais → acessa S3, DynamoDB, SSM etc. │
└─────────────────────────────────────────────────────────────────────┘
[FACT] EKS hosts a public OIDC discovery endpoint per cluster: https://oidc.eks.<region>.amazonaws.com/id/<CLUSTER_ID>. STS uses this endpoint to verify the JWT signature without needing to call the cluster directly.
[FACT] The OIDC signing keys rotate every 7 days. EKS keeps old public keys until they expire. External OIDC clients that cache keys need to refresh before expiration.
[FACT] The ProjectedServiceAccountToken (implemented in K8s 1.12) is the token mounted into the pod. It has a default expiration of 1 hour. EKS grants an extended expiration period of 90 days (to avoid disruptions in SDKs that don't renew well). Requests with tokens > 90 days are rejected by the API server.
2.2 IRSA Trust Policy
[FACT] The IAM role's trust policy requires the OIDC provider ARN as a Federated principal and two conditions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789012: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:production:checkout-api-sa",
"oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE:aud": "sts.amazonaws.com"
}
}
}
]
}
[FACT] To allow any ServiceAccount in a namespace (without pinning the SA name), replace StringEquals with StringLike and use * in the SA name:
"StringLike": {
"oidc.eks.us-east-1.amazonaws.com/id/XXXX:sub": "system:serviceaccount:production:*"
}
[FACT] The default trust policy size is 2048 characters. With the long OIDC issuer URL (~100 chars) and each condition consuming ~120 chars, a trust policy typically supports 4 ServiceAccounts per role (and at most ~8 with a quota increase).
2.3 IRSA Limits
[FACT] IAM OIDC provider limit: 100 per AWS account by default. An account with more than 100 EKS clusters using IRSA hits this limit. In that case, use Pod Identity (which doesn't require an OIDC provider per cluster).
3. Pod Identity — How It Works
3.1 Architecture
[FACT] EKS Pod Identity uses the EKS Auth Service (managed by AWS) to assume the role, instead of making STS calls from inside the pod. The result is less STS load and credentials cached at the node level.
┌─────────────────────────────────────────────────────────────────────┐
│ Fluxo Pod Identity │
│ │
│ Pod Node AWS │
│ ┌──────────┐ ┌──────────────────┐ ┌────────────────┐ │
│ │ AWS SDK │ 1. GET │ Pod Identity │ │ EKS Auth │ │
│ │ │──────────▶│ Agent (DaemonSet)│──▶│ Service │ │
│ │ │ http:// │ 169.254.170.23 │ │ (AWS managed) │ │
│ │ │ :80 │ port 80/2703 │ │ │ │
│ └──────────┘ └────────┬─────────┘ └───────┬────────┘ │
│ │ 2. Agent chama EKS │ │
│ │ Auth API com │ │
│ │ pod service acct │ │
│ └─────────────────────┘ │
│ 3. EKS Auth assume o role no STS e retorna temp credentials │
│ 4. Agent devolve as credenciais ao SDK do pod │
│ │
│ env vars injetadas pelo webhook: │
│ AWS_CONTAINER_CREDENTIALS_FULL_URI=http://169.254.170.23/... │
│ AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE=/var/run/... │
└─────────────────────────────────────────────────────────────────────┘
[FACT] The Pod Identity Agent is a DaemonSet (amazon-eks-pod-identity-agent) installed as a managed add-on. It runs in hostNetwork and occupies ports 80 and 2703 on the link-local address 169.254.170.23 (IPv4) or fd00:ec2::23 (IPv6).
[FACT] The role's trust policy for Pod Identity uses a fixed service principal — it doesn't reference an OIDC provider or a specific cluster:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "pods.eks.amazonaws.com"
},
"Action": ["sts:AssumeRole", "sts:TagSession"]
}
]
}
[FACT] sts:TagSession is required because Pod Identity includes session tags with cluster attributes: eks-cluster-name, eks-cluster-arn, kubernetes-namespace, kubernetes-service-account. This enables ABAC (attribute-based access control) — a single role can be reused by multiple SAs with different effective permissions based on tags of S3/DynamoDB resources etc.
[FACT] Pod Identity limits: up to 5,000 associations per cluster.
3.2 Pod Identity — Restrictions
[FACT] Pod Identity is not supported on:
- Fargate (Linux and Windows) — use IRSA for Fargate pods
- Windows EC2 nodes — use IRSA
- AWS Outposts, EKS Anywhere, self-managed K8s — use IRSA
- Requires K8s 1.24+ / platform eks.4 for K8s 1.28 (earlier versions already support it on all platform versions)
4. Comparison: IRSA vs Pod Identity
[FACT] Official comparison table (docs.aws.amazon.com/eks/latest/userguide/service-accounts.html):
╔══════════════════════════════════╦═══════════════════════════╦═══════════════════════════╗
║ Atributo ║ Pod Identity ║ IRSA ║
╠══════════════════════════════════╬═══════════════════════════╬═══════════════════════════╣
║ Configuração ║ EKS API (nenhum obj. K8s) ║ Annotation em ServiceAcct ║
║ Requer OIDC provider por cluster ║ NÃO ║ SIM ║
║ Trust policy atualizada por clust║ NÃO (service principal ║ SIM (OIDC ARN por cluster)║
║ ║ fixo reutilizável) ║ ║
║ Limit OIDC providers conta ║ N/A ║ 100 por conta ║
║ Trust policy size limit ║ N/A (sem cluster-bound) ║ ~4–8 SAs por role ║
║ STS calls no pod ║ NÃO (EKS Auth assume) ║ SIM (SDK chama STS) ║
║ Session tags (ABAC) ║ SIM (cluster, ns, sa) ║ NÃO ║
║ Role reusável multi-cluster ║ SIM (sem alteração) ║ NÃO (atualizar trust) ║
║ Cross-account access ║ Via role chaining ║ Diretamente ║
║ Suporte Fargate ║ NÃO ║ SIM ║
║ Ambientes suportados ║ EKS apenas ║ EKS, EKSa, ROSA, self-mgd ║
╚══════════════════════════════════╩═══════════════════════════╩═══════════════════════════╝
[FACT] AWS Recommendation: use Pod Identity whenever possible. Use IRSA when: Fargate pods, cross-account without role chaining, non-EKS environments, or gradual migration.
5. CDK Python — IRSA and Pod Identity
from aws_cdk import (
Stack, CfnOutput,
aws_eks as eks,
aws_iam as iam,
aws_s3 as s3,
aws_dynamodb as dynamodb,
)
from constructs import Construct
class IrsaStack(Stack):
"""
IRSA: IAM Roles for Service Accounts
Pré-requisito: cluster com OIDC habilitado (withOIDC: true no eksctl ou
eks.Cluster com openIdConnectProvider automaticamente criado pelo L2)
"""
def __init__(self, scope: Construct, construct_id: str,
cluster: eks.Cluster, **kwargs):
super().__init__(scope, construct_id, **kwargs)
# ──────────────────────────────────────────────────────────────
# Recursos AWS que o workload vai acessar
# ──────────────────────────────────────────────────────────────
config_bucket = s3.Bucket(self, "ConfigBucket",
bucket_name=f"checkout-config-{self.account}",
)
orders_table = dynamodb.Table(self, "OrdersTable",
table_name="orders",
partition_key=dynamodb.Attribute(name="orderId", type=dynamodb.AttributeType.STRING),
)
# ──────────────────────────────────────────────────────────────
# IAM Role com trust policy IRSA
# O CDK L2 eks.Cluster.add_service_account() cria automaticamente:
# 1. IAM Role com trust policy referenciando o OIDC provider do cluster
# 2. Kubernetes ServiceAccount com annotation eks.amazonaws.com/role-arn
# ──────────────────────────────────────────────────────────────
checkout_sa = cluster.add_service_account("CheckoutApiSA",
name="checkout-api-sa",
namespace="production",
# O CDK cria a trust policy com StringEquals para namespace + SA name
)
# Permissões mínimas para o workload
config_bucket.grant_read(checkout_sa)
orders_table.grant_read_write_data(checkout_sa)
# SSM Parameter Store — parametros do namespace da aplicação
checkout_sa.role.add_to_policy(iam.PolicyStatement(
actions=["ssm:GetParameter", "ssm:GetParametersByPath"],
resources=[
f"arn:aws:ssm:{self.region}:{self.account}:parameter/checkout/prod/*"
],
))
# Secrets Manager — secret de banco de dados
checkout_sa.role.add_to_policy(iam.PolicyStatement(
actions=["secretsmanager:GetSecretValue"],
resources=[
f"arn:aws:secretsmanager:{self.region}:{self.account}:secret:checkout/prod/db*"
],
))
# ──────────────────────────────────────────────────────────────
# SA para VPC CNI (IRSA própria, sem herdar node role)
# Best practice: isolar permissões do CNI do node role
# ──────────────────────────────────────────────────────────────
vpc_cni_sa = cluster.add_service_account("VpcCniSA",
name="aws-node",
namespace="kube-system",
)
vpc_cni_sa.role.add_managed_policy(
iam.ManagedPolicy.from_aws_managed_policy_name("AmazonEKS_CNI_Policy")
)
# ──────────────────────────────────────────────────────────────
# Deployment que referencia a ServiceAccount com IRSA
# ──────────────────────────────────────────────────────────────
cluster.add_manifest("CheckoutApiDeployment", {
"apiVersion": "apps/v1",
"kind": "Deployment",
"metadata": {"name": "checkout-api", "namespace": "production"},
"spec": {
"replicas": 3,
"selector": {"matchLabels": {"app": "checkout-api"}},
"template": {
"metadata": {"labels": {"app": "checkout-api"}},
"spec": {
# Referência à ServiceAccount com IRSA — basta isso
# O webhook injeta automaticamente:
# AWS_ROLE_ARN
# AWS_WEB_IDENTITY_TOKEN_FILE
"serviceAccountName": "checkout-api-sa",
"containers": [{
"name": "api",
"image": "my-ecr.dkr.ecr.us-east-1.amazonaws.com/checkout-api:v1",
"env": [
# Forçar endpoint regional STS (evitar global, que tem mais latência)
{"name": "AWS_STS_REGIONAL_ENDPOINTS", "value": "regional"},
{"name": "AWS_DEFAULT_REGION", "value": "us-east-1"},
],
"resources": {
"requests": {"cpu": "500m", "memory": "512Mi"},
"limits": {"cpu": "2", "memory": "2Gi"},
},
}],
},
},
},
})
CfnOutput(self, "CheckoutSARoleArn",
value=checkout_sa.role.role_arn,
description="IAM Role ARN da ServiceAccount do checkout-api",
)
class PodIdentityStack(Stack):
"""
Pod Identity: abordagem mais recente, sem OIDC por cluster.
Requer add-on 'eks-pod-identity-agent' instalado no cluster.
"""
def __init__(self, scope: Construct, construct_id: str,
cluster: eks.Cluster, **kwargs):
super().__init__(scope, construct_id, **kwargs)
config_bucket = s3.Bucket(self, "ConfigBucketPodId",
bucket_name=f"checkout-config-podid-{self.account}",
)
# ──────────────────────────────────────────────────────────────
# IAM Role com trust policy Pod Identity (service principal fixo)
# Este role pode ser reutilizado em múltiplos clusters sem alteração
# ──────────────────────────────────────────────────────────────
pod_identity_role = iam.Role(self, "CheckoutPodIdentityRole",
role_name="CheckoutApiPodIdentityRole",
description="Role para checkout-api via EKS Pod Identity (reutilizável multi-cluster)",
assumed_by=iam.CompositePrincipal(
iam.ServicePrincipal("pods.eks.amazonaws.com"),
),
)
# IMPORTANTE: TagSession é obrigatório para Pod Identity funcionar com session tags
pod_identity_role.assume_role_policy.add_statements(
iam.PolicyStatement(
effect=iam.Effect.ALLOW,
principals=[iam.ServicePrincipal("pods.eks.amazonaws.com")],
actions=["sts:AssumeRole", "sts:TagSession"],
)
)
config_bucket.grant_read(pod_identity_role)
pod_identity_role.add_to_policy(iam.PolicyStatement(
actions=["ssm:GetParameter", "ssm:GetParametersByPath"],
resources=[f"arn:aws:ssm:{self.region}:{self.account}:parameter/checkout/*"],
))
# ──────────────────────────────────────────────────────────────
# Associação Pod Identity: mapeamento role ↔ ServiceAccount
# Feito via EKS API (não há objeto Kubernetes para criar)
# CDK L1 CfnPodIdentityAssociation
# ──────────────────────────────────────────────────────────────
pod_id_association = eks.CfnPodIdentityAssociation(self, "CheckoutPodIdAssoc",
cluster_name=cluster.cluster_name,
namespace="production",
service_account="checkout-api-sa", # SA que já existe no cluster
role_arn=pod_identity_role.role_arn,
)
# ──────────────────────────────────────────────────────────────
# Add-on Pod Identity Agent (instala o DaemonSet no cluster)
# Deve ser instalado ANTES das associações
# ──────────────────────────────────────────────────────────────
pod_id_agent_addon = eks.CfnAddon(self, "PodIdentityAgentAddon",
cluster_name=cluster.cluster_name,
addon_name="eks-pod-identity-agent",
resolve_conflicts_on_update="OVERWRITE",
)
# Garantir ordem: add-on instalado antes da associação
pod_id_association.node.add_dependency(pod_id_agent_addon)
# ──────────────────────────────────────────────────────────────
# Kubernetes ServiceAccount — sem annotation de IAM role
# Com Pod Identity, a associação é feita fora do K8s
# ──────────────────────────────────────────────────────────────
cluster.add_manifest("CheckoutSAPodId", {
"apiVersion": "v1",
"kind": "ServiceAccount",
"metadata": {
"name": "checkout-api-sa",
"namespace": "production",
# SEM annotation eks.amazonaws.com/role-arn (diferença do IRSA)
},
})
CfnOutput(self, "PodIdentityRoleArn",
value=pod_identity_role.role_arn,
)
6. Python — Runtime Credential Verification
"""
Código da aplicação que usa credenciais injetadas via IRSA ou Pod Identity.
Nenhuma modificação é necessária no código — o AWS SDK usa a default credential chain.
"""
import os
import boto3
import json
import logging
logger = logging.getLogger(__name__)
# SDK usa automaticamente:
# IRSA: AWS_ROLE_ARN + AWS_WEB_IDENTITY_TOKEN_FILE
# Pod Identity: AWS_CONTAINER_CREDENTIALS_FULL_URI + AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE
# Nenhuma linha de código extra necessária para IRSA ou Pod Identity
def verify_credentials() -> dict:
"""
Verifica qual identidade o pod está usando.
Útil para debugging e asserting em testes.
"""
sts = boto3.client("sts")
identity = sts.get_caller_identity()
# Identifica se está usando IRSA, Pod Identity ou node role
role_arn = identity["Arn"]
account = identity["Account"]
credential_type = "unknown"
if os.environ.get("AWS_WEB_IDENTITY_TOKEN_FILE"):
credential_type = "IRSA"
elif os.environ.get("AWS_CONTAINER_CREDENTIALS_FULL_URI"):
credential_type = "Pod Identity"
elif "assumed-role" not in role_arn:
credential_type = "Node Role (WARNING: violates least privilege)"
return {
"account": account,
"role_arn": role_arn,
"credential_type": credential_type,
"user_id": identity["UserId"],
}
def access_s3_with_irsa(bucket_name: str, key: str) -> str:
"""Lê objeto do S3. Usa credenciais injetadas automaticamente."""
s3 = boto3.client("s3")
response = s3.get_object(Bucket=bucket_name, Key=key)
content = response["Body"].read().decode("utf-8")
logger.info("S3 read successful: s3://%s/%s", bucket_name, key)
return content
def access_dynamodb_with_irsa(table_name: str, item_key: dict) -> dict | None:
"""Lê item do DynamoDB."""
dynamodb = boto3.resource("dynamodb")
table = dynamodb.Table(table_name)
response = table.get_item(Key=item_key)
return response.get("Item")
def get_ssm_param(name: str) -> str:
"""Lê parâmetro SSM. Credenciais via IRSA/Pod Identity."""
ssm = boto3.client("ssm")
response = ssm.get_parameter(Name=name, WithDecryption=True)
return response["Parameter"]["Value"]
def get_secret(secret_arn: str) -> dict:
"""Lê secret do Secrets Manager."""
sm = boto3.client("secretsmanager")
response = sm.get_secret_value(SecretId=secret_arn)
return json.loads(response["SecretString"])
# Lambda handler / app init
def app_init():
"""Chamado no startup da aplicação para verificar configuração."""
creds = verify_credentials()
if creds["credential_type"] == "Node Role (WARNING: violates least privilege)":
logger.error("SECURITY WARNING: pod is using node IAM role — configure IRSA or Pod Identity")
logger.info(
"Running as: %s (%s) in account %s",
creds["role_arn"],
creds["credential_type"],
creds["account"],
)
# Verificação de ABAC com Pod Identity session tags
def check_abac_access_example() -> None:
"""
Pod Identity injeta session tags no role assumption:
aws:RequestedRegion, eks-cluster-name, kubernetes-namespace,
kubernetes-service-account
Exemplo de policy IAM que usa ABAC para restringir acesso a bucket
por namespace (sem precisar de um role por namespace):
{
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:PutObject"],
"Resource": "arn:aws:s3:::checkout-data-${aws:PrincipalTag/kubernetes-namespace}/*"
}
Com isso, um role único pode ser usado por SAs em namespaces diferentes,
mas cada SA só acessa o prefixo do seu próprio namespace.
"""
pass
7. CLI — Complete IRSA and Pod Identity Setup
# ═══════════════════════════════════════════════════════════════
# IRSA — Setup passo a passo
# ═══════════════════════════════════════════════════════════════
CLUSTER_NAME="checkout-prod"
REGION="us-east-1"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
NAMESPACE="production"
SA_NAME="checkout-api-sa"
ROLE_NAME="CheckoutApiIRSARole"
# Step 1: Criar OIDC provider para o cluster
# (só precisa fazer uma vez por cluster)
eksctl utils associate-iam-oidc-provider \
--cluster "$CLUSTER_NAME" \
--region "$REGION" \
--approve
# Verificar OIDC provider criado
aws iam list-open-id-connect-providers --query 'OpenIDConnectProviderList[*].Arn'
# Obter OIDC issuer URL do cluster
OIDC_URL=$(aws eks describe-cluster \
--name "$CLUSTER_NAME" \
--region "$REGION" \
--query "cluster.identity.oidc.issuer" \
--output text | sed 's|https://||')
echo "OIDC Provider: $OIDC_URL"
# Saída: oidc.eks.us-east-1.amazonaws.com/id/EXAMPLED539D4633E53DE1B71EXAMPLE
# Step 2: Criar trust policy com o OIDC provider
cat > trust-policy.json << EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${ACCOUNT_ID}:oidc-provider/${OIDC_URL}"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"${OIDC_URL}:sub": "system:serviceaccount:${NAMESPACE}:${SA_NAME}",
"${OIDC_URL}:aud": "sts.amazonaws.com"
}
}
}
]
}
EOF
# Step 3: Criar IAM Role
aws iam create-role \
--role-name "$ROLE_NAME" \
--assume-role-policy-document file://trust-policy.json \
--description "IRSA role para checkout-api"
# Step 4: Anexar policies necessárias
aws iam attach-role-policy \
--role-name "$ROLE_NAME" \
--policy-arn "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
# Step 5: Criar namespace e ServiceAccount no K8s
kubectl create namespace "$NAMESPACE" --dry-run=client -o yaml | kubectl apply -f -
# Step 6: Criar SA com annotation do role ARN
kubectl create serviceaccount "$SA_NAME" \
--namespace "$NAMESPACE" \
--dry-run=client -o yaml | kubectl apply -f -
kubectl annotate serviceaccount "$SA_NAME" \
--namespace "$NAMESPACE" \
eks.amazonaws.com/role-arn="arn:aws:iam::${ACCOUNT_ID}:role/${ROLE_NAME}"
# Verificar annotation
kubectl describe serviceaccount "$SA_NAME" -n "$NAMESPACE"
# Deve mostrar: eks.amazonaws.com/role-arn: arn:aws:iam::123:role/CheckoutApiIRSARole
# ─────────────────────────────────────────────────────────────
# Verificar que o pod usa as credenciais corretas
# ─────────────────────────────────────────────────────────────
# Criar pod de teste que usa a SA
kubectl run test-irsa \
--image=amazon/aws-cli \
--namespace="$NAMESPACE" \
--serviceaccount="$SA_NAME" \
--restart=Never \
--command -- aws sts get-caller-identity
# Aguardar completar e ver output
kubectl logs test-irsa -n "$NAMESPACE"
# Esperado: "Arn": "arn:aws:sts::123:assumed-role/CheckoutApiIRSARole/..."
# Verificar env vars injetadas pelo webhook
kubectl exec -n "$NAMESPACE" test-irsa -- env | grep AWS
# Esperado:
# AWS_ROLE_ARN=arn:aws:iam::123:role/CheckoutApiIRSARole
# AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
# Verificar token montado
kubectl exec -n "$NAMESPACE" test-irsa -- cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token | \
cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# Mostra o payload do JWT: iss, sub, aud, exp
kubectl delete pod test-irsa -n "$NAMESPACE"
# ═══════════════════════════════════════════════════════════════
# Pod Identity — Setup
# ═══════════════════════════════════════════════════════════════
POD_ID_ROLE="CheckoutApiPodIdentityRole"
# Step 1: Instalar o add-on Pod Identity Agent
aws eks create-addon \
--cluster-name "$CLUSTER_NAME" \
--addon-name eks-pod-identity-agent \
--region "$REGION"
# Aguardar add-on ficar ACTIVE
aws eks wait addon-active \
--cluster-name "$CLUSTER_NAME" \
--addon-name eks-pod-identity-agent \
--region "$REGION"
# Verificar DaemonSet instalado
kubectl get daemonset -n kube-system eks-pod-identity-agent
# Step 2: Criar IAM Role com trust policy Pod Identity (service principal fixo)
cat > pod-identity-trust.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "pods.eks.amazonaws.com"
},
"Action": ["sts:AssumeRole", "sts:TagSession"]
}
]
}
EOF
aws iam create-role \
--role-name "$POD_ID_ROLE" \
--assume-role-policy-document file://pod-identity-trust.json \
--description "Pod Identity role para checkout-api (reutilizável multi-cluster)"
aws iam attach-role-policy \
--role-name "$POD_ID_ROLE" \
--policy-arn "arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess"
# Step 3: Criar Pod Identity Association via EKS API
# (não precisa fazer nada no K8s — sem annotation)
aws eks create-pod-identity-association \
--cluster-name "$CLUSTER_NAME" \
--namespace "$NAMESPACE" \
--service-account "$SA_NAME" \
--role-arn "arn:aws:iam::${ACCOUNT_ID}:role/${POD_ID_ROLE}" \
--region "$REGION"
# Listar associações
aws eks list-pod-identity-associations \
--cluster-name "$CLUSTER_NAME" \
--region "$REGION" \
--query 'associations[*].{NS:namespace,SA:serviceAccount,Role:associationArn}'
# Verificar que a SA não tem annotation (Pod Identity não precisa)
kubectl describe serviceaccount "$SA_NAME" -n "$NAMESPACE"
# Não deve ter eks.amazonaws.com/role-arn
# Verificar credenciais (mesmo teste do IRSA)
kubectl run test-pod-id \
--image=amazon/aws-cli \
--namespace="$NAMESPACE" \
--serviceaccount="$SA_NAME" \
--restart=Never \
--command -- aws sts get-caller-identity
kubectl logs test-pod-id -n "$NAMESPACE"
# Esperado: "Arn": "arn:aws:sts::123:assumed-role/CheckoutApiPodIdentityRole/..."
# Verificar env vars injetadas (diferentes do IRSA)
kubectl exec -n "$NAMESPACE" test-pod-id -- env | grep AWS
# Esperado:
# AWS_CONTAINER_CREDENTIALS_FULL_URI=http://169.254.170.23/v1/credentials
# AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE=/var/run/secrets/pods.eks.amazonaws.com/serviceaccount/eks-pod-identity-token
kubectl delete pod test-pod-id -n "$NAMESPACE"
# ─────────────────────────────────────────────────────────────
# ABAC com Pod Identity — verificar session tags
# ─────────────────────────────────────────────────────────────
# Quando o pod faz AssumeRole via Pod Identity, o STS inclui session tags.
# Verificar as tags na sessão:
kubectl run test-tags \
--image=amazon/aws-cli \
--namespace="$NAMESPACE" \
--serviceaccount="$SA_NAME" \
--restart=Never \
--command -- aws sts get-caller-identity --query 'Arn'
# O ARN da sessão inclui as tags codificadas:
# arn:aws:sts::123:assumed-role/CheckoutApiPodIdentityRole/eks-checkout-prod-...
8. Pitfalls
[FACT] IRSA requires the pod to use the SA correctly — serviceAccountName in the spec: if the pod uses the default ServiceAccount (the default when not specified), it won't receive IRSA credentials even if the annotated SA exists in the namespace.
[FACT] The mutating webhook must be running to inject the env vars: the pod-identity-webhook is part of the control plane managed by EKS — no separate installation needed. If the env vars AWS_ROLE_ARN and AWS_WEB_IDENTITY_TOKEN_FILE don't appear in the pod, verify the SA has the correct annotation.
[FACT] Unrestricted IMDS allows pods without IRSA/Pod Identity to access the node role: in clusters without IMDS blocking (IMDSv2 not enforced, hop limit > 1), any pod can access the node role credentials via http://169.254.169.254/latest/meta-data/iam/security-credentials/. Solution: configure --metadata-options HttpPutResponseHopLimit=1 in the node group's launch template.
[FACT] Pod Identity doesn't work on Fargate — for Fargate pods, always use IRSA. A common mistake is creating a Pod Identity Association expecting it to work for Fargate pods — the pod starts without valid credentials.
[FACT] Pod Identity is eventually consistent: there may be a delay of a few seconds after creating or updating an association via the EKS API. Don't create associations in the critical path of bootstrap; do it in separate initialization routines.
[FACT] Pods using a proxy need to exclude the Pod Identity Agent endpoint from the proxy: if HTTP_PROXY or HTTPS_PROXY are configured, the SDK will try to route the call to 169.254.170.23 through the proxy, failing. Add 169.254.170.23 to NO_PROXY.
[CONSENSUS] Remove AmazonEKS_CNI_Policy from the node role after configuring IRSA for the VPC CNI: by default, the CNI uses the node role (via IMDS) to manage ENIs. With a separate IRSA for the aws-node DaemonSet, the CNI policy can be removed from the node role, reducing the blast radius in case of node compromise.
Reflection Exercise
A startup is migrating a monolith to an EKS platform with 3 initial services and expected growth to 50 services in 2 years, distributed across 3 clusters (dev, staging, prod) in the same AWS account:
- Checkout API (EC2 nodes, prod): needs to read/write DynamoDB, publish to SNS, and read secrets from Secrets Manager
- Analytics Processor (Fargate, prod): needs to read from S3 and write to Kinesis
- Admin Dashboard (EC2 nodes, prod): needs cross-account access to an S3 bucket in another account (audit account)
Answer:
- For each service, choose IRSA or Pod Identity and justify based on the technical limitations and context (not "I prefer X").
- How many IAM OIDC providers will be created in the account with 3 clusters? Is that a problem?
- The Admin Dashboard needs to access the audit account. With Pod Identity, how would you configure this cross-account access (describe the role chaining)? With IRSA, how would it be different?
- In 2 years, with 50 services across 3 clusters (150 service accounts), a security team wants to use a single IAM Role for multiple services within the same business domain (e.g., "checkout-domain-role"). Which mechanism (IRSA or Pod Identity) makes this easier and why?
- How do you ensure that no pod can use node role credentials (IMDS) instead of service-specific credentials?
References
- [FACT] IAM roles for service accounts — docs.aws.amazon.com
- [FACT] Learn how EKS Pod Identity grants pods access to AWS services — docs.aws.amazon.com
- [FACT] Grant Kubernetes workloads access to AWS using Kubernetes Service Accounts — docs.aws.amazon.com
- [FACT] Assign IAM roles to Kubernetes service accounts — docs.aws.amazon.com
- [FACT] Authenticate to another account with IRSA — docs.aws.amazon.com