Lab 54 — Pipeline: CodePipeline ou GitHub Actions com OIDC
O problema, e a empresa que o tem
A Cadência, a mesma equipe de duas pessoas do L01, L03, L39 e L41, contratou uma auditoria de segurança antes de fechar um cliente maior. O relatório encontrou algo que ninguém tinha ido procurar: a chave de acesso do usuário IAM "ana-deploy" está ativa há 11 meses, nunca foi rotacionada, e tem permissão para ecr:PutImage e ecs:UpdateService — o suficiente para publicar código em produção a partir de qualquer máquina que a possua.
O L41 já resolveu a policy da task role: o que a aplicação pode fazer depois de subir. O L42 separou task role de execution role e trocou a chave estática do EKS por IRSA. Nenhum dos dois tocou nisto, porque é uma pergunta diferente: quem tem permissão de INICIAR um deploy — não o que o código faz depois de rodando. A chave da Ana responde a essa pergunta hoje, e a resposta é "qualquer pessoa que tenha essa chave, para sempre, até alguém lembrar de desativar".
A recomendação do relatório é direta: eliminar credencial de longa duração com permissão de deploy. A pergunta que a Cadência levou ao laboratório não é "como esconder a chave melhor" — é como o deploy pode acontecer sem que exista, em nenhum momento entre uma publicação e outra, uma chave para alguém roubar.
Identidade do pipeline vs. identidade da aplicação, lado a lado
A task role do L42 é avaliada quando a APLICAÇÃO já está rodando e precisa falar com outro serviço AWS. A role deste laboratório é avaliada no instante em que o PIPELINE publica uma versão nova — antes de qualquer task existir. São dois papéis com blast radius diferente: comprometer a task role expõe o que a aplicação toca; comprometer a role de deploy expõe a capacidade de publicar qualquer código.
O que este laboratório NÃO é
Não redesenha o rollout: a troca de versão continua sendo o disjuntor, o excedente e os três relógios do L03 — só quem chama `update-service` muda. Também não é sobre blue/green: se as suas migrations não são compatíveis para trás, o L39 continua sendo a peça que falta, e este pipeline pode disparar `CreateDeployment` do CodeDeploy no lugar de `update-service` sem mudar nada do que está descrito aqui.
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 federação de identidade.
- Explicar por que uma access key de usuário IAM nunca expira sozinha, e o que isso custa numa auditoria.
- Distinguir a role que o PIPELINE assume (este laboratório) da task role que a APLICAÇÃO usa depois de subir (L42).
- Registrar um provedor OIDC do GitHub na conta, sem duplicar um que já exista.
- Escrever uma trust policy que restringe `AssumeRoleWithWebIdentity` a um repositório e uma branch exatos, usando `sub` e `aud`.
- Configurar um workflow do GitHub Actions com `id-token: write` e nada além do necessário em `permissions`.
- Publicar uma imagem com tag imutável e disparar o rollout do L03 a partir da credencial da execução, não de uma pessoa.
- Provar, com CloudTrail, que um token de branch ou repositório errado é recusado antes de qualquer permissão IAM ser avaliada.
- Diagnosticar `AssumeRoleWithWebIdentity` recusado a partir dos claims do token, sem adivinhar.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Federação OIDC e AssumeRoleWithWebIdentity | SAA-C03, DVA-C02, SOA-C02 | trust policy com `sub` e `aud` | a diferença entre `AssumeRole` (papel assumindo papel, dentro da conta ou entre contas) e `AssumeRoleWithWebIdentity` (identidade EXTERNA federada) |
| Provedor de identidade OIDC no IAM | SAA-C03, SOA-C02, DOP-C02 | um provedor por emissor, reusado se já existir | quando criar um provider novo e quando ler um existente com `data` — o L42 já fez isso para o provider do EKS |
| Condição de trust policy (`sub`/`aud`) | DVA-C02, SAA-C03 | `StringEquals` restringindo repositório e branch | por que `StringLike` com wildcard amplo aproxima o risco do de uma chave compartilhada |
| Credencial temporária via STS | todas as associate/professional | sessão de minutos, nunca gravada em disco | onde a credencial "mora" durante a execução — variável de ambiente do processo, nunca arquivo |
| Menor privilégio na role de automação | SAA-C03, SOA-C02 | `Resource` restrito ao repositório ECR e ao serviço ECS específicos | por que uma role de deploy não deveria alcançar nenhum outro serviço além do que ela publica |
| `iam:PassRole` como vetor de escalonamento | SAA-C03, DVA-C02 | `PassRole` restrito às duas roles da task (L42) | por que `PassRole` sem `Resource` deixa a role de deploy "virar" qualquer role da conta que ela consiga passar |
| CI/CD nativo AWS vs. GitHub-hosted | DOP-C02 | CodePipeline/CodeBuild como alternativa funcionalmente equivalente | quando a fonte já-no-ecossistema decide, e quando não decide nada |
Onde isto costuma ser cobrado errado
A pergunta clássica dá uma role com trust policy usando `Principal: "*"` e federação OIDC, e pede o que está errado. A resposta não é "falta MFA" — é que sem a condição de `sub`, QUALQUER token válido daquele emissor (de qualquer repositório, de qualquer conta do GitHub) pode assumir a role. O emissor autentica QUEM emitiu o token; só a condição de `sub` decide QUAL repositório e branch.
Requisitos, e como cada um muda o desenho
Requisito não funcional que não aparece numa condição de trust policy ou numa linha de Terraform é intenção. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Nenhuma credencial de longa duração com permissão de deploy | zero | elimina a access key; força federação OIDC com credencial de vida curta |
| Escopo do deploy | só a branch main deste repositório | condição `sub` com `StringEquals` exato, não `StringLike` com wildcard |
| Sessão da execução | curta o suficiente para não sobreviver a um vazamento do runner | `max_session_duration` de 20 min na role — o pipeline mede 6 a 9 min |
| Teste antes de publicar | obrigatório, bloqueante | etapa `dotnet test` antes de qualquer `docker build`, sem exceção manual |
| Rastreabilidade de quem publicou | por execução, não por pessoa | `role-session-name` carrega o `run_id` — CloudTrail responde "quem" numa consulta |
| Reaproveitar o rollout confiável | obrigatório | o pipeline chama o mesmo `update-service` com excedente e disjuntor do L03; não redesenha |
| Revogação | imediata, sem depender de rotação | não há nada para rotacionar — desabilitar a trust policy ou o provider já corta tudo |
Arquitetura mínima: o deploy que sai do laptop de uma pessoa
Este é o desenho que a Cadência tem hoje, e ele publica de verdade — é por isso que sobreviveu 11 meses sem ninguém notar o problema. O laboratório começa por tornar o defeito visível, não por presumir que ele é óbvio.
- → aws configure com access key + secret key
- → docker push autenticado pela chave pessoal
- → aws ecs update-service com a mesma chave
- → pull da imagem versionada
- Fora da AWS
- Segurança e identidade
- Compute
Este desenho publica, e por isso sobrevive onze meses sem ninguém notar: a chave funciona todo dia. O defeito não está em nenhuma configuração errada — está em uma coisa que existe entre os deploys e não devia. Percorra os passos e repare que cada um deles é uma pergunta que a auditoria fez e não teve resposta.
- A chave não expira porque ninguém pediu que expirasse. Uma access key de usuário IAM não tem prazo embutido: fica válida até alguém desativá-la ou rotacioná-la à mão. A auditoria achou onze meses porque nada no sistema força rotação — é uma tarefa que depende de alguém lembrar.
- Uma identidade, dois papéis que a auditoria não separa. A mesma chave que a Ana usa para investigar um incidente no console também publica em produção. O CloudTrail registra "ana-deploy" para as duas coisas, e não há como saber, só pelo log, se foi ela decidindo ou um script agindo.
- Publicar em duas mãos significa a chave em dois laptops. Quando a Ana tirou férias, a chave foi compartilhada com o colega para ele poder publicar. Duplicar a credencial dobra a superfície de exposição sem dobrar nenhum controle sobre ela.
- update-service pula direto do terminal para produção. Não existe etapa de build isolada nem de teste entre o commit e o deploy: o comando sai de quem decidiu publicar, e nada além do bom senso garante que os testes passaram antes.
- Revogar significa adivinhar tudo que a chave tocou. Desativar a chave da Ana não avisa nada que dependia dela. Sem uma sessão nomeada por execução, reconstruir "o que essa chave fez nos últimos onze meses" é arqueologia de log, não um relatório que já existe.
- Por que alguém publica assim. Porque funciona no primeiro dia e não pede provisionar nada. É a mesma resposta do L03 para a tag `:latest`, aplicada à identidade: menor esforço, funciona, vira hábito antes de virar risco visível.
# Antes de mudar qualquer coisa, meça a exposição real — nao a percepcao dela.
aws iam get-access-key-last-used --access-key-id "$AKIAXXXX"
aws iam list-access-keys --user-name ana-deploy \
--query "AccessKeyMetadata[].{Id:AccessKeyId,Criada:CreateDate,Status:Status}"
# Na Cadencia: CreateDate de 11 meses atras, Status Active, ultimo uso ha 2 dias
# (um "ecs update-service" de deploy legitimo). A chave nao e um resquicio
# esquecido — e o caminho de producao em uso todo dia.O que a auditoria realmente encontrou
Não foi uma chave abandonada: foi uma chave EM USO, publicando em produção toda semana, sem nenhum mecanismo que force rotação ou expire sozinha. `access-key-last-used` mostrando atividade recente é o pior resultado possível numa auditoria — significa que o caminho de exposição é o caminho de produção, não um esquecimento isolado para corrigir e arquivar.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. Se você não conseguir apontar o requisito, a peça é adorno — e este desenho não tem nenhuma.
- → pede um token JWT assinado, TTL de minutos
- → AssumeRoleWithWebIdentity valida sub e aud
- → devolve credencial STS de vida curta
- → push com tag = SHA, IMMUTABLE
- → aws ecs update-service com a nova revisão
- → pull por digest imutável
- → sobe a nova sem parar a velha (L03)
- → alarme dispara volta automática
- Fora da AWS
- Segurança e identidade
- Compute
- Gestão e governança
A troca não é "guardar a chave num lugar mais seguro" — é a chave deixar de existir entre execuções. O que muda no desenho é a existência de uma etapa de federação ANTES de qualquer permissão AWS ser avaliada. Percorra os passos: a trust policy decide quem entra antes de qualquer policy IAM decidir o que fazer.
- O job pede prova, não posse. Cada execução do workflow solicita ao GitHub um token JWT novo, assinado pelo emissor do GitHub e válido por minutos. Não existe um arquivo de chave para vazar, porque não existe arquivo de chave nenhum — o token nasce e morre com o job.
- A trust policy decide quem pode pedir, não uma senha. A condição compara `token.actions.githubusercontent.com:sub` com `repo:ffv/api:ref:refs/heads/main` e `:aud` com `sts.amazonaws.com`. Um token do mesmo repositório numa branch diferente, ou de um fork inteiramente, falha AQUI — antes de qualquer policy IAM ser avaliada.
- STS devolve minutos, não meses. `AssumeRoleWithWebIdentity` troca o token por uma credencial temporária — chave de acesso, chave secreta e token de sessão que expiram sozinhos, sem ninguém precisar lembrar de revogar nada depois do deploy terminar.
- A credencial mora na memória do runner, não no disco. A action que assume a role injeta a credencial como variável de ambiente do processo de build; ela nunca é escrita em arquivo, nunca aparece em log (a action mascara o valor), e desaparece quando o job termina.
- A tag continua sendo o SHA — essa parte não mudou. A imutabilidade do registro é do L03; o que este laboratório muda é QUEM tem permissão de escrever nele. A role de deploy é restrita ao repositório ECR específico, nunca a `ecr:*` da conta inteira.
- O deploy reaproveita o rollout confiável, não o redesenha. A etapa final chama o mesmo `update-service` com excedente e disjuntor do L03. Trocar QUEM dispara o deploy não deveria significar reescrever COMO ele acontece — e é por isso que este diagrama termina onde o do L03 termina.
- A rede sob a rede continua de pé. Se a versão nova sobe saudável e responde errado, o alarme de 5xx do alvo do L03 dispara a volta automática. A mudança de identidade não tira nem substitui essa proteção — ela continua sendo o segundo par de olhos.
A diferença estrutural em relação ao desenho anterior não é "a mesma coisa com uma caixa a mais": é a existência de uma etapa de federação que não tinha equivalente antes. No desenho mínimo, a identidade e a permissão são a MESMA coisa, verificada uma vez quando a chave foi criada. Aqui, a identidade é verificada A CADA execução, contra um token que não existia um minuto antes.
O que fica exatamente igual, de propósito
O rollout — excedente, disjuntor, três relógios do L03 — não aparece redesenhado neste diagrama porque não muda. Trocar quem tem permissão de chamar `update-service` é uma mudança de identidade, não de arquitetura de deploy. Redesenhar os dois ao mesmo tempo é o jeito mais comum de gastar uma tarde depurando o problema errado.
Uma execução do pipeline, ponta a ponta
O nome de cada campo do token não é jargão: é o que a trust policy compara. sub diz de qual repositório e branch o token veio; aud diz para qual audiência ele foi emitido. Os dois são avaliados ANTES de qualquer policy de permissão — é possível ter a política IAM perfeita e ainda assim ser recusado, porque a identidade nem chegou a ser aceita.
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:ffv/api:ref:refs/heads/main"
}
}
}]
}
O formato de `sub` mudou para repositórios novos, e vale conferir o seu
Documentado pelo GitHub: repositórios criados a partir de 15 de julho de 2026 (ou que optaram por migrar) recebem `sub` com IDs imutáveis — algo como `repo:org@123456/repo@456789:ref:refs/heads/main`, em vez do formato só com nomes. Uma trust policy escrita com o formato antigo, aplicada a um repositório novo, nunca vai bater — e o sintoma é `AssumeRoleWithWebIdentity` recusado sem nenhuma pista óbvia do motivo. Confira qual formato o SEU repositório usa antes de fixar o valor.
Um detalhe que a documentação da GitHub confirma e vale citar com precisão: o formato de sub muda conforme o evento. Um push direto numa branch produz repo:org/repo:ref:refs/heads/main; um deploy amarrado a um environment do GitHub produz repo:org/repo:environment:prod — são strings diferentes, e uma trust policy escrita para uma não cobre a outra.
As decisões, e o que se perde em cada uma
📋 Publicar uma API .NET 8 em ECS Fargate várias vezes por dia, para um cliente que exige, em contrato, que nenhuma credencial de longa duração tenha permissão de deploy — com o código já hospedado no GitHub.
Resolve o requisito contratual sem introduzir um segundo sistema: o código já está no GitHub, e a integração nativa (`aws-actions/configure-aws-credentials`) cobre a troca de token por credencial em uma action mantida pela AWS. CodePipeline resolveria a mesma pergunta de identidade, mas exigiria espelhar o repositório para o CodeCommit ou configurar uma conexão — complexidade sem ganho quando o código já vive no GitHub.
Alt: CodePipeline + CodeBuild, com GitHub como fonte via conexão — É o caminho certo quando o time já vive no ecossistema AWS (CloudFormation, notificação via EventBridge, aprovação manual nativa) ou quando o repositório não é GitHub. Aqui, adiciona um serviço a manter sem resolver nada que o GitHub Actions não resolvesse com uma role a menos.
Alt: Chave de acesso IAM guardada como GitHub Secret — É o desenho mínimo deste módulo. Continua sendo o caminho mais rápido para o primeiro deploy automatizado, e é exatamente o que o contrato do cliente proíbe — a chave existe entre execuções, é o oposto do requisito.
Alt: Self-hosted runner dentro da VPC, com a task role do L42 — Elimina a chamada de rede pública para a AWS, mas transfere o problema: agora é preciso manter o runner com patch em dia, e ele passa a ser um alvo fixo — o token OIDC federado não precisa de nenhum host de longa duração para existir.
Alt: Assinar a imagem com KMS e validar na admissão do ECS — Resolve "essa imagem veio do meu pipeline", não "quem pode disparar o pipeline". É complementar, não substituto — e é o L98/nível de plataforma, não este laboratório.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Mecanismo de federação | OIDC do GitHub Actions com `AssumeRoleWithWebIdentity` | chave IAM em secret; CodePipeline com conexão ao GitHub; runner self-hosted na VPC | nenhuma credencial de longa duração persiste em lugar nenhum | depende do GitHub como fonte da verdade da identidade; se o GitHub tiver um incidente de emissão de token, o deploy também para |
| Escopo da trust policy | `StringEquals` exato em repositório + branch | `StringLike` com wildcard de repositório ou de branch | um token de qualquer outro repositório ou branch é recusado na federação, antes de qualquer permissão IAM | cada novo repositório ou ambiente exige sua própria condição — mais Terraform a manter |
| Duração da sessão | 20 min, explícito na role | o padrão de 1 h; ou tão curto quanto o job real (6-9 min) | folga suficiente para variação de duração sem deixar uma sessão vazada útil por horas | nenhuma perda real aqui — é o parâmetro mais barato de acertar do módulo inteiro |
| Onde o teste roda | dentro do mesmo job, antes do build | job separado com artefato compartilhado; suite de teste externa acionada por webhook | simplicidade: um pipeline com poucas etapas erra menos do que um com muitas | testes mais longos (integração pesada) fariam esse job estourar o timeout de 15 min |
| CI/CD nativo AWS vs. GitHub Actions | GitHub Actions, porque o código já mora lá | CodePipeline + CodeBuild com conexão ao GitHub | zero serviço adicional para hospedar o pipeline; a integração é mantida pela AWS (`aws-actions/configure-aws-credentials`) | perde a aprovação manual nativa e a notificação via EventBridge do CodePipeline — ambas dá para reconstruir, nenhuma vem de graça |
| Identidade do PIPELINE vs. identidade da APLICAÇÃO | roles separadas, sem sobreposição | reusar a task role do L42 também para o pipeline | quem publica código e quem executa código são papéis diferentes, avaliados em momentos diferentes, com blast radius diferente se comprometidos | mais uma role para nomear e documentar — o preço de separar responsabilidades |
A dívida que este laboratório não paga
O `dotnet test` bloqueia a publicação, mas continua sendo o mesmo conjunto de testes que existia antes — este módulo não aumenta cobertura nem adiciona teste de contrato. Um pipeline rápido publicando código mal testado é só um jeito mais rápido de publicar código mal testado.
Construir: o provedor OIDC e a role que só o pipeline pode assumir
Esta é a peça central. Tudo o que vem depois — build, teste, push, deploy — roda com a credencial que sai daqui, e só existe enquanto a execução dura.
# oidc.tf — o provedor federado, e a role que so o pipeline pode assumir
# Um provedor OIDC por EMISSOR, nao por repositorio. Se a conta ja tiver um
# provider para token.actions.githubusercontent.com (outro time criou), REUSE
# com `data`, como o L42 fez para o provider do EKS — criar um segundo para o
# mesmo emissor falha, porque a URL tem de ser unica na conta.
data "tls_certificate" "github_actions" {
url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}
resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
# "aud" que a AWS aceita para esta federacao. GitHub pode emitir token para
# outras audiencias tambem, mas so esta e reconhecida do lado da AWS.
client_id_list = ["sts.amazonaws.com"]
# A AWS hoje valida o certificado TLS do endpoint JWKS contra sua propria
# lista de CAs confiaveis, e so recorre ao thumbprint configurado se essa
# validacao falhar. O campo continua obrigatorio no recurso — obtido aqui
# dinamicamente para nao apodrecer quando o GitHub rotacionar o certificado.
thumbprint_list = [data.tls_certificate.github_actions.certificates[0].sha1_fingerprint]
}
data "aws_iam_policy_document" "confianca_github" {
statement {
effect = "Allow"
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.github_actions.arn]
}
# aud: sempre presente, sempre "sts.amazonaws.com" para este uso. Sem esta
# condicao, um token emitido pelo MESMO emissor para OUTRA integracao
# tambem passaria — o emissor e o mesmo, a audiencia nao.
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:aud"
values = ["sts.amazonaws.com"]
}
# sub: o repositorio E a branch, exatos — nao "qualquer repositorio da
# org" e nao "qualquer branch deste repositorio". StringLike com
# "repo:${var.org_github}/*:*" pareceria mais conveniente e autorizaria
# pull request de fork a tentar a mesma role (o "sub" de PR de fork tem
# outro formato, mas o wildcard tambem cobriria ele).
#
# ATENCAO: repositorios criados apos 15/jul/2026 (ou que optaram por
# migrar) recebem "sub" com IDs imutaveis:
# "repo:org@123456/repo@456789:ref:refs/heads/main". Confira no PROPRIO
# repositorio qual formato esta em uso antes de fixar este valor —
# nao existe "padrao geral" que sirva para os dois.
condition {
test = "StringEquals"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:${var.org_github}/${var.repo_github}:ref:refs/heads/main"]
}
}
}
resource "aws_iam_role" "deploy_pipeline" {
name = "${var.projeto}-deploy-pipeline"
assume_role_policy = data.aws_iam_policy_document.confianca_github.json
# Sessao curta: mesmo que o job va acima do usual, a credencial nao serve
# depois disso. Nao e o TTL do token do GitHub (quem decide isso e o
# GitHub); e o teto que esta ROLE impoe sobre a sessao assumida.
max_session_duration = 1200 # 20 min — o pipeline completo mede 6 a 9 min
}
data "aws_caller_identity" "atual" {}
data "aws_iam_policy_document" "permissoes_deploy" {
statement {
sid = "PublicarImagem"
effect = "Allow"
actions = [
"ecr:GetDownloadUrlForLayer", "ecr:BatchGetImage", "ecr:BatchCheckLayerAvailability",
"ecr:PutImage", "ecr:InitiateLayerUpload", "ecr:UploadLayerPart", "ecr:CompleteLayerUpload",
]
resources = [aws_ecr_repository.api.arn] # so ESTE repositorio, nunca "ecr:*" da conta
}
statement {
sid = "TokenDeAutenticacao"
effect = "Allow"
actions = ["ecr:GetAuthorizationToken"] # operacao de CONTA — nao aceita recurso especifico
resources = ["*"]
}
statement {
sid = "AtualizarServico"
effect = "Allow"
actions = [
"ecs:DescribeServices", "ecs:UpdateService",
"ecs:RegisterTaskDefinition", "ecs:DescribeTaskDefinition",
]
resources = [
"arn:aws:ecs:${var.regiao}:${data.aws_caller_identity.atual.account_id}:service/${aws_ecs_cluster.principal.name}/${aws_ecs_service.api.name}",
"arn:aws:ecs:${var.regiao}:${data.aws_caller_identity.atual.account_id}:task-definition/${var.projeto}-api:*",
]
}
statement {
sid = "PassarAsRolesDaTask"
effect = "Allow"
actions = ["iam:PassRole"]
resources = [aws_iam_role.execucao.arn, aws_iam_role.task.arn]
# PassRole sem Resource restrito e o jeito mais comum de uma role de
# deploy virar role de QUALQUER coisa: quem pode passar qualquer role
# pode fazer o ECS rodar com privilegio maior do que o pipeline deveria ter.
}
}
resource "aws_iam_role_policy" "deploy_pipeline" {
role = aws_iam_role.deploy_pipeline.id
policy = data.aws_iam_policy_document.permissoes_deploy.json
}
output "role_de_deploy" {
value = aws_iam_role.deploy_pipeline.arn
description = "ARN a colocar em role-to-assume no workflow do GitHub Actions"
}
A condição que, sozinha, autorizaria mais do que parece
Restringir só `sub` protege contra outro repositório ou branch assumirem a role. Não protege contra um token emitido pelo MESMO emissor para uma audiência diferente — um caso raro, mas real quando a conta tem mais de uma integração usando o provedor OIDC do GitHub. As duas condições (`sub` e `aud`) juntas fecham a lacuna; uma sozinha deixa a porta encostada, não trancada.
O que a role de deploy NÃO consegue fazer, e é por isso que ela é segura
Ela não lê segredo do Secrets Manager, não lista outro repositório ECR, não toca em RDS, não cria usuário IAM novo. `PassRole` está restrito às duas roles do L42, então nem "publicar código que roda com mais privilégio que o pipeline tem" é possível. Um token vazado desta role, na pior hipótese, publica uma imagem ruim — não abre a conta.
Construir: o workflow do GitHub Actions
O bloco de permissions é a primeira coisa a conferir quando algo falha, e a mais fácil de copiar errado: um exemplo antigo com permissions: write-all dá ao job permissão sobre issues, packages e conteúdo do repositório que não têm nada a ver com deploy.
# .github/workflows/deploy.yml
name: Deploy para producao
on:
push:
branches: [main]
# Sem isto, o job nao consegue pedir token nenhum — e a falha e um erro de
# PERMISSAO no primeiro passo, nao um erro misterioso de rede.
permissions:
id-token: write # pedir o token OIDC
contents: read # checkout do codigo
concurrency:
group: deploy-producao
cancel-in-progress: false # nunca cancele um deploy em andamento pela metade
jobs:
build-test-publicar:
runs-on: ubuntu-latest
timeout-minutes: 15 # nao e o SLA do rollout (L03); e o teto do PIPELINE
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Testar antes de construir a imagem
run: dotnet test --configuration Release
# Um push que falha aqui NUNCA chega ao ECR. E a diferenca central em
# relacao ao laptop: o pipeline nao pede que alguem lembre de rodar o
# teste, ele bloqueia sozinho.
# AssumeRoleWithWebIdentity acontece DENTRO desta action. Ela troca o
# token OIDC do job pela credencial STS e a expoe como variavel de
# ambiente para os passos seguintes — nunca como arquivo em disco.
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/ffv-lab-deploy-pipeline
role-session-name: gh-${{ github.run_id }}-${{ github.run_attempt }}
aws-region: us-east-1
- name: Construir e publicar a imagem
env:
REPO: 111122223333.dkr.ecr.us-east-1.amazonaws.com/ffv-lab-api
SHA: ${{ github.sha }}
run: |
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin "${REPO%%/*}"
docker build --platform linux/amd64 -t "$REPO:$SHA" .
# Tag imutavel (L03): reenviar o mesmo SHA falha, e a falha e
# desejavel — diz que nao ha nada novo a publicar.
docker push "$REPO:$SHA" || { echo "nada a publicar: SHA ja existe"; exit 0; }
- name: Implantar com o rollout do L03
env:
SHA: ${{ github.sha }}
run: |
terraform init -input=false
terraform apply -input=false -auto-approve -var="sha_da_imagem=${SHA}"
aws ecs wait services-stable --cluster ffv-lab --services ffv-lab-api
Por que o teste vem ANTES da credencial AWS
A ordem no workflow não é estética. Rodar `dotnet test` antes de `configure-aws-credentials` significa que uma suíte que falha nunca chega a solicitar a credencial STS — o pipeline nem gasta a chamada de `AssumeRoleWithWebIdentity` para um código que já reprovou. É a mesma lógica de "falhar rápido e barato" aplicada à ordem das etapas, não só ao teste em si.
Construir: a mesma identidade, se o pipeline for CodePipeline
A trust policy e a role deste laboratório não mudam se você preferir CodeBuild em vez de GitHub Actions como executor — o que muda é o emissor do token. CodeBuild com um projeto configurado para OIDC usa seu PRÓPRIO provedor (o da conta AWS), e a condição de confiança passa a comparar claims do CodeBuild, não do GitHub.
| Onde roda | Emissor do token | Variável comparada na trust policy | Quando escolher |
|---|---|---|---|
| GitHub Actions | token.actions.githubusercontent.com | `sub` = `repo:org/repo:ref:refs/heads/main` | código já vive no GitHub; equipe pequena sem infraestrutura de CI própria |
| CodeBuild (projeto AWS) | o STS da própria conta | a role de execução do projeto CodeBuild, sem passo de federação externa | quando o pipeline inteiro já mora dentro da AWS e o código está espelhado ou em CodeCommit |
| Self-hosted runner na VPC | depende: pode usar a task role (L42) direto | nenhuma federação externa — a task role do runner já é a identidade | quando a chamada de rede pública para pedir token é uma restrição de compliance |
O que NÃO muda entre as três linhas da tabela
A restrição por repositório e branch, o teste bloqueante antes do build, a tag imutável por SHA e o rollout do L03 continuam idênticos nas três opções. O que este laboratório ensina é a MECÂNICA da federação — token de curta duração trocado por credencial de curta duração —, não uma ferramenta específica.
Implantar, e provar que a chave deixou de ser necessária
Cinco provas. Nenhuma aceita "o workflow ficou verde" como resultado — cada uma tem um comando e um resultado que reprova nomeado.
# provas.sh — cinco medicoes; nenhuma conclusao vem de "o workflow ficou verde"
ROLE_ARN="arn:aws:iam::111122223333:role/ffv-lab-deploy-pipeline"
# ── Prova 1: a chave pessoal nao publica mais ────────────────────────────────
# Desative a access key da Ana e repita o deploy manual antigo. Tem de falhar.
aws iam update-access-key --user-name ana-deploy --access-key-id "$AKIAXXXX" \
--status Inactive
./implantar-manual-antigo.sh \
&& echo "FALHA DA PROVA: o deploy manual ainda funciona" \
|| echo "OK: sem a chave, o caminho antigo nao publica mais"
# ── Prova 2: um token de outra branch e recusado ─────────────────────────────
# Rode o workflow numa branch diferente de main (ou mude o "on:" temporariamente
# num branch de teste). O passo de configure-aws-credentials tem de falhar com
# "not authorized to perform sts:AssumeRoleWithWebIdentity".
gh workflow run deploy.yml --ref branch-de-teste
gh run watch --exit-status \
&& echo "FALHA DA PROVA: token de outra branch foi aceito" \
|| echo "OK: a trust policy recusou o sub que nao bate"
# ── Prova 3: quem publicou fica registrado, por execucao ────────────────────
# O role-session-name carrega o run_id — a resposta a "quem publicou essa
# versao" fica em UMA consulta, nao em correlacionar logs a mao.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=AssumeRoleWithWebIdentity \
--query 'Events[?contains(Username, `gh-`)].[EventTime,Username]' --output table
# ── Prova 4: a credencial nao sobrevive ao job ───────────────────────────────
# Nao ha comando de "revogar" a rodar — e essa e a prova. Confira que a role
# nao aparece em nenhum lugar do repositorio como secret persistido.
gh secret list --repo ffv/api | grep -iE "AWS_SECRET|AWS_ACCESS_KEY" \
&& echo "FALHA DA PROVA: ha uma chave de longa duracao guardada como secret" \
|| echo "OK: nenhuma credencial AWS persistida no repositorio"
# ── Prova 5: o rollout do L03 continua identico ──────────────────────────────
# O criterio de aceite do L03 (zero 5xx durante a troca) tem de continuar
# valendo — so a origem do "quem chamou update-service" mudou.
URL="https://$(terraform output -raw dominio)/api/health/ready"
( while true; do curl -s -o /dev/null -w '%{http_code} ' "$URL"; sleep 0.2; done ) & LACO=$!
gh workflow run deploy.yml --ref main && gh run watch --exit-status
kill $LACO
# Esperado: fileira continua de 200, exatamente como no L03.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · A chave pessoal não publica mais | desativar a access key e repetir o deploy manual | o deploy manual falha | se ainda funciona, existe um segundo caminho de publicação que a auditoria não viu |
| 2 · Branch errada é recusada | disparar o workflow numa branch diferente de main | `AssumeRoleWithWebIdentity` recusado | se aceito, a condição de `sub` está com wildcard ou comparando o valor errado |
| 3 · Quem publicou fica registrado | CloudTrail filtrando `AssumeRoleWithWebIdentity` | uma linha por execução, com o `run_id` no nome da sessão | sessão sem nome identificável significa `role-session-name` não configurado no workflow |
| 4 · Nenhuma credencial persiste | listar secrets do repositório procurando padrão de chave AWS | nenhum resultado | um `AWS_SECRET_ACCESS_KEY` ainda cadastrado é uma porta dos fundos que ninguém fechou |
| 5 · O rollout do L03 continua idêntico | laço de curl durante um deploy disparado pelo pipeline | fileira contínua de 200, igual ao L03 | qualquer 5xx aqui é regressão no ROLLOUT, não na identidade — volte ao L03, não a este módulo |
Quebrar de propósito: três falhas e o diagnóstico
As três acontecem de verdade em times migrando de chave pessoal para OIDC, e as três se parecem no sintoma superficial — "o deploy não publicou". O que as separa é ONDE a cadeia quebra.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Trust policy com `sub` amplo demais | troque a condição para `StringLike` com `repo:org/*:*` | um workflow de outro repositório da organização também consegue publicar | CloudTrail mostra `AssumeRoleWithWebIdentity` bem-sucedido com um `sub` inesperado | voltar para `StringEquals` com repositório e branch exatos |
| Chave pessoal ainda ativa em paralelo | implemente o pipeline mas esqueça de desativar a access key antiga | CloudTrail mostra dois caminhos publicando: `gh-` (pipeline) e o usuário IAM (pessoa) | filtrar `PutImage`/`UpdateService` por `userIdentity.type` — deveria ser só `AssumedRole` | desativar a chave é parte do laboratório, não um passo opcional posterior |
| `permissions` do workflow copiada de outro exemplo | use `permissions: write-all` em vez de listar `id-token` e `contents` explicitamente | o job funciona, mas tem permissão sobre issues, packages e conteúdo que não usa | a aba de permissões da execução no GitHub Actions | listar exatamente `id-token: write` e `contents: read` — nada a mais |
A combinação que anula todo o esforço deste laboratório
Implementar o pipeline com OIDC e deixar a chave pessoal ativa "por garantia" não é uma rede de segurança — é reintroduzir exatamente o que o laboratório existe para eliminar, só que agora escondido atrás de um pipeline que parece correto. A prova 1 desta seção existe porque essa combinação é a mais comum de todas, não a mais rara.
Segurança: o que muda quando a credencial deixa de existir entre deploys
Tirar a chave de longa duração não elimina risco — move o risco para outro lugar: a correção da trust policy, e quem tem permissão de mudar o workflow que a invoca.
| Risco | Probabilidade | Impacto | Controle preventivo | Detecção | Resposta |
|---|---|---|---|---|---|
| Trust policy com `sub` amplo demais (wildcard de repo/branch) | média | alto | `StringEquals` exato, nunca `StringLike` amplo | CloudTrail: `AssumeRoleWithWebIdentity` com `sub` fora do repositório/branch esperado | apertar a condição; revisar quais execuções já assumiram a role com o `sub` largo |
| Pull request de fork tentando assumir a role | média em repositório público | alto | `sub` restrito a `ref:refs/heads/main` — PR de fork tem `sub` de `pull_request`, formato diferente | `AccessDenied` em `AssumeRoleWithWebIdentity`, esperado e não é incidente | nenhuma; é o comportamento correto. Investigar só se o volume for anormal |
| `iam:PassRole` sem `Resource` restrito | baixa | alto | `PassRole` limitado às roles de execução e task específicas do L42 | IAM Access Analyzer sobre a policy da role de deploy | apertar o `Resource`; auditar se alguma passagem de role fora do esperado ocorreu |
| Chave de longa duração reintroduzida como secret "de garantia" | média | alto | revisão de PR bloqueando `AWS_SECRET_ACCESS_KEY`/`AWS_ACCESS_KEY_ID` como secret do repositório | busca periódica nos secrets do repositório | remover o secret; ele é redundante pelo desenho, nunca necessário |
| Runner comprometido durante a janela da sessão | baixa | médio | `max_session_duration` curto (20 min); dependências do workflow fixadas por hash | CloudTrail correlacionando `run_id` a atividade fora do esperado na janela da sessão | a sessão expira sozinha; revisar o workflow por dependência de terceiro comprometida |
O `*` que aparece na política, e por que ele se justifica
`ecr:GetAuthorizationToken` é uma operação de CONTA, não de repositório: não aceita recurso específico, e escrevê-la com o ARN do repositório simplesmente não autoriza nada. As outras ações da mesma política são restritas ao repositório ECR e ao serviço ECS exatos. A regra não é "nunca use `*`" — é "todo `*` tem de ter uma frase explicando por que não pode ser mais estreito".
Uma trust policy usa `Federated` apontando para o provedor OIDC correto do GitHub, mas a condição inclui só `token.actions.githubusercontent.com:sub`, sem `:aud`. Qual é a consequência mais precisa?
Observabilidade: as perguntas que o painel tem de responder
Um painel de pipeline federado tem uma função que o painel de deploy do L03 não tinha: confirmar que a IDENTIDADE de quem publicou é a esperada, não só que o rollout funcionou.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| Quem publicou essa versão? | CloudTrail: `AssumeRoleWithWebIdentity` + `role-session-name` | correlação direta run_id → deploy → commit | sempre respondível em uma consulta |
| O pipeline ainda usa OIDC, ou uma chave voltou? | CloudTrail filtrando `userIdentity.type` | qualquer `IAMUser` publicando é regressão | qualquer ocorrência dispara alarme |
| O token está sendo rejeitado com frequência anormal? | `AccessDenied` em `AssumeRoleWithWebIdentity` | PR de fork tentando, ou trust policy quebrada após mudança de repositório | acima da linha de base semanal |
| Quanto tempo o pipeline leva? | duração do job no GitHub Actions | acima do timeout de 15 min indica trava, não lentidão gradual | > 15 min |
| A sessão expirou no meio do deploy? | `ExpiredTokenException` no passo de deploy | job real mais longo que o `max_session_duration` configurado | qualquer ocorrência |
A métrica que não existe neste desenho, e por quê
Não há alarme de "chave prestes a expirar" porque não há chave para expirar — a credencial nasce e morre com a execução. É um alarme inteiro que este desenho torna desnecessário, não um que falta configurar.
Escala: um repositório, vários repositórios, uma organização
| Volume | O que acontece com a identidade | O que passa a doer | O que fazer |
|---|---|---|---|
| 1 repositório, 5-8 deploys/semana | uma role, uma condição de `sub` | nada; é o cenário deste laboratório | nada |
| 5 repositórios do mesmo produto | uma role por repositório, ou uma role com `sub` em `StringLike` restrito a um PREFIXO comum | gerenciar 5 trust policies manualmente | módulo Terraform reutilizável (L55) — uma definição, cinco instâncias |
| Múltiplos ambientes (dev/staging/prod) | uma role por ambiente, cada uma restrita a uma branch ou `environment` do GitHub | a mesma branch main não deveria poder publicar direto em prod sem aprovação | `environment` protegido do GitHub com revisores obrigatórios, mais L56 (conta por ambiente) |
| Dezenas de times, uma organização | provedor OIDC único, roles por time/repositório | auditar centenas de trust policies à mão deixa de escalar | Access Analyzer em nível de organização (L43) e SCP proibindo criação de access key |
O gargalo que só aparece com muitos repositórios
Cada `AssumeRoleWithWebIdentity` é uma chamada de API, sujeita à cota de STS da conta. Numa organização com centenas de repositórios publicando ao mesmo tempo (todos rodando CI na mesma janela, por exemplo, num deploy coordenado), isso pode aparecer como limitação de taxa — o mesmo tipo de cota que qualquer chamada de STS respeita.
O `concurrency` do workflow evita uma corrida que a identidade não previne
Federação resolve QUEM pode publicar, não QUANTAS execuções podem publicar ao mesmo tempo. Dois pushes seguidos em main, sem o bloco `concurrency` do workflow, disparam dois jobs que tentam chamar `update-service` em paralelo — e o resultado é uma corrida entre duas revisões da task definition, não um erro de identidade. `cancel-in-progress: false` (visto na seção de construção) garante que o segundo espera o primeiro, em vez de os dois competirem pelo mesmo rollout do L03.
Custo: o que este laboratório acrescenta à fatura
É 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 está em outro lugar.
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| Provedor OIDC, role, trust policy | nada — recursos IAM não têm custo próprio | não confundir com CloudTrail, que cobra por eventos de gestão além do limite gratuito |
| Minutos de execução do GitHub Actions | minuto de runner, com faixa gratuita mensal | repositório privado consome a cota; público costuma ser gratuito — confira o plano contratado |
| CodeBuild, se escolhido no lugar de GitHub-hosted | minuto de build por tipo de instância | cresce com paralelismo de builds, não com número de deploys isolados |
| CloudTrail | eventos de gestão além do free tier; trilha em S3 tem custo de armazenamento | é onde a auditoria deste laboratório mora — vale a pena mesmo custando |
O ganho de custo que não está na fatura da AWS
O ganho real não é uma linha de infraestrutura mais barata: é o tempo de investigação que a auditoria da Cadência gastaria hoje tentando reconstruir "o que a chave da Ana fez em 11 meses" contra uma consulta de CloudTrail que responde isso em segundos, porque cada execução carrega sua própria identidade.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | deploy disparado por push, com teste bloqueante e rastreabilidade por execução | ainda depende do GitHub como fonte única de verdade da identidade | plano de contingência documentado para incidente de emissão de token do IdP | média |
| Segurança | nenhuma credencial de longa duração com permissão de deploy; escopo por repo+branch | trust policy mal escrita (wildcard) tem o mesmo efeito que a chave que se eliminou | Access Analyzer revisando trust policies periodicamente (L43) | alta |
| Confiabilidade | rollout do L03 inalterado; disjuntor e alarme continuam cobrindo o deploy | timeout do pipeline (15 min) pode cortar um build legitimamente mais lento | ajustar o timeout com dado real de duração, não com o valor padrão | baixa |
| Eficiência de performance | pipeline mede 6 a 9 min, dominado pelo teste e pelo build da imagem | sem cache de camada Docker entre execuções | cache de build (L55 trata IaC; cache de imagem é candidato a evolução futura) | baixa |
| Otimização de custos | nenhum recurso novo cobrando por hora; custo é por minuto de execução | nenhum ainda — volume baixo (5-8 deploys/semana) não justifica otimização | reavaliar se o volume crescer uma ordem de grandeza | baixa |
| Sustentabilidade | nenhuma infraestrutura ociosa criada por este laboratório | nenhum | nenhuma ação necessária no volume atual | 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 trocar de desenho de identidade. Cada nível resolve um risco e compra outro.
Deploy manual com chave de acesso pessoal, disparado do laptop de quem decide publicar. É onde a Cadência estava.GitHub Actions com OIDC, trust policy restrita a repositório e branch, credencial de vida curta que não sobrevive à execução.`environment` protegido do GitHub com revisor obrigatório antes de publicar em produção; ambientes separados por conta (L56).Módulo Terraform reutilizável (L55) para provedor + role; uma condição de `sub` por prefixo de repositório do mesmo produto.Organizations com SCP proibindo criação de `iam:CreateAccessKey` para usuários humanos; Identity Center para acesso interativo; pipeline como serviço interno padronizado (L43/L56).O histórico de execuções — duração, taxa de falha por autor, arquivos tocados, cobertura de teste — vira DADOS. Um MODELO treinado sobre esse histórico aponta qual mudança tem maior chance de quebrar o deploy, e a IA prioriza revisão humana onde o risco é alto.A ordem não é negociável, e o motivo é concreto
O nível 6 depende de correlacionar `run_id` a resultado — que depende do nível 2 ter dado a cada execução uma identidade rastreável. Sem isso, "qual mudança quebrou o deploy" não tem como ser respondido por dado nenhum, porque não existe dado por execução: existiria só um log genérico de uma chave pessoal usada por duas pessoas.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve o problema central, e forçá-la seria o antipadrão que a própria série critica. "Quem pode assumir esta role" tem resposta determinística: a trust policy compara `sub` e `aud` contra valores exatos. Um modelo não melhora essa comparação — ela já é binária e correta ou incorreta, sem zona cinzenta para inferir.
O lugar onde IA acrescentaria valor real é o do Nível 6: graduar RISCO por mudança, em vez de aplicar o mesmo pipeline com a mesma rigidez a toda alteração — de uma correção de texto a uma migration destrutiva.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | priorizar revisão humana nas mudanças com maior histórico de causar falha, em vez de tratar toda mudança igual |
| Por que uma regra não bastaria? | uma regra cobre o caso óbvio: "toca migration → exige aprovação". IA só se justifica depois que regras simples mostrarem o limite delas |
| De onde viriam os dados? | CloudTrail das execuções, resultado de teste, arquivos tocados no diff, duração do job — tudo já produzido pelo pipeline deste laboratório |
| Qual o risco? | aprender de poucos incidentes (a Cadência tem uma dúzia de deploys por mês) e classificar mudança arriscada como segura, ou o oposto |
| Por que não agora? | porque não existe ainda no catálogo desta série um laboratório dedicado a essa avaliação — é honesto dizer que a resposta certa hoje é a regra simples |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "decidir se este token deveria ser aceito" trocaria uma verificação criptográfica determinística — a validação da assinatura do JWT contra o JWKS do emissor — por uma probabilística. Onde existe prova matemática, IA só acrescenta latência e a chance de errar com confiança.
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 |
|---|---|---|---|---|---|
| `StringLike` com `repo:org/*:*` na trust policy | parece menos manutenção conforme repositórios são adicionados | qualquer repositório da organização assume a mesma role — o escopo que se queria restringir some | workflow de um repositório não relacionado publicando com sucesso | `StringEquals` exato, um repositório e branch por vez, ou prefixo deliberado e revisado | organização inteira sob o mesmo dono, com revisão central de todo repositório |
| Guardar uma chave de acesso "de garantia" ao lado do OIDC | parece uma rede de segurança contra o pipeline falhar | reintroduz exatamente o que o OIDC elimina, e raramente alguém desativa depois | CloudTrail mostrando publicação por `IAMUser`, não `AssumedRole` | nenhuma chave; se o pipeline falha, o problema é o pipeline, não a ausência de atalho | nunca em produção; investigação isolada com credencial temporária é diferente |
| Reusar a mesma role de deploy para repositórios não relacionados | menos Terraform para escrever | comprometer um repositório dá acesso de deploy a todos que a role toca | um push num repositório secundário conseguindo publicar num serviço que não é o dele | uma role por repositório, ou por produto, com `sub` restrito a cada um | monorepo de um único produto, onde os repositórios lógicos SÃO a mesma unidade de deploy |
| `max_session_duration` no padrão de 1 hora sem revisar | ninguém pensou nisso — é o valor que o console sugere | um build de 6 minutos carrega uma credencial válida por uma hora se vazar do runner | nenhum sintoma direto — é exposição de janela, não uma falha visível | ajustar ao tempo real do pipeline mais uma folga curta, não ao padrão | pipelines com etapas legitimamente longas (build de imagem grande, teste de integração pesado) |
| `permissions: write-all` no workflow | copiado de um exemplo antigo, ou "para não ter que descobrir o que falta" | o job ganha permissão sobre issues, packages e conteúdo que não usa para nada | nenhum sintoma até um passo do workflow, comprometido por dependência de terceiro, usar essa permissão extra | listar exatamente `id-token: write` e `contents: read` | nunca — listar as duas permissões necessárias custa duas linhas |
| Pular `dotnet test` "porque o deploy anterior passou" | pipeline mais rápido, pressão de publicar logo | publica regressão que o rollout do L03 só vai mostrar como 5xx depois de já estar no ar | o alarme de 5xx do alvo (L03) disparando depois de um deploy "sem incidente" | teste sempre bloqueante, sem etapa de pular manualmente | nunca em produção; ambiente de experimentação isolado é diferente |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| `Not authorized to perform sts:AssumeRoleWithWebIdentity` | o `sub` ou o `aud` do token não bate com a condição da trust policy | decodifique o token do passo que falhou (o log da action mostra os claims) e compare com a condição | log do passo `configure-aws-credentials`; `assume_role_policy` da role | corrigir o valor exato de `sub`/`aud`, ou o formato (IDs imutáveis, se o repositório for novo) |
| Funciona em pull request, falha no push a main (ou o oposto) | `sub` muda de formato conforme o evento que disparou o workflow | confira qual evento gerou a execução e qual formato de `sub` ele produz | documentação do GitHub sobre claims por tipo de evento | ou cobrir os dois formatos na trust policy, ou restringir deliberadamente a um só |
| `ExpiredTokenException` no meio do deploy | `max_session_duration` menor que a duração real do job | compare a duração medida do job com o valor configurado na role | duração do job no GitHub Actions vs. `max_session_duration` do Terraform | aumentar com cautela, sempre com folga curta sobre o medido — nunca "só para resolver" |
| Push de imagem falha com `AccessDenied` mesmo com a role certa assumida | a policy da role não inclui o ARN exato do repositório ECR | compare o `Resource` da statement `PublicarImagem` com o ARN real do repositório | política anexada à role de deploy | corrigir o `Resource` para o ARN exato — não `ecr:*` |
| CloudTrail mostra um usuário IAM ainda publicando, ao lado do pipeline | a chave pessoal continua ativa em paralelo ao OIDC | filtre `PutImage`/`UpdateService` por `userIdentity.type` | CloudTrail; `list-access-keys` do usuário suspeito | desativar a chave — implementar o pipeline sem isso deixa as duas portas abertas |
| Workflow não solicita nenhum token | falta `permissions: id-token: write` no arquivo do workflow | confira o bloco `permissions` no topo do arquivo YAML | `.github/workflows/deploy.yml` | adicionar `id-token: write` — sem ele, a action nem tenta pedir o token |
A pergunta que resolve metade destes casos
Antes de mexer em Terraform, pergunte: a federação falhou (nunca chegou a ter credencial AWS) ou a permissão falhou (tinha credencial, mas o recurso negou)? A primeira aparece como `AssumeRoleWithWebIdentity` recusado; a segunda como `AccessDenied` numa ação específica DEPOIS da credencial obtida. São diagnósticos completamente diferentes atrás do mesmo "não publicou".
Limpeza: o que o destroy não leva
Este laboratório acrescenta pouco recurso administrado por Terraform, mas parte do que ele cria vive FORA do Terraform — nas configurações do repositório GitHub.
# 1. Derrube o que o Terraform administra.
terraform destroy -auto-approve
# 2. PROVEDOR OIDC: pode sobreviver se outro repositorio o estiver usando. Confira
# antes de apagar — apagar o provider quebra TODO pipeline federado pelo mesmo emissor.
aws iam list-open-id-connect-providers
aws iam list-roles --query "Roles[?contains(AssumeRolePolicyDocument, 'token.actions.githubusercontent.com')].RoleName" --output table
# 3. CHAVE PESSOAL: o laboratorio nao a cria, mas o objetivo dele so se cumpre com
# ela desativada. Confirme, nao presuma.
aws iam list-access-keys --user-name ana-deploy
# 4. SECRET NO REPOSITORIO: se alguem cadastrou uma chave de garantia, ela nao sai
# com terraform destroy nenhum — vive nas configuracoes do GitHub.
gh secret list --repo ffv/api
# 5. Prova final: nada com o nome do projeto de pe que nao devia estar.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values=ffv-lab --query "ResourceTagMappingList[].ResourceARN"| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Role de deploy e trust policy | sim | não | recursos IAM não têm custo próprio |
| Provedor OIDC | sim, se administrado por este Terraform | não | mas apagá-lo quebra QUALQUER outro pipeline que o reuse — confira antes |
| Chave de acesso pessoal antiga | não; nunca foi criada por este módulo | não diretamente | continua sendo o risco de segurança original até alguém desativar à mão |
| Secret do repositório no GitHub | não; vive fora da AWS e do Terraform | não | só sai com `gh secret delete`, e ninguém lembra de rodar isso sozinho |
| Infraestrutura do L03 (ALB, ECS, RDS) | não é deste módulo | sim, por hora | este laboratório não toca nela; a limpeza do L03 continua sendo responsabilidade separada |
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Chave de acesso pessoal com permissão de deploy | provedor OIDC + role federada | a credencial passa a nascer e morrer com cada execução, em vez de existir entre elas |
| Qualquer repositório poderia, em tese, assumir a role | trust policy com `sub` exato | o token só é aceito se vier do repositório e da branch declarados, antes de qualquer permissão IAM ser avaliada |
| Token emitido para outra finalidade não deveria valer aqui | condição de `aud` | fecha a lacuna que `sub` sozinho deixaria: mesmo emissor, audiência diferente |
| Ninguém sabia quem publicou uma versão específica | `role-session-name` com o `run_id` | CloudTrail responde "quem publicou" numa consulta, não numa investigação |
| Código não testado podia chegar a produção | teste bloqueante antes do build | a pessoa não precisa lembrar de rodar o teste; o pipeline não segue sem ele |
| Redesenhar o rollout junto com a identidade | reaproveitar o `update-service` do L03 | trocar QUEM publica não deveria custar reescrever COMO o deploy acontece |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Chave vazada de um laptop | não existir mais nenhuma chave para vazar | segredo da aplicação (banco, API externa) vazado por outro caminho — é o L46 |
| Repositório errado publicando | condição `sub` exata na trust policy | código malicioso DENTRO do repositório correto, que passou no teste |
| Sessão vazada do runner | `max_session_duration` curto | ataque que ocorre inteiramente dentro da janela de 20 min |
| Deploy sem teste | etapa bloqueante no workflow | teste que existe mas não cobre o caso que quebrou — cobertura é responsabilidade separada |
- Push na branch main dispara o workflow do GitHub Actions.
- O job solicita ao GitHub um token OIDC válido por minutos.
- A action troca o token por credencial STS, validando `sub` e `aud` contra a trust policy.
- Com a credencial temporária, `dotnet test` roda — se falhar, nada mais acontece.
- A imagem é construída e enviada ao ECR com tag = SHA, imutável (L03).
- O mesmo `update-service` do L03 é chamado, agora pela credencial da execução.
- O rollout segue com excedente, disjuntor e alarme de 5xx do alvo, inalterados.
- A credencial temporária expira com o fim do job — não há nada para revogar depois.
Desafio — sem roteiro
O requisito
Um segundo ambiente/conta AWS de destino precisa receber deploy pela mesma pipeline, usando a mesma federação OIDC — sem criar nenhuma credencial estática nova.
Critério de aceite — executável, não "verifique se funciona"
A pipeline consegue fazer deploy para as DUAS contas (a original e a nova) na mesma execução, e uma busca no repositório e nos secrets do CI por qualquer `AWS_SECRET_ACCESS_KEY`/chave de acesso estática retorna vazio.
- Dica 1: Cada conta de destino precisa do SEU PRÓPRIO IAM role com trust policy confiando no provedor OIDC do CI — o role não é compartilhado entre contas, só o provedor OIDC (se configurado por conta) ou a claim do token é.
- Dica 2: A trust policy do role na conta nova deve restringir o `sub` claim do token OIDC ao repositório/branch certo — uma trust policy aberta demais permite que QUALQUER pipeline do GitHub assuma aquele papel.
- Dica 3: Se o segundo deploy falhar com "not authorized to perform sts:AssumeRoleWithWebIdentity", o problema quase sempre é o ARN do role errado no step de deploy daquele ambiente específico, não a configuração OIDC em si.
Lembrete de limpeza
Todo recurso que este desafio criar entra no mesmo `terraform destroy` do laboratório — nada fica para trás cobrando sozinho.
Perguntas frequentes
❓ Por que não guardar a chave de acesso AWS como um GitHub Secret em vez de usar OIDC?
❓ Preciso criar um novo provedor OIDC se já existe um para o EKS do L42?
❓ O que a condição `sub` na trust policy realmente impede?
❓ O que acontece se um pull request de outro fork tentar disparar o deploy?
❓ Qual a diferença entre AssumeRole e AssumeRoleWithWebIdentity?
❓ Por que restringir a branch main e não usar um wildcard que cubra qualquer branch?
❓ Devo usar CodePipeline/CodeBuild da AWS ou GitHub Actions para este pipeline?
❓ Quanto tempo a credencial da sessão dura, e o que acontece se o job passar disso?
Fixando
Um workflow do GitHub Actions em `feature/experimento` tenta assumir a role de deploy cuja trust policy tem `sub` restrito a `repo:ffv/api:ref:refs/heads/main`. O que acontece?
A Cadência implementou o pipeline com OIDC, mas a auditoria seguinte ainda encontra a chave de acesso da Ana com `PutImage` bem-sucedido na semana anterior. O pipeline está configurado corretamente. Qual é a explicação mais provável?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L03 (rollout confiável), L39 (blue/green, se as migrations não forem aditivas) e L42 (task role vs. execution role) — este módulo assume os três resolvidos |
| Conhecimentos adquiridos | federação OIDC e AssumeRoleWithWebIdentity; trust policy restrita por sub/aud; a diferença entre identidade do PIPELINE e identidade da APLICAÇÃO; por que credencial temporária elimina a necessidade de rotação |
| Limitação que fica | o pipeline ainda publica direto na branch main sem aprovação manual; ambientes de staging e produção continuam na mesma conta |
| Próximo exemplo recomendado | L55 — Terraform em módulo, com estado remoto e drift. Transforma o `oidc.tf` deste laboratório de arquivo único em módulo reutilizável para múltiplos repositórios |
| Também habilitado por este módulo | L56 (um ambiente por conta) usa a mesma mecânica de federação, restrita por conta em vez de por repositório; L82 (prompt versionado) reaproveita este pipeline para testar regressão de prompt antes de publicar |
| Data da última validação técnica | 8 de agosto de 2026 |
Documentação oficial consultada: IAM: Create an OpenID Connect (OIDC) identity provider — a validação do certificado TLS contra a lista de CAs confiáveis da AWS, com o thumbprint como fallback, e os requisitos de `aud`/`sub`; e GitHub Docs: Configuring OpenID Connect in Amazon Web Services — o formato exato do claim `sub` por tipo de evento, o bloco de `permissions` necessário no workflow, e a mudança para IDs imutáveis em repositórios criados a partir de 15/jul/2026. Os valores de preço não aparecem neste módulo por decisão: minutos de runner e de build variam por plano contratado e por região — use a calculadora do seu provedor de CI e o AWS Pricing Calculator.
O que não foi verificado, e você deve conferir na sua conta
A duração real do pipeline (6 a 9 min) é a medida na aplicação de exemplo, não uma referência geral — o `max_session_duration` deve ser derivado da SUA medição, com uma folga curta, não copiado deste módulo. O formato do claim `sub` também depende da data de criação do seu repositório: confira em `.well-known/openid-configuration` do emissor e no primeiro log de execução do seu próprio workflow antes de fixar a condição em produção.
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…