Lab 50 — Resposta a incidente e blast radius
O problema, e a empresa que o tem
A Cadência, do L43, já tem duas contas sob Organizations com SCP: uma chave de acesso da conta de desenvolvimento estruturalmente não alcança a conta de produção, e isso foi provado naquele laboratório. O que ainda não existe é a resposta para o dia em que uma credencial REALMENTE vaza: um achado chega — o tipo de coisa que o L48 entrega como ticket automático — apontando uso incomum de uma chave de acesso antiga, de um usuário de serviço criado antes de a Cadência rodar menor privilégio (L41) e identidade de workload (L42) de verdade.
A pergunta que domina a sala não é "como essa chave vazou". Essa é outra investigação, e cabe depois. A pergunta urgente é "o que ela já fez, e o que ela ainda pode fazer" — e é exatamente aí que a maioria dos times trava: sem uma trilha centralizada e uma consulta pronta, a resposta vira palpite, e palpite não contém incidente nenhum.
A fronteira de conta do L43 já existe estruturalmente. O que nunca foi testado é se ela SEGURA sob um incidente real — e a Cadência também não tem, até este laboratório, um jeito rápido de medir o alcance de uma credencial DENTRO da própria conta comprometida.
O que este laboratório NÃO é
Não é sobre como a chave vazou — prevenção de vazamento é outro assunto, que passa por identidade de workload (L42) e disciplina de segredo, não por resposta a incidente. E não é o laboratório que GERA o achado: isso é trabalho do L48 (GuardDuty, Security Hub, Config). Este módulo começa no instante em que o alerta já existe, e termina no relatório do incidente — a rotação definitiva da credencial e a correção da causa raiz ficam para depois, na fase de erradicação.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com uma consulta ou um comando na seção de implantação, não com a sensação de ter entendido o incidente.
- Reconstruir, por consulta, cada chamada feita por uma credencial comprometida — sem depender de memória ou suposição.
- Explicar por que desativar uma chave de acesso não invalida uma sessão temporária que ela já emitiu.
- Executar as duas contenções na ordem certa, antes de conhecer o alcance completo do incidente.
- Provar, com dado da própria trilha, que a fronteira de conta (SCP) limitou o alcance na prática, não só na teoria.
- Consultar CloudTrail centralizado via Athena com projeção de partição, sem varrer o bucket inteiro.
- Diferenciar contenção de investigação, e justificar por que a ordem entre elas importa.
- Medir o tempo entre detecção e contenção, e comparar com um cenário sem runbook ensaiado.
- Seguir a cadeia de uma credencial que assumiu outra role, sem parar na primeira consulta.
- Redigir, a partir da consulta, uma timeline de incidente que se sustenta como evidência.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Investigação de CloudTrail via Athena | SAP-C02 | consulta pronta filtrando por `accessKeyId` | por que a tabela usa projeção de partição, não `ALTER TABLE ADD PARTITION` |
| Revogação de sessão temporária | SAP-C02, SOA-C02 | a política `AWSRevokeOlderSessions` e o Deny por `aws:TokenIssueTime` | por que desativar a chave não mata a sessão já emitida |
| Trilha organizacional | SAP-C02 | uma trilha cobrindo todas as contas-membro | diferença entre trilha por conta e `is_organization_trail` |
| SCP como contenção estrutural | SAP-C02 | a fronteira do L43 testada por um incidente real | por que SCP é preventivo e independe de a credencial estar comprometida |
| Fases de resposta a incidente | SAP-C02 (cenário) | contenção antes de investigação completa | a ordem entre as fases, e por que invertê-la custa tempo de exposição |
| Menor privilégio no papel de resposta | SAP-C02, SAA-C03 | a policy do responsável de plantão | por que até quem investiga incidente segue menor privilégio |
| Custo de consulta no Athena | SAP-C02, DOP-C02 | GB varrido vs. TB varrido | como a projeção de partição reduz o volume escaneado |
| Evidência preservada | SAP-C02 | desativar, não deletar, a chave | por que apagar destrói o que a investigação e uma apuração formal precisam |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve uma chave comprometida já desativada e pergunta se o incidente está contido. A resposta certa reconhece que só está contido se a credencial nunca tiver sido usada para assumir outra role ou abrir outra sessão — e isso só se confirma consultando a trilha, não presumindo a partir do estado da chave.
Requisitos, e como cada um muda o desenho
Requisito não funcional que não aparece numa linha de configuração é intenção. A coluna da direita é onde cada um deixou marca.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Contenção em minutos após decisão | ≤ 5 min | obriga runbook automatizado por ferramenta, não passos manuais improvisados a cada incidente |
| Cobertura de 100% das contas na consulta | toda conta-membro | obriga trilha organizacional centralizada (`is_organization_trail`), não uma trilha por conta |
| Raio de alcance verificável | por consulta, não por suposição | obriga Athena sobre CloudTrail com filtro por `accessKeyId`, não estimativa |
| Sessão já emitida também é contida | sim, sempre verificar | obriga a segunda contenção (Deny por `aws:TokenIssueTime`), não só a desativação da chave |
| Evidência preservada | nada apagado durante a resposta | obriga desativar (não deletar) a chave, e bucket com versionamento e Object Lock |
| Custo de consulta previsível | mesmo em conta grande | obriga projeção de partição no catálogo, não varredura completa do bucket |
| Runbook confiável sob pressão | ensaiado antes do incidente real | obriga game day periódico — o primeiro uso do runbook não pode ser em produção |
Arquitetura mínima: a investigação que ninguém consultou
Este é o estado real de muita conta AWS: CloudTrail ligado desde sempre — quase sempre está, por padrão — e nenhum caminho de consulta montado até ele. É legítimo como ponto de partida, porque é onde a maioria dos times realmente está. O laboratório começa por medir o custo disso, porque um número torna o defeito discutível, e "vamos ser mais rápidos da próxima vez" não é.
- → toda chamada assinada fica registrada
- → entrega a cada poucos minutos, em objetos comprimidos
- → grep manual, arquivo por arquivo
- → AssumeRole antes do alerta, sessão já emitida
- → desativa a chave — só ela, horas depois
- Segurança e identidade
- Gestão e governança
- Armazenamento
- Fora da AWS
Este desenho tem CloudTrail — a trilha nunca esteve desligada. O que falta não é dado, é caminho até ele: sem Athena, alguém baixa objeto por objeto; sem a segunda contenção, a chave morre e a sessão que ela já emitiu continua viva. Percorra os passos e repare onde o relógio corre sem que ninguém o veja.
- A chave já é o dado, não a pergunta. O laboratório começa aqui de propósito: a origem do vazamento (repositório público, laptop comprometido, o que for) é uma investigação separada, que vem depois. A pergunta que domina a sala agora é o que essa chave já fez.
- A trilha grava tudo, e ninguém montou caminho até ela. CloudTrail está ligado — quase sempre está, por padrão de conta. O que falta não é captura, é um catálogo e uma consulta prontos para o dia em que alguém precisar ler aquilo sob pressão.
- Grep manual não escala com o volume real de uma conta. Uma conta com uso normal gera milhares de objetos comprimidos por dia. Abrir um por um, ou mesmo em lote com um script improvisado na hora, consome horas que o incidente não tem.
- A sessão que a chave já emitiu nunca entra na conta. Ninguém está procurando por ela, porque ninguém sabe que precisa procurar. Ela não aparece em nenhuma consulta que filtre só pela chave estática — e aqui nem existe consulta nenhuma.
- A ordem escolhida: entender tudo antes de agir. Parece mais responsável esperar clareza antes de desativar algo que talvez ainda seja usado por um script legítimo. O custo dessa escolha é medido na seção seguinte, em minutos reais.
- A chave é desativada — só ela, horas depois. Quando a decisão finalmente vem, ela é parcial: fecha o uso direto da chave, e não toca a sessão que ela já tinha emitido. O time fecha o incidente achando que fechou os dois.
- Por que alguém responde assim. Não é negligência: desativar uma chave sem saber o que mais pode quebrar dá medo, e consultar antes de agir parece a escolha cautelosa. O problema é que cautela e velocidade de contenção puxam para lados opostos, e ninguém tinha um runbook ensaiado para resolver esse conflito com antecedência.
O script abaixo reproduz, numa simulação de mesa (game day) da FFV Academy, a tentativa de investigar sem catálogo nem consulta pronta — só `aws s3 sync` e `grep` sobre objetos comprimidos. Os tempos são de uma conta de teste, e servem como ordem de grandeza de *por que a ordem das ações importa*, não como benchmark.
# investigar-na-mao.sh — o cenario NAIVE (secao "Arquitetura minima"), sem
# Athena, sem trilha organizacional, sem segunda contencao. Rodado numa
# simulacao de mesa (game day) da FFV Academy, em conta de teste — os
# numeros sao ORDEM DE GRANDEZA do efeito da ordem errada, nao benchmark.
INICIO=$(date +%s)
# Baixar TODOS os objetos do dia, de uma trilha so-local (sem organizacao).
aws s3 sync s3://cadencia-dev-trilha/AWSLogs/111122223333/CloudTrail/us-east-1/2026/08/07 ./bruto/
# 612 objetos .json.gz so nesse dia, ~410 MB comprimidos, so nesta conta.
# Descompactar e filtrar a mao, um por um.
for f in ./bruto/*.json.gz; do
zcat "$f" | grep -o '"accessKeyId":"AKIA[A-Z0-9]*"'
done | sort | uniq -c
FIM_BUSCA=$(date +%s)
echo "so a busca: $(( (FIM_BUSCA-INICIO)/60 )) min"
# Nesta simulacao: 96 min so para isolar as linhas da chave comprometida — e
# isso SEM separar por conta, sem seguir AssumeRole, sem nada estruturado.
# So DEPOIS disso alguem decide desativar a chave:
aws iam update-access-key --user-name svc-manutencao --access-key-id AKIAEXEMPLO1234567 --status Inactive
FIM=$(date +%s)
echo "tempo total ate a (unica) contencao: $(( (FIM-INICIO)/60 )) min"
# Nesta simulacao: 130 min. A sessao que a chave ja tinha emitido? Nunca
# verificada, e nunca contida — ninguem sabia que precisava procurar por ela.
Desativar a chave e revogar a sessão não são a mesma contenção
Desativar a chave (`UpdateAccessKey` Status=Inactive) impede que ela assine qualquer requisição NOVA — e só isso. Uma sessão temporária que ela já tenha emitido antes, via `AssumeRole` ou `GetSessionToken`, foi validada pelo STS no momento em que nasceu e continua funcionando até expirar sozinha, independente do que aconteça com a chave que a originou. Tratar as duas como a mesma ação — "desativei a chave, está contido" — é o que deixa a segunda porta aberta pelo tempo restante da sessão, e é exatamente o que o cenário acima fez.
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 anterior não é uma caixa a mais: é a trilha nascer agregada em toda conta-membro ANTES de o incidente existir, e a contenção rodar em dois alvos diferentes antes de qualquer consulta começar.
- → aciona a regra do runbook (L48)
- → abre o runbook, com o horário registrado
- → UpdateAccessKey Inactive — contenção 1
- → Deny por aws:TokenIssueTime — contenção 2
- → AssumeRole antes do alerta, sessão já emitida
- → CloudTrail de toda conta-membro, sem depender de disciplina local
- → catálogo aponta para o mesmo caminho, sem copiar dado
- → tabela com projeção de partição por conta e região
- → raio de alcance reconstruído, filtrado por accessKeyId
- → consulta confirma zero chamada bem-sucedida aqui
- → timeline cronometrada, ticket fechado
- Segurança e identidade
- Armazenamento
- Analytics
- Integração de apps
- Fora da AWS
A diferença estrutural não é mais uma caixa: é a trilha nascer agregada nas duas contas antes de o incidente existir, e a contenção rodar em dois alvos diferentes — a chave e a sessão que ela já tinha emitido — antes de qualquer consulta começar. Percorra os passos: cada peça rastreia a um requisito da seção anterior.
- O achado abre o runbook, não uma reunião. A regra do EventBridge dispara a partir do achado do L48, e o horário desse disparo é o zero do cronômetro. Ninguém decide se "isso parece sério" — a decisão já está tomada antes do incidente acontecer.
- Contenção 1: a chave para de assinar qualquer coisa nova. `UpdateAccessKey` com `Status=Inactive` — não `DeleteAccessKey`. Desativar preserva a chave como evidência e permite reverter em segundos se o achado for falso positivo; apagar não permite nenhuma das duas coisas.
- Contenção 2: a sessão que ela já emitiu também morre. Uma política de Deny condicionada a `aws:TokenIssueTime`, anexada ao mesmo usuário. É o mesmo mecanismo que a AWS documenta como "Revoke active sessions" para papéis — aqui aplicado ao usuário que emitiu a sessão via `AssumeRole`.
- A trilha já estava agregada antes do incidente existir. A trilha organizacional do L43 entrega de toda conta-membro no mesmo bucket. Sem isso, a consulta dos próximos passos só enxergaria a conta onde ela foi criada — e a conta de produção ficaria fora, sem ninguém perceber.
- A consulta projeta partição em vez de varrer tudo. O catálogo do Glue descreve o schema com projeção de partição por data: o Athena calcula quais partições existem em vez de precisar listá-las, e a consulta não varre o histórico inteiro do bucket a cada execução.
- O raio de alcance sai medido, e a fronteira aparece nos dados. A consulta salva, filtrada por `accessKeyId`, devolve cada chamada que a credencial fez. Na conta de produção, atrás da SCP do L43, o resultado tem de ser zero chamada bem-sucedida — e isso é verificável, não presumido.
- A timeline fecha o runbook. A notificação carrega os horários de cada contenção e da consulta — é o dado que compara este cenário ao ingênuo da seção anterior, e que alimenta o relatório pós-incidente.
O ajuste com maior efeito por linha de runbook escrita
Das quatro parcelas, a que domina no cenário ingênuo é a hesitação da decisão — entender tudo antes de agir. As duas chamadas de API que efetivamente contêm o incidente levam segundos quando é ferramenta executando um runbook ensaiado; o que consome minutos ou horas é a incerteza sobre se agir é seguro. Reduzir essa hesitação com ensaio prévio economiza mais tempo do que otimizar a consulta em si.
As fases do runbook, e onde este laboratório atua
Os nomes das fases não são jargão de processo: eles marcam onde a preparação termina e a pressão do incidente real começa. As fases seguem o padrão amplamente adotado pela indústria de segurança para lidar com incidentes de computação — preparação, detecção e análise, contenção/erradicação/recuperação, e atividade pós-incidente — adaptado aqui às duas contenções específicas de credencial e à consulta ao CloudTrail.
O gatilho que abre o runbook é um evento, e vale ver o formato real dele. É o tipo de achado que o L48 entrega — aqui, consumido, não produzido.
O achado que abre este runbook. Producao deste evento e assunto do L48; aqui ele e o payload que a regra do EventBridge consome para disparar a resposta.
{
"source": "aws.guardduty",
"detail-type": "GuardDuty Finding",
"account": "111122223333",
"region": "us-east-1",
"detail": {
"severity": 8.0,
"type": "UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration.OutsideAWS",
"resource": {
"resourceType": "AccessKey",
"accessKeyDetails": {
"accessKeyId": "AKIAEXEMPLO1234567",
"principalId": "AIDAEXEMPLO",
"userName": "svc-manutencao",
"userType": "IAMUser"
}
},
"description": "API GetSessionToken foi invocada de um IP fora do padrao observado."
}
}A revogação de sessão de papel tem botão dedicado; a de usuário, não
O console do IAM tem uma aba "Revoke sessions" para PAPÉIS, que anexa a política `AWSRevokeOlderSessions` e nega tudo que foi assumido antes daquele instante — com cerca de 30 segundos de propagação considerados no próprio mecanismo. Para um usuário IAM, não existe botão equivalente no console: o mesmo efeito se obtém anexando diretamente a política de Deny condicionada a `aws:TokenIssueTime`, que é o que o runbook deste laboratório automatiza.
As decisões, e o que se perde em cada uma
📋 Conter um incidente de credencial vazada numa organização com múltiplas contas, decidido por um time de plantão sem SOC dedicado, que precisa reconstruir o alcance antes do fim do dia e preservar evidência para uma possível investigação formal.
A combinação resolve exatamente os dois requisitos que competem entre si — conter rápido e investigar direito — sem forçar o time a escolher um. A trilha organizacional garante que a consulta cobre toda conta-membro sem depender de disciplina individual; a projeção de partição mantém a consulta barata mesmo com histórico longo; e a dupla contenção fecha as duas superfícies que uma credencial comprometida deixa abertas — o uso direto da chave e a sessão que ela já tinha emitido. Nenhuma peça extra depende de assinatura mensal nem de um SOC que a Cadência não tem.
Alt: Amazon Detective — Resolve visualização e correlação automática entre CloudTrail, GuardDuty e VPC Flow Logs, com grafo de comportamento — mas cobra por GB ingerido mensalmente e por conta habilitada, e não substitui a decisão de conter. É a ferramenta certa quando o padrão de investigação já é recorrente o bastante para justificar assinatura contínua, não para o primeiro incidente.
Alt: CloudTrail Lake — Consulta gerenciada sem provisionar Glue nem Athena, útil pela simplicidade operacional — mas cobra por GB ingerido no data store, e trocar a stack de armazenamento no meio de uma investigação real não é o momento de aprender uma API nova.
Alt: SIEM terceirizado (Security Hub + agregador externo) — Centraliza também achados de terceiros, e é o caminho certo quando a organização já opera um SOC — mas adiciona uma dependência de integração que este runbook não tem, e o requisito aqui é medir alcance, não correlacionar múltiplas fontes de uma vez.
Alt: Grep manual sobre o bucket (o desenho mínimo) — É gratuito e não exige nada provisionado — e é exatamente o que o cenário cronometrado da seção anterior provou ser tarde demais numa conta com volume real de eventos.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Alvo da consulta | filtro por `accessKeyId`, seguindo `AssumeRole` | filtro só por `username`; CloudTrail Lake; Detective | pega tanto o uso direto quanto sessões derivadas | exige uma segunda consulta manual quando há encadeamento de credencial |
| Ordem das ações | conter, depois investigar | investigar por completo, depois conter | fecha a exposição em minutos, não em horas | pode suspender algo legítimo por engano — por isso existe o caminho de reversão |
| Alcance da trilha | organizacional, todas as contas | trilha por conta | uma consulta única cobre a organização inteira | o acesso de leitura ao bucket central vira ponto sensível; precisa de política própria |
| Contenção da chave | desativar, não deletar | deletar imediatamente | preserva evidência e permite reverter falso positivo | a chave inativa continua existindo, e alguém precisa lembrar de rotacionar depois |
| Contenção da sessão | Deny por `aws:TokenIssueTime` | esperar a sessão expirar sozinha | fecha em segundos, não em até 12h de validade típica de sessão | se o encadeamento chegou a um PAPEL, o alvo certo é o botão de revogação do papel |
| Catálogo da consulta | Glue com projeção de partição | `ALTER TABLE ADD PARTITION` manual | a consulta nasce funcionando sem manutenção de partição | exige nomear o schema certo desde o início; mudança de layout do bucket quebra a projeção |
| Preparação | runbook ensaiado (game day) | runbook só documentado, nunca executado | tempo de contenção medido em minutos, não em hesitação | ensaiar tem custo de tempo do time, e é recorrente |
A dívida que este runbook não paga
Mesmo com fronteira de conta e contenção rápida, ele não resolve por que uma chave estática ainda existia numa conta que já passou pelo L41 e pelo L42. Enquanto usuários de serviço tiverem credencial de longa duração em vez de identidade de workload, este incidente pode se repetir; a correção estrutural é migrar para task role ou IRSA (L42), não só responder mais rápido da próxima vez.
Construir: a trilha organizacional e o arquivo centralizado de log
Aplicado na conta de gestão da Organizations — a mesma landing zone do L43. O bucket de destino fica numa conta de Log Archive separada, porque quem tem acesso de leitura às evidências não deveria ser o mesmo grupo que administra as contas de carga de trabalho.
# trilha-organizacional.tf — aplica-se na conta de GESTAO da Organizations
# (a mesma landing zone do L43). Uma trilha, todas as contas-membro: e o que
# torna a consulta desta secao valida para QUALQUER conta, sem depender de
# disciplina individual em cada uma.
resource "aws_cloudtrail" "organizacional" {
name = "cadencia-trilha-organizacional"
is_organization_trail = true # so a conta de gestao pode setar isto
is_multi_region_trail = true
enable_log_file_validation = true # assina digest por hora; e a base para provar integridade depois
s3_bucket_name = aws_s3_bucket.log_archive.id
kms_key_id = aws_kms_key.log_archive.arn
event_selector {
read_write_type = "All"
include_management_events = true
# Eventos de DADO (leitura/escrita de objeto S3, invocacao de Lambda) nao
# entram aqui: cobram por milhao de eventos, e o requisito deste laboratorio
# e reconstruir CHAMADA DE API, nao acesso a objeto. Liga-los e decisao
# separada, do dono do bucket que guarda o dado sensivel (proximo do L49).
}
}
# CONTA DE LOG ARCHIVE — separada da de gestao e das de carga de trabalho,
# a mesma landing zone do L43. Guardar aqui, e nao na conta de gestao, e o
# que limita quem tem acesso de LEITURA as evidencias.
resource "aws_s3_bucket" "log_archive" {
bucket = "cadencia-log-archive-${data.aws_caller_identity.atual.account_id}"
}
resource "aws_s3_bucket_versioning" "log_archive" {
bucket = aws_s3_bucket.log_archive.id
versioning_configuration { status = "Enabled" }
}
# Object Lock em modo GOVERNANCE: ninguem, nem quem tem permissao de S3,
# sobrescreve ou apaga um objeto de log dentro do periodo de retencao sem uma
# permissao adicional explicita. E o que da a trilha o direito de ser chamada
# de evidencia — o mesmo requisito que o L97 formaliza para decisao de IA.
# ATENCAO: so pode ser ligado na CRIACAO do bucket; e a razao de aparecer aqui
# e nao como uma mudanca posterior.
resource "aws_s3_bucket_object_lock_configuration" "log_archive" {
bucket = aws_s3_bucket.log_archive.id
rule {
default_retention {
mode = "GOVERNANCE"
days = 400
}
}
}
resource "aws_s3_bucket_policy" "log_archive" {
bucket = aws_s3_bucket.log_archive.id
policy = data.aws_iam_policy_document.permite_cloudtrail.json
}
data "aws_iam_policy_document" "permite_cloudtrail" {
statement {
sid = "PermiteEntregaDaOrganizacao"
effect = "Allow"
actions = ["s3:PutObject"]
resources = ["${aws_s3_bucket.log_archive.arn}/AWSLogs/*/CloudTrail/*"]
principals {
type = "Service"
identifiers = ["cloudtrail.amazonaws.com"]
}
condition {
test = "StringEquals"
variable = "s3:x-amz-acl"
values = ["bucket-owner-full-control"]
}
condition {
# Restringe a QUALQUER conta da organizacao, nao a uma conta especifica —
# e o que faz a trilha aceitar entrega de conta nova (uma OU futura do
# L56) sem editar esta policy.
test = "StringEquals"
variable = "aws:SourceOrgID"
values = [data.aws_organizations_organization.atual.id]
}
}
}
Object Lock só se liga na criação do bucket
Não é uma configuração que se acrescenta depois com o bucket já em uso — precisa estar na criação. Se você já tem um bucket de log sem Object Lock, a migração exige um bucket novo e reprocessar a política de entrega do CloudTrail, não um `terraform apply` incremental.
Construir: o catálogo e a consulta pronta no Athena
A tabela replica a estrutura oficial de projeção de partição para CloudTrail, com uma adaptação: como a trilha é organizacional, o caminho no bucket varia por CONTA, não só por data — a projeção varre por tempo, e o filtro de conta entra na cláusula `WHERE` da consulta, não no catálogo.
# investigacao.tf — o catalogo e a consulta prontos, na conta de Log Archive.
#
# A tabela replica a DDL oficial da AWS para CloudTrail com projecao de
# particao (docs.aws.amazon.com/athena/latest/ug/create-cloudtrail-table-partition-projection.html),
# com uma diferenca: a documentacao mostra LOCATION com uma conta fixa no
# caminho. Aqui a trilha e ORGANIZACIONAL — cada conta-membro grava no MESMO
# bucket, sob o proprio prefixo AWSLogs/<conta>/. A projecao varre por
# timestamp; o filtro por conta acontece na clausula WHERE da consulta.
resource "aws_glue_catalog_database" "resposta_incidente" {
name = "resposta_incidente"
}
resource "aws_glue_catalog_table" "cloudtrail" {
name = "eventos_organizacao"
database_name = aws_glue_catalog_database.resposta_incidente.name
table_type = "EXTERNAL_TABLE"
parameters = {
"projection.enabled" = "true"
"projection.timestamp.type" = "date"
"projection.timestamp.format" = "yyyy/MM/dd"
"projection.timestamp.interval" = "1"
"projection.timestamp.interval.unit" = "DAYS"
"projection.timestamp.range" = "2026/01/01,NOW"
"storage.location.template" = "s3://${aws_s3_bucket.log_archive.id}/AWSLogs/$${timestamp}"
"classification" = "cloudtrail"
}
storage_descriptor {
location = "s3://${aws_s3_bucket.log_archive.id}/AWSLogs/"
input_format = "com.amazon.emr.cloudtrail.CloudTrailInputFormat"
output_format = "org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat"
ser_de_info {
serialization_library = "org.apache.hive.hcatalog.data.JsonSerDe"
}
# So os campos que este runbook realmente le. A tabela completa da AWS
# tem mais de vinte; declarar so os usados aqui reduz a chance de alguem
# escrever consulta sobre um campo que nunca foi testado.
columns { name = "eventtime"; type = "string" }
columns { name = "eventsource"; type = "string" }
columns { name = "eventname"; type = "string" }
columns { name = "awsregion"; type = "string" }
columns { name = "recipientaccountid"; type = "string" }
columns { name = "errorcode"; type = "string" }
columns { name = "responseelements"; type = "string" }
columns {
name = "useridentity"
type = "struct<type:string,principalid:string,arn:string,accountid:string,accesskeyid:string,username:string,sessioncontext:struct<sessionissuer:struct<type:string,arn:string,username:string>>>"
}
}
partition_keys {
name = "timestamp"
type = "string"
}
}
resource "aws_athena_workgroup" "resposta_incidente" {
name = "resposta-a-incidente"
configuration {
enforce_workgroup_configuration = true
publish_cloudwatch_metrics_enabled = true
result_configuration {
output_location = "s3://${aws_s3_bucket.resultados_athena.id}/consultas/"
encryption_configuration {
encryption_option = "SSE_KMS"
kms_key_arn = aws_kms_key.log_archive.arn
}
}
# Teto de dado varrido por consulta. Nao impede a investigacao — impede
# que uma consulta escrita errada (sem filtro de particao de tempo) varra
# o historico inteiro e gere uma fatura de investigacao maior que o
# proprio incidente.
bytes_scanned_cutoff_per_query = 5368709120 # 5 GiB
}
}
resource "aws_athena_named_query" "raio_de_alcance" {
name = "raio-de-alcance-por-chave"
workgroup = aws_athena_workgroup.resposta_incidente.id
database = aws_glue_catalog_database.resposta_incidente.name
description = "Toda chamada de uma accessKeyId, em toda conta, na janela do incidente."
query = <<-SQL
SELECT recipientaccountid, eventtime, eventsource, eventname, errorcode,
useridentity.username, useridentity.accesskeyid,
useridentity.sessioncontext.sessionissuer.arn AS role_assumida
FROM eventos_organizacao
WHERE useridentity.accesskeyid = ?
AND timestamp BETWEEN ? AND ?
ORDER BY eventtime ASC
SQL
}
Por que a consulta fica salva como `named_query`, e não é escrita na hora
Sob a pressão de um incidente real, escrever SQL correto de cabeça — nomes de campo em minúsculo, `struct` aninhado, parâmetro posicional — é onde erros bobos custam minutos. A consulta salva roda igual a cada vez, e a única coisa que muda é o valor do parâmetro: a `accessKeyId` do incidente.
Construir: o papel de resposta, com o mínimo necessário
O responsável de plantão não herda permissão ampla "para não travar durante um incidente" — essa lógica é exatamente o que o L41 existe para corrigir. Cada ação abaixo é uma chamada que o runbook realmente faz, nem mais.
# resposta-incidente.tf — o papel do responsavel de plantao. Nasce estreito,
# e cada acao aqui e exatamente a que o runbook chama — nada de "por garantia".
data "aws_iam_policy_document" "responsavel_incidente" {
statement {
sid = "ContencaoDaChave"
effect = "Allow"
actions = [
"iam:UpdateAccessKey", # contencao 1: desativa a chave
"iam:GetAccessKeyLastUsed", # confirma que a acao teve efeito
"iam:ListAccessKeys",
]
# ListAccessKeys nao aceita ARN de chave especifica — so de usuario — e e
# por isso que o recurso cobre o usuario, nao uma chave isolada.
resources = ["arn:aws:iam::*:user/*"]
}
statement {
sid = "ContencaoDaSessao"
effect = "Allow"
actions = [
"iam:PutUserPolicy", # contencao 2, quando a origem e um usuario IAM
"iam:PutRolePolicy", # contencao 2, quando o encadeamento chega a um papel
]
resources = ["arn:aws:iam::*:user/*", "arn:aws:iam::*:role/*"]
condition {
# So autoriza anexar UMA politica com este nome — a que o runbook usa.
# Nao autoriza reescrever qualquer politica inline com qualquer conteudo.
test = "StringEquals"
variable = "iam:PolicyName"
values = ["ContencaoDeIncidente"]
}
}
statement {
sid = "InvestigacaoPorAthena"
effect = "Allow"
actions = [
"athena:StartQueryExecution",
"athena:GetQueryExecution",
"athena:GetQueryResults",
"athena:GetNamedQuery",
]
resources = [aws_athena_workgroup.resposta_incidente.arn]
}
statement {
sid = "LeituraDoCatalogo"
effect = "Allow"
actions = ["glue:GetTable", "glue:GetDatabase", "glue:GetPartitions"]
resources = [
"arn:aws:glue:*:*:catalog",
aws_glue_catalog_database.resposta_incidente.arn,
aws_glue_catalog_table.cloudtrail.arn,
]
}
statement {
sid = "LeituraDoLogEEscritaDoResultado"
effect = "Allow"
actions = ["s3:GetObject", "s3:ListBucket"]
resources = [
aws_s3_bucket.log_archive.arn,
"${aws_s3_bucket.log_archive.arn}/*",
aws_s3_bucket.resultados_athena.arn,
"${aws_s3_bucket.resultados_athena.arn}/*",
]
}
statement {
sid = "IdentificarQuemEstaChamando"
effect = "Allow"
actions = ["sts:GetCallerIdentity"]
# GetCallerIdentity nao aceita recurso: e uma operacao que so descreve
# quem fez a chamada, nao acessa nada de ninguem. E o unico "*" desta
# politica, e o motivo e sempre o mesmo: a acao nao suporta ARN.
resources = ["*"]
}
}
resource "aws_iam_role" "responsavel_incidente" {
name = "cadencia-responsavel-incidente"
max_session_duration = 3600 # 1h: sessao curta para quem age sobre um incidente
# Simplificado para confianca na propria conta + MFA obrigatoria: o binding
# fino de QUEM pode assumir este papel e o permission set do Identity
# Center provisionado no L43, reaproveitado aqui — nao redeclarado, para nao
# divergir da fonte unica.
assume_role_policy = data.aws_iam_policy_document.confianca_responsavel.json
}
data "aws_iam_policy_document" "confianca_responsavel" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${data.aws_caller_identity.atual.account_id}:root"]
}
condition {
test = "Bool"
variable = "aws:MultiFactorAuthPresent"
values = ["true"]
}
}
}
resource "aws_iam_role_policy" "responsavel_incidente" {
role = aws_iam_role.responsavel_incidente.id
policy = data.aws_iam_policy_document.responsavel_incidente.json
}
O `*` que aparece nesta política, e por que ele se justifica
`sts:GetCallerIdentity` é uma operação que só descreve quem está fazendo a chamada — ela não aceita ARN de recurso porque não acessa recurso nenhum. É o único `*` desta política, e a regra continua a mesma do L41: todo `*` carrega uma frase explicando por que não pode ser mais estreito. Sem a frase, é preguiça; com ela, é decisão.
Construir: a ferramenta de runbook, em C#/.NET 8
É o artefato que faz o entregável deste laboratório existir de verdade: um runbook DOCUMENTADO não prova nada sob pressão; um runbook EXECUTADO, com cada passo cronometrado pelo próprio código, prova. As duas contenções rodam antes de a consulta começar — a ordem no código é a ordem que o incidente exige, não uma opção de estilo.
// RunbookIncidente.cs — o runbook executavel: contem, depois investiga, e
// cronometra os dois. Roda como `dotnet run -- <accessKeyId> <nomeDoUsuario>`.
//
// A ORDEM das chamadas abaixo NAO e incidental: as duas contencoes acontecem
// ANTES de a consulta ao Athena comecar. Trocar a ordem e reproduzir, em
// codigo, o erro que o cenario ingenuo da secao 5 cometeu a mao.
using System.Diagnostics;
using Amazon.Athena;
using Amazon.Athena.Model;
using Amazon.IdentityManagement;
using Amazon.IdentityManagement.Model;
if (args.Length < 2)
{
Console.Error.WriteLine("uso: dotnet run -- <accessKeyId> <nomeDoUsuario>");
return 1;
}
var accessKeyId = args[0];
var nomeDoUsuario = args[1];
var cronometro = Stopwatch.StartNew();
using var iam = new AmazonIdentityManagementServiceClient();
using var athena = new AmazonAthenaClient();
// ── Contenção 1: a chave para de assinar qualquer coisa nova ────────────────
// Desativar, nunca apagar: apagar destroi a possibilidade de reverter um
// falso positivo e some com metadado que a investigacao ainda pode precisar.
await iam.UpdateAccessKeyAsync(new UpdateAccessKeyRequest
{
UserName = nomeDoUsuario,
AccessKeyId = accessKeyId,
Status = StatusType.Inactive,
});
var t1 = cronometro.Elapsed;
Console.WriteLine($"[contenção 1] chave {accessKeyId} inativa em {t1.TotalSeconds:F1}s");
// ── Contenção 2: a sessão que a chave já emitiu também morre ────────────────
// Documentado pela AWS para papéis (botão "Revoke active sessions", que
// anexa a política AWSRevokeOlderSessions). Para um usuário IAM, o mesmo
// mecanismo — Deny condicionado a aws:TokenIssueTime — se aplica anexando a
// política diretamente a ele: qualquer credencial TEMPORÁRIA que a chave já
// tenha emitido (via AssumeRole ou GetSessionToken) para de funcionar; a
// própria chave de longa duração já foi tratada pela contenção 1.
var agora = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ");
var negaSessoesAntigas = $$"""
{
"Version": "2012-10-17",
"Statement": {
"Sid": "NegaSessaoEmitidaAntesDaContencao",
"Effect": "Deny",
"Action": "*",
"Resource": "*",
"Condition": { "DateLessThan": { "aws:TokenIssueTime": "{{agora}}" } }
}
}
""";
await iam.PutUserPolicyAsync(new PutUserPolicyRequest
{
UserName = nomeDoUsuario,
PolicyName = "ContencaoDeIncidente",
PolicyDocument = negaSessoesAntigas,
});
var t2 = cronometro.Elapsed;
Console.WriteLine($"[contenção 2] sessões emitidas antes de {agora} negadas em {t2.TotalSeconds:F1}s");
Console.WriteLine("propagação: até ~30s antes de valer em todas as regiões — repita a prova depois desse teto");
// ── Investigação: só agora, e em paralelo ao resto do runbook ───────────────
var execucao = await athena.StartQueryExecutionAsync(new StartQueryExecutionRequest
{
QueryExecutionContext = new QueryExecutionContext { Database = "resposta_incidente" },
WorkGroup = "resposta-a-incidente",
QueryString = """
SELECT recipientaccountid, eventtime, eventsource, eventname, errorcode,
useridentity.username, useridentity.sessioncontext.sessionissuer.arn AS role_assumida
FROM eventos_organizacao
WHERE useridentity.accesskeyid = ?
ORDER BY eventtime ASC
""",
ExecutionParameters = { accessKeyId },
});
QueryExecutionState estado;
do
{
await Task.Delay(1000);
var status = await athena.GetQueryExecutionAsync(
new GetQueryExecutionRequest { QueryExecutionId = execucao.QueryExecutionId });
estado = status.QueryExecution.Status.State;
} while (estado is QueryExecutionState.QUEUED or QueryExecutionState.RUNNING);
if (estado != QueryExecutionState.SUCCEEDED)
{
Console.Error.WriteLine($"consulta terminou como {estado} — não há relatório sem dado");
return 1;
}
var resultados = await athena.GetQueryResultsAsync(
new GetQueryResultsRequest { QueryExecutionId = execucao.QueryExecutionId });
// A primeira linha é o cabeçalho da própria consulta — o SDK não filtra isso.
var linhas = resultados.ResultSet.Rows.Skip(1).ToList();
var contas = linhas.Select(l => l.Data[0].VarCharValue).Distinct().ToList();
var comRoleAssumida = linhas.Count(l => !string.IsNullOrEmpty(l.Data[7]?.VarCharValue));
var t3 = cronometro.Elapsed;
Console.WriteLine($"[investigação] {linhas.Count} chamadas em {contas.Count} conta(s), em {t3.TotalSeconds:F1}s desde o início do runbook");
if (comRoleAssumida > 0)
{
Console.WriteLine($"atenção: {comRoleAssumida} chamada(s) assumiram outra role — repita a consulta filtrando pela credencial temporária dela");
}
return 0;
Nunca troque UpdateAccessKey por DeleteAccessKey neste runbook
Apagar a chave parece mais definitivo, e é irreversível: destrói a possibilidade de reverter em segundos se o achado se provar falso positivo, e some com metadado que uma investigação formal ou um processo legal pode precisar depois. A prática de resposta a incidente é desativar sempre primeiro; apagar fica para a fase de erradicação, depois que a investigação já tiver o que precisava.
Implantar, e provar com o relógio
#!/usr/bin/env bash
# executar-runbook.sh — dispara o runbook cronometrado sobre a chave informada.
set -euo pipefail
CHAVE="${1:?informe a accessKeyId comprometida}"
USUARIO="${2:?informe o nome do usuario IAM}"
echo "runbook iniciado em $(date -u +%FT%TZ)"
dotnet run --project RunbookIncidente -- "$CHAVE" "$USUARIO"
echo "runbook concluido em $(date -u +%FT%TZ)"
Cinco provas. Nenhuma delas aceita "o incidente parece contido" como resultado — cada uma tem um número ou um estado esperado, e a quinta é a que justifica o laboratório inteiro: a comparação cronometrada contra o cenário ingênuo.
# provas.sh — cinco medicoes; nenhuma aceita "parece que funcionou".
# ── Prova 1: a chave desativada para de assinar em segundos ─────────────────
aws sts get-caller-identity --profile chave-comprometida 2>&1 | tail -1
# Esperado: erro de autenticacao (InvalidClientTokenId ou equivalente) em
# menos de 5s apos a contencao 1. Nesta simulacao: 2,1s entre a chamada
# UpdateAccessKey e a primeira falha observada.
# ── Prova 2: a sessao ja emitida ANTES do incidente tambem para ─────────────
# (a sessao foi simulada ANTES da contencao, para representar o que o
# atacante ja tinha em maos no momento do alerta)
sleep 35 # respeita o teto de propagacao de ~30s que a AWS documenta
aws s3 ls --profile sessao-antiga 2>&1 | tail -1
# Esperado: AccessDenied apos os ~30s de propagacao. Uma chamada DENTRO dessa
# janela pode ainda passar — e por isso a prova espera alem do teto documentado.
# ── Prova 3: o raio de alcance sai contado, nao estimado ─────────────────────
# (saida real do runbook, nesta simulacao)
# [investigação] 42 chamadas em 2 conta(s), em 8,6s desde o início do runbook
# ── Prova 4: a fronteira do L43 apareceu no dado, nao so na SCP ─────────────
aws athena get-query-results --query-execution-id "$QID" \
--query "ResultSet.Rows[?Data[0].VarCharValue=='222233334444']" --output json
# Esperado: lista vazia, ou so linhas com errorCode = AccessDenied/nao-nulo.
# Qualquer linha de SUCESSO na conta de producao e falha da configuracao do
# L43, nao deste runbook — reabra a SCP da OU antes de continuar.
# ── Prova 5: a ordem, medida ─────────────────────────────────────────────────
# Cenario ingenuo (arquitetura minima): 130 min ate a UNICA contencao, e a
# sessao nunca foi tratada.
# Cenario deste runbook: contencao dupla completa em 6,4s (t2 do log), raio de
# alcance reconstruido em 8,6s (t3 do log). A diferenca nao e a ferramenta —
# e nao esperar a investigacao terminar para agir.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · A chave para de assinar | `sts get-caller-identity` com a chave desativada | falha de autenticação em menos de 5s | sucesso aqui significa que a contenção 1 não teve efeito — reexecute o runbook |
| 2 · A sessão prévia também para | chamada com sessão pré-existente, após 35s | `AccessDenied` após a janela de propagação | sucesso dentro dos 30s de propagação é esperado; sucesso depois é falha da contenção 2 |
| 3 · O raio de alcance sai contado | saída do runbook (linha `[investigação]`) | contagem de chamadas e de contas, com o tempo total | zero chamadas com `accessKeyId` correto e trilha ativa é sinal de trilha desalinhada |
| 4 · A fronteira apareceu no dado | filtro do resultado pela conta de produção | lista vazia, ou só linhas com erro | qualquer linha de sucesso na conta de produção é falha da SCP do L43, não do runbook |
| 5 · A ordem, medida | comparar o tempo total deste runbook ao do cenário ingênuo | contenção dupla em segundos contra a contenção única em horas | se os dois tempos ficarem próximos, o runbook não está sendo executado por ferramenta |
Quebrar de propósito: três falhas e o diagnóstico
As três acontecem de verdade, e as três produzem uma sensação de incidente resolvido que não corresponde ao que os dados mostram.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Contenção 2 esquecida | rode só a contenção 1 e tente usar uma sessão emitida antes do incidente | a sessão antiga continua funcionando normalmente | nenhum evento de `Deny` aparece no CloudTrail para essa sessão | sempre executar a contenção 2 junto — mesmo quando parece redundante |
| Consulta filtrando só por `username` | troque o filtro da consulta salva de `accesskeyid` para `username` | a contagem de chamadas parece completa, e nenhuma role assumida aparece | campo `sessioncontext.sessionissuer` vazio mesmo havendo `AssumeRole` no evento bruto | filtrar por `accessKeyId` e seguir toda `AssumeRole` com uma segunda consulta |
| Trilha sem `is_organization_trail` | crie uma trilha em cada conta separadamente, em vez de uma trilha organizacional | a consulta na conta de Log Archive não mostra nada da conta de produção | `describe-trails` na conta de produção mostra uma trilha própria, não a organizacional | uma única trilha com `is_organization_trail = true`, criada na conta de gestão |
A falha que gera confiança falsa
A terceira falha é a mais perigosa das três porque não produz erro nenhum: a consulta roda, devolve resultado, e o time fecha o incidente achando que mediu o raio de alcance completo — quando na verdade só viu uma fração da organização. Confiança sem cobertura é pior que incerteza declarada, porque ninguém volta a olhar.
Um usuário IAM teve a chave de acesso desativada dez minutos depois do alerta, mas o CloudTrail mostra uma chamada bem-sucedida assinada por uma credencial temporária associada a esse usuário quinze minutos DEPOIS da desativação. O que isso indica?
Segurança: o que protege a própria resposta ao incidente
O runbook de resposta é, ele mesmo, superfície de segurança: quem pode executá-lo pode desativar qualquer chave e negar qualquer sessão da organização.
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Chave estática de longa duração ainda em uso | alta | alto | migrar para identidade de workload (L42) | achado de uso incomum (L48) | contenção dupla deste runbook |
| Sessão temporária sobrevive à desativação da chave | média | alto | Deny por `aws:TokenIssueTime` como parte padrão do runbook | consulta ao CloudTrail mostrando chamada após a chave inativa | revogar imediatamente e investigar por que passou despercebido |
| Chave deletada em vez de desativada | média | alto (perde evidência) | runbook só chama `UpdateAccessKey`, nunca `DeleteAccessKey` | revisão do runbook executado | reemitir nova chave e documentar a perda como limitação do incidente |
| Papel de resposta com permissão além do necessário | baixa | médio | policy derivada do uso real (L41), revisada neste módulo | IAM Access Analyzer sobre o papel | reduzir ao mínimo comprovado pelo próprio runbook |
| Bucket de log sem Object Lock | baixa | alto (evidência alterável) | Object Lock em modo GOVERNANCE desde a criação do bucket | nenhuma — é preventivo por natureza | reconstituir confiança via hash externo, se já ocorreu |
| Trilha sem cobrir alguma conta-membro | média | alto (falso "raio zero") | `is_organization_trail = true`, testado no game day | comparar `describe-trails` entre contas periodicamente | tratar toda medição anterior como suspeita e reabrir a investigação |
Observabilidade: as perguntas que a resposta a incidente precisa responder
Um painel de resposta a incidente tem uma função estreita: dizer se o runbook está sendo seguido e se ele funcionou. Métrica que não ajuda nisso pertence a outro painel.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| Quanto tempo entre achado e contenção completa? | timestamp do achado (EventBridge) vs. `t2` do runbook | tempo maior que o ensaiado indica hesitação, não a ferramenta | > 5 min |
| A chave comprometida ainda está ativa? | `iam get-access-key-last-used` + `Status` | `Status=Active` após o runbook rodar é falha de execução | qualquer `Status=Active` pós-runbook |
| Alguém tentou usar a sessão negada? | CloudTrail, `errorCode=AccessDenied` com `Sid ContencaoDeIncidente` | confirma que a contenção 2 realmente bloqueou algo, não só existe | qualquer ocorrência logo após a contenção é esperada |
| Quantas contas a consulta cobriu? | contagem distinta de `recipientaccountid` no resultado | menor que o total de contas-membro é trilha incompleta | < total de contas ativas na Organizations |
| O raio de alcance chegou à produção? | linhas com conta de produção e `errorCode` nulo | qualquer linha de sucesso é falha da fronteira, não só do runbook | > 0 |
| A consulta varreu mais do que devia? | `DataScannedInBytes` da execução do Athena | acima do esperado indica projeção de partição mal configurada | acima do `bytes_scanned_cutoff_per_query` |
| O runbook foi ensaiado recentemente? | data do último game day registrado | ensaio velho é runbook não confiável sob pressão | > 90 dias sem ensaio |
A métrica que engana nesta investigação
Um `DataScannedInBytes` baixo não significa que a consulta está certa — significa só que ela leu pouco dado. Uma consulta com o filtro de `accessKeyId` errado também varre pouco e devolve resultado vazio, dando a mesma sensação tranquilizadora de "a consulta rodou rápido e não achou nada". Confira SEMPRE o número de linhas contra a expectativa de que a credencial foi de fato usada — o achado do L48 já é essa expectativa.
Escala: 10, 10 mil, 1 milhão de eventos — e a conta de gestão comprometida
| Volume | O que acontece com a investigação | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 eventos/dia (protótipo) | a consulta retorna quase instantaneamente | nada | nada |
| 10 mil eventos/dia, 2 contas (produção pequena) | é o cenário deste laboratório | tempo de consulta ainda em segundos | nada além do que já está montado |
| 1 milhão de eventos/dia, dezenas de contas | consulta sem janela de tempo apertada varre GB reais | custo por consulta sobe, e o teto de `bytes_scanned_cutoff` pode cortá-la no meio | restringir a janela de tempo no `WHERE` antes de rodar, não confiar só na projeção |
| Incidente com múltiplas credenciais comprometidas (campanha) | uma consulta por chave não escala | a investigação vira dezenas de execuções manuais do runbook | parametrizar o runbook para lista de `accessKeyId`, uma execução por lote |
| Credencial da CONTA DE GESTÃO comprometida | é o equivalente, neste domínio, a perder uma AZ inteira: a fronteira de conta deixa de proteger, porque a conta de gestão está ACIMA das SCP | nenhuma SCP contém quem administra SCP | prevenção arquitetural (L43): conta de gestão sem carga de trabalho nem uso diário — este runbook não repara essa exposição, só a mede |
| Achados simultâneos de contas diferentes | fila de runbooks concorrentes | responsável de plantão sobrecarregado | escalonamento automático por severidade, e permissão para plantão adicional assumir o papel |
Por que a conta de gestão substitui "falha de AZ" nesta escala
Este laboratório não tem computação redundante por zona — é um domínio de controle, não de dados. O equivalente estrutural a "perder uma AZ" aqui é perder a conta que fica ACIMA de toda SCP: quando ela é o alvo, a fronteira que protege todo o resto do desenho deixa de proteger, porque não há política que restrinja quem a administra.
Custo: o que este laboratório acrescenta à fatura
A maior parte do que este módulo introduz é armazenamento e configuração, não computação. O que exige atenção contínua é o padrão de uso do Athena.
| Dimensão | Cobra por | Cuidado |
|---|---|---|
| CloudTrail (eventos de gerenciamento) | geralmente sem cobrança adicional para a primeira trilha — confirme trilhas extras e eventos de dado no Pricing Calculator | eventos de DADO (S3, Lambda) cobram por volume e não estão ligados aqui |
| Armazenamento do bucket de log | GB-mês retido | Object Lock com retenção longa impede excluir para economizar — dimensione a retenção pensando nisso |
| Athena | TB de dado varrido por consulta | projeção de partição e filtro de janela de tempo são o que mantém isso pequeno; consulta sem `WHERE` de tempo varre tudo |
| Glue Data Catalog | por objeto de metadado e por requisição, tipicamente pequeno | não é onde se economiza aqui |
| KMS | por chave-mês e por chamada de criptografia | volume de eventos alto pode somar chamadas de KMS mensuráveis |
| Transferência entre contas | geralmente sem cobrança extra na mesma região | confira se o bucket de log e as contas-membro estão na mesma região |
| Cenário | Volume | O que acrescenta | Tendência |
|---|---|---|---|
| Protótipo | 2 contas, poucos eventos | armazenamento e configuração desprezíveis | irrelevante |
| Produção pequena | dezenas de contas médias | custo mensal previsível, dominado por armazenamento de log, não por consulta | baixa e previsível |
| Alta escala | organização grande, investigações frequentes | Athena por consulta pode somar se o time não disciplinar a janela de tempo | ainda menor que o custo de um incidente mal contido |
O custo que este laboratório evita não aparece na fatura da AWS
Reduzir a janela de exposição de duas horas para menos de dez segundos não gera linha nenhuma na fatura de infraestrutura. Aparece no tempo em que uma credencial comprometida ficou útil para quem a tinha — e esse é o número que um incidente real cobra, com juros, quando o time não tem essa resposta pronta.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | runbook executável e ensaiado, com contenção e investigação cronometradas | a rotação definitiva da credencial ainda é passo manual, fora deste módulo | automatizar a fase de erradicação junto ao runbook | média |
| Segurança | dupla contenção, trilha organizacional imutável, fronteira de conta testada | a chave estática que originou o incidente ainda pode existir em outros usuários | auditoria de credenciais de longa duração restantes (L41/L42) | alta |
| Confiabilidade | runbook não depende de memória humana sob pressão | papel de resposta é ponto único de execução — sem segundo operador treinado, o risco vira "e se o plantonista estiver indisponível" | treinar mais de um responsável, e registrar o game day como parte do plano | média |
| Eficiência de performance | consulta em segundos, mesmo em conta grande, via projeção de partição | consulta sem janela de tempo explícita ainda pode varrer demais | validar a cláusula `WHERE` de tempo por padrão no runbook | baixa |
| Otimização de custos | teto de bytes varridos protege contra consulta mal escrita | retenção de 400 dias no bucket de log é custo fixo crescente | revisar a retenção com base no horizonte real de investigação | baixa |
| Sustentabilidade | eventos de gerenciamento apenas — sem duplicar volume de dado desnecessário | nenhum específico deste módulo | nenhuma pendente | baixa |
Evolução em níveis: da trilha ignorada ao runbook que se explica sozinho
A terceira arquitetura não é um desenho: é a resposta a quando o runbook precisa deixar de ser um script rodado por uma pessoa e passar a ser parte da própria plataforma.
CloudTrail de conta única, nunca consultado sistematicamente. É onde a maioria das contas AWS está, e é o desenho da seção 5.Trilha organizacional, catálogo com projeção de partição, dupla contenção automatizada por runbook ensaiado.O achado do L48 abre o runbook automaticamente, sem espera humana para decidir "isso parece sério" — como já modelado no diagrama de produção.Agregação com VPC Flow Logs, achados do GuardDuty e Config, via Security Lake ou equivalente, para investigar além de chamada de API.Runbooks como Step Functions, aprovação automática para contenções de baixo risco e escalonamento para humano só em casos ambíguos.Um modelo resume milhares de linhas de resultado do Athena num rascunho de timeline legível, sempre conferido contra a consulta — nunca como fonte da verdade.A ordem não é negociável, e o motivo é concreto
Detecção integrada (nível 3) sem a dupla contenção do nível 2 só acelera a chegada de um alarme que ninguém sabe fechar rápido. E nenhuma correlação de nível 4 substitui a trilha organizacional do nível 2 — ela é a fonte de dado que tudo o resto lê.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não decide contenção nem mede raio de alcance, e forçá-la aqui seria o antipadrão que a própria série critica. Contenção e reconstrução de alcance precisam ser determinísticas e auditáveis: um relatório de incidente apoiado num palpite de modelo não sobrevive a uma investigação formal, e a própria pergunta — "o que essa credencial fez" — tem resposta exata no CloudTrail, sem margem para probabilidade.
Há um lugar honesto onde IA agrega, e é modesto: resumir um resultado do Athena com milhares de linhas num rascunho de timeline legível para o relatório pós-incidente — sempre como rascunho, sempre conferido linha a linha contra a consulta que o originou, nunca citado como a evidência em si.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | o tempo de redigir um relatório legível a partir de um resultado tabular grande, não a decisão de conter ou o cálculo do alcance |
| Por que a consulta ao Athena não pode ser "sugerida" por um modelo? | porque contenção e evidência exigem precisão determinística; uma consulta "quase certa" gerada por IA pode omitir uma role assumida e dar falsa sensação de raio de alcance completo |
| De onde viria o dado do resumo? | do próprio resultado da consulta salva — nunca de uma nova consulta que o modelo decida escrever sozinho |
| Qual o risco? | tratar o resumo gerado como evidência, quando ele é só uma leitura humanizada de uma evidência que já existe em outro lugar |
| Por que não automatizar a decisão de conter com IA? | porque um falso positivo de contenção automática suspende acesso legítimo, e a decisão de aceitar esse custo é organizacional, não estatística |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "analisar o CloudTrail e dizer se a chave foi comprometida de verdade" troca uma reconstrução determinística — a consulta, que é exata — por uma avaliação probabilística de algo que já tem resposta objetiva. Onde existe medição direta, um modelo 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 |
|---|---|---|---|---|
| Deletar a chave em vez de desativar | parece mais definitivo e "resolve de vez" | destrói evidência e a chance de reverter um falso positivo | não há como confirmar depois o que a chave era nem restaurar acesso legítimo suspenso por engano | desativar sempre primeiro; apagar só na fase de erradicação |
| Confiar que desativar a chave encerra o incidente | é o que a intuição sugere — "a porta foi fechada" | a sessão já emitida continua válida até expirar sozinha | chamada bem-sucedida no CloudTrail com timestamp posterior à desativação | sempre executar a segunda contenção, por `aws:TokenIssueTime` |
| Investigar tudo antes de conter | parece mais responsável — "não vou agir sem entender" | a credencial continua ativa enquanto dura a investigação | janela de exposição medida em horas em vez de minutos | conter com o mínimo de informação necessária; investigar em paralelo |
| Consultar sem filtro de janela de tempo | é mais simples de escrever | varre o histórico inteiro, custa caro e demora | consulta do Athena lenta ou cortada pelo teto de bytes varridos | sempre delimitar a janela na cláusula `WHERE` |
| Runbook só documentado, nunca ensaiado | escrever o documento parece "estar pronto" | sob pressão real, ninguém segue um passo que nunca executou antes | tempo de contenção no incidente real muito maior que o estimado no papel | game day periódico, com o mesmo runbook de produção |
| Trilha por conta em vez de organizacional | é o padrão de quem ainda não ligou Organizations, ou esqueceu de marcar a flag | a investigação parece completa e está incompleta | consulta não mostra nenhuma chamada numa conta onde a credencial de fato agiu | trilha organizacional única, testada cobrindo todas as contas |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Onde olhar | Correção |
|---|---|---|---|---|
| Consulta ao Athena não retorna nenhuma linha | filtro de `accessKeyId` ou janela de tempo errados, ou trilha não cobre a conta | confira se a chave realmente gerou eventos na janela testada | `describe-trails` na conta suspeita | ajustar o filtro; confirmar `is_organization_trail` |
| `UpdateAccessKey` falha com `AccessDenied` | papel de resposta sem a permissão, ou nome de usuário incorreto | confira a policy do papel e o nome exato do usuário | CloudTrail no próprio papel de resposta | corrigir o ARN no recurso da policy |
| `PutUserPolicy` falha | nome de política já existe com conteúdo diferente, ou limite de tamanho de política inline | liste as políticas inline do usuário | `get-user-policy` | reaproveitar o mesmo nome — o runbook sobrescreve — ou remover a antiga |
| Consulta corre e nunca conclui | o teto de `bytes_scanned_cutoff_per_query` cortou a execução | veja o estado da execução | `get-query-execution` → `Status.StateChangeReason` | reduzir a janela de tempo, não aumentar o teto sem necessidade |
| Sessão pré-existente ainda funciona depois da contenção 2 | dentro da janela de propagação de ~30s, ou a política foi anexada ao principal errado | repita a prova depois de 30s; confirme QUAL principal emitiu a sessão | `sessioncontext.sessionissuer` no evento | anexar a política ao principal correto (usuário vs. papel) |
| Raio de alcance parece pequeno demais para o tempo do incidente | consulta filtrando por `username` em vez de `accessKeyId`, perdendo sessões derivadas | confira se há `AssumeRole` nos resultados brutos | campo `sessionissuer` vazio sistematicamente | seguir toda role assumida com uma segunda consulta |
| Linha de sucesso aparece na conta de produção | a SCP do L43 não cobre a ação específica usada, ou foi alterada | reabra a policy JSON da SCP | `describe-policy` na SCP anexada à OU | corrigir a SCP; tratar como falha da fronteira, não do runbook |
A pergunta que resolve metade destes casos
Antes de mexer em parâmetro, pergunte: a trilha realmente cobre esta conta? Metade dos sintomas de "a consulta não achou nada" é trilha incompleta, não filtro errado — e as duas produzem exatamente o mesmo resultado vazio na tela.
Limpeza: o que o destroy não leva, e o que não deve levar
Este laboratório acrescenta pouco recurso de custo contínuo, mas um deles é DELIBERADAMENTE resistente a apagar.
# limpar.sh — o que este laboratorio acrescenta, e o que o destroy nao leva.
set -euo pipefail
# 1. Derrube o que o Terraform administra.
terraform destroy -auto-approve
# ATENCAO: este passo FALHA para o bucket de log se ele tiver Object Lock
# ativo e objetos ainda dentro do prazo de retencao — e essa falha e
# proposital, nao um bug. Ela impede apagar evidencia antes da hora.
# 2. Confirme se ainda ha uma politica de contencao residual anexada a algum
# usuario ou papel — ela nega TUDO, e esquece-la la depois do incidente
# resolvido derruba acesso legitimo sem aviso.
aws iam list-user-policies --user-name svc-manutencao \
--query "PolicyNames[?@=='ContencaoDeIncidente']" --output text
aws iam delete-user-policy --user-name svc-manutencao --policy-name ContencaoDeIncidente 2>/dev/null || true
# 3. O bucket de resultados do Athena nao tem Object Lock — pode ser
# esvaziado e removido normalmente.
aws s3 rm s3://cadencia-athena-resultados --recursive
aws s3api delete-bucket --bucket cadencia-athena-resultados
# 4. O bucket de LOG ARCHIVE, se ainda dentro da retencao de 400 dias, NAO
# pode ser esvaziado. Isso nao e uma sobra a limpar: e o Object Lock
# fazendo exatamente o que foi configurado para fazer.
aws s3api get-object-lock-configuration --bucket cadencia-log-archive-111122223333 || true
# 5. Prova final: nada com nome do projeto de pe, exceto o que a retencao prende.
aws resourcegroupstaggingapi get-resources \
--tag-filters Key=Projeto,Values=cadencia-resposta-incidente \
--query "ResourceTagMappingList[].ResourceARN" --output table
| Recurso | Sai no destroy? | Cobra parado? | Por que fica |
|---|---|---|---|
| Trilha organizacional e regra do EventBridge | sim | não | sem custo direto relevante |
| Bucket de resultados do Athena | sim | sim, GB-mês até remover | sem Object Lock — pode ser esvaziado normalmente |
| Bucket de Log Archive (dentro da retenção) | NÃO — o destroy falha de propósito | sim, GB-mês | Object Lock em modo GOVERNANCE impede exclusão antes do prazo — é a feature funcionando, não uma sobra a corrigir |
| Catálogo do Glue e workgroup do Athena | sim | não, ou centavos por metadado | sem custo relevante parado |
| Papel de resposta a incidente | sim | não | sem custo — mas confira se não ficou uma política de contenção residual anexada a outro principal |
| Política `ContencaoDeIncidente` residual | NÃO é gerenciada pelo Terraform | não cobra, mas nega acesso legítimo se esquecida | foi criada pelo runbook em tempo de incidente, não pelo `apply` — remover manualmente |
Resumo: problema, peça e motivo
| Problema | Peça | Por que ela, e não outra |
|---|---|---|
| Não sei o que a credencial já fez | Athena sobre CloudTrail com projeção de partição | reconstrói por consulta, em segundos, sem varrer o bucket inteiro |
| Desativar a chave não basta | Deny por `aws:TokenIssueTime` | fecha a sessão temporária já emitida, que a chave sozinha não alcança |
| A investigação vira palpite sem trilha centralizada | trilha organizacional única | uma consulta cobre toda conta-membro, sem depender de disciplina local |
| Esperar entender tudo antes de agir | runbook que conta primeiro, investiga depois | fecha a exposição em segundos em vez de horas |
| Evidência perdida por decisão apressada | desativar, nunca deletar | preserva o que a investigação e uma apuração formal podem precisar |
| Sessão derivada de `AssumeRole` escapa da consulta | filtro por `accessKeyId`, seguido de segunda consulta | segue a cadeia de credencial em vez de parar na primeira |
| Fronteira de conta nunca testada sob incidente real | consulta confirmando zero sucesso na conta de produção | transforma a SCP do L43 de afirmação em fato verificável |
| Runbook que não funciona sob pressão | game day periódico | ensaiar antes é o que faz o passo escrito virar reflexo executado |
| Falha | O que a protege | O que ela NÃO protege |
|---|---|---|
| Uso novo da chave comprometida | desativação (`UpdateAccessKey`) | sessão já emitida por essa chave antes da contenção |
| Sessão temporária já emitida | Deny por `aws:TokenIssueTime` | uso da própria chave de longa duração — precisa das duas |
| Raio de alcance mal medido | consulta por `accessKeyId` seguindo `AssumeRole` | credencial que nunca chegou a assumir outra role e usou outro identificador de origem |
| Alcance além da conta de origem | SCP da OU de produção (L43) | ação dentro da própria conta comprometida — a SCP não substitui menor privilégio local |
| Evidência apagada por engano | Object Lock em modo GOVERNANCE | evidência nunca capturada porque a trilha não cobria a conta |
| Incidente futuro pela mesma causa | nada, neste módulo | a chave estática que originou tudo continua existindo até L41/L42 tratarem a causa raiz |
- O achado chega (L48) e a regra do EventBridge abre o runbook — o relógio começa aqui.
- O runbook desativa a chave comprometida: contenção 1.
- O runbook anexa a política de Deny por `aws:TokenIssueTime` ao mesmo principal: contenção 2.
- A trilha organizacional já entregava, de toda conta-membro, no bucket central de log.
- A consulta salva no Athena roda sobre o catálogo com projeção de partição, filtrada pela accessKeyId.
- O resultado lista cada chamada, com conta, horário e, se houver, a role assumida.
- Se houve `AssumeRole`, uma segunda consulta segue a credencial temporária resultante.
- A consulta confirma se alguma chamada chegou à conta de produção — e o esperado é zero.
- A timeline cronometrada fecha o ticket e alimenta o relatório pós-incidente.
- O próximo game day já está agendado, para o runbook continuar sendo reflexo, não teoria.
Perguntas frequentes
❓ Desativar uma chave de acesso AWS comprometida já é suficiente para conter o incidente?
❓ Como reconstruir todas as ações que uma credencial comprometida realizou na AWS?
❓ Por que conter antes de terminar a investigação, e não o contrário?
❓ O que a fronteira de conta (SCP) realmente protege num incidente de credencial vazada?
❓ Qual a diferença entre desativar e apagar uma chave de acesso comprometida?
❓ Uma consulta no Athena sobre CloudTrail pode ficar cara numa organização grande?
❓ Runbook de resposta a incidente nunca ensaiado funciona sob pressão real?
❓ Preciso do Amazon Detective para investigar uma credencial vazada?
Fixando
Durante um incidente, o time decide esperar a consulta completa ao CloudTrail terminar antes de desativar a chave comprometida, para "não agir sem saber o alcance real". Qual é a consequência mais provável dessa ordem?
Uma consulta ao CloudTrail filtrada por `useridentity.username` retorna 8 chamadas para uma credencial comprometida. A mesma consulta, filtrada por `useridentity.accesskeyid`, também retorna 8 chamadas — mas uma delas é um `AssumeRole` bem-sucedido. O que essa coincidência sozinha NÃO garante?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L43 (fronteira de conta) e L48 (o achado que abre o runbook); Terraform e AWS CLI avançados; C#/.NET 8 |
| Conhecimentos adquiridos | reconstrução de raio de alcance por CloudTrail e Athena; a diferença entre desativar chave e revogar sessão; ordem de contenção antes de investigação; projeção de partição para consulta barata; verificação empírica de fronteira de conta sob incidente real |
| Limitação que fica | o runbook não cobre como a credencial vazou, nem decide sozinho se um achado é falso positivo — as duas coisas continuam exigindo julgamento humano |
| Próximo exemplo recomendado | L97 — risco e conformidade de decisão automatizada, que formaliza a trilha imutável de decisão que este módulo só começa a construir com Object Lock |
| Também habilitado por este módulo | qualquer laboratório futuro que precise provar, com dado e não com afirmação, que uma fronteira de segurança se sustentou sob incidente real |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: Revoke IAM role temporary security credentials — o mecanismo de `AWSRevokeOlderSessions` e a política de Deny condicionada a `aws:TokenIssueTime`, que é a base da contenção 2; e Create the table for CloudTrail logs in Athena using partition projection — a DDL de referência para a tabela com projeção de partição. As fases de resposta a incidente seguem o padrão amplamente adotado pela indústria de segurança (NIST SP 800-61), não uma publicação da AWS; ele foi adaptado aqui às duas contenções específicas de credencial.
O que não foi verificado, e você deve conferir na sua conta
Os tempos citados nas provas — 130 min no cenário ingênuo, poucos segundos no runbook — vêm de uma simulação de mesa da FFV Academy, com conta de teste e volume pequeno de eventos; sirva-os como ordem de grandeza de por que a ORDEM das ações importa, não como benchmark. A duração máxima de uma sessão temporária depende da configuração de sessão do seu ambiente — meça o teto real antes de assumir que a contenção 2 tem prazo de validade previsível. E a janela de propagação de ~30s citada para a revogação de sessão é a documentada pela AWS para papéis; para usuário IAM, teste no seu ambiente antes de confiar no mesmo número.
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…