Post-mortem hipotético: a chave de API que vazou no log e ficou 40 dias válida

Escrito por

em

Uma chave de API de um gateway de pagamento aparece em texto puro numa linha de log de erro. O log vai para o agregador, o agregador retém 90 dias, e três times têm acesso de leitura ao índice. A chave só é revogada quarenta dias depois, quando alguém percebe o padrão numa busca por outra coisa. Não houve uso indevido detectado, o que é diferente de não ter havido uso indevido.

Este post-mortem é hipotético, mas a sequência de falhas é banal o bastante para servir de checklist. O ponto não é a chave: é que quatro controles independentes deveriam ter interrompido a cadeia e nenhum interrompeu.

A cadeia de falhas, na ordem em que aconteceu

O serviço fazia uma chamada HTTP ao gateway. Diante de um erro 4xx, o cliente HTTP logava o objeto de request inteiro para facilitar depuração — incluindo o dicionário de headers. O Authorization estava ali dentro. Em operação normal isso nunca aparecia, porque erros 4xx do gateway eram raros. Uma mudança de contrato do lado do gateway passou a rejeitar um campo, os 4xx dispararam, e o header foi parar em milhares de linhas.

A primeira falha é de design do cliente HTTP: logar o request completo é conveniente até o dia em que não é. A segunda é que o pipeline de ingestão de logs não tinha nenhuma regra de redação — nada procurava por padrões de secret na entrada. A terceira é que a credencial era estática, criada manualmente no painel do gateway dois anos antes, sem data de expiração e sem dono registrado. A quarta é que ninguém monitorava uso da chave por origem: se ela tivesse sido usada de outro IP, o alerta não existia para disparar.

Repare que as quatro falhas são de camadas diferentes — aplicação, plataforma, gestão de identidade e detecção. É por isso que corrigir só a primeira não resolve. Um logger.debug mal colocado em outro serviço reabre o mesmo buraco na semana seguinte.

Redação no ponto de saída, não no ponto de entrada

A correção intuitiva é ir ao código que logou e remover o header. Funciona uma vez. A correção que escala é filtrar no ponto onde o log é serializado, de forma que nenhum caminho de código consiga emitir um campo sensível — mesmo um caminho escrito por alguém que não leu a convenção.

Em Python, isso é um filtro no logger raiz; em Node, um serializer no pino ou equivalente. A lista de chaves proibidas é curta e deve ser tratada como configuração de plataforma, não de serviço.

Filtro aplicado no logger raiz, não em cada chamada. Chaves comparadas em minúsculo para pegar Authorization, authorization e variações:

REDACT = {"authorization", "cookie", "set-cookie", "x-api-key",
          "proxy-authorization", "password", "secret", "token"}

class RedactFilter(logging.Filter):
    def filter(self, record):
        if isinstance(record.args, dict):
            record.args = {
                k: ("[REDACTED]" if k.lower() in REDACT else v)
                for k, v in record.args.items()
            }
        return True

logging.getLogger().addFilter(RedactFilter())

Duas ressalvas. A primeira: filtro por nome de chave não pega secret embutido no corpo de uma string já formatada — f"falhou: {request}" escapa de qualquer filtro estruturado. A regra que fecha esse caso é proibir interpolação de objetos de request em mensagem de log e passar contexto sempre como campo estruturado. A segunda: redação no cliente protege o que sai da aplicação, não o que já está armazenado. O expurgo do histórico no agregador é um trabalho separado, e a maioria dos agregadores não apaga por linha — apaga por índice inteiro.

A camada que realmente resolve: credencial sem valor duradouro

Redação reduz a chance de vazar. Credencial de vida curta reduz o valor do que vaza. As duas resolvem problemas diferentes e a segunda tem impacto maior, porque transforma um incidente de quarenta dias num incidente de quinze minutos.

Modelo Janela de exposição Esforço de adoção Rastreabilidade
Chave estática em variável de ambiente Até alguém revogar manualmente Nenhum Nenhuma — a chave não sabe quem a usou
Secret manager com rotação agendada Até a próxima rotação (dias) Médio: exige recarregar credencial em runtime Parcial — versão da credencial identifica a janela
Credencial efêmera via OIDC / STS Minutos Alto na primeira vez, baixo depois Alta — cada token carrega identidade da carga de trabalho

A recomendação, quando o provedor externo suporta federação de identidade: credencial efêmera. Quando não suporta — e muitos gateways de pagamento e ERPs brasileiros ainda não suportam —, secret manager com rotação e um cliente que releia a credencial sem redeploy. O anti-padrão é adotar secret manager e continuar carregando o valor uma única vez no boot: a rotação existe no papel e a aplicação segue usando a versão antiga até reiniciar, o que na prática significa que a revogação derruba produção.

Detecção: o alerta que faltava não era sobre o log

Varredura de secret em código e em log é útil e barata — uma etapa de gitleaks ou equivalente no pipeline, mais uma regra de padrão no agregador que dispara quando uma linha casa com o formato de chave de um provedor conhecido. Vale ter. Mas é detecção de sintoma.

O sinal com menor taxa de falso negativo é o uso da credencial fora do perfil esperado. Isso exige duas coisas que raramente estão prontas: saber de quais IPs ou identidades a chave deveria ser usada, e ter acesso ao log de uso do lado do provedor. Quando o provedor expõe esse log, uma regra simples — chamada originada de faixa não conhecida, ou volume fora do horário de operação — cobre o caso em que a chave vaza por um caminho que ninguém previu, incluindo o notebook de um contratado.

Sem esse acesso, a alternativa honesta é encurtar a janela em vez de tentar detectar. Rotacionar a cada sete dias não impede o vazamento, mas garante que a resposta a “há quanto tempo isso está exposto?” tenha um teto conhecido. Um teto conhecido é o que permite dimensionar a resposta ao incidente sem adivinhar.

Três correções tentadoras que não valem a prioridade

Reescrever o histórico do repositório. Se o secret nunca esteve versionado — e neste caso não esteve, ele veio de variável de ambiente —, limpar histórico é trabalho sem retorno. Mesmo quando o secret está no Git, a reescrita só faz sentido depois da revogação, e ela não desfaz os forks e clones existentes. Revogar é o que resolve; limpar histórico é higiene posterior.

Trocar o agregador de logs por um que ofereça mascaramento nativo. Migração de observabilidade consome trimestres e o mascaramento do lado do agregador roda depois da transmissão — o secret já saiu da rede da aplicação e já passou pelo agente de coleta. A redação precisa acontecer antes, no processo que gera a linha.

Bloquear logger.debug em produção via política. A tentação é tratar o incidente como indisciplina e responder com regra. O header vazou num log de erro, não de debug, e teria vazado igual com nível INFO. Controle que depende de alguém lembrar da regra falha na primeira semana em que o time está apertado — o filtro estrutural não depende.

O padrão comum às três: são respostas ao evento específico, não à classe de falha. A pergunta que separa uma da outra é se a correção impediria um vazamento de um secret diferente, num serviço diferente, escrito por alguém que não participou do post-mortem.

Ordem de execução e como medir

As quatro correções não têm o mesmo custo nem o mesmo retorno. A ordem que faz sentido para a maioria dos times:

  1. Filtro de redação no logger de plataforma — horas de trabalho, cobre todos os serviços que usam a biblioteca comum.
  2. Inventário de credenciais externas com dono e data de criação — desconfortável, porque quase sempre revela chaves que ninguém sabe de onde vieram e cujo desligamento ninguém quer testar.
  3. Rotação automatizada com releitura em runtime — a parte cara é o releitura, não a rotação.
  4. Federação de identidade onde o provedor suportar — migração por serviço, não big bang.

Três métricas dizem se o trabalho pegou, e nenhuma delas é “número de vazamentos”, que é raro demais para servir de sinal. A idade mediana das credenciais externas ativas — se não cai mês a mês, a rotação não está rodando de verdade. O percentual de credenciais com dono identificado, que deveria caminhar para 100% antes de qualquer automação. E o tempo entre a decisão de revogar e a revogação efetiva em produção, medido num exercício deliberado: escolher uma credencial, revogar, e cronometrar. Times que nunca fizeram esse exercício costumam descobrir que o número está em horas, não em minutos, e quase sempre por causa de um serviço que só relê o secret no boot.

O próximo passo concreto, se nada disso existe hoje: rodar o inventário. Sem saber quantas credenciais externas estão ativas e quem responde por cada uma, as outras três correções são exercício de arquitetura sobre um sistema imaginário.

Comentários

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *