Lab 42 — Identidade de workload: task role, execution role, IRSA
O problema, e a empresa que o tem
A Cadência (L01, L03) abriu um segundo workload: um worker de conciliação, separado da API, que lê os arquivos de fechamento diário do dia no S3 e grava linhas de conciliação no DynamoDB. Depois que o time abriu o cluster EKS do L34 para o Operator do parceiro Aegis, um segundo pedaço da mesma conciliação passou a rodar lá também: um CronJob que consome a fila de pedidos pagos e lê uma chave de verificação do parceiro no Secrets Manager.
A task role do worker ainda não tinha sido revisada quando o prazo apertou, e um dev criou um usuário IAM com uma chave de acesso para testar contra recursos reais da AWS naquela mesma tarde — "só até a role ser aprovada". A role foi aprovada, anexada à task definition, e a chave nunca foi removida. Ninguém percebeu: a aplicação continuou funcionando exatamente igual, porque a variável de ambiente vencia a task role toda vez, silenciosamente.
Seis meses depois, numa revisão de segurança que também preparava o terreno para o L41, alguém rodou um grep no Terraform state em busca de padrão de chave de acesso — e encontrou a chave, ainda válida, com permissão de leitura no bucket inteiro de fechamentos. O incidente que este laboratório evita já tinha meio ano de vida.
A chave que este laboratório corrige já era um incidente real
Uma chave de acesso de longa duração com permissão de leitura sobre um bucket inteiro, válida por seis meses sem ninguém saber que existia, é exposição de dado — não é hipótese didática. O grep que a encontrou poderia ter sido um invasor em vez de um revisor de segurança, e o tempo entre a criação e a descoberta é exatamente o tempo em que ela ficou disponível para os dois.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação, não com a sensação de ter entendido a diferença entre os três mecanismos.
- Distinguir execution role de task role no ECS, e dizer qual delas a sua aplicação usa.
- Explicar por que a instância EC2 ou o nó do EKS não deveriam emprestar sua role a um workload específico.
- Nomear a ordem da cadeia de credenciais do SDK e prever qual fonte vence quando há mais de uma presente.
- Configurar uma task role sem nenhuma chave estática na definição, e provar isso por comando.
- Configurar IRSA para uma ServiceAccount, restringindo a confiança a `sub` E `aud`, não só a `sub`.
- Provar, pelo tipo de credencial resolvida em runtime, qual provedor respondeu em cada workload.
- Diferenciar, no CloudTrail, uma sessão de usuário IAM de uma sessão de task role e de uma sessão de IRSA.
- Diagnosticar uma role corretamente configurada e nunca usada, por causa de uma variável esquecida.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Execution role vs. task role | DVA-C02, SOA-C02, SAA-C03 | a task de conciliação usa as duas, com escopos diferentes | execution role é do agente (ECR, logs, segredo referenciado); task role é da aplicação |
| Cadeia de credenciais padrão do SDK | DVA-C02 | a ordem que faz a chave estática vencer a task role em silêncio | variável de ambiente vem antes do provedor de credencial do contêiner |
| STS AssumeRole / AssumeRoleWithWebIdentity | SAA-C03, SAP-C02 | o mecanismo comum por trás de task role e de IRSA | a diferença entre quem CHAMA o AssumeRole (o agente vs. o próprio SDK) |
| IAM roles for service accounts (IRSA) | SAP-C02, DVA-C02 | ServiceAccount vinculada por OIDC a uma role | a condição de `sub` no trust policy, e por que `aud` também precisa estar lá |
| Problema do "confused deputy" | SAP-C02 | `aws:SourceArn`/`aws:SourceAccount` na trust policy da task role | por que uma trust policy sem essas condições autoriza mais do que deveria |
| Credencial temporária vs. de longa duração | todas | o requisito central do módulo, provado por ausência de chave | temporária expira sozinha; estática só expira se alguém revogar |
| Isolamento de identidade por workload | SAP-C02, DOP-C02 | por que a role da instância/nó não serve para um workload específico | todo processo na mesma máquina herdaria a mesma permissão |
| Auditabilidade via CloudTrail | SOA-C02, SAP-C02 | `taskArn` e `sub` como contexto de sessão | sessão de role carrega contexto de origem; sessão de usuário IAM não |
Onde isto costuma ser cobrado errado
A pergunta clássica dá um contêiner com `AccessDenied` ao chamar S3, mostra que uma policy foi anexada a uma role, e pede a causa. Quem responde "a policy está errada" sem checar QUAL role recebeu a policy erra o alvo com frequência: a policy pode estar perfeita e anexada à execution role, que a aplicação nunca usa. A resposta certa é sempre conferir qual role a aplicação de fato assume antes de revisar o conteúdo dela.
Requisitos, e como cada um muda o desenho
Requisito que não aparece numa linha de Terraform é intenção, não arquitetura. A coluna da direita é onde cada um deixou marca neste laboratório.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Zero chave estática gravável | obrigatório, em qualquer lugar | task role e IRSA como únicas fontes de credencial; guarda de partida que recusa iniciar se achar uma |
| Identidade por workload, não por máquina | obrigatório | uma role para o worker ECS, outra para o CronJob EKS — nunca a role da instância ou do nó |
| Rotação sem intervenção humana | obrigatório | credencial vem de STS com expiração curta; o SDK renova sozinho, sem job de rotação |
| Reaproveitar o cluster e o OIDC do L34 | restrição de custo e de risco operacional | o provedor OIDC é lido por `data`, nunca recriado — duplicar provedor confunde qual trust policy vale |
| Auditoria por processo, não por pessoa | obrigatório para investigação de incidente | sessão de role carrega `taskArn` ou `sub`; nenhuma chamada aparece como um nome de usuário humano |
| Proteção contra "confused deputy" | boa prática exigida na trust policy | `aws:SourceArn`/`aws:SourceAccount` na task role; `sub` E `aud` na role IRSA |
Arquitetura mínima: a chave que ainda não foi removida
Este é o desenho que a Cadência tinha até a revisão de segurança encontrar a chave: funciona, publica valor, e o defeito não está visível em nenhum lugar óbvio. É exatamente por isso que ele serve de ponto de partida — o laboratório existe para tornar um risco invisível em algo que se prova por comando.
- → cria a chave para desbloquear o teste
- → AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY na variável de ambiente
- → RunTask diário, sem tocar em nenhuma credencial
- → lê exports com a sessão da chave estática
- → grava conciliação com a sessão da chave estática
- → sessão do usuário IAM, sem taskArn
- Fora da AWS
- Segurança e identidade
- Integração de apps
- Compute
- Armazenamento
- Banco de dados
- Gestão e governança
Este desenho funciona, e é exatamente por isso que ele sobrevive meses sem ninguém notar: nada aqui produz erro, alerta ou log diferente do desenho correto. Percorra os passos e repare que o defeito não está em nenhuma caixa — está numa variável que uma delas carrega.
- A chave nasce de um atalho com prazo. A task role certa ainda não tinha sido revisada, e o dev precisava testar contra recursos reais da AWS na mesma tarde. A chave era para durar uma sessão de trabalho e virou variável de ambiente na task definition — porque isso é o que "resolve na hora" sempre parece.
- A variável de ambiente vence antes de a role ser sequer consultada. A cadeia de credenciais padrão do SDK testa variável de ambiente ANTES do provedor de credencial do contêiner. Semanas depois, quando a task role foi corretamente configurada e anexada à definição, ela nunca chegou a ser usada — a chave, que continuava lá, sempre vencia primeiro.
- O disparo diário não muda nada sobre o risco. A periodicidade do EventBridge Scheduler é irrelevante para este defeito: ele mora na origem da credencial, não na frequência de execução. A task roda todo dia, e todo dia usa a chave errada, sem que isso apareça em nenhum lugar.
- A aplicação funciona idêntica com ou sem a task role — nada acusa a role morta. Leitura do S3 e escrita no DynamoDB funcionam do mesmo jeito, com o mesmo tempo de resposta, com o mesmo código HTTP. Não existe um "modo degradado" visível: a role configurada corretamente e nunca usada é indistinguível, do ponto de vista da aplicação, de uma role que nunca existiu.
- A auditoria perde o contexto que a task role daria de graça. Uma sessão de task role carrega o `taskArn` automaticamente, e o CloudTrail mostra exatamente qual task fez qual chamada. Uma sessão de chave estática mostra o nome do usuário IAM — que não diz qual processo, qual deploy, qual dia. Rastrear a origem de uma chamada suspeita fica mais lento justamente onde mais importa ser rápido.
- Por que a chave nunca foi removida. Ninguém tem o hábito de reabrir uma task definition que já está funcionando para conferir uma variável que nunca deu erro. Remover exigiria coragem para mexer em produção sem motivo aparente — e "sem motivo aparente" é exatamente o estado de uma chave que está silenciosamente ativa.
Uma chave estática com acesso de leitura a um bucket inteiro não é teórica
O cenário de abertura não é hipérbole didática: chave de acesso em variável de ambiente de task definition, esquecida por meses, é um dos achados mais comuns em revisão de segurança de ambiente ECS real. Ela não aparece em nenhum alarme, porque do ponto de vista da AWS ela é uma credencial válida sendo usada normalmente.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. A diferença estrutural em relação ao desenho mínimo não é "a mesma coisa com uma role a mais": são DOIS mecanismos de atestação diferentes, um por ambiente, convergindo para o mesmo tipo de sessão temporária.
- → RunTask diário, sem credencial na definição
- → agenda o CronJob no namespace conciliação
- → busca a sessão no endpoint do agente ECS
- → devolve sessão com contexto de taskArn
- → lê exports com a sessão da task role
- → grava conciliação com a sessão da task role
- → apresenta o token OIDC projetado da ServiceAccount
- → AssumeRoleWithWebIdentity valida o token e autoriza
- → consome pedidos pagos com a sessão do IRSA
- → lê a chave do parceiro com a sessão do IRSA
- → log da sessão com taskArn
- → log da sessão com sub da ServiceAccount
- Integração de apps
- Conceito de arquitetura
- Segurança e identidade
- Compute
- Armazenamento
- Banco de dados
- Gestão e governança
ECS e EKS respondem à mesma pergunta — quem está chamando a AWS? — de formas estruturalmente diferentes: uma é vendida pronta pelo agente, a outra é obtida pelo próprio SDK trocando um token. Percorra os passos: as duas terminam na mesma STS, e nenhuma delas grava um segredo em lugar nenhum.
- A definição da task não carrega nenhuma credencial. O disparo do EventBridge Scheduler não toca em segredo nenhum — ele só inicia a task, e a task definition não tem bloco `secrets` nem `environment` com chave. É a ausência que se prova, não uma presença.
- A sessão da task role é buscada pelo agente ECS, não pedida pelo código. O SDK não chama `sts:AssumeRole`: ele faz uma requisição HTTP simples a um endpoint interno (`169.254.170.2`, exposto pela variável `ECS_CONTAINER_CREDENTIALS_RELATIVE_URI`), e recebe de volta uma sessão que o agente do ECS já tinha assumido por trás da task.
- A mesma sessão grava e lê, e carrega a identidade da TASK — não da máquina. Se duas tasks diferentes rodassem no mesmo host EC2 (fora do Fargate), cada uma ainda receberia sua própria sessão, escopada à SUA task role — nunca a role da instância. É a separação que o L34 já cobrou para pods; aqui é para tasks.
- No EKS, quem pede a credencial é o próprio SDK, com o token da ServiceAccount. Aqui a mecânica é oposta à do ECS: não existe um agente vendendo sessão pronta. O SDK lê um arquivo de token JWT montado no pod, apresenta esse token à STS via `AssumeRoleWithWebIdentity`, e só então recebe a sessão temporária.
- O pod usa a sessão do IRSA para os dois destinos que o CronJob precisa. Fila e segredo do parceiro são acessados com a MESMA sessão, obtida uma vez e reaproveitada pelo SDK até perto da expiração. Nenhuma das duas chamadas carrega uma chave gravada em lugar nenhum — nem no manifesto, nem no Secret do Kubernetes.
- A auditoria distingue os dois caminhos pelo mesmo serviço. O CloudTrail mostra `taskArn` para a sessão do ECS e o `sub` da ServiceAccount para a sessão do IRSA — nos dois casos, uma identidade de WORKLOAD, nunca um nome de usuário humano. É a prova de que a auditoria melhorou, não só a segurança.
Repare no que NÃO mudou entre os dois ambientes: o binário .NET é o mesmo, sem branch de "se EKS, faça X". A diferença inteira mora na infraestrutura — quem injeta o quê no ambiente do processo antes dele começar a rodar.
Task role e IRSA não competem — cada um resolve o ambiente onde já está
Não existe decisão de "qual dos dois é melhor" fora de contexto: task role só existe para ECS, IRSA só existe para EKS. A escolha real deste laboratório não foi entre os dois mecanismos — foi decidir que NENHUM dos dois workloads teria chave estática, e cada ambiente resolve isso com a ferramenta que já tem embutida.
Da instanciação do SDK à primeira chamada AWS
A cadeia de credenciais do SDK para .NET é documentada e determinística — não é "tentativa e erro", é uma ordem fixa. O que este módulo prova é que essa ordem tem consequência de segurança, não só de conveniência.
A ordem exata, porque ela é o mecanismo inteiro
A cadeia do SDK para .NET testa, nesta sequência: credenciais de sessão vindas de `AWS_ACCESS_KEY_ID`/`AWS_SECRET_ACCESS_KEY`/`AWS_SESSION_TOKEN`; depois as mesmas duas primeiras variáveis sem a terceira; depois `AWS_WEB_IDENTITY_TOKEN_FILE` + `AWS_ROLE_ARN` (o mecanismo do IRSA, quando vem por variável); depois perfis nomeados; só then o provedor de credencial do contêiner (task role do ECS); e por último a metadata do EC2. Qualquer chave estática nas duas primeiras posições vence tudo o que vem depois.
// Formato que o endpoint interno do agente ECS devolve ao SDK, na porta
// 169.254.170.2, quando ele busca a sessao da task role. O SDK nunca chama
// STS diretamente neste caminho — o agente ja fez isso por tras da task.
{
"RoleArn": "arn:aws:iam::111122223333:role/ffv-lab-conciliacao-task",
"AccessKeyId": "ASIAEXEMPLO000000000000",
"SecretAccessKey": "***",
"Token": "***",
"Expiration": "2026-08-07T16:45:12Z"
}
// A forma exata pode variar por versao do agente — confirme na sua conta com
// um curl de dentro do conteiner antes de depender de um campo especifico.As decisões, e o que se perde em cada uma
📋 Dois workloads da Cadência — um worker de conciliação em ECS Fargate e um CronJob no cluster EKS do L34 — precisam ler S3, escrever DynamoDB, consumir uma fila SQS e ler um segredo do Secrets Manager, sem nenhuma chave de acesso gravada em lugar nenhum.
Os dois mecanismos custam zero na fatura — IAM e STS não cobram — e resolvem exatamente o requisito declarado: identidade por WORKLOAD, temporária, renovada sozinha. A alternativa de usar a role da instância EC2 ou do nó do EKS falha no requisito mais básico, que é isolamento: todo processo na mesma máquina herdaria a mesma permissão, e não há como dar ao worker de conciliação acesso ao DynamoDB sem dar o mesmo acesso a qualquer outro processo que um dia rode ali. A chave estática funciona tecnicamente, mas fere o requisito de rotação — ela não expira sozinha — e o de auditoria, porque a sessão não carrega contexto de qual processo a usou.
Alt: Role da instância EC2 / role do nó do EKS — Concede a mesma permissão a TODOS os processos que rodam na máquina, não só ao worker de conciliação. Num ambiente multi-tenant de tasks ou pods, isso é o oposto de menor privilégio — é exatamente o problema que task role e IRSA existem para resolver.
Alt: Chave de acesso de longa duração, em Secrets Manager (não em variável de ambiente) — Move o problema, não resolve: a chave continua estática, continua sem expirar sozinha, e agora precisa de rotação manual ou de um Lambda de rotação adicional — trabalho de engenharia para reproduzir o que STS já entrega de graça.
Alt: EKS Pod Identity, em vez de IRSA — É mais simples de configurar (sem provedor OIDC, `sts:AssumeRole` em vez de `AssumeRoleWithWebIdentity`) e legítima em cluster novo. Não é a escolha aqui porque o cluster já existe do L34 com IRSA configurado, e migrar o mecanismo de identidade de um cluster em produção não é uma decisão deste laboratório.
Alt: Uma única role compartilhada entre os dois workloads — Simplifica a criação e destrói a auditoria: uma chamada suspeita na fila SQS não diria se veio do worker de conciliação ou de qualquer outro serviço que também usasse a mesma role. Role por workload é o que torna o CloudTrail respondível.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Mecanismo no ECS | task role, via provedor de credencial do contêiner | chave estática; role da instância EC2 | sessão temporária, escopada à task, renovada sozinha, sem chamada explícita de STS | nada — é estritamente melhor para este caso, sem trade-off técnico real |
| Mecanismo no EKS | IRSA, via OIDC federado | EKS Pod Identity; role do nó; Secret do Kubernetes com chave | reaproveita o provedor OIDC que o L34 já criou, sem recurso novo | configuração mais verbosa que Pod Identity — vale reconsiderar em cluster novo |
| Condição na trust policy da task role | `SourceArn` + `SourceAccount` | nenhuma condição; só `SourceAccount` | impede que outra task da mesma conta, fora deste projeto, assuma a role | mais duas linhas de Terraform, sem custo operacional real |
| Condição na trust policy do IRSA | `sub` + `aud` | só `sub` | token emitido para audiência diferente de `sts.amazonaws.com` também é recusado | nenhum — é a forma recomendada, sem trade-off |
| Guarda de partida contra chave estática | sim, falha alto e cedo | nenhuma verificação; alerta só em auditoria periódica | transforma um incidente de meses em um erro de deploy, visível no primeiro minuto | uma linha de código a mais para manter, em cada serviço |
A dívida que este módulo não paga
A policy anexada às duas roles aqui é escopada ao recurso, mas não tem condição fina de tag, prefixo dinâmico por dia ou `dynamodb:LeadingKeys`. Isso é decisão do L41, que trata menor privilégio em profundidade. Este módulo resolve QUEM está chamando; o L41 resolve O QUANTO essa identidade pode fazer.
Construir: a task role do ECS, sem chave nenhuma
Duas condições na confiança, um escopo real na permissão. Nenhuma delas é opcional — remover qualquer uma reabre um caminho que a arquitetura já fechou.
# ecs-identidade.tf — a task role, e nenhuma chave em lugar nenhum
# Confianca: so o ECS pode assumir esta role, e so em nome de UMA task deste
# projeto. As duas condicoes protegem contra o "confused deputy": sem elas,
# uma task de OUTRO servico na mesma conta tambem poderia assumi-la.
data "aws_iam_policy_document" "confianca_ecs_task" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
condition {
test = "ArnLike"
variable = "aws:SourceArn"
values = ["arn:aws:ecs:${var.regiao}:${var.conta}:*"]
}
condition {
test = "StringEquals"
variable = "aws:SourceAccount"
values = [var.conta]
}
}
}
resource "aws_iam_role" "conciliacao_task" {
name = "${var.projeto}-conciliacao-task"
assume_role_policy = data.aws_iam_policy_document.confianca_ecs_task.json
}
# Escopo real: um prefixo do bucket, uma tabela. A condicao por padrao de
# chave/tag mais fina fica para o L41 — aqui o alvo e a IDENTIDADE, nao o
# refinamento maximo da policy.
data "aws_iam_policy_document" "conciliacao_task_permissoes" {
statement {
sid = "LerExportsDoDia"
effect = "Allow"
actions = ["s3:GetObject"]
resources = ["${aws_s3_bucket.fechamentos.arn}/exports/*"]
}
statement {
sid = "GravarConciliacao"
effect = "Allow"
actions = ["dynamodb:PutItem", "dynamodb:UpdateItem"]
resources = [aws_dynamodb_table.conciliacao.arn]
}
}
resource "aws_iam_role_policy" "conciliacao_task" {
role = aws_iam_role.conciliacao_task.id
policy = data.aws_iam_policy_document.conciliacao_task_permissoes.json
}
resource "aws_ecs_task_definition" "conciliacao" {
family = "${var.projeto}-conciliacao"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = 256
memory = 512
execution_role_arn = aws_iam_role.execucao.arn # so ECR + logs
task_role_arn = aws_iam_role.conciliacao_task.arn # so o que a APLICACAO usa
container_definitions = jsonencode([{
name = "conciliacao"
image = "${aws_ecr_repository.conciliacao.repository_url}:${var.sha_da_imagem}"
essential = true
# NENHUM bloco "environment" com AWS_ACCESS_KEY_ID ou AWS_SECRET_ACCESS_KEY.
# A ausencia e o ponto central deste arquivo — nao ha nada aqui para
# "esconder melhor". A credencial nunca existe em forma gravavel.
environment = [
{ name = "BUCKET_EXPORTS", value = aws_s3_bucket.fechamentos.bucket },
{ name = "TABELA_CONCILIACAO", value = aws_dynamodb_table.conciliacao.name },
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.conciliacao.name
"awslogs-region" = var.regiao
"awslogs-stream-prefix" = "conciliacao"
}
}
}])
}
# Disparo diario. A periodicidade nao muda nada sobre a identidade — e por
# isso que o defeito do desenho minimo nao aparece aqui: ele nunca dependeu
# de QUANDO a task roda, so de COMO ela se autentica.
resource "aws_scheduler_schedule" "conciliacao_diaria" {
name = "${var.projeto}-conciliacao-diaria"
schedule_expression = "cron(0 6 * * ? *)"
flexible_time_window { mode = "OFF" }
target {
arn = aws_ecs_cluster.principal.arn
role_arn = aws_iam_role.scheduler_ecs.arn
ecs_parameters {
task_definition_arn = aws_ecs_task_definition.conciliacao.arn
launch_type = "FARGATE"
network_configuration {
subnets = aws_subnet.privada[*].id
security_groups = [aws_security_group.conciliacao.id]
assign_public_ip = false
}
}
}
}
Por que a permissão de S3 mora na task role, não na execution role
A execution role só serve ao AGENTE — puxar imagem do ECR, escrever log, ler segredo referenciado na definição. Ela nunca é herdada pelo contêiner, então anexar a policy de S3 e DynamoDB nela não muda nada para a aplicação: é a confusão mais comum desta banda, e o L34 já a nomeou para o caso análogo em EKS.
Construir: IRSA para o CronJob do EKS, reaproveitando o cluster do L34
A diferença central em relação ao L34: aqui o provedor OIDC é LIDO com `data`, não criado de novo. O cluster e o provedor já existem — recriar duplicaria o provedor OIDC e deixaria duas fontes de verdade sobre o mesmo cluster.
# eks-identidade.tf — o CronJob, e a role que so a ServiceAccount dele assume
# O provedor OIDC e um recurso POR CLUSTER, criado uma vez no L34. Recria-lo
# aqui duplicaria o provedor — cada IAM OIDC Provider tem endpoint proprio, e
# ter dois para o MESMO cluster confunde qual trust policy referencia qual.
# Por isso: leitura, nao criacao.
data "aws_iam_openid_connect_provider" "cluster" {
url = "https://${data.aws_eks_cluster.principal.identity[0].oidc[0].issuer}"
}
data "aws_eks_cluster" "principal" {
name = "${var.projeto}-eks" # o cluster ja existe, criado no L34
}
# A condicao dupla e o que faz esta role NAO ser assumivel por qualquer pod
# do cluster. `sub` restringe a UMA ServiceAccount; `aud` confirma que o token
# foi emitido para se apresentar ao STS — sem ela, um token emitido para OUTRA
# audiencia tambem passaria na checagem de `sub`.
data "aws_iam_policy_document" "confianca_irsa_conciliacao" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [data.aws_iam_openid_connect_provider.cluster.arn]
}
condition {
test = "StringEquals"
variable = "${replace(data.aws_iam_openid_connect_provider.cluster.url, "https://", "")}:sub"
values = ["system:serviceaccount:conciliacao:conciliacao-sa"]
}
condition {
test = "StringEquals"
variable = "${replace(data.aws_iam_openid_connect_provider.cluster.url, "https://", "")}:aud"
values = ["sts.amazonaws.com"]
}
}
}
resource "aws_iam_role" "irsa_conciliacao" {
name = "${var.projeto}-irsa-conciliacao"
assume_role_policy = data.aws_iam_policy_document.confianca_irsa_conciliacao.json
}
data "aws_iam_policy_document" "irsa_conciliacao_permissoes" {
statement {
sid = "ConsumirPedidosPagos"
effect = "Allow"
actions = ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"]
resources = [data.aws_sqs_queue.pedidos_pagos.arn] # a fila do L03/L34
}
statement {
sid = "LerChaveDoParceiro"
effect = "Allow"
actions = ["secretsmanager:GetSecretValue"]
resources = [aws_secretsmanager_secret.chave_parceiro.arn]
}
}
resource "aws_iam_role_policy" "irsa_conciliacao" {
role = aws_iam_role.irsa_conciliacao.id
policy = data.aws_iam_policy_document.irsa_conciliacao_permissoes.json
}
Confiança sem `aud` autoriza mais do que parece
Restringir só `sub` protege contra OUTRA ServiceAccount assumir a role. Não protege contra um token emitido para uma audiência diferente de `sts.amazonaws.com` — um caso raro, mas real em cluster com múltiplos consumidores de OIDC. As duas condições juntas fecham a lacuna; uma sozinha deixa a porta encostada.
# conciliacao-cronjob.yaml — a linha que liga a ServiceAccount a role IRSA
apiVersion: v1
kind: ServiceAccount
metadata:
name: conciliacao-sa
namespace: conciliacao
annotations:
# E esta linha, e so ela, que faz o pod receber AWS_ROLE_ARN e
# AWS_WEB_IDENTITY_TOKEN_FILE no ambiente. Sem ela, o pod sobe com a
# identidade fraca padrao do namespace — sem AccessDenied nenhum na hora
# de subir, so quando a primeira chamada AWS acontecer.
eks.amazonaws.com/role-arn: arn:aws:iam::111122223333:role/ffv-lab-irsa-conciliacao
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: conciliacao-diaria
namespace: conciliacao
spec:
schedule: "0 6 * * *" # mesmo horario da task ECS, por decisao de produto — nao tecnica
jobTemplate:
spec:
template:
spec:
serviceAccountName: conciliacao-sa # sem isto, IRSA nao se aplica a este pod
restartPolicy: OnFailure
containers:
- name: conciliacao
image: 111122223333.dkr.ecr.us-east-1.amazonaws.com/ffv-lab-conciliacao-eks:SHA_DA_IMAGEM
env:
- name: FILA_URL
value: https://sqs.us-east-1.amazonaws.com/111122223333/ffv-lab-pedidos-pagos
- name: SEGREDO_PARCEIRO_ARN
value: arn:aws:secretsmanager:us-east-1:111122223333:secret:ffv-lab-chave-parceiro
# Nenhum "envFrom" apontando para Secret do Kubernetes com chave
# AWS dentro. Se voce ve isso num manifesto, e a chave estatica
# de novo, so que trocou de gaveta.
Construir: o código, e a guarda que recusa iniciar com chave estática
O desenho mínimo e o de produção rodam o MESMO binário — a diferença inteira está na infraestrutura em volta. A guarda abaixo é a única linha de aplicação que este módulo acrescenta, e ela existe para transformar silêncio em erro visível.
// Program.cs — o desenho minimo. O codigo parece limpo; o risco mora no Terraform.
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddAWSService<IAmazonS3>();
builder.Services.AddAWSService<IAmazonDynamoDB>();
// Nenhuma credencial aqui. O construtor sem argumento deixa o SDK escolher
// sozinho, pela cadeia padrao — e e exatamente essa escolha silenciosa que
// torna a chave estatica da task definition perigosa: o codigo nao tem como
// saber, e nao pergunta, qual fonte respondeu.
var app = builder.Build();
var s3 = app.Services.GetRequiredService<IAmazonS3>();
var dynamo = app.Services.GetRequiredService<IAmazonDynamoDB>();
var bucket = Environment.GetEnvironmentVariable("BUCKET_EXPORTS")!;
var tabela = Environment.GetEnvironmentVariable("TABELA_CONCILIACAO")!;
var exports = await s3.ListObjectsV2Async(new ListObjectsV2Request
{
BucketName = bucket,
Prefix = $"exports/{DateOnly.FromDateTime(DateTime.UtcNow):yyyy-MM-dd}/",
});
foreach (var obj in exports.S3Objects)
{
// processamento do arquivo de fechamento, omitido — nao e o foco do laboratorio
await dynamo.PutItemAsync(tabela, new Dictionary<string, AttributeValue>
{
["pk"] = new() { S = $"CONCILIACAO#{obj.Key}" },
["processadoEm"] = new() { S = DateTimeOffset.UtcNow.ToString("O") },
});
}
// Program.cs — o desenho de producao: recusa iniciar se achar chave estatica
var builder = Host.CreateApplicationBuilder(args);
var log = LoggerFactory.Create(b => b.AddConsole()).CreateLogger("Conciliacao");
// A GUARDA que o desenho minimo nao tem. Nao e defesa contra ataque: e defesa
// contra o proprio time, seis meses depois, colando uma chave "so para testar"
// e esquecendo de tirar. Falhar AQUI, alto e cedo, e mais barato que descobrir
// num scan de Terraform state.
var chaveEstatica = Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID");
if (!string.IsNullOrEmpty(chaveEstatica))
{
log.LogCritical(
"AWS_ACCESS_KEY_ID presente no ambiente — credencial estatica detectada. " +
"Encerrando antes de processar qualquer coisa. Remova a variavel da task " +
"definition ou do manifesto; a task role/IRSA ja resolve isto.");
Environment.Exit(1);
}
builder.Services.AddAWSService<IAmazonS3>();
builder.Services.AddAWSService<IAmazonDynamoDB>();
var app = builder.Build();
// Prova em runtime de QUAL provedor respondeu — a mesma pergunta que a secao
// de implantacao verifica de fora, aqui verificada de dentro do processo.
var credenciais = FallbackCredentialsFactory.GetCredentials();
log.LogInformation("credencial resolvida pela cadeia padrao: {Tipo}", credenciais.GetType().Name);
// Esperado em ECS: "ECSTaskCredentials" (ou equivalente do provedor de
// contêiner). Esperado em EKS: "AssumeRoleWithWebIdentityCredentials".
// Qualquer coisa contendo "Environment" ou "Basic" e a falha que este
// laboratorio existe para impedir.
var s3 = app.Services.GetRequiredService<IAmazonS3>();
var dynamo = app.Services.GetRequiredService<IAmazonDynamoDB>();
var bucket = Environment.GetEnvironmentVariable("BUCKET_EXPORTS")!;
var tabela = Environment.GetEnvironmentVariable("TABELA_CONCILIACAO")!;
var exports = await s3.ListObjectsV2Async(new ListObjectsV2Request
{
BucketName = bucket,
Prefix = $"exports/{DateOnly.FromDateTime(DateTime.UtcNow):yyyy-MM-dd}/",
});
foreach (var obj in exports.S3Objects)
{
await dynamo.PutItemAsync(tabela, new Dictionary<string, AttributeValue>
{
["pk"] = new() { S = $"CONCILIACAO#{obj.Key}" },
["processadoEm"] = new() { S = DateTimeOffset.UtcNow.ToString("O") },
});
}
log.LogInformation("conciliacao concluida: {Total} exports processados", exports.S3Objects.Count);
Falhar cedo é a defesa, não a elegância
A guarda não impede um ataque — impede que o PRÓPRIO TIME reintroduza o problema sem perceber. Um `AWS_ACCESS_KEY_ID` colado "só para testar" agora derruba o deploy em vez de ficar ativo por seis meses. É a mesma lógica do disjuntor de implantação do L03: transformar um risco silencioso em um evento que alguém precisa tratar.
Implantar, e provar qual credencial cada workload realmente usa
Cinco provas. Nenhuma aceita "a aplicação está funcionando" como resultado — funcionar é justamente o que a chave estática também fazia.
# achar-a-chave.sh — a prova que abre o laboratorio: a chave estava exposta
# Terraform state guarda o valor de toda variavel que passou por um recurso,
# em TEXTO PLANO, a menos que voce marque como sensitive — e "environment"
# de task definition raramente e marcado.
terraform show -json terraform.tfstate \
| jq -r '.. | objects | select(has("environment")) | .environment[]? | select(.name=="AWS_ACCESS_KEY_ID")'
# Esperado ANTES da correcao: uma linha com "value": "AKIA...".
# Esperado DEPOIS: nenhuma saida — a variavel nao existe mais na definicao.
# Segundo lugar onde ela aparece: log de aplicacao que imprime configuracao
# na partida "para depurar". Aconteceu na Cadencia numa versao antiga.
aws logs filter-log-events \
--log-group-name /ecs/ffv-lab-conciliacao \
--filter-pattern "AKIA" \
--query "events[].message" --output text
# Qualquer resultado aqui e um incidente, nao um achado academico: a chave
# apareceu em texto plano num grupo de logs que dezenas de pessoas podem ler.
# prova-ordem-da-cadeia.sh — confirma qual credencial o SDK realmente usou
# ANTES da correcao: a task tem tanto a variavel de ambiente quanto a task
# role anexada. O log de partida do CS_GUARDA (ou um log equivalente) mostra
# qual delas venceu.
aws logs filter-log-events \
--log-group-name /ecs/ffv-lab-conciliacao \
--filter-pattern "credencial resolvida" \
--query "events[].message" --output text
# Esperado ANTES: "... Tipo: EnvironmentVariablesAWSCredentials" (ou Basic).
# Isso PROVA que a task role, mesmo anexada e correta, nunca foi consultada.
# Se o log mostrar o tipo do provedor de contêiner mesmo com a chave presente,
# a ordem da cadeia documentada nao bate com o comportamento observado — pare
# e releia a task definition, porque algo nao confere.
# prova-correcao.sh — depois de remover a variavel, a sessao muda de tipo
terraform apply -auto-approve # aplica ecs-identidade.tf sem environment de chave
aws ecs wait services-stable --cluster ffv-lab --services ffv-lab-conciliacao
aws logs filter-log-events \
--log-group-name /ecs/ffv-lab-conciliacao \
--filter-pattern "credencial resolvida" \
--query "events[-1:].message" --output text
# Esperado DEPOIS: o tipo do provedor de credencial do conteiner (nao mais
# Environment nem Basic). Se ainda aparecer um tipo estatico, a variavel
# continua presente em algum lugar — confira tambem o Dockerfile e a imagem
# base, que podem ter um .env copiado por engano.
# prova-irsa.sh — dentro do pod, nenhuma chave; so token e role ARN
kubectl exec -n conciliacao deploy/conciliacao-debug -- env | grep ^AWS_
# Esperado: AWS_ROLE_ARN=arn:aws:iam::...:role/ffv-lab-irsa-conciliacao
# AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
# NENHUMA linha com AWS_ACCESS_KEY_ID ou AWS_SECRET_ACCESS_KEY. Se aparecer
# uma delas, alguem colocou um Secret do Kubernetes com chave estatica dentro
# — a mesma falha do ECS, so que morando em outro objeto.
kubectl exec -n conciliacao deploy/conciliacao-debug -- aws sts get-caller-identity
# Esperado: "Arn" contendo "assumed-role/ffv-lab-irsa-conciliacao/..." — a
# sessao carrega o NOME DA ROLE, nao um nome de usuario. E a prova de que a
# troca do token por sessao realmente aconteceu.
# prova-cloudtrail.sh — a auditoria diferencia os dois caminhos
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=GetObject \
--query "Events[?contains(CloudTrailEvent, 'ffv-lab-conciliacao-task')].CloudTrailEvent" \
--max-results 5 --output text | jq '.userIdentity.arn, .userIdentity.sessionContext.sessionIssuer.arn'
# Esperado: um "assumed-role" com o taskArn no sessionContext — nao um "user".
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=ReceiveMessage \
--query "Events[?contains(CloudTrailEvent, 'irsa-conciliacao')].CloudTrailEvent" \
--max-results 5 --output text | jq '.userIdentity.arn'
# Esperado: um "assumed-role" citando irsa-conciliacao, com o sub da
# ServiceAccount visivel no token — nao um nome de usuario IAM em nenhum caso.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · A chave estava exposta | grep no Terraform state e nos logs | nenhuma ocorrência de `AKIA` depois da correção | qualquer ocorrência antes da correção confirma o incidente do cenário de abertura |
| 2 · A ordem da cadeia decide | log de partida com o tipo de credencial resolvida | antes da correção: tipo estático, mesmo com a role anexada | se o tipo já vier do contêiner com a chave presente, a ordem documentada não bateu — investigue |
| 3 · A correção muda o tipo | mesmo log, depois de remover a variável | tipo do provedor de credencial do contêiner | tipo ainda estático indica que a variável sobrevive em outro lugar — confira o Dockerfile |
| 4 · IRSA não deixa chave no pod | `env | grep ^AWS_` dentro do pod | só `AWS_ROLE_ARN` e `AWS_WEB_IDENTITY_TOKEN_FILE` | qualquer `AWS_ACCESS_KEY_ID` indica um Secret do Kubernetes com chave estática dentro |
| 5 · A auditoria diferencia os caminhos | CloudTrail nas duas chamadas | `assumed-role` com `taskArn` ou com o `sub` da ServiceAccount, nunca um usuário | um `arn:aws:iam::...:user/` no `userIdentity` é a mesma falha do cenário de abertura |
As cinco provas, lidas juntas, contam uma história só
Nenhuma prova isolada fecha o caso: a primeira mostra que a chave existiu, a segunda mostra que ela vencia a role, a terceira mostra a correção, a quarta mostra que o EKS nunca teve esse problema para começo de conversa, e a quinta mostra que a auditoria melhorou como consequência. É a diferença entre "consertei" e "provei que consertei".
Quebrar de propósito: três falhas e o diagnóstico
As três acontecem de verdade, e as três parecem, de fora, "está sem permissão". O que as separa é onde mora o erro — na role errada, na condição errada, ou na variável que nunca saiu.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Policy colada na execution role | mova a permissão de S3/DynamoDB da task role para a execution role | contêiner recebe `AccessDenied`, mesmo com a policy existindo e anexada em algum lugar | confira QUAL role tem `taskRoleArn` vs. `executionRoleArn` na definição da task | permissão de negócio sempre na task role; execution role só para ECR/logs/segredo referenciado |
| Chave estática sobrevive à task role correta | anexe a task role corretamente e deixe a variável de ambiente antiga no lugar | nada quebra — é o cenário de abertura reproduzido de propósito | log de credencial resolvida (guarda de partida) mostra tipo estático | remover a variável; a guarda de partida devia ter barrado isto antes do deploy |
| `sub` do IRSA com curinga de namespace | troque a condição para `StringLike` com `system:serviceaccount:conciliacao:*` | QUALQUER pod do namespace consegue assumir a role, não só o CronJob pretendido | trust policy da role IRSA; teste criando uma ServiceAccount nova no mesmo namespace | condição exata, uma ServiceAccount por role — nunca curinga de namespace inteiro |
O sintoma mais enganoso desta lista
A segunda falha não produz sintoma nenhum, e é por isso que ela é a mais perigosa das três: as outras duas quebram alto — `AccessDenied` visível, ou permissão vazando para quem não devia. Esta funciona silenciosamente errada, exatamente como no incidente real que abriu o módulo.
Uma task ECS tem uma task role corretamente anexada, com a permissão certa de S3. A task definition também tem `AWS_ACCESS_KEY_ID` e `AWS_SECRET_ACCESS_KEY` numa variável de ambiente, esquecidos de um teste antigo. O que a aplicação usa para chamar o S3?
Segurança: o que muda quando a identidade é do workload
Trocar chave estática por credencial temporária não elimina risco — desloca ele. O risco deixa de ser "a chave vaza" e passa a ser "a trust policy autoriza mais do que deveria", que é mais raro e mais silencioso.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Chave de acesso de longa duração esquecida em variável de ambiente | alta | alto | guarda de partida que recusa iniciar; revisão de task definition antes do merge | grep periódico no Terraform state e nos logs por padrão de chave | revogar a chave imediatamente, remover a variável, redeployar |
| Trust policy da task role sem `SourceArn`/`SourceAccount` | baixa | médio | condição obrigatória em toda trust policy de task role | IAM Access Analyzer sobre a role | adicionar as condições; nenhuma sessão ativa precisa ser revogada, a mudança é preventiva |
| Trust policy do IRSA com `sub` em curinga de namespace | média | alto | condição exata por ServiceAccount, nunca `StringLike` com curinga amplo | listar ServiceAccounts do namespace e comparar com a condição da role | restringir a condição; auditar CloudTrail por chamadas fora do pod esperado |
| Role compartilhada entre workloads diferentes | média | médio | uma role por workload, mesmo que a permissão pareça idêntica hoje | CloudTrail sem contexto suficiente para separar origem — é o próprio sintoma | dividir a role antes de investigar, senão a investigação fica cega |
| Segredo do parceiro sem rotação, mesmo lido via IRSA | baixa | médio | este módulo resolve QUEM lê o segredo, não a rotação dele — é o L46 | idade da versão do segredo no Secrets Manager | configurar rotação automática, fora do escopo deste laboratório |
O `*` que não aparece na policy deste módulo, e por quê
Diferente do L03, aqui nenhuma ação usada — `s3:GetObject`, `dynamodb:PutItem`, `sqs:ReceiveMessage`, `secretsmanager:GetSecretValue` — exige recurso `*`: todas aceitam ARN específico. Se seu caso de uso precisar de uma ação de conta (como `ecr:GetAuthorizationToken` no L03), a regra continua a mesma: `*` só com a frase que explica por que o recurso não pode ser mais estreito.
Observabilidade: as perguntas que a auditoria de identidade responde
Um painel de identidade não mede performance — mede se a credencial em uso é a que você desenhou, e se alguém está tentando usar uma role fora do escopo pretendido.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| Qual tipo de credencial cada workload está usando agora? | log de partida da aplicação | tipo estático (Environment/Basic) é o incidente deste módulo | qualquer ocorrência |
| Alguma chamada AWS veio de um usuário IAM em vez de uma role? | CloudTrail `userIdentity.type` | `IAMUser` num workload que deveria usar role | > 0 por dia |
| Uma ServiceAccount inesperada assumiu a role IRSA? | CloudTrail `sub` no `sessionContext` | valor diferente de `system:serviceaccount:conciliacao:conciliacao-sa` | qualquer valor diferente |
| Existe chave de acesso de usuário IAM ativa há mais de 90 dias? | IAM Credential Report | chave de longa duração sem rotação é dívida acumulando | > 0 chaves acima de 90 dias |
| A task role está sendo assumida por task de outro serviço? | CloudTrail `taskArn` fora do esperado | trust policy sem `SourceArn` suficiente | qualquer taskArn fora do prefixo do projeto |
A métrica que não existe, e por que sua ausência é o ponto
Não há um "alarme de chave estática" nativo da AWS que dispare sozinho quando alguém cola `AWS_ACCESS_KEY_ID` numa task definition. IAM Access Analyzer e Credential Report chegam perto, mas nenhum dos dois olha dentro de variável de ambiente de contêiner. A guarda de partida da seção de construção existe justamente porque este buraco de observabilidade é real.
Escala: 10, 10 mil, 1 milhão de chamadas
| Volume | O que acontece com a identidade | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 chamadas/dia, 2 workloads | 2 roles, 2 confianças, nada disputa limite | nada; é o cenário do laboratório | nada |
| 1.000 chamadas/dia, 10 workloads | 10 roles distintas para manter | role sprawl: dez trust policies quase idênticas, revisadas manualmente | módulo de Terraform reutilizável para "role de workload", parametrizado por permissão |
| 100 mil chamadas/dia, 50 workloads | STS não é gargalo — é serviço regional com alta cota | a cota de IAM roles POR CONTA (padrão baixo, ajustável) passa a importar | checar `Service Quotas` para roles por conta antes de escalar o número de workloads |
| Renovação de sessão sob carga | o SDK renova perto da expiração, por processo | milhares de processos renovando quase ao mesmo tempo, se todos subiram juntos | não é problema em condição normal — STS é dimensionado para isto; medir se notar latência |
| Falha de AZ | IAM e STS são serviços regionais, não por AZ | nada aqui; a identidade não é a camada afetada por perda de AZ | a falha de AZ afeta o workload que USA a credencial, não a credencial em si |
O verdadeiro limite não é técnico, é operacional
IAM não vai ficar lento com mais roles, e STS não fica sobrecarregado com mais AssumeRole. O que escala mal é gente revisando cinquenta trust policies parecidas à mão. A resposta é o módulo de Terraform reutilizável citado acima, não uma otimização de infraestrutura.
Custo: por que este laboratório é honestamente de graça
É incomum dizer isso sem ressalva, mas aqui cabe: IAM, STS e o provedor OIDC não têm linha de cobrança direta. O custo real deste laboratório não está na fatura da AWS.
| Cenário | O que a AWS cobra | O custo real | Onde ele aparece |
|---|---|---|---|
| Protótipo — 2 workloads, 1 execução/dia | nada por IAM/STS/OIDC | tempo de engenharia para escrever as duas trust policies corretamente | horas de configuração, não linha de fatura |
| Produção — 50 workloads, roles individuais | nada por IAM/STS/OIDC | tempo de revisão de cada trust policy nova, e o módulo reutilizável para reduzir isso | processo de engenharia, não infraestrutura |
| O custo de NÃO fazer isto | nada, imediatamente | uma chave de longa duração exposta com acesso de leitura a um bucket inteiro, por tempo indeterminado | incidente de segurança — o cenário de abertura deste módulo |
O custo oculto é o único que importa aqui
Nenhuma das três linhas acima tem valor em dólar por hora. O risco de uma chave estática exposta não aparece na fatura da AWS de jeito nenhum — aparece, se aparecer, na fatura de um incidente: dado lido por quem não deveria, tempo de resposta, confiança perdida. É o único módulo desta série em que "quanto custa" tem resposta zero e "quanto custa não fazer" não tem teto.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | guarda de partida transforma chave esquecida em erro de deploy, visível cedo | a revisão de trust policy ainda é manual, workload por workload | módulo de Terraform reutilizável para role de workload | média |
| Segurança | zero chave estática, condições de `SourceArn`/`sub`+`aud` nas duas trust policies | policy de permissão ainda sem condição fina de tag/prefixo/chave | menor privilégio detalhado — L41 | alta |
| Confiabilidade | credencial renovada automaticamente pelo SDK, sem job de rotação manual | segredo do parceiro no Secrets Manager ainda sem rotação configurada | rotação automática de segredo — L46 | média |
| Eficiência de performance | nenhum overhead perceptível: STS responde na casa de dezenas de milissegundos | nenhum, para o volume atual | nenhuma ação necessária neste nível de escala | baixa |
| Otimização de custos | zero custo direto de IAM/STS/OIDC | nenhum, tecnicamente — o risco de não fazer isto não é um risco de custo de nuvem | nenhuma | baixa |
| Sustentabilidade | nenhum recurso físico adicional; identidade é metadado, não hardware | nenhum | nenhuma ação necessária | baixa |
Evolução em níveis: o que muda, e o que passa a doer
A terceira arquitetura não é um desenho: é a resposta a QUANDO a identidade de workload precisa amadurecer além do que este laboratório entrega.
Chave de acesso em variável de ambiente. Funciona, publica valor, e é onde a Cadência estava antes da revisão de segurança.Task role no ECS, IRSA no EKS, guarda de partida contra chave estática, trust policy com condição de confused deputy.Policy derivada do uso medido, condição por tag e por padrão de chave, Access Analyzer revisando continuamente (L41).Ambientes em contas separadas, trust policy restrita por conta, SCP como teto que nenhuma role, por mais permissiva, ultrapassa (L43).O último humano com credencial de longa duração — quem faz deploy — passa a usar OIDC federado de pipeline, não chave pessoal (L54).Correlacionar CloudTrail de todas as roles com detecção de anomalia (GuardDuty) para achar uso fora do padrão histórico de cada workload.Por que a ordem entre os níveis 2 e 3 não se inverte
Menor privilégio detalhado (nível 3) exige saber QUEM está chamando antes de decidir O QUANTO essa identidade pode fazer. Tentar afinar a policy de uma role compartilhada entre workloads (nível 1) não tem alvo claro: refinar permissão de uma identidade que não é sua é adivinhação, não engenharia.
Onde IA entra nesta arquitetura, e onde não entra
A pergunta central deste módulo — "qual mecanismo de identidade uso, e como configuro a confiança certa" — é determinística. Task role, execution role e IRSA seguem regras fixas e documentadas; um modelo não decide isso melhor que a leitura correta da documentação, e forçar IA aqui seria o antipadrão que a série inteira evita.
O lugar honesto onde IA acrescenta é o nível 6 da evolução: detecção de ANOMALIA sobre o uso de uma identidade já corretamente configurada. Uma role com permissão para ler um bucket que normalmente é acessado às 6h da manhã, uma vez por dia, e que de repente é chamada centenas de vezes às 3h — isso não quebra nenhuma regra de policy, e é exatamente o tipo de padrão que aprendizado não supervisionado detecta melhor que um limiar fixo.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | detectar uso de uma role fora do padrão histórico do workload |
| Por que uma regra não bastaria? | um limiar fixo ("mais de X chamadas por hora") gera alarme em todo pico legítimo de fim de mês; o padrão normal muda por workload e por dia da semana |
| De onde viriam os dados? | CloudTrail — já existe, já tem `sub` e `taskArn` por chamada |
| Qual o risco? | falso positivo em pico legítimo; exige limiar de confiança e caminho de revisão humana antes de qualquer ação automática de revogação |
| Por que não neste laboratório? | porque este módulo resolve a IDENTIDADE — o pré-requisito sem o qual a anomalia nem teria contexto de workload para comparar. É o L48 |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "decidir se esta trust policy está segura" troca uma checagem determinística — a condição existe ou não existe, o `sub` está restrito ou não está — por um julgamento probabilístico sobre algo que tem resposta certa. Revisão de IAM é exatamente o tipo de problema onde regra vence modelo, porque a regra está certa 100% das vezes que se aplica.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| Chave estática "só até a role ser aprovada" | desbloqueia teste imediato, sem esperar revisão | a cadeia de credenciais faz a chave vencer a role para sempre, mesmo depois de aprovada | role configurada corretamente e nunca de fato usada | guarda de partida que recusa iniciar com chave presente | nunca em ambiente que chega perto de produção; em notebook local, com chave revogada ao fim do dia |
| Policy de negócio na execution role | as duas roles parecem intercambiáveis quando se olha rápido | a execution role nunca é herdada pelo contêiner — a aplicação continua sem acesso | `AccessDenied` mesmo com a policy existindo e anexada em algum lugar | permissão de aplicação sempre na task role | nunca; são papéis estruturalmente diferentes |
| `sub` do IRSA com curinga de namespace inteiro | evita reconfigurar a trust policy a cada ServiceAccount nova | qualquer pod do namespace assume a role, não só o pretendido | chamada AWS partindo de um pod que a equipe não esperava | uma condição exata por ServiceAccount, uma role por workload | nunca em produção; talvez em namespace de sandbox isolado, sem dado real |
| Uma role só para todos os workloads do cluster | menos Terraform para escrever e manter | CloudTrail perde a capacidade de dizer QUAL workload fez a chamada | investigação de incidente travada porque a role não distingue origem | uma role por workload, mesmo com permissão parecida | protótipo pessoal, sem dado sensível envolvido |
| Usar a role do nó do EKS em vez de IRSA | já existe, já funciona, parece "menos uma coisa para configurar" | TODO pod do nó herda a mesma permissão — inclusive um comprometido por outro motivo | pod sem ServiceAccount vinculada ainda consegue chamar AWS | IRSA (ou EKS Pod Identity) por ServiceAccount, e IMDS restrito no nó | cluster de uso único, sem multi-tenancy de workload nenhuma — raro em produção |
| Copiar a chave estática para um Secret do Kubernetes | parece mais seguro que variável de ambiente pura | continua sendo a mesma chave estática, só que numa gaveta diferente — não expira, não rotaciona | a mesma exposição do cenário de abertura, só que mais difícil de encontrar num grep simples | IRSA elimina a necessidade de guardar a chave em QUALQUER lugar | nunca; Secret do Kubernetes não resolve o problema de fundo, só o esconde melhor |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| `AccessDenied` mesmo com a policy anexada | permissão foi anexada à execution role em vez da task role | confira `taskRoleArn` vs. `executionRoleArn` na definição, e onde a policy está anexada | task definition e `aws iam list-attached-role-policies` em cada role | mover a policy de negócio para a task role |
| Aplicação funciona igual antes e depois de anexar a task role | variável de ambiente com chave estática ainda presente, vencendo a cadeia | log de credencial resolvida (guarda de partida), ou `env` dentro do contêiner | task definition → bloco `environment` | remover a variável; a role passa a ser efetivamente usada |
| Pod não consegue chamar AWS, `env` mostra `AWS_ROLE_ARN` presente | ServiceAccount sem `serviceAccountName` referenciado no pod, ou anotação ausente | confira se o Deployment/CronJob referencia a ServiceAccount certa | manifesto do workload e da ServiceAccount | adicionar `serviceAccountName` e a anotação `eks.amazonaws.com/role-arn` |
| `AccessDenied` na chamada `AssumeRoleWithWebIdentity` | condição de `sub` ou `aud` na trust policy não bate com o token do pod | decodifique o JWT do pod (`cat $AWS_WEB_IDENTITY_TOKEN_FILE`, base64) e compare com a condição | trust policy da role IRSA vs. claims do token | ajustar a condição para o `sub` exato: `system:serviceaccount:<ns>:<sa>` |
| Chamada bem-sucedida, mas o CloudTrail mostra `IAMUser` em vez de `assumed-role` | ainda existe uma chave estática ativa sendo usada em algum caminho | busque no CloudTrail por `userIdentity.type == "IAMUser"` neste workload | CloudTrail, filtrando pelo nome do recurso ou da task | localizar e remover a origem da chave — repita a prova de "achar a chave" |
| Deploy novo falha na inicialização, log mostra "credencial estática detectada" | a guarda de partida está funcionando como projetado | não é bug — é a proteção pegando uma chave que alguém reintroduziu | log de partida da aplicação | remover a variável reintroduzida; não desative a guarda para contornar |
A pergunta que resolve metade destes casos
Antes de mexer em policy, pergunte: esta é uma pergunta de CONFIANÇA (quem pode assumir a role) ou de PERMISSÃO (o que a role pode fazer)? `AssumeRole` negado é sempre confiança — a trust policy. `AccessDenied` numa chamada de negócio é sempre permissão — a policy anexada. Os dois erros se parecem no texto e apontam para arquivos diferentes.
Limpeza: o que o destroy não leva
Este laboratório cria pouco recurso cobrável — a maior parte é IAM, que não cobra — mas o que sobrevive ao destroy é justamente o que mantém uma porta aberta.
# 1. Derrube o que o Terraform administra.
terraform destroy -auto-approve
# 2. USUARIO IAM E CHAVE DA PROVA DE CONCEITO: se voce recriou o cenario de
# abertura para testar, ele NAO esta no Terraform (foi criado a mao, e e
# esse o ponto). O destroy nao toca nele.
aws iam list-access-keys --user-name cadencia-conciliacao-poc
aws iam delete-access-key --user-name cadencia-conciliacao-poc \
--access-key-id "$(aws iam list-access-keys --user-name cadencia-conciliacao-poc \
--query 'AccessKeyMetadata[0].AccessKeyId' --output text)"
aws iam delete-user --user-name cadencia-conciliacao-poc
# 3. ROLE IRSA E O PROVEDOR OIDC: a role sai no destroy. O PROVEDOR OIDC nao
# sai, porque foi lido com `data`, nao criado aqui — ele pertence ao L34 e
# outros workloads do cluster podem depender dele.
aws iam get-open-id-connect-provider \
--open-id-connect-provider-arn "$(terraform output -raw oidc_provider_arn)" \
>/dev/null && echo "provedor OIDC preservado, como esperado — pertence ao L34"
# 4. GRUPO DE LOGS da task de conciliacao: retencao propria, sobrevive ao servico.
aws logs delete-log-group --log-group-name /ecs/ffv-lab-conciliacao 2>/dev/null || true
# 5. Prova final: nenhuma role, usuario ou chave com o nome do projeto de pe.
aws iam list-roles --query "Roles[?contains(RoleName, 'ffv-lab')].RoleName" --output table
aws iam list-users --query "Users[?contains(UserName, 'ffv-lab')].UserName" --output table| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Task role e execution role | sim | não | IAM não cobra por role existente |
| Role IRSA | sim | não | IAM não cobra por role existente |
| Provedor OIDC do cluster | não — foi lido, não criado | não | pertence ao L34; destruí-lo quebraria qualquer outra role IRSA do mesmo cluster |
| Usuário IAM de prova de conceito | não, se criado fora do Terraform | não diretamente | é justamente o padrão do incidente: recurso criado à mão, fora do controle de estado |
| Grupo de logs | depende de `skip_destroy` | sim, retenção | ciclo próprio, sobrevive ao serviço que o alimentava |
O usuário IAM da prova de conceito é o item mais importante desta lista
Se você recriou o cenário de abertura para acompanhar o laboratório, apagar o usuário e a chave dele não é opcional: é literalmente fechar o mesmo incidente que o módulo inteiro existe para evitar. Um `terraform destroy` limpo com essa chave ainda ativa reproduz o problema original, só que com um nome de laboratório em vez de produção.
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Aplicação ECS precisa chamar S3 e DynamoDB | task role, via provedor de credencial do contêiner | sessão temporária, escopada à task, sem chamada explícita de STS pelo código |
| Pod EKS precisa chamar SQS e Secrets Manager | IRSA, via OIDC federado | sessão temporária escopada à ServiceAccount, obtida pelo próprio SDK com o token do pod |
| Puxar imagem do ECR e escrever log | execution role, separada da task role | papel do agente, nunca herdado pelo contêiner — confundir as duas é o erro mais comum |
| Chave estática esquecida vencendo a role | guarda de partida no código | transforma um risco silencioso de meses num erro de deploy visível no primeiro minuto |
| Confusão sobre quem pode assumir a role | condição de `SourceArn`/`sub`+`aud` na trust policy | fecha o caminho de "confused deputy" que uma trust policy genérica deixaria aberto |
| Auditoria que não distingue processo de pessoa | sessão com `taskArn` ou `sub`, nunca usuário IAM | é o que torna uma investigação de incidente respondível em minutos, não em dias |
- O SDK é instanciado sem nenhuma credencial explícita no código, em ECS e em EKS.
- A cadeia de credenciais padrão é testada, NA ORDEM — variável de ambiente primeiro.
- Sem chave estática, o ECS busca uma sessão já assumida pelo agente, via endpoint interno.
- Sem chave estática, o EKS/IRSA faz o próprio SDK trocar um token OIDC por sessão via STS.
- As duas terminam no mesmo formato: credenciais temporárias, com expiração e renovação automática.
- Cada chamada carrega o contexto de origem — taskArn ou sub — visível no CloudTrail.
- Uma chave estática nas primeiras posições da cadeia vence tudo isso, em silêncio.
- A guarda de partida transforma esse silêncio em erro de deploy, visível no primeiro minuto.
Perguntas frequentes
❓ Preciso remover a chave de acesso se a task role já está configurada?
❓ Qual a diferença entre a execution role e a task role do ECS?
❓ Por que o IRSA existe se o nó do EKS já tem uma role de instância?
❓ O que é o token de identidade web que o IRSA usa para autenticar?
❓ Preciso criar um provedor OIDC novo para cada role IRSA nova?
❓ A credencial da task role expira? Como a aplicação lida com isso?
❓ O CloudTrail diferencia chamada de task role e de usuário IAM?
❓ IRSA e o EKS Pod Identity mais recente são a mesma coisa?
Fixando
Um time anexa a policy de leitura do S3 à execution role de uma task ECS, mas a aplicação continua recebendo `AccessDenied` ao chamar `GetObject`. A trust policy está correta e a task sobe normalmente. Qual é a causa mais provável?
A trust policy de uma role IRSA restringe a condição a `StringLike "system:serviceaccount:conciliacao:*"`. Qual é o risco concreto disso?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L34 (cluster EKS e IRSA aplicado ao Load Balancer Controller) e noção geral de IAM (trust policy vs. policy de permissão) |
| Conhecimentos adquiridos | a ordem exata da cadeia de credenciais do SDK e por que ela pode deixar uma role morta; a diferença estrutural entre task role (sessão vendida pelo agente) e IRSA (sessão obtida pelo próprio SDK via OIDC); execution role vs. task role; condições de confused deputy em trust policy |
| Limitação que fica | a policy de permissão das duas roles ainda não tem condição fina de tag ou prefixo dinâmico — é menor privilégio em profundidade, não em identidade |
| Próximo exemplo recomendado | L46 — criptografia com KMS, envelope encryption e CMK. Reutiliza as duas roles deste módulo, e é onde o segredo do parceiro passa a ter chave própria e rotação |
| Também habilitado por este módulo | L41 (menor privilégio) refina a permissão das roles construídas aqui; L54 (pipeline com OIDC) aplica o mesmo princípio de identidade federada a quem FAZ o deploy, não só a quem RODA |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Credential and profile resolution — AWS SDK for .NET (V3) — a ordem exata da cadeia de credenciais, incluindo a posição de variável de ambiente antes do provedor de credencial do contêiner; Amazon ECS task IAM role — que a task usa a credencial da task role exclusivamente, sem herdar nada da instância, e o contexto de `taskArn` na sessão; Amazon ECS task execution IAM role — o escopo separado do papel do agente; e IAM roles for service accounts (IRSA) — Amazon EKS — o mecanismo de OIDC, a rotação da chave de assinatura a cada sete dias e o isolamento de credencial por Pod. Valores de custo não aparecem neste módulo porque, neste caso específico, a resposta real é zero: IAM, STS e o provedor OIDC não têm cobrança direta.
O que não foi verificado, e você deve conferir na sua conta
O formato exato do JSON devolvido pelo endpoint interno do agente ECS (seção "Como funciona ponta a ponta") é ilustrativo, baseado na forma documentada do provedor de credencial de contêiner — confirme com um `curl` de dentro do seu próprio contêiner antes de escrever código que dependa de um campo específico dele. Da mesma forma, o nome exato da classe de credencial que aparece no log da guarda de partida (`FallbackCredentialsFactory.GetCredentials().GetType().Name`) pode variar por versão do SDK for .NET — trate o CONTEÚDO do nome ("Environment", "Basic", "ECS", "WebIdentity") como o sinal, não o nome exato da classe.
Terminou de ler?
Marcar como concluído registra o XP, mantém sua sequência e coloca 3 cartas deste módulo na fila de revisão espaçada.
Próximos passos sugeridos
Temas deste módulo
Discussão
Carregando comentários…