Destino Dev

Blog

Configuração segura em Python: Pydantic v2 + HashiCorp Vault na prática

Por John Pires · 29 de março de 2026

Configuração segura em Python: Pydantic v2 + HashiCorp Vault na prática

1) Motivacao e trade-offs

Por que não deixar segredo em variáveis ou arquivos

Guardar segredos em .env, em arquivos de configuração checked-in, ou simplesmente como strings literais em código é prática comum em projetos pequenos, mas é uma porta aberta para vazamento. Logs acidentais, dumps de memória, backups e commits humanos transformam segredos em riscos reais. Mesmo com permissões de arquivo restritas, a superfície de ataque de uma máquina cresce (sistemas CI, agentes de deploy, engenheiros que precisam de acesso), aumentando a probabilidade de exposição.

Trade-offs de usar um cofre (Vault)

Adicionar HashiCorp Vault melhora a segurança mas traz complexidade operacional: autenticação (AppRole, Kubernetes auth, tokens), rotação de credenciais, latência nas leituras e necessidade de inicialização/bootstrapping. Use Vault quando os benefícios (rotacionar credenciais, auditar acessos, separar segredo/infra) superarem a sobrecarga. Nem toda aplicação precisa de Vault — para scripts locais, um .env com boa proteção pode bastar.

Como regra prática, prefira Vault quando:

  • Você precisa rotacionar credenciais sem redeploy
  • Vários serviços acessam diferentes segredos centralmente
  • Auditoria e controle de acessos são requisitos

Em troca, prepare propaganda técnica: políticas, paths, TTLs e estratégia de autenticação. Decida também qual modelo de segredo usar (static vs dynamic, KV vs Database secrets backend).

2) Fundamentos tecnicos

Pydantic v2: BaseSettings e fontes de configuração

Pydantic v2 trouxe mudanças e um sistema de settings com possibilidade de customizar fontes. O ponto chave aqui é ter um objeto de configuração central, validado, com tipos e coerência entre ambientes. Em vez de espalhar os env vars pelo app, centralize em uma classe de settings. Pydantic permite inserir uma fonte personalizada que consulta o Vault no momento da criação do objeto.

HashiCorp Vault: KV v2, AppRole e tokens

Vault suporta múltiplos backends. O mais usado para segredos estáticos é KV v2. Para credenciais dinâmicas (rotacionadas automaticamente por Vault) existem backends como database e AWS. Autenticação com AppRole é comum em infra: você distribui role_id ao serviço e obtém secret_id via processo seguro; o serviço troca ambos por um token de Vault. Tokens também precisam de renovação/recuperação.

Entender paths KV v2 é importante: a API retorna um wrapper com dados e metadata. Uso típico com a biblioteca hvac em Python:

# Exemplo de leitura KV v2 com hvac
# O código se conecta ao Vault e lê o segredo em secret/data/app
import hvac

client = hvac.Client(url='https://vault.example.com', token='s.XXXX')
resp = client.secrets.kv.v2.read_secret_version(path='app')
# resp['data']['data'] contém o dicionário de chaves do segredo
print(resp['data']['data'])

O bloco acima mostra a estrutura retornada. Em produção, não passe tokens em texto claro: use autenticação por AppRole ou Kubernetes.

3) Exemplo 1: caso simples

Jeito errado — variáveis espalhadas e impressão de segredos

Veja um exemplo comum e problemático: uma aplicação que lê um token Vault diretamente do env e imprime para debug. É o tipo de código que acaba no log e vira incidente.

# ERRADO: expondo segredos em logs e sem validação
import os

VAULT_TOKEN = os.environ.get('VAULT_TOKEN')
VAULT_ADDR = os.environ.get('VAULT_ADDR', 'http://localhost:8200')
print('Conectando a Vault com token', VAULT_TOKEN)  # vazamento direto

# sem validação do conteúdo, sem tipagem, sem fallback seguro

Resultado: token aparece no stdout, histórico de CI e outros lugares. Sem tipagem, você não sabe se a aplicação tem o que precisa e erros aparecem em tempo de execução.

Jeito certo — Pydantic v2 + source customizada para Vault

Use uma classe de settings que combina env vars e uma fonte de Vault. O padrão: manter env vars mínimos (endereço do Vault, método de autenticação) e buscar segredos sensíveis no Vault apenas quando necessário.

# CERTO: settings com pydantic v2 + fonte que consulta Vault (hvac)
from pydantic_settings import BaseSettings, SettingsConfigDict
import hvac
from typing import Any, Dict

class AppConfig(BaseSettings):
    model_config = SettingsConfigDict(env_prefix='APP_')

    db_user: str
    db_pass: str
    db_host: str = 'localhost'

    # Fonte de Vault: será injetada nas sources
    @classmethod
    def settings_customise_sources(cls, init_settings, env_settings, file_secret_settings):
        def vault_source(settings: 'AppConfig') -> Dict[str, Any]:
            addr = env_settings(settings)['VAULT_ADDR'] if 'VAULT_ADDR' in env_settings(settings) else 'http://localhost:8200'
            token = env_settings(settings).get('VAULT_TOKEN')
            if not token:
                return {}
            client = hvac.Client(url=addr, token=token)
            try:
                secret = client.secrets.kv.v2.read_secret_version(path='app')
                return secret['data']['data']
            except Exception:
                return {}

        return (init_settings, env_settings, vault_source)

# Ao criar AppConfig(), Pydantic vai consultar: init->env->vault_source
config = AppConfig()
print('db_user', config.db_user)  # não imprime db_pass

O código anterior integra Vault como uma fonte adicional. Observações importantes: nunca logue segredos (note que apenas o nome do usuário é impresso), tratar falhas na leitura e permitir fallback para env vars em casos de emergência.

4) Exemplo 2: caso avancado do mundo real

Contexto: FastAPI + SQLAlchemy com credenciais rotativas

Cenário real: service FastAPI precisa criar conexões ao banco onde credenciais são rotacionadas pelo Vault (backend database). O erro comum é criar engine global na inicialização com credenciais estáticas — quando o Vault rotaciona, a aplicação continua usando credenciais inválidas.

Jeito errado — engine global sem rotação

Exemplo anti-pattern: cria engine na import time com segredos lidos uma vez.

# ERRADO: engine fixo, sem refresh quando o secret expira
from sqlalchemy import create_engine
import os

DB_URL = os.environ.get('DB_URL')  # p.ex. 'postgresql://user:pass@host/db'
engine = create_engine(DB_URL, pool_size=5)

# FastAPI usa este engine por toda a vida do processo

Problema: quando o Vault rotear credenciais (ou trocar password), a engine continua tentando com credenciais antigas; se o backend fabrica rotacionamento programado, o serviço quebra até redeploy.

Jeito certo — provider com refresh e dependency do FastAPI

Crie um objeto que mantém credenciais em memória e as renova antes de expirar. Use dependency injection do FastAPI para obter o engine atual e reconstituí-lo quando necessário. Evite recrear engine a cada request; prefira política de refresh com locking.

# CERTO: provider que refresca credenciais e reconstrói engine com lock
import threading
import time
from typing import Optional
from sqlalchemy import create_engine
from sqlalchemy.engine import Engine
import hvac

class DBCredProvider:
    def __init__(self, vault_addr: str, token: str, role: str):
        self.client = hvac.Client(url=vault_addr, token=token)
        self.role = role
        self._lock = threading.RLock()
        self._engine: Optional[Engine] = None
        self._cred_expiry = 0

    def _fetch_creds(self):
        # exemplo com database/creds/<role>
        res = self.client.secrets.database.generate_credentials(name=self.role)
        creds = res['data']
        return creds  # contains username, password, lease_duration

    def get_engine(self) -> Engine:
        with self._lock:
            now = time.time()
            if self._engine is None or now >= self._cred_expiry - 30:
                creds = self._fetch_creds()
                user = creds['username']
                pwd = creds['password']
                lease = creds.get('lease_duration', 3600)
                self._cred_expiry = now + lease
                url = f"postgresql://{user}:{pwd}@db-host/dbname"
                # recreate engine with new credentials
                self._engine = create_engine(url, pool_pre_ping=True)
            return self._engine

# Uso no FastAPI: dependency que chama provider.get_engine()

Resultado: credentials são geradas dinamicamente via backend database do Vault e substituem o engine antes do timeout, mantendo pool autêntico com credenciais válidas.

5) Debugging e troubleshooting

Erros comuns e armadilhas

Erros que aparecem com frequência:

  1. Token expirado ou não renovado — aplicações geram erros 403/unauthorized ao acessar Vault.
  2. Path errado no KV v2 — diferenças entre /v1/secret/data/... e o path lógico.
  3. Logs acidentais — print() ou logger.exception() vazando segredos.
  4. Ambiente local sem fallback — Devs não conseguem trabalhar sem um modo de fallback (mock, arquivo local criptografado, ou env var limitado).
Prática: sempre ter um modo seguro de fallback para desenvolvimento que não reduza segurança em produção.

Adicione monitoramento dos erros de autenticação ao Vault e traceabilide de acessos para identificar rapidamente serviços que perderam acesso.

Como depurar problemas específicos

Passos diretos para diagnosticar falhas:

  • Verifique conectividade de rede entre aplicação e Vault (curl /api/health).
  • Confirme método de autenticação (AppRole vs token) e valide role_id/secret_id.
  • Habilite logs em nível DEBUG localmente apenas para investigação — remova depois.
  • Recrie a chamada manualmente com hvac em um REPL para isolar se o problema é do client ou da configuração.

Exemplo de debug manual com hvac para validar leitura KV v2:

# DEBUG: testar leitura diretamente para isolar problema
import hvac

client = hvac.Client(url='https://vault.example.com', token='s.TEST')
print('is_authenticated', client.is_authenticated())
try:
    resp = client.secrets.kv.v2.read_secret_version(path='app')
    print('secrets', resp['data']['data'])
except hvac.exceptions.InvalidPath:
    print('path invalido ou sem permissoes')

Se client.is_authenticated() retorna False, o problema é de autenticação — role/token inválido ou expirado. Se a exceção é InvalidPath, verifique a política no Vault.

Dicas práticas de segurança operacional

Algumas recomendações práticas e direto ao ponto:

  • Never log secrets. Trate qualquer string identificada como secreto com mascaramento.
  • Use policies de menor privilégio no Vault. Cada aplicação deve ter acesso apenas ao path necessário.
  • Revise rotinas de bootstrap: como a aplicação obtém role_id/secret_id? Automatize de forma segura (Kubernetes ServiceAccount, Instance Metadata, CI secrets store).
  • Implemente métricas: falhas de autenticação, latência de leitura de segredo e contadores de rotação bem sucedida.

6) Conclusao pratica

Resumo das recomendações

Centralize configuração com Pydantic v2, use fontes customizadas para integrar o Vault e nunca exponha segredos em logs. Para credenciais rotativas, construa provider com estratégia de refresh e locking em vez de criar resources estáticos à inicialização. Use políticas mínimas no Vault e monitore autenticações e leituras.

Pela ordem: checklist de implementação

  1. Defina o que é segredo e se precisa estar no Vault.
  2. Crie uma classe AppConfig com Pydantic v2 e injete Vault como source.
  3. Escolha método de autenticação seguro (AppRole, Kubernetes, etc.).
  4. Implemente providers com refresh para recursos que dependem de credenciais rotativas.
  5. Adicione monitoramento e políticas de menor privilégio.

Seguir esse checklist reduz surpresas em produção e torna o ciclo de vida de segredos previsível.

Este artigo mostrou, no estilo de code review, o que evitar e como corrigir padrões comuns. A combinação Pydantic v2 + Vault fornece validação, tipagem e separação de responsabilidades — força que se traduz em menos bugs e menos incidentes de segurança.

Para aprofundar: leia a documentação do pydantic-settings (v2) e a API do hvac, e ajuste políticas do Vault para atender seu modelo de risco.

Assuntos: Python, pydantic, hashicorp-vault, Segurança

Leia também