Lab 46 — Criptografia: KMS, envelope, CMK e rotação
O problema, e a empresa que o tem
A Cadência atravessou uma auditoria de um cliente enterprise para fechar contrato. Uma das perguntas do questionário de segurança era simples de escrever e impossível de responder direito: "qual chave criptográfica protege a tabela de clientes, e quem pode usá-la?". A resposta que o time deu — "está tudo criptografado, é padrão da AWS" — não satisfez o auditor, e o motivo técnico só ficou claro depois: todo RDS, todo S3 e todo Secrets Manager da conta usa a MESMA chave por serviço, a gerenciada pela AWS, compartilhada com qualquer outro produto que a empresa venha a rodar na mesma conta.
O segredo do banco já saiu do código no L04, e a aplicação já usa identidade de workload em vez de chave estática desde o L42. Este laboratório não adiciona criptografia — ela já existia nos dois. Ele troca QUEM controla a chave que a faz funcionar, e mede a diferença que isso faz numa auditoria de verdade.
Há uma segunda motivação, mais concreta que compliance: o campo CPF do cliente vive numa tabela que qualquer credencial de leitura do Postgres consegue consultar em texto claro — a criptografia de armazenamento do RDS é transparente para conexão autenticada. Uma credencial vazada, ou uma injeção de SQL bem-sucedida, lê o CPF normalmente, mesmo com o volume inteiro cifrado. Fechar esse caminho específico exige cifrar o campo no código, não só no disco.
O que este laboratório NÃO é
Não é classificação automática de dado sensível em escala, nem minimização de log — isso é o L49, e ele depende deste. Também não é controle de acesso por coluna dentro do lake (L69). Aqui a pergunta é mais estreita: dado o dado JÁ classificado como sensível, qual chave o protege, quem pode usá-la, e como isso se prova.
O que você vai conseguir fazer
Objetivos verificáveis: cada um se prova com um comando na seção de implantação, não com a sensação de ter entendido a diferença entre os dois tipos de chave.
- Explicar por que "está criptografado" com a chave gerenciada pela AWS não responde a uma pergunta de auditoria por recurso.
- Nomear a diferença de POLÍTICA entre chave gerenciada pela AWS e CMK, e por que só a segunda funciona como uma trava independente do IAM.
- Descrever a criptografia de envelope com precisão: o papel da chave de dados efêmera e o papel da CMK, e por que o segundo nunca cifra o payload diretamente.
- Configurar a política de uma CMK restringindo o uso a um principal específico via `kms:ViaService`, e provar que isso bloqueia uso fora do serviço esperado.
- Habilitar rotação automática e explicar, com precisão, o que ela troca e o que preserva.
- Cifrar um campo sensível na aplicação com envelope explícito, usando contexto de cifragem para amarrar o ciphertext ao registro certo.
- Provar, com CloudTrail, que o uso de cada CMK é filtrável isoladamente por ARN de chave.
- Diagnosticar os dois defeitos mais comuns de política de chave: falta de `kms:ViaService` e ausência do principal raiz.
O que a certificação cobra disto
| Conceito | Certificação | Como aparece aqui | O que dominar |
|---|---|---|---|
| Criptografia de envelope | SAA-C03, DVA-C02, SCS-C02 | chave de dados efêmera cifra o payload; a CMK só cifra a chave de dados | por que isso permite cifrar objetos grandes sem chamar o KMS por byte |
| CMK vs chave gerenciada pela AWS | SAA-C03, SCS-C02 | política própria e nomeada vs política fixada pelo serviço | que a chave gerenciada concede uso a qualquer principal já autorizado no serviço |
| Política de chave e IAM: interseção | SAA-C03, SCS-C02 | as duas políticas têm de concordar; nenhuma isolada basta | a mesma lógica de SCP+IAM, aplicada a `kms:` em vez de organização |
| `kms:ViaService` | SAA-C03, DVA-C02, SCS-C02 | restringe o uso da chave a um serviço e endpoint regional específico | que a condição bloqueia chamada direta fora do fluxo do serviço nomeado |
| Rotação de chave | SAA-C03, SCS-C02 | automática (CMK, opcional) vs obrigatória (chave gerenciada, anual, fixa) | que o KeyId nunca muda e o dado antigo continua decifrável após rotacionar |
| Exclusão de chave | SAA-C03, SCS-C02 | janela de espera cancelável, e o que acontece depois dela | que é a única operação deste catálogo verdadeiramente irreversível |
| Encryption context | SCS-C02 | campo extra que amarra o ciphertext a um contexto, verificado na decifragem | que não é segredo — é autenticação adicional, e aparece em claro no CloudTrail |
Onde isto costuma ser cobrado errado
A pergunta clássica descreve uma política de Secrets Manager correta, com `GetSecretValue` liberado, e a leitura falha com acesso negado mesmo assim. A causa não está na política do segredo — está faltando `kms:Decrypt` na política ou no IAM do principal, porque ler um segredo cifrado por CMK exige as DUAS permissões, nunca só uma. Com a chave gerenciada pela AWS essa pegadinha não aparece, porque a política dela já concede o uso pelo serviço.
Requisitos, e como cada um muda o desenho
Requisito não funcional sem marca no desenho é intenção. A coluna da direita é onde cada um vira Terraform.
| Requisito | Valor declarado | O que ele decide no desenho |
|---|---|---|
| Auditoria de uso por classe de dado | obrigatória, por classe | obriga CMK própria em vez da chave gerenciada — só ela tem trilha filtrável por ARN |
| Isolamento entre classes de dado | dado de cliente ≠ documento fiscal | duas CMKs, não uma — cada classe com blast radius próprio |
| Quem pode decifrar | só a task role da API, por serviço | política de chave com principal nomeado e `kms:ViaService`, não `Principal: "*"` |
| Proteção contra credencial de leitura vazada | para o CPF, especificamente | envelope explícito no código, além da criptografia de armazenamento do RDS |
| Rotação de material criptográfico | anual, auditável | rotação automática habilitada na CMK, com prova via `get-key-rotation-status` |
| Recuperação de exclusão acidental | janela mínima | não deletar a chave: agendar com `deletion_window_in_days`, alarme em `ScheduleKeyDeletion` |
| Custo de API previsível em alta escala | sem surpresa na fatura | S3 Bucket Keys habilitado; cache de chave de dados considerado para cifra de campo em volume |
Arquitetura mínima: tudo cifrado, e nenhuma pergunta com resposta isolada
Este é o estado atual da Cadência, e ele não é negligente: cada serviço está cifrado por padrão, sem que ninguém tenha decidido nada. É exatamente essa ausência de decisão que o laboratório resolve — não a ausência de criptografia.
- → lê e grava o pedido do cliente
- → lê a credencial de conexão do Postgres
- → grava o PDF da nota fiscal
- → Decrypt do volume, chave aws/rds
- → Decrypt do volume, a MESMA chave aws/rds
- → GenerateDataKey por objeto, chave aws/s3
- → Decrypt da chave de dados, chave aws/secretsmanager
- Compute
- Banco de dados
- Armazenamento
- Segurança e identidade
Toda peça deste desenho está cifrada — nenhuma exceção — e mesmo assim nenhuma pergunta de auditoria por recurso tem resposta isolada. A chave que protege o banco da Cadência é a MESMA que protege o de qualquer outro produto da mesma conta, porque "criptografado" aqui significa "a chave padrão do serviço", não uma decisão sua. Percorra os passos: o defeito não é a ausência de criptografia, é a ausência de uma segunda trava independente dela.
- Três serviços, três "está criptografado". O app fala com o banco, o segredo e o S3, e os três respondem "cifrado" se alguém perguntar. É verdade e é uma afirmação vazia: nenhuma delas diz COM QUAL chave, nem quem mais usa essa mesma chave.
- A mesma chave protege o banco da Cadência e o de outro produto. A chave gerenciada pela AWS é por CONTA e por SERVIÇO, não por recurso. Todo RDS da conta que não aponta uma CMK própria usa `aws/rds` — o mesmo objeto de chave, com a mesma política, para dados que não têm nada em comum.
- Você não edita a política desta chave. A AWS controla a política de uma chave gerenciada por ela: você pode VER, não pode MUDAR. Não existe como restringir o uso de `aws/rds` a um principal específico, nem como excluir um recurso dela — a chave inteira é tudo ou nada.
- A rotação é obrigatória, e você não escolhe o prazo. Chave gerenciada pela AWS rotaciona automaticamente a cada ano, sem opção de desligar nem de customizar o período. Não é falha de configuração — é assim que ela funciona, e é a única alavanca que você não tem aqui.
- A trava dupla nunca foi dupla. A política de `aws/secretsmanager` já concede uso a qualquer principal que a IAM autorizou a chamar o Secrets Manager. Então "precisa de permissão no segredo E na chave" colapsa em UMA trava só: quem passa pela primeira já passa pela segunda, porque as duas são a mesma decisão vista de dois lugares.
- Por que ninguém trocou isso antes. Porque funciona, é gratuito e não pede uma linha de Terraform a mais. O defeito só aparece quando alguém de fora — um auditor, um cliente enterprise — pergunta "qual chave protege ESTA tabela, separada de todo o resto da conta" e a resposta honesta é "nenhuma, é a mesma de tudo".
A trava dupla nunca foi dupla
A política da chave gerenciada pela AWS já concede uso a QUALQUER principal que a IAM autorizou a chamar aquele serviço. Então "preciso de permissão no recurso E na chave" colapsa numa trava só: quem passa pela permissão de serviço já passa pela da chave, porque as duas são avaliações da MESMA decisão vista de dois lugares diferentes. Isso não é uma falha de configuração — é o comportamento documentado da chave gerenciada, e é exatamente o motivo por que ela não serve como segunda camada de controle.
Arquitetura para produção
Cada peça nova abaixo rastreia a uma linha da tabela de requisitos. A criptografia em si — o algoritmo, o AES-256, o envelope — é a mesma dos dois desenhos. O que muda é quem escreve a política, quem ela nomeia, e para onde a trilha de uso aponta.
- → lê e grava o pedido do cliente
- → lê a credencial de conexão do Postgres
- → grava o PDF da nota fiscal
- → Decrypt/GenerateDataKey no armazenamento do cluster
- → Decrypt da chave de dados do segredo
- → GenerateDataKey por objeto gravado com SSE-KMS
- → Decrypt e GenerateDataKey, com o KeyId da CMK
- → Decrypt e GenerateDataKey, com o KeyId da CMK
- Compute
- Banco de dados
- Segurança e identidade
- Armazenamento
- Gestão e governança
A troca não foi "ligar criptografia" — já estava ligada nos dois desenhos. A troca foi dar a cada classe sensível uma chave com política PRÓPRIA, que nomeia o principal exato em vez de confiar em quem a IAM já deixa passar, e uma trilha de uso que finalmente responde à pergunta que o desenho anterior não respondia: quem usou ESTA chave, e quando. Repare que documento e dado de cliente foram para chaves DIFERENTES — vazar acesso a uma não expõe a outra.
- Duas classes, duas chaves, dois blast radius. O dado de cliente (banco + o segredo que o abre) e o documento fiscal são classes de sensibilidade distintas, então cada uma recebe sua própria CMK. Comprometer a política ou o material de uma não dá acesso nenhum à outra — era exatamente essa separação que a chave única do desenho anterior não tinha.
- O segredo do banco entra no perímetro do banco, não do documento. Segredo e dado de cliente respondem à mesma pergunta de identidade — "quem é este cliente e como falo com o banco dele" — então compartilham a MESMA CMK por decisão de classificação, não por economia de chave.
- A política de chave nomeia o principal, não "qualquer um do serviço". A política da CMK lista o ARN da task role e exige `kms:ViaService`. Isso é a segunda trava que a chave gerenciada nunca teve: mesmo um principal com permissão de RDS na IAM não decifra nada aqui se não estiver nomeado ali.
- Rotação troca o material, não o ID. Com rotação automática ligada, a CMK troca a cada ~365 dias (customizável) o material criptográfico por trás do KeyId — e o KeyId, o que o código referencia, nunca muda. Dado cifrado com material antigo continua decifrável sem ação nenhuma.
- A trilha de uso passa a ser filtrável por chave. Antes, perguntar "quem usou a chave dos dados de cliente" não tinha resposta isolada, porque a chave era de toda a conta. Agora o CloudTrail filtra por ARN de CMK, e todo evento que aparece ali é, por construção da política, do principal nomeado — nenhum outro consegue gerar esse evento.
- A criptografia em si não mudou — o controle, sim. Os bytes do volume do RDS e do objeto do S3 são cifrados com AES-256 nos dois desenhos, pelo mesmo mecanismo de envelope. O que este desenho compra não é "mais cifrado" — é uma política que você escreve, um principal que você nomeia e uma trilha que você filtra.
A prova de que a política realmente restringe
Com `kms:ViaService`, um principal com permissão ampla de KMS na IAM — inclusive a própria task role — não consegue chamar `kms:Decrypt` diretamente na CMK fora do Secrets Manager ou do S3. A restrição vive na política da CHAVE, não depende de o IAM do chamador estar bem escrito. É a diferença entre confiar em uma política e confiar em duas que precisam concordar.
O caminho de uma nota fiscal, e o envelope nas duas pontas
A criptografia de envelope não é um conceito abstrato aqui: é o que acontece, chamada por chamada, todo `PutObject` e todo `GetObject` do bucket de notas fiscais. O S3 nunca manda o arquivo inteiro para o KMS cifrar — ele pede uma chave, cifra localmente, e descarta a chave em claro assim que termina.
Por que a chave de dados existe, e por que ela nunca é a CMK
A operação de cifra direta do KMS aceita um payload pequeno — a ordem de poucos kilobytes, insuficiente para um objeto do S3 ou um arquivo de banco (confira o limite atual na referência da API antes de decidir com precisão o seu caso). Chamar o KMS uma vez por byte também não escalaria em latência. O envelope resolve isso: UMA chamada gera uma chave de dados, e a cifragem do payload inteiro acontece localmente, com AES. A CMK nunca toca no payload — ela só cifra e decifra a chave de dados, que é pequena por natureza.
A cópia em texto claro da chave de dados existe fora do KMS por um instante — o tempo de ela viajar de volta ao chamador pela conexão TLS e ser usada localmente — e depois é descartada. O que persiste ao lado do dado cifrado é só a cópia CIFRADA pela CMK. É por isso que o material da CMK em si "nunca sai da AWS": ele nunca precisa sair, porque quem sai e volta é a chave de dados, uma vez por objeto.
O que aparece no CloudTrail para a chamada do passo 2. Repare em duas coisas: o KeyId identifica QUAL CMK, e nao ha material de chave nenhum no evento — nem cifrado, nem em claro. O log prova USO, nunca expõe a chave.
{
"eventSource": "kms.amazonaws.com",
"eventName": "GenerateDataKey",
"awsRegion": "us-east-1",
"userIdentity": {
"type": "AWSService",
"invokedBy": "s3.amazonaws.com"
},
"requestParameters": {
"keyId": "arn:aws:kms:us-east-1:111122223333:key/1234abcd-clientes",
"keySpec": "AES_256",
"encryptionContext": {
"aws:s3:arn": "arn:aws:s3:::ffv-lab-notas-fiscais/2026/08/nf-000123.pdf"
}
},
"responseElements": null
}As decisões, e o que se perde em cada uma
📋 A Cadência precisa decidir o escopo da mudança: trocar TODAS as chaves gerenciadas pela AWS por CMKs próprias, em toda a conta, ou só onde a classificação de dado exige — dado de cliente (CPF, endereço) e o segredo que abre esse banco.
Cada CMK cobra mensalidade e pede manutenção de política: aplicar isso em recurso sem dado sensível é custo sem controle correspondente a proteger. O requisito real da Cadência — responder "qual chave protege este dado, separado do resto" — só existe onde há dado que justifica a pergunta. Classificar primeiro e depois isolar por classe entrega o mesmo controle com uma fração das chaves.
Alt: CMK única compartilhada entre RDS e S3 — Mistura duas classes de dado com perfil de risco diferente num único blast radius, contradizendo o motivo de ter chave própria: se o objetivo é isolar, uma chave para tudo isola de tudo, menos entre si.
Alt: Deixar tudo na chave gerenciada e confiar só no IAM — É exatamente o estado que gerou a pergunta do auditor sem resposta. A IAM decide o que um principal pode fazer; sem política de chave própria, não existe segunda camada — é o desenho mínimo deste laboratório.
Alt: Material de chave importado (BYOK) — Resolve uma exigência regulatória específica de controlar o material fora da geração da AWS, mas soma a operação de gerenciar um HSM externo e rotação manual — sobra aqui, porque a Cadência não tem esse requisito declarado.
Alt: Chave multi-região desde o início — Só se justifica com um requisito de disaster recovery entre regiões que este laboratório não tem. Adicionar sincronização de material entre regiões sem usar a réplica é complexidade sem retorno.
| Decisão | Escolha | Alternativas | Motivo | O que se perde |
|---|---|---|---|---|
| Escopo da mudança | CMK só nas classes sensíveis | CMK em toda a conta; manter tudo na chave gerenciada | controle sem pagar mensalidade por chave onde não há dado que a justifique | recurso novo criado sem classificação explícita nasce na chave padrão, por omissão |
| Separação de CMK por classe | clientes ≠ documentos, duas chaves | uma CMK única para tudo sensível | blast radius separado: comprometer uma política não expõe a outra classe | duas políticas para manter em vez de uma; duas mensalidades |
| Restrição de uso | `kms:ViaService` + principal nomeado | só o principal, sem `ViaService`; `Principal: "*"` com condição de conta | fecha o caminho de chamar o KMS diretamente com a mesma credencial de serviço | toda vez que um novo serviço legítimo precisar da chave, a condição tem que crescer |
| Rotação | automática, período padrão | manual; sob demanda apenas quando exigido | não depende de alguém lembrar; roda sozinha e é auditável | mensalidade adicional na primeira e segunda rotação mantida (documentado pela AWS) |
| Proteção do CPF | envelope explícito no código | confiar só na criptografia de armazenamento do RDS | fecha o gap de credencial vazada ou SQL injection lendo o campo em claro | toda leitura do campo custa uma chamada ao KMS a mais, e o código fica mais complexo |
A dívida que o envelope de campo não paga sozinho
Cifrar o CPF na aplicação protege contra leitura direta da tabela. Não protege contra um relatório, um log de depuração ou uma exportação que capturem o valor DEPOIS de a aplicação já o ter decifrado legitimamente — nesse ponto o dado está em claro na memória do processo, como qualquer variável. Minimizar onde o valor decifrado circula (nunca logar, nunca exportar em claro) é do L49, e ele depende deste.
Construir: as duas CMKs, com política que nomeia o principal
A política é o coração deste laboratório — mais até que o `enable_key_rotation`. É ela que transforma a chave de "mais uma configuração cifrada" em uma trava independente.
# kms.tf — duas CMKs, uma por classe de dado, com politica propria
# ── CMK 1: dado de cliente (RDS + o segredo que abre o banco) ────────────────
resource "aws_kms_key" "clientes" {
description = "Cadencia — dado de cliente: RDS de pedidos e o segredo do Postgres"
# Janela de espera antes da exclusao FINAL, se um dia a chave for agendada
# para exclusao. Enquanto a janela corre, a operacao e CANCELAVEL; depois
# dela, nao. Ver a secao de Seguranca sobre por que isto e o unico risco
# verdadeiramente irreversivel deste laboratorio.
deletion_window_in_days = 30
# Rotacao automatica, periodo padrao de 365 dias a partir de quando foi
# HABILITADA (nao da criacao da chave). NAO VERIFICADO nesta versao do
# provider: se `rotation_period_in_days` esta disponivel no seu provider
# para customizar o periodo, confira a documentacao do provider antes de
# usar — aqui fica no padrao.
enable_key_rotation = true
policy = data.aws_iam_policy_document.chave_clientes.json
tags = var.tags
}
resource "aws_kms_alias" "clientes" {
name = "alias/${var.projeto}-clientes"
target_key_id = aws_kms_key.clientes.key_id
}
# A politica e um documento SEPARADO da politica IAM de quem usa a chave — os
# dois precisam concordar (interseccao, nao uniao), mesma logica de SCP+IAM do
# L43. Omitir o principal raiz da conta e o erro que mais custa: sem ele, uma
# politica escrita errado pode tirar de TODO MUNDO — inclusive de quem
# administra a conta — a capacidade de corrigir a propria politica da chave.
data "aws_iam_policy_document" "chave_clientes" {
statement {
sid = "PermiteAContaRaizAdministrarComoUltimoRecurso"
effect = "Allow"
actions = ["kms:*"]
resources = ["*"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${data.aws_caller_identity.atual.account_id}:root"]
}
# "kms:*" e "*" aqui SO se justificam porque o principal e a conta raiz e
# o efeito e a rede de seguranca contra lockout — nao e o padrao de acesso
# do dia a dia, que vem nos statements abaixo, restritos por proposito.
}
statement {
sid = "PermiteAdministradoresDeChaveGerenciarem"
effect = "Allow"
actions = [
"kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*",
"kms:Put*", "kms:Update*", "kms:Revoke*", "kms:Disable*",
"kms:Get*", "kms:Delete*", "kms:TagResource", "kms:UntagResource",
"kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion", "kms:CreateGrant",
]
resources = ["*"]
principals {
type = "AWS"
identifiers = [var.role_administrador_arn]
}
# CreateGrant entra aqui porque e o principal do PIPELINE (quem roda o
# terraform apply) quem precisa dele: e essa identidade que chama
# CreateDBInstance, e o RDS usa a permissao de QUEM CHAMOU para criar a
# concessao automatica que ele mesmo vai usar depois. A task role da
# aplicacao nunca chama kms:CreateGrant.
}
statement {
sid = "PermiteTaskRoleUsarViaSecretsManager"
effect = "Allow"
actions = ["kms:Decrypt", "kms:DescribeKey"]
resources = ["*"]
principals {
type = "AWS"
identifiers = [aws_iam_role.task.arn]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["secretsmanager.${var.regiao}.amazonaws.com"]
}
# Esta e a trava que a chave gerenciada pela AWS nunca teve: o principal e
# NOMEADO (o ARN da task role, nao "qualquer um que a IAM deixar"), e o
# uso e restrito a UM servico. Chamar kms:Decrypt fora do Secrets Manager
# com a mesma credencial e negado por esta politica, mesmo que a IAM do
# principal permita a acao.
}
}
# ── CMK 2: documentos fiscais (S3) — classe de dado diferente ────────────────
resource "aws_kms_key" "documentos" {
description = "Cadencia — documentos fiscais em PDF no S3"
deletion_window_in_days = 30
enable_key_rotation = true
policy = data.aws_iam_policy_document.chave_documentos.json
tags = var.tags
}
resource "aws_kms_alias" "documentos" {
name = "alias/${var.projeto}-documentos"
target_key_id = aws_kms_key.documentos.key_id
}
data "aws_iam_policy_document" "chave_documentos" {
statement {
sid = "PermiteAContaRaizAdministrarComoUltimoRecurso"
effect = "Allow"
actions = ["kms:*"]
resources = ["*"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${data.aws_caller_identity.atual.account_id}:root"]
}
}
statement {
sid = "PermiteTaskRoleGravarELerViaS3"
effect = "Allow"
actions = ["kms:GenerateDataKey", "kms:Decrypt", "kms:DescribeKey"]
resources = ["*"]
principals {
type = "AWS"
identifiers = [aws_iam_role.task.arn]
}
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = ["s3.${var.regiao}.amazonaws.com"]
}
# GenerateDataKey entra porque quem GRAVA precisa pedir a chave de dados;
# Decrypt entra porque quem LE precisa decifra-la de volta. Omitir
# GenerateDataKey e o defeito mais comum desta politica — ver a secao de
# "Quebrar de proposito".
}
}
data "aws_caller_identity" "atual" {}
A operação que pode trancar a própria chave para sempre
Se a política de uma CMK não incluir um statement que autorize a conta raiz (ou um administrador nomeado com `kms:PutKeyPolicy`), e a política aplicada não permitir mais ninguém corrigi-la, a chave fica permanentemente sem administrador — nem o dono da conta consegue mudar a política de volta pelo caminho normal. Diferente da exclusão (que tem janela e é cancelável), um lockout de política pode exigir abrir um caso de suporte com a AWS, sem garantia de reversão simples. É por isso que o statement da conta raiz vem primeiro no documento, sempre, mesmo parecendo redundante.
O alias é um ponteiro mutável — o código deveria confiar nisso, com cautela
Referenciar `alias/${var.projeto}-clientes` em vez do ARN da chave permite trocar qual CMK física está por trás do nome sem alterar código nem configuração — útil para uma troca planejada de chave. O outro lado da mesma moeda: quem tem permissão de `kms:UpdateAlias` pode redirecionar silenciosamente qual chave uma aplicação usa. A política de quem administra alias merece a mesma atenção que a política da própria CMK.
Construir: RDS e o segredo apontando para a CMK de clientes
A mudança no recurso do banco é uma linha — `kms_key_id`. O controle de acesso inteiro vive na política que a seção anterior escreveu, não aqui.
# rds.tf e segredo.tf — apontando para a CMK de clientes, nao mais para aws/rds
resource "aws_db_instance" "principal" {
identifier = "${var.projeto}-pedidos"
engine = "postgres"
engine_version = "16"
instance_class = "db.t4g.medium"
# Antes deste laboratorio: sem `kms_key_id`, o RDS usava `aws/rds`. A unica
# mudanca de configuracao no recurso do banco em si e esta linha — o resto
# do controle de acesso vive na POLITICA da chave, nao aqui.
storage_encrypted = true
kms_key_id = aws_kms_key.clientes.arn
db_name = "cadencia"
username = "app_admin"
# A senha inicial NAO fica em variavel nem em environment: o Secrets
# Manager gera e guarda, e o segredo abaixo referencia este banco.
manage_master_user_password = false
password = random_password.senha_inicial.result
db_subnet_group_name = aws_db_subnet_group.principal.name
vpc_security_group_ids = [aws_security_group.db.id]
skip_final_snapshot = false
final_snapshot_identifier = "${var.projeto}-pedidos-final"
# O snapshot final HERDA a criptografia do banco de origem — continua sob a
# cmk_clientes, e continua contando para a fatura depois do destroy. Ver a
# secao de Limpeza.
tags = var.tags
}
resource "random_password" "senha_inicial" {
length = 32
special = false # o driver do Postgres deste laboratorio nao escapa alguns especiais
}
# O segredo do L04, agora explicitamente sob a CMK de clientes em vez da
# chave gerenciada padrao. E o mesmo objeto, so a chave que o protege mudou.
resource "aws_secretsmanager_secret" "banco" {
name = "${var.projeto}/banco/app-rw"
kms_key_id = aws_kms_key.clientes.arn
tags = var.tags
}
resource "aws_secretsmanager_secret_version" "banco" {
secret_id = aws_secretsmanager_secret.banco.id
secret_string = jsonencode({
host = aws_db_instance.principal.address
port = 5432
dbname = aws_db_instance.principal.db_name
username = aws_db_instance.principal.username
password = random_password.senha_inicial.result
})
}
O engano mais comum ao dar permissão de KMS para a task role
É tentador dar à task role `kms:Decrypt` "para o RDS", porque parece simétrico ao que se fez para o Secrets Manager. Mas a task role NUNCA chama o KMS para ler o banco: a decifragem do volume acontece na camada de armazenamento do RDS, usando uma concessão (grant) que o próprio serviço gerencia. Quem precisa de `kms:CreateGrant` na política da chave é o principal que CRIA o banco — o pipeline ou quem roda `terraform apply` — não quem consulta depois. Dar essa permissão à task role não quebra nada, mas é superfície de acesso que não corresponde a nenhum uso real: uma auditoria de menor privilégio vai perguntar por quê.
Construir: S3 com SSE-KMS, Bucket Keys e política que recusa a chave errada
Além de apontar para a CMK certa, o bucket recusa upload que declare outra chave — porque "criptografado por padrão" só vale enquanto ninguém enviar um cabeçalho diferente.
# s3.tf — SSE-KMS com a CMK de documentos, e Bucket Keys ligado
resource "aws_s3_bucket" "notas_fiscais" {
bucket = "${var.projeto}-notas-fiscais-${data.aws_caller_identity.atual.account_id}"
tags = var.tags
}
resource "aws_s3_bucket_server_side_encryption_configuration" "notas_fiscais" {
bucket = aws_s3_bucket.notas_fiscais.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.documentos.arn
}
# Bucket Keys: o S3 gera UMA chave de dados por bucket (renovada
# periodicamente) em vez de uma por objeto, e a reusa para varios
# PutObject/GetObject. Isso corta a maior parte das chamadas ao KMS —
# e e a linha que mais separa "poucos reais" de "linha visivel na
# fatura" em alta escala. Ver Custo.
bucket_key_enabled = true
}
}
resource "aws_s3_bucket_public_access_block" "notas_fiscais" {
bucket = aws_s3_bucket.notas_fiscais.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Recusa qualquer PutObject que nao declare SSE-KMS com ESTA chave. Sem esta
# politica, um upload sem cabecalho de criptografia caberia na configuracao
# "por padrao" mesmo assim — mas um cliente que envia SSE-S3 explicitamente,
# ou outra CMK, passaria por cima da intencao deste bucket.
data "aws_iam_policy_document" "bucket_notas_fiscais" {
statement {
sid = "NegaUploadForaDaCmkDeDocumentos"
effect = "Deny"
principals {
type = "*"
identifiers = ["*"]
}
actions = ["s3:PutObject"]
resources = ["${aws_s3_bucket.notas_fiscais.arn}/*"]
condition {
test = "StringNotEquals"
variable = "s3:x-amz-server-side-encryption-aws-kms-key-id"
values = [aws_kms_key.documentos.arn]
}
}
}
resource "aws_s3_bucket_policy" "notas_fiscais" {
bucket = aws_s3_bucket.notas_fiscais.id
policy = data.aws_iam_policy_document.bucket_notas_fiscais.json
}
Bucket Keys é a alavanca de custo deste módulo, não um detalhe
Sem Bucket Keys, cada objeto gravado ou lido gera uma chamada nova ao KMS. Com Bucket Keys, o S3 mantém uma chave de dados no nível do BUCKET por um tempo e a reaproveita entre operações, cortando a maior parte dessas chamadas. A criptografia continua sendo por objeto — cada objeto tem sua própria chave de dados derivada — só a chamada ao KMS que deixa de se repetir a cada requisição.
Construir: cifra de campo no código — o envelope explícito
Até aqui, o envelope aconteceu por trás da cortina, dentro do RDS e do S3. Esta classe faz a MESMA coisa de propósito, no código, para um campo que precisa de proteção que a criptografia de armazenamento não dá: contra uma credencial de leitura vazada, não só contra o roubo do disco.
// CifraDeCampo.cs — envelope explicito para o CPF, alem da cifra do volume do RDS
//
// A criptografia de armazenamento do RDS e transparente para qualquer conexao
// AUTENTICADA: uma credencial valida — vazada, ou usada por uma injecao de SQL
// bem sucedida — recebe o CPF em texto claro, porque a decifragem ja aconteceu
// antes de o Postgres processar a consulta. Esta classe fecha ESSE gap
// especifico: mesmo com a credencial do banco, quem le a linha sem a CMK de
// clientes nao decifra o campo.
using Amazon.KeyManagementService;
using Amazon.KeyManagementService.Model;
using System.Security.Cryptography;
public sealed class CifraDeCampoCliente
{
private readonly IAmazonKeyManagementService _kms;
private readonly string _cmkClientesArn;
public CifraDeCampoCliente(IAmazonKeyManagementService kms, string cmkClientesArn)
{
_kms = kms;
_cmkClientesArn = cmkClientesArn;
}
// O contexto de cifragem amarra o ciphertext ao REGISTRO. Sem ele, um
// ciphertext de CPF trocado entre duas linhas da mesma tabela decifraria
// sem erro nenhum — o KMS nao tem como saber que aquele valor pertence a
// outro cliente. Com contexto, decifrar com o ID errado falha alto.
private static Dictionary<string, string> ContextoDoRegistro(Guid clienteId) => new()
{
["cliente_id"] = clienteId.ToString(),
["campo"] = "cpf",
};
public async Task<byte[]> CifrarAsync(Guid clienteId, string cpfEmClaro)
{
// Uma chave de dados NOVA por operacao — nunca reaproveitada entre
// registros. E a mesma logica de "uma vez, ou poucas vezes" que
// justifica a rotacao da propria CMK, aplicada aqui ao nivel do campo.
var dataKey = await _kms.GenerateDataKeyAsync(new GenerateDataKeyRequest
{
KeyId = _cmkClientesArn,
KeySpec = DataKeySpec.AES_256,
EncryptionContext = ContextoDoRegistro(clienteId),
});
using var aes = new AesGcm(dataKey.Plaintext.ToArray(), AesGcm.TagByteSizes.MaxSize);
var nonce = RandomNumberGenerator.GetBytes(AesGcm.NonceByteSizes.MaxSize);
var textoClaro = System.Text.Encoding.UTF8.GetBytes(cpfEmClaro);
var ciphertext = new byte[textoClaro.Length];
var tag = new byte[AesGcm.TagByteSizes.MaxSize];
aes.Encrypt(nonce, textoClaro, ciphertext, tag);
// A copia em texto claro da chave de dados so existe ate aqui. Ela
// NUNCA e logada, nunca e persistida — so a copia CIFRADA (o
// CiphertextBlob) segue para o banco, ao lado do CPF cifrado.
Array.Clear(dataKey.Plaintext.ToArray());
// Formato gravado: [tamanho do blob da CMK][blob][nonce][tag][ciphertext].
// Cada linha carrega sua PROPRIA copia cifrada da chave de dados — nao
// ha uma chave de dados global para "o campo CPF" da tabela inteira.
using var saida = new MemoryStream();
using var w = new BinaryWriter(saida);
w.Write(dataKey.CiphertextBlob.ToArray().Length);
w.Write(dataKey.CiphertextBlob.ToArray());
w.Write(nonce);
w.Write(tag);
w.Write(ciphertext);
return saida.ToArray();
}
public async Task<string> DecifrarAsync(Guid clienteId, byte[] registroCifrado)
{
using var leitor = new BinaryReader(new MemoryStream(registroCifrado));
var tamanhoBlob = leitor.ReadInt32();
var blob = leitor.ReadBytes(tamanhoBlob);
var nonce = leitor.ReadBytes(AesGcm.NonceByteSizes.MaxSize);
var tag = leitor.ReadBytes(AesGcm.TagByteSizes.MaxSize);
var ciphertext = leitor.ReadBytes((int)(leitor.BaseStream.Length - leitor.BaseStream.Position));
// O contexto informado aqui tem de ser IDENTICO ao usado na cifragem.
// Se o clienteId nao bate com o registro (linha errada, migracao que
// moveu o valor), o KMS recusa decifrar — e essa recusa e a prova de
// que o contexto realmente amarra o dado ao dono certo.
DecryptResponse chaveDeDados;
try
{
chaveDeDados = await _kms.DecryptAsync(new DecryptRequest
{
CiphertextBlob = new MemoryStream(blob),
EncryptionContext = ContextoDoRegistro(clienteId),
});
}
catch (InvalidCiphertextException)
{
throw new InvalidOperationException(
"contexto de cifragem nao confere: o registro pode pertencer a outro cliente");
}
using var aes = new AesGcm(chaveDeDados.Plaintext.ToArray(), AesGcm.TagByteSizes.MaxSize);
var textoClaro = new byte[ciphertext.Length];
aes.Decrypt(nonce, ciphertext, tag, textoClaro);
Array.Clear(chaveDeDados.Plaintext.ToArray());
return System.Text.Encoding.UTF8.GetString(textoClaro);
}
}
Duas formas de anular o envelope sem perceber
A primeira é logar `dataKey.Plaintext` — mesmo "só para depurar" — porque isso é a chave de dados em claro indo parar num destino que ninguém trata como segredo, e pode ficar retido por meses num agregador de log. A segunda é decifrar SEM contexto de cifragem, ou aceitar qualquer contexto — nesse caso, um ciphertext de CPF trocado entre duas linhas da mesma tabela decifraria sem erro nenhum, porque nada amarra o valor ao dono certo. As duas falhas são silenciosas: nenhuma delas derruba a aplicação nem aparece em teste automatizado que não pense nelas de propósito.
Implantar, e provar cada afirmação com número
#!/usr/bin/env bash
set -euo pipefail
PROJETO="${PROJETO:?defina PROJETO}"
REGIAO="${REGIAO:-us-east-1}"
terraform init -input=false
terraform apply -auto-approve
CMK_CLIENTES=$(terraform output -raw cmk_clientes_arn)
CMK_DOCUMENTOS=$(terraform output -raw cmk_documentos_arn)
echo "CMK de clientes: ${CMK_CLIENTES}"
echo "CMK de documentos: ${CMK_DOCUMENTOS}"
Cinco provas. Nenhuma aceita "parece que funcionou": cada uma tem um comando, um resultado que aprova, e o significado do que reprova.
| Prova | Comando | Resultado que aprova | O que reprova, e o que significa |
|---|---|---|---|
| 1 · A chave gerenciada é fixa | `aws kms put-key-policy` em `alias/aws/rds` | chamada recusada — você não é o dono da política | se passasse, a premissa inteira do módulo estaria errada: a AWS diz explicitamente que você não controla a política de uma chave gerenciada por ela |
| 2 · A CMK compartilhada some do RDS de clientes | `describe-db-instances --query "[].[DBInstanceIdentifier,KmsKeyId]"` | o banco da Cadência mostra o ARN da `cmk_clientes`, distinto do padrão `aws/rds` | se ainda mostrar `alias/aws/rds`, o `kms_key_id` não foi aplicado — recriação do banco é necessária, porque a chave de um RDS não muda em `apply` incremental |
| 3 · `kms:ViaService` bloqueia uso direto | assumir a task role e chamar `aws kms decrypt` diretamente, fora do Secrets Manager | AccessDeniedException | se decifrar, a condição `ViaService` não está na política, ou o ARN da chave errado está sendo usado em outro lugar |
| 4 · A rotação está ligada e ativa | `aws kms get-key-rotation-status --key-id $CMK_CLIENTES` | `KeyRotationEnabled: true` | se `false`, a rotação nunca foi habilitada — revise `enable_key_rotation` no recurso |
| 5 · O contexto de cifragem realmente amarra o registro | decifrar um CPF cifrado com `cliente_id` de OUTRO cliente no contexto | `InvalidCiphertextException` | se decifrar com o contexto errado, o código não está passando `EncryptionContext` na chamada de `Decrypt` — a proteção contra troca de linha não existe |
# Prova adicional: a trilha filtra por chave, isoladamente.
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \
--max-results 10 \
--query "Events[?contains(CloudTrailEvent, '$CMK_CLIENTES')].CloudTrailEvent" \
--output text | python3 -m json.tool
# Esperado: todo evento retornado tem userIdentity apontando para a task role da
# Cadencia — nenhum outro principal aparece, porque a politica so autoriza este.
Não dá para esperar um ano para testar a rotação — use a rotação sob demanda
A rotação automática só troca o material no aniversário da habilitação, o que não ajuda a validar nada num laboratório. `aws kms rotate-key-on-demand --key-id $CMK_CLIENTES` força uma rotação imediata, sem alterar o calendário automático, e `aws kms list-key-rotations --key-id $CMK_CLIENTES` mostra o histórico resultante — com o mesmo KeyId em todas as entradas.
Quebrar de propósito: três falhas e o diagnóstico
As três falhas acontecem de verdade, e as três reforçam o mesmo mecanismo por ângulos diferentes: permissão de recurso e permissão de chave são coisas distintas, e uma não substitui a outra.
| Falha | Como provocar | Sintoma | Onde olhar | Correção |
|---|---|---|---|---|
| Falta a metade do S3 | remova `kms:GenerateDataKey` da política da task role para a CMK de documentos, mantendo `s3:PutObject` intacto | upload falha com AccessDenied mesmo com permissão de S3 correta | CloudTrail mostra `GenerateDataKey` negado logo após o `PutObject` | devolver `kms:GenerateDataKey` na política — é o mesmo padrão de duas permissões do L04 |
| Contexto de cifragem inconsistente | grave um CPF com `cliente_id` A e tente decifrar informando `cliente_id` B | `InvalidCiphertextException` na leitura, sem erro nenhum na gravação | comparar o contexto usado na gravação (auditável no CloudTrail) com o da leitura | usar sempre o identificador correto do registro como parte do contexto — nunca "tentar sem contexto" como atalho |
| Credencial de leitura vazada | conceda a um script fora da aplicação a mesma credencial de leitura do Postgres, e consulte a tabela de clientes diretamente | o CPF aparece em texto claro na consulta direta, mesmo com o volume do RDS cifrado | não há "onde olhar" no RDS — o comportamento é o documentado; o gap é a ausência de cifra de campo | é exatamente o motivo de existir a seção "Construir: cifra de campo" — sem ela, este não é um bug, é uma lacuna conhecida |
A criptografia de armazenamento não é uma segunda camada contra credencial
Isto é o achado central do laboratório, e vale repetir fora da tabela: a criptografia do RDS protege contra o roubo do DISCO ou de um SNAPSHOT sem a chave — não contra uma consulta autenticada. Qualquer credencial válida, obtida de forma legítima ou por uma injeção de SQL, recebe o dado já decifrado, porque a decifragem acontece na camada de armazenamento, antes de o Postgres processar a consulta. Quem promete a um auditor que "o CPF está protegido porque o banco é criptografado" está descrevendo proteção contra um cenário — disco roubado — e sendo ouvido como se tivesse descrito outro — credencial comprometida.
Por que trocar a chave gerenciada pela AWS por uma CMK própria muda o controle de acesso, se a criptografia já estava ligada nas duas?
Segurança: o risco que este laboratório introduz, e o único irreversível
Trocar chave gerenciada por CMK reduz um tipo de risco (acesso sem trava independente) e introduz outro, específico de administrar chaves próprias: agora existe uma operação que, se descuidada, não tem volta.
| Risco | Probabilidade | Impacto | Prevenção | Detecção | Resposta |
|---|---|---|---|---|---|
| Exclusão da CMK usada em produção | baixa | catastrófico — todo dado cifrado por ela fica permanentemente irrecuperável após a janela | tag de produção + processo com dois aprovadores antes de `ScheduleKeyDeletion` | alarme do EventBridge no evento `ScheduleKeyDeletion`, disparado na hora | chamar `CancelKeyDeletion` ENQUANTO a janela ainda corre — depois dela, não há resposta |
| Política sem statement de conta raiz | baixa | alto — lockout de administração da chave | sempre incluir a conta raiz como principal administrador, revisar antes de aplicar | `put-key-policy` recusado para todo administrador, inclusive o esperado | abrir caso de suporte com a AWS; nem sempre reversível pelo caminho normal |
| `kms:ViaService` ausente na política | média | alto — decifragem fora do fluxo pretendido | sempre condicionar o uso a `kms:ViaService` e ao serviço exato | CloudTrail com `Decrypt` cujo `invokedBy` não é o serviço esperado | restringir a política e investigar todo uso anterior fora do padrão |
| Chave de dados em claro logada por engano | baixa | alto — anula o envelope inteiro para todo registro cifrado com aquela chave de dados | nunca logar `Plaintext`; `Array.Clear` logo após o uso | varredura por padrão de chave de 32 bytes em base64 nos grupos de log | rotacionar a CMK e recifrar os registros protegidos pela chave de dados exposta |
| Rotação desabilitada em CMK de produção | média | médio — falha de conformidade silenciosa | padrão do módulo Terraform com `enable_key_rotation = true` | regra gerenciada do AWS Config para rotação de chave, ou auditoria periódica | habilitar a rotação — ela não é retroativa, passa a valer da próxima janela |
| Contexto de cifragem previsível demais | baixa | médio — não vaza o dado, mas enfraquece a amarração se o contexto for adivinhável e reconstruível por um atacante com acesso parcial | compor o contexto com identificador interno, não com dado público do cliente | revisão de código na criação de qualquer novo campo cifrado | ajustar o contexto e recifrar; a decifragem antiga permanece válida com o contexto antigo |
Observabilidade: as perguntas que o painel de chaves tem de responder
Um painel de KMS existe para uma função estreita: dizer se o uso de cada chave está dentro do esperado, e avisar antes de uma operação irreversível acontecer.
| Pergunta | Métrica ou consulta | O que significa mudar | Limiar inicial |
|---|---|---|---|
| Quantas vezes cada CMK foi usada, e por quem? | CloudTrail `Decrypt`/`GenerateDataKey` agrupado por KeyId e por `userIdentity` | principal novo aparecendo é uso não previsto | qualquer principal fora da lista esperada |
| Alguém tentou usar a chave fora do serviço esperado? | CloudTrail com `invokedBy` divergente do `ViaService` da política | tentativa de contornar a restrição — mesmo negada, vale investigar | qualquer ocorrência, mesmo com resultado negado |
| A rotação está habilitada e em dia? | `get-key-rotation-status`, ou a regra gerenciada do AWS Config para rotação | chave fora de conformidade | qualquer CMK de produção com `false` |
| Alguma chave foi agendada para exclusão? | EventBridge no evento `ScheduleKeyDeletion` | início da única janela cancelável antes da perda permanente | qualquer ocorrência — alarme crítico, imediato |
| A taxa de erro na decifragem de campo subiu? | métrica customizada de `InvalidCiphertextException` no log da aplicação | contexto de cifragem incompatível — dado errado ou migração mal-feita | acima da taxa de erro de base, qualquer aumento sustentado |
CloudTrail para KMS é evento de gerenciamento, e isso já basta aqui
As chamadas `Decrypt`, `GenerateDataKey`, `Encrypt` e `CreateGrant` são registradas por padrão como eventos de gerenciamento — não é preciso configurar eventos de dados específicos do KMS para tê-las na trilha. O ponto que exige atenção é outro: um trail com retenção curta apaga o único registro de quem usou uma chave antes de uma investigação chegar a tempo. Retenção da trilha é decisão de conformidade, não de KMS.
Escala: de dez registros a um milhão, e o que muda no KMS
| Volume | O que acontece | O que passa a doer | O que fazer |
|---|---|---|---|
| 10 pedidos/dia | poucas chamadas de API por dia, nenhuma cota chega perto | nada; é o cenário deste laboratório | nada |
| 10 mil pedidos/dia | ainda dentro da cota padrão, mas GenerateDataKey por CPF cifrado já soma milhares de chamadas | nada visível ainda, mas é o volume em que vale medir antes de prometer mais | medir a taxa real de chamadas por chave em Service Quotas antes de escalar |
| 1 milhão de registros/dia | cifra de campo por registro pode aproximar a cota de requisições por segundo DA CHAVE — a cota é compartilhada entre todos os chamadores | throttling em `GenerateDataKey`/`Decrypt` durante pico, sem relação com CPU ou memória da aplicação | cache de chave de dados por período curto (padrão de data key caching), em vez de uma chamada por registro |
| Falha de AZ | o KMS é um serviço regional, gerenciado multi-AZ pela própria AWS — a chave em si não fica indisponível por uma AZ cair | o que pode falhar é o CAMINHO até o KMS, se o endpoint de interface da VPC existir só numa AZ | endpoint de interface do KMS redundante entre AZs, como qualquer outro endpoint deste catálogo |
| Rotação durante pico de tráfego | a rotação — automática ou sob demanda — não interrompe cifragem nem decifragem | nada; o KMS usa a versão de material correta de forma transparente | nenhuma ação — não é um evento operacional que peça plantão |
A cota de requisições é por chave, não por conta — e o valor exato está em Service Quotas
O KMS aplica um limite de requisições por segundo COMPARTILHADO entre todos os chamadores de uma mesma chave, e o valor varia por tipo de operação, tipo de chave e região. Não repita um número decorado aqui: confira o valor atual em Service Quotas antes de dimensionar um caminho de alto volume, e projete com folga.
Custo: o que trocar de chave acrescenta à fatura
A criptografia em si não é o que muda de preço — ela já estava ligada nos dois desenhos. O que muda é a existência de chaves próprias e o padrão de chamadas de API.
| Cenário | Volume | O que acrescenta | Tendência | Otimização |
|---|---|---|---|---|
| Protótipo | duas CMKs, uso esporádico | a mensalidade fixa de duas chaves; chamadas de API desprezíveis | baixa e previsível | nenhuma; é o piso do módulo |
| Produção pequena | duas CMKs, ~9 mil pedidos/mês, uma nota fiscal por pedido | mensalidade das duas chaves + chamadas de `GenerateDataKey`/`Decrypt` do campo CPF, reduzidas no S3 por Bucket Keys | baixa, cresce com o volume de campos cifrados, não com o de objetos no S3 | medir a proporção de chamadas por campo cifrado antes de estender a mais campos |
| Alta escala | cifra de campo por registro em volume alto, sem cache de chave de dados | chamadas de API do KMS passam a ser linha visível, especialmente no caminho de leitura | cresce linearmente com o número de registros cifrados por segundo | cache de chave de dados com expiração curta corta a maior parte das chamadas repetidas |
A mensalidade é por CHAVE existente, não por uso
Uma CMK cobra a mensalidade mesmo em um mês sem uma única chamada de API. É a razão concreta, não só arquitetural, por trás da decisão deste módulo de criar CMK só para as classes sensíveis: cada chave a mais é uma linha fixa recorrente, independente de quanto ela é usada.
Well-Architected nos seis pilares
| Pilar | Situação ao fim deste laboratório | Risco que fica | Melhoria | Prioridade |
|---|---|---|---|---|
| Excelência operacional | política de chave e rotação como código, versionadas no Terraform | mudança de política feita fora do Terraform, direto no console, diverge do estado | checagem de drift periódica sobre `aws_kms_key` e suas políticas | média |
| Segurança | CMK por classe, `kms:ViaService`, rotação, campo sensível cifrado com contexto | a criptografia de campo decifrado ainda circula em claro na memória do processo, em logs ou exportações se não houver disciplina de código | minimização de onde o valor decifrado aparece — é o L49 | alta |
| Confiabilidade | dado antigo continua decifrável após rotação; janela cancelável antes de exclusão | agendar exclusão de CMK em produção sem tag nem alarme é o único ponto sem rede | alarme obrigatório em `ScheduleKeyDeletion`, com aprovação dupla | alta |
| Eficiência de performance | Bucket Keys reduz chamadas do S3; envelope local evita cifrar o objeto inteiro no KMS | cifra de campo por registro, sem cache, pode aproximar a cota por chave em alta escala | data key caching para o caminho de leitura de campo | média |
| Otimização de custos | CMK só nas classes sensíveis, não em toda a conta | novo recurso criado sem classificação explícita nasce na chave gerenciada, por padrão | checklist de classificação de dado antes de provisionar recurso novo | média |
| Sustentabilidade | poucas chaves, uso sob demanda, sem HSM dedicado (custom key store) | nenhum específico neste volume | nada adicional; revisar se o volume crescer o suficiente para justificar caching | 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 patamar de controle sobre chave. Cada nível resolve um risco e compra outro.
Tudo na chave gerenciada pela AWS: `aws/rds`, `aws/s3`, `aws/secretsmanager`. Zero configuração de KMS.CMK por classe sensível, política nomeando o principal, `kms:ViaService`, rotação automática, trilha por ARN de chave.Envelope explícito no código para o campo mais sensível (CPF), além da criptografia de armazenamento, com contexto de cifragem por registro.Chave multi-região para recuperação entre regiões, ou AWS KMS External Key Store quando a exigência regulatória pede o material fora da geração padrão da AWS.Regra do AWS Config que exige CMK em todo recurso com tag de dado sensível; Security Hub agregando chaves sem rotação; Organizations vetando chave gerenciada pela AWS para dado classificado.Classificação automática de dado sensível decidindo A QUAL classe uma tabela nova pertence — e portanto qual CMK ela deveria usar.A ordem não é negociável, e o motivo é concreto
Governança em escala (nível 5) que exige CMK por tag só funciona se já existir a distinção entre classes do nível 2 — senão a regra não sabe qual CMK cobrar. E classificação automática (nível 6) só tem para onde apontar o resultado se as CMKs por classe já existirem. Pular direto para automação sem o nível 2 produz uma regra que aponta para chave nenhuma.
Onde IA entra nesta arquitetura, e onde não entra
Neste módulo, IA não resolve nada do mecanismo central. Política de chave, `kms:ViaService`, rotação e envelope são determinísticos — ou a condição bate, ou não bate; ou o contexto confere, ou a decifragem falha. Um modelo não melhora nenhuma dessas decisões, porque elas não são ambíguas.
O lugar onde IA agregaria valor real, nesta trilha, não é aqui: é decidir A QUAL classe um dado NOVO pertence, antes de alguém lembrar de configurar a chave certa. Um classificador que varre uma tabela recém-criada e sinaliza "isto parece CPF, isto parece endereço" resolve um problema genuinamente ambíguo — reconhecer padrão em texto livre — que regra determinística cobre mal. É o Amazon Macie, e é o L49.
| Pergunta | Resposta honesta para este módulo |
|---|---|
| Qual problema a IA resolveria? | reconhecer que um campo novo contém dado sensível, antes de alguém classificar manualmente |
| Por que uma regra não bastaria? | nome de coluna não é confiável — "documento", "doc_id" e "cpf_cliente" podem ou não conter CPF; reconhecer o PADRÃO do valor é o que o classificador faz e a regra de nome não faz |
| De onde viriam os dados? | o próprio conteúdo das tabelas e buckets, varrido pelo Macie sobre amostras dos dados armazenados |
| Qual o risco? | falso negativo (campo sensível não sinalizado) é pior que falso positivo aqui — por isso a varredura complementa, não substitui, a classificação declarada no design do sistema |
| Por que não agora? | a Cadência tem duas tabelas e um bucket — classificação manual, feita uma vez, já resolve. Automatizar isso com um volume de dado tão pequeno é complexidade sem retorno |
O uso de IA que parece atraente e é armadilha aqui
Pedir a um modelo para "olhar a política de chave e dizer se está segura" troca uma verificação determinística — a política bate com o padrão exigido, ou não bate — por uma probabilística, sujeita a alucinar uma condição que não existe no JSON. Política de IAM e de KMS têm ferramenta própria para isso: IAM Access Analyzer e o `simulate-policy` do próprio KMS avaliam a política de verdade, contra a semântica real do serviço.
Anti-padrões deste laboratório
| Anti-padrão | Por que alguém faz | Por que é problema | Sintoma em produção | Forma correta | Quando é aceitável |
|---|---|---|---|---|---|
| Chave gerenciada pela AWS em todo dado classificado | é o padrão, zero configuração, "já funciona" | nenhuma trava independente por recurso, nenhuma trilha filtrável por classe | auditor pergunta "qual chave protege X" e a resposta é "a mesma de tudo" | CMK própria por classe de dado sensível | dado não classificado, ambiente de desenvolvimento |
| Política de chave sem statement de conta raiz | parece redundante — o administrador já tem outra permissão | pode causar lockout permanente de administração da própria chave | ninguém, nem o dono da conta, consegue mais alterar a política | sempre incluir a conta raiz como principal administrador | nunca |
| `kms:Decrypt` sem `kms:ViaService` | é mais rápido escrever, e "já funciona no teste" | qualquer chamada direta ao KMS com a mesma credencial decifra fora do fluxo pretendido | CloudTrail mostra `Decrypt` com `invokedBy` diferente do serviço esperado | condicionar toda permissão de uso a `ViaService` e ao endpoint exato | chave legitimamente usada por múltiplos serviços, com lista explícita de `ViaService` |
| Logar o resultado de `GenerateDataKey` para depurar | debugar um problema de cifragem em produção, sob pressão | o campo `Plaintext` É a chave de dados em claro — logá-lo anula o envelope inteiro | chave de dados em claro aparece em log agregado, possivelmente retido por meses | logar só metadados — KeyId, contexto — nunca o material | nunca, nem com dado sintético em teste: o hábito é o risco, não o dado |
| CMK única para todas as classes de dado | menos chaves para gerenciar, política mais simples de escrever uma vez | mistura blast radius de classes com perfil de risco diferente | investigar um incidente numa classe obriga revisar TODAS as outras que compartilham a chave | uma CMK por classe de dado com perfil de risco distinto | dados da mesma classe de sensibilidade e mesmo dono, por decisão explícita |
| Cifra de campo sem contexto de cifragem | menos código, funciona no caminho feliz do teste | sem contexto, o KMS não amarra o ciphertext ao registro — um valor trocado entre linhas decifra sem erro nenhum | nenhum erro visível; o defeito é dado atribuído ao registro errado, silencioso | contexto derivado do identificador do próprio registro, verificado nas duas pontas | nunca, para dado com múltiplas linhas do mesmo tipo |
Quando algo não funciona
| Sintoma | Causa provável | Como investigar | Log/métrica | Correção |
|---|---|---|---|---|
| AccessDenied em `PutObject`, mesmo com permissão de S3 correta | falta `kms:GenerateDataKey` na política da task role para a CMK de documentos | olhar o evento imediatamente anterior ao `PutObject` recusado | CloudTrail: `GenerateDataKey` com `errorCode: AccessDenied` | adicionar a permissão com `kms:ViaService = s3...` |
| AccessDenied em `GetSecretValue`, com a policy do segredo correta | falta `kms:Decrypt` na CMK de clientes para a task role | mesma checagem: o evento de KMS logo antes da chamada recusada | CloudTrail: `Decrypt` com `errorCode: AccessDenied` | adicionar a permissão com `kms:ViaService = secretsmanager...` |
| `InvalidCiphertextException` ao decifrar o campo CPF | contexto de cifragem não bate entre gravação e leitura | comparar o `encryptionContext` do evento de `GenerateDataKey` (gravação) com o de `Decrypt` (leitura) no CloudTrail | exceção da SDK + os dois eventos de CloudTrail lado a lado | reconstituir e usar o contexto original — nunca tentar decifrar sem contexto como atalho |
| AccessDenied ao tentar alterar a política de uma chave | é uma chave gerenciada pela AWS (`aws/*`), que não aceita `PutKeyPolicy` de ninguém | `describe-key` mostra `KeyManager: AWS` | CloudTrail: `PutKeyPolicy` com `errorCode: AccessDenied` | não é bug — crie uma CMK própria se precisar de política customizada |
| `terraform apply` falha ao criar o RDS: AccessDenied em `kms:CreateGrant` | o principal que roda o Terraform precisa de `kms:CreateGrant` na política da CMK | CloudTrail do principal do pipeline, não da task role | `CreateGrant` com `errorCode: AccessDenied` | adicionar o statement de administração ao principal correto na política da CMK |
| Rotação automática "parece" não ter acontecido | confundir a data de HABILITAÇÃO com a data de próxima rotação | `get-key-rotation-status` e `list-key-rotations` | ausência de evento `RotateKey` no CloudTrail no período esperado | nada quebrado — confira o cálculo: 365 dias a partir de quando a rotação foi ligada, não da criação da chave |
Limpeza: o que o destroy não leva
#!/usr/bin/env bash
set -euo pipefail
# Esvazie o bucket ANTES do destroy — objeto cifrado com SSE-KMS nao impede o
# terraform destroy no bucket, mas um bucket nao vazio, sem force_destroy, faz o
# destroy falhar na hora errada, no meio de uma cadeia de dependencias.
aws s3 rm "s3://$(terraform output -raw bucket_notas_fiscais)" --recursive
terraform destroy -auto-approve
# As duas CMKs NAO desaparecem aqui — elas entram em exclusao AGENDADA, com a
# janela de deletion_window_in_days. Confirme:
aws kms list-keys --query "Keys[].KeyId" --output text | while read -r k; do
aws kms describe-key --key-id "$k" \
--query "KeyMetadata.[KeyId,KeyState,DeletionDate]" --output text
done | grep PendingDeletion || echo "nenhuma chave pendente de exclusao"
| Recurso | O que o destroy faz | O que fica cobrando ou pendente |
|---|---|---|
| CMK (clientes e documentos) | agenda para exclusão, com a janela configurada | a chave continua existindo — e sujeita à mensalidade — até o fim da janela; ela pode ser cancelada com `CancelKeyDeletion` enquanto isso |
| Alias da chave | removido imediatamente | nada — mas sem o alias, referenciar a chave pendente exige o Key ID, não o nome |
| Secrets Manager | agenda para exclusão (7 a 30 dias, salvo `recovery_window_in_days = 0`) | o segredo continua existindo, e continua sob a CMK, até o fim da janela dele — independente da janela da própria chave |
| Snapshot final do RDS | criado no destroy, se `skip_final_snapshot = false` | permanece cifrado pela CMK de clientes e continua cobrando armazenamento até ser removido manualmente |
| Bucket S3 | removido só se vazio | com `force_destroy` não usado, um bucket não vazio trava o destroy inteiro |
| Grupo de log do CloudWatch | removido só se declarado como recurso Terraform | log criado implicitamente por outro serviço não some sozinho, e continua sob retenção |
A janela de exclusão da CMK é a única rede de segurança que este módulo tem
Ela existe justamente porque `ScheduleKeyDeletion` é a operação irreversível deste catálogo inteiro. Trate a confirmação de "nenhuma chave pendente de exclusão" acima como parte da limpeza, não como opcional — uma CMK esquecida em `PendingDeletion` numa conta de estudo, se o prazo vencer, leva junto qualquer dado de teste que ainda dependa dela.
Resumo: problema, peça e motivo
| Problema | Peça | Motivo |
|---|---|---|
| Auditor não consegue isolar uso de chave por recurso | CMK própria por classe de dado | política e trilha passam a ser por ARN de chave, não compartilhadas pela conta |
| Qualquer principal com permissão de serviço decifra | `kms:ViaService` + principal nomeado na política da chave | fecha a segunda trava que a chave gerenciada pela AWS nunca teve de verdade |
| Credencial vazada lê o CPF em claro, mesmo com RDS cifrado | envelope explícito no código, com contexto de cifragem | decifrar exige a CMK e o contexto certo — não só a credencial do banco |
| Ninguém sabe se a chave rotaciona | `enable_key_rotation` + prova por `get-key-rotation-status` | rotação vira fato observável, com data e status, não uma afirmação |
| Exclusão de chave por engano | janela de espera + tag + alarme em `ScheduleKeyDeletion` | única rede de segurança contra a única operação irreversível deste módulo |
O fluxo, em oito passos: o app pede um recurso protegido → o serviço (RDS, S3 ou Secrets Manager) pede uma chave de dados nova à CMK correspondente → o KMS gera a chave, devolve as cópias em claro e cifrada → o serviço cifra ou decifra localmente com a cópia em claro e a descarta → só a cópia cifrada persiste ao lado do dado → a política da CMK avalia o principal e o `ViaService` a cada chamada → o CloudTrail registra o uso com o KeyId exato → a rotação troca o material por trás do mesmo KeyId, sem quebrar nada cifrado antes.
Perguntas frequentes
❓ Se a chave gerenciada pela AWS já criptografa, por que trocar para uma CMK?
❓ `kms:ViaService` restringe o uso da chave a um serviço específico?
❓ A rotação automática de uma CMK muda o ID da chave?
❓ É possível rotacionar uma CMK com mais frequência do que uma vez por ano?
❓ A criptografia de armazenamento do RDS protege contra uma credencial de banco vazada?
❓ O que é a chave de dados na criptografia de envelope, e por que ela nunca é a CMK?
❓ Por que a operação de cifra direta do KMS não serve para cifrar um objeto do S3 inteiro?
❓ A política de chave e a política IAM de quem usa a chave precisam dizer a mesma coisa?
Fixando
Depois que uma CMK com rotação automática troca o material criptográfico, o que acontece com o dado cifrado ANTES da rotação?
No laboratório, o CPF é cifrado no código antes de chegar ao Postgres, além da criptografia de armazenamento do RDS. Qual lacuna essa camada extra fecha?
Conhecimentos, próximo módulo e documentação
| Item | Conteúdo |
|---|---|
| Conhecimentos anteriores necessários | L04 (segredo fora do código, com rotação — inclui a introdução à criptografia de envelope que este módulo aprofunda) e L42 (identidade de workload, task role) no ar |
| Conhecimentos adquiridos | a diferença de política entre chave gerenciada pela AWS e CMK; a criptografia de envelope em profundidade — chave de dados efêmera vs CMK; `kms:ViaService` como segunda trava; semântica exata de rotação (o que troca, o que preserva); a lacuna entre criptografia de armazenamento e proteção contra credencial vazada; contexto de cifragem como amarração do ciphertext ao registro |
| Limitação que fica | o valor decifrado do campo ainda circula em claro na memória do processo depois de decifrado — minimizar onde ele aparece (log, exportação, relatório) é o L49 |
| Próximo módulo recomendado | L49 — dado pessoal: minimizar, mascarar, não logar. Parte das duas CMKs criadas aqui e trata classificação automática, mascaramento e retenção de log |
| Também habilitado por este módulo | L69 (permissão por coluna no lake) depende da classificação que o L49 constrói sobre a base deste laboratório |
| Data da última validação técnica | 7 de agosto de 2026 |
Documentação oficial consultada: AWS KMS keys — concepts — a distinção entre customer managed key, AWS managed key e AWS owned key, política, cobrança e cota, que é a fonte da tabela de escopo de controle usada neste módulo; e Rotate AWS KMS keys — como a rotação automática e sob demanda funcionam, o período padrão de 365 dias, e a confirmação de que o KeyId nunca muda entre rotações. Nenhum preço absoluto aparece neste módulo por decisão: use o AWS Pricing Calculator, porque preço varia por região e envelhece mais rápido que o conteúdo.
O que não foi verificado nesta versão, e você deve conferir na sua conta
O limite exato de payload da operação de cifra direta do KMS é citado aqui como "poucos kilobytes", sem um número fechado — confira o valor atual na referência da API antes de decidir se um payload específico cabe nela. O suporte a `rotation_period_in_days` para customizar o período de rotação também não foi verificado na versão do provedor Terraform usada neste laboratório; se você precisa de um período diferente do padrão de 365 dias, confira a documentação do provider antes de escrever o argumento.
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…