Gerenciador de Segredos API com Rotação Automática: Seu Cofre Digital em Python e Systemd
Você já perdeu horas trocando uma chave de API porque expirou do nada e nenhum script te avisou? Ou pior — aquela chave do serviço SaaS que você usou em seis automações diferentes e agora não sabe onde está hardcoded? Se isso soa familiar, respira fundo. Você não está sozinho, e hoje vamos resolver isso de vez.
Neste artigo, vou te mostrar como construir um gerenciador de segredos de API com rotação automática usando Python puro, sqlite3 e systemd timers. Sem vaults enterprise caros. Sem AWS Secrets Manager. Sem depender de serviço de terceiros para guardar o que é seu.
Se você automa qualquer coisa — desde um bot de Telegram até pipelines de deploy — esse sistema vai te salvar de perrengues reais. E eu sei porque já queimei os dedos antes de chegar nessa solução.

O Problema Que Ninguém Te Conta Sobre Chaves de API
Todo tutorial começa com “crie sua chave no dashboard e cole no .env“. E funciona — até parar de funcionar. Aí o dia vira caos:
- A chave expirou e o webhook parou de entregar sem aviso
- Você rotacionou manualmente e esqueceu de atualizar três microserviços
- Uma chave vazou num commit e agora é corrida contra o tempo
- Você tem 47 chaves ativas espalhadas em 12 repositórios e nenhuma documentação
O problema não é técnico no início. É operacional. Falta um sistema centralizado que gerencie o ciclo de vida das suas chaves. E é exatamente isso que vamos construir.
Arquitetura do Gerenciador de Segredos
A ideia é simples — mas funcional de verdade:
- Um banco SQLite armazena os segredos com metadata (serviço, data de criação, TTL, status)
- Um módulo Python faz CRUD + validação + rotação automática
- Um systemd timer dispara a verificação de expiração a cada X horas
- Um script de notificação avisa no Telegram quando uma chave precisa ser trocada ou foi rotacionada
Nada de Docker compose com 14 serviços. Um arquivo Python, um banco, um timer. Isso é Fortaleza Digital de verdade: segurança que você consegue manter.
Passo 1: O Banco de Segredos
Vamos criar uma tabela que armazena tudo que importa sobre cada segredo. Não só o valor, mas o contexto:
import sqlite3
import hashlib
import os
from datetime import datetime, timedelta
from pathlib import Path
DB_PATH = Path.home() / ".secrets_vault" / "segredos.db"
def init_db():
"""Cria a estrutura do banco se não existir."""
DB_PATH.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(DB_PATH))
conn.execute("""
CREATE TABLE IF NOT EXISTS secrets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
service TEXT NOT NULL UNIQUE,
secret_key TEXT NOT NULL,
secret_value TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
ttl_days INTEGER DEFAULT 90,
status TEXT DEFAULT 'active',
rotation_count INTEGER DEFAULT 0,
last_rotated_at TIMESTAMP,
notes TEXT
)
""")
conn.execute("""
CREATE TABLE IF NOT EXISTS rotation_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
secret_id INTEGER NOT NULL,
old_hash TEXT,
rotated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
reason TEXT DEFAULT 'scheduled',
FOREIGN KEY (secret_id) REFERENCES secrets(id)
)
""")
conn.commit()
return conn
Observe dois detalhes que fazem diferença:
secret_keyé o identificador (ex:OPENAI_API_KEY), não o valorrotation_logmantém histórico — quando algo der errado, você sabe exatamente quando a última rotação aconteceu
Passo 2: Armazenando e Recuperando Segredos com Criptografia
Aqui entra a parte que muita gente erra: armazenar chaves em texto puro sem criptografia. Vamos usar cryptography.fernet para encriptar os valores no banco. Sim, é uma dependência a mais — mas a diferença entre “criptografado” e “texto puro” é a diferença entre um cofre e um bilhete colado no monitor.
from cryptography.fernet import Fernet
import os
from pathlib import Path
KEY_FILE = Path.home() / ".secrets_vault" / "master.key"
def get_encryption_key():
"""Retorna ou gera a chave mestre de encriptação."""
if not KEY_FILE.exists():
KEY_FILE.parent.mkdir(parents=True, exist_ok=True)
key = Fernet.generate_key()
KEY_FILE.write_bytes(key)
os.chmod(str(KEY_FILE), 0o600) # Só o dono lê
return key
return KEY_FILE.read_bytes()
def encrypt_value(value: str, key: bytes) -> str:
"""Encripta o valor do segredo."""
f = Fernet(key)
return f.encrypt(value.encode()).decode()
def decrypt_value(encrypted: str, key: bytes) -> str:
"""Decripta o valor do segredo."""
f = Fernet(key)
return f.decrypt(encrypted.encode()).decode()
🔧 Box Perrengue: Primeira vez que montei isso, cometi um erro idiota: salvei a chave mestre no mesmo diretório do banco sem restrição de permissão. Qualquer processo no servidor podia ler. Resultado? Se alguém comprometesse o servidor, tinha acesso a tudo. Sempre rode
chmod 600na chave mestre e considere usar permissões de grupo restritas em produção. Segurança que dá trabalho de configurar mas não protege de nada não é segurança — é teatro.
Agora a função que salva um segredo de verdade:
def store_secret(conn, service: str, key_name: str, value: str, ttl_days: int = 90):
"""Armazena um novo segredo encriptado."""
master_key = get_encryption_key()
encrypted = encrypt_value(value, master_key)
expires = datetime.now() + timedelta(days=ttl_days)
conn.execute("""
INSERT OR REPLACE INTO secrets
(service, secret_key, secret_value, expires_at, ttl_days)
VALUES (?, ?, ?, ?, ?)
""", (service, key_name, encrypted, expires, ttl_days))
conn.commit()
print(f"[✓] Segredo '{key_name}' para {service} armazenado com sucesso.")
def get_secret(conn, service: str, key_name: str) -> str:
"""Recupera e decripta um segredo."""
master_key = get_encryption_key()
cursor = conn.execute(
"SELECT secret_value, status FROM secrets WHERE service=? AND secret_key=?",
(service, key_name)
)
row = cursor.fetchone()
if not row:
raise KeyError(f"Segredo não encontrado: {service}/{key_name}")
if row[1] != 'active':
raise ValueError(f"Segredo {service}/{key_name} está com status '{row[1]}'")
return decrypt_value(row[0], master_key)

Passo 3: Rotação Automática — O Coração do Sistema
Essa é a parte que separa um script amador de um sistema profissional. Rotação automática significa que suas chaves se renovam sozinhas antes de expirar — e você só é notificado se algo der errado.
def check_expiring_secrets(conn, days_threshold: int = 7) -> list:
"""Retorna segredos que expiram nos próximos `days_threshold` dias."""
threshold = datetime.now() + timedelta(days=days_threshold)
cursor = conn.execute(
"""SELECT id, service, secret_key, expires_at, status
FROM secrets
WHERE expires_at < ? AND status = 'active'
ORDER BY expires_at ASC""",
(threshold,)
)
return cursor.fetchall()
def rotate_secret(conn, service: str, key_name: str, new_value: str):
"""Rotaciona um segredo: armazena o novo valor, loga o antigo."""
master_key = get_encryption_key()
cursor = conn.execute(
"SELECT id, secret_value FROM secrets WHERE service=? AND secret_key=?",
(service, key_name)
)
row = cursor.fetchone()
if not row:
raise KeyError(f"Segredo não encontrado: {service}/{key_name}")
secret_id, old_encrypted = row
old_hash = hashlib.sha256(old_encrypted.encode()).hexdigest()[:16]
new_encrypted = encrypt_value(new_value, master_key)
expires = datetime.now() + timedelta(days=90)
conn.execute("""
UPDATE secrets
SET secret_value = ?, expires_at = ?, rotation_count = rotation_count + 1,
last_rotated_at = CURRENT_TIMESTAMP
WHERE service = ? AND secret_key = ?
""", (new_encrypted, expires, service, key_name))
conn.execute("""
INSERT INTO rotation_log (secret_id, old_hash, reason)
VALUES (?, ?, 'manual')
""", (secret_id, old_hash))
conn.commit()
print(f"[↻] Segredo {service}/{key_name} rotacionado.")
Passo 4: O Timer do Systemd — Executando Tudo Sozinho
Agora vem a mágica que faz isso funcionar sem você precisar lembrar de nada. Um systemd timer que roda a verificação de expiração e dispara a rotação quando necessário.
Primeiro, crie o serviço:
# /etc/systemd/system/secret-rotator.service
[Unit]
Description=Verifica e rotaciona segredos de API automaticamente
After=network.target
[Service]
Type=oneshot
User=alisson
ExecStart=/usr/bin/python3 /home/alisson/.secrets_vault/rotator.py
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
E o timer correspondente:
# /etc/systemd/system/secret-rotator.timer
[Unit]
Description=Timer para verificação de segredos de API
[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true
RandomizedDelaySec=3600
[Install]
WantedBy=timers.target
O Persistent=true garante que, se o servidor cair, o timer roda assim que voltar. O RandomizedDelaySec evita picos se você tiver múltiplos serviços fazendo coisas parecidas.
sudo systemctl enable --now secret-rotator.timer
systemctl list-timers --all | grep secret
Passo 5: Notificações — Porque Silêncio Não É Tranquilidade
De que adianta rotacionar se ninguém sabe? Integrei com a API do Telegram para notificações automáticas. Quando um segredo expira em breve ou é rotacionado, você recebe:
import requests
TELEGRAM_BOT_TOKEN = "seu:bot_token"
TELEGRAM_CHAT_ID = "seu_chat_id"
def notify_telegram(message: str):
"""Envia notificação via Telegram."""
url = f"https://api.telegram.org/bot{TELEGRAM_BOT_TOKEN}/sendMessage"
payload = {
"chat_id": TELEGRAM_CHAT_ID,
"text": f"🔐 <b>Secret Vault</b>\n{message}",
"parse_mode": "HTML"
}
try:
resp = requests.post(url, json=payload, timeout=10)
resp.raise_for_status()
except requests.RequestException as e:
# Se a notificação falhar, log local pelo menos
with open("/tmp/secret_vault_errors.log", "a") as f:
f.write(f"[{datetime.now()}] Falha ao notificar Telegram: {e}\n")
Detalhe importante: a função de notificação nunca deve quebrar o fluxo de rotação. Se o Telegram cair, a rotação ainda acontece. Log local, segue em frente. Isso é resiliência.
Como Usar na Prática: Exemplo Real
Imagine que você tem uma automação que usa a API da OpenAI. Em vez de hardcodear a chave no código ou num .env que viaja pelo Git:
from secrets_vault import init_db, store_secret, get_secret
# Setup inicial (roda uma vez)
conn = init_db()
store_secret(conn, "openai", "OPENAI_API_KEY", "sk-proj-xyz123...", ttl_days=90)
# Na sua automação (roda todo dia)
conn = init_db()
api_key = get_secret(conn, "openai", "OPENAI_API_KEY")
# Use api_key normalmente
client = OpenAI(api_key=api_key)
Quando o timer detectar que a chave está perto de expirar, ele te notifica. Você gera uma nova no dashboard, roda um comando de rotação, e pronto. Zero downtime. Zero “esqueci de atualizar o serviço X”.
Limitações e Quando Isso Não É Suficiente
Vamos ser honestos: esse sistema é ótimo para uso pessoal e times pequenos. Mas ele não substitui um HashiCorp Vault se você precisa de:
- Auditoria com compliance (SOC 2, PCI-DSS)
- Controle de acesso granular por usuário/role
- Replicação geográfica de segredos
- Dynamic secrets (credenciais geradas sob demanda com TTL de minutos)
Para 95% dos devs que rodam automações próprias, esse cofre SQLite resolve. Para os outros 5% — bem, você já sabe que precisa do Vault. Não precisa de artigo nenhum pra te contar isso.
Próximos Passos: Melhorias Que Você Pode Fazer
Se quiser levar isso além do básico, aqui vão algumas ideias:
- Integração com AWS/GCP: usar KMS para encriptar a chave mestre em vez de salvar em arquivo
- Webhook de rotação: alguns serviços (Cloudflare, Stripe) permitem gerar novas chaves via API — automatize a rotação end-to-end
- Health check endpoint: um route HTTP simples que mostra o status de todos os segredos para monitoramento
- Git-crypt para versionamento: versionar o banco (sem os valores) para ter histórico de metadata
Passo 6: O Script Rotator Completo — Juntando Tudo
Até aqui cobrimos as peças individuais. Agora vamos montar o script que o systemd timer vai executar. Esse é o arquivo rotator.py que amarra banco, encriptação, verificação e notificação num único fluxo:
#!/usr/bin/env python3
"""
Secret Vault Rotator — Verifica segredos expirando e notifica.
Executado pelo systemd timer secret-rotator.timer
"""
import sqlite3
import sys
from pathlib import Path
from datetime import datetime, timedelta
from cryptography.fernet import Fernet
DB_PATH = Path.home() / ".secrets_vault" / "segredos.db"
KEY_FILE = Path.home() / ".secrets_vault" / "master.key"
def main():
if not DB_PATH.exists():
print("[!] Banco de segredos nao encontrado. Execute init_db primeiro.")
sys.exit(1)
conn = sqlite3.connect(str(DB_PATH))
threshold = datetime.now() + timedelta(days=7)
cursor = conn.execute(
"SELECT service, secret_key, expires_at, rotation_count "
"FROM secrets WHERE expires_at < ? AND status = 'active' "
"ORDER BY expires_at ASC",
(threshold,)
)
expiring = cursor.fetchall()
if not expiring:
print("[OK] Nenhum segredo expirando nos proximos 7 dias. Tudo limpo.")
return
print("[ALERTA] " + str(len(expiring)) + " segredo(s) expirando em breve:")
for svc, key, expires, count in expiring:
days_left = (datetime.fromisoformat(expires) - datetime.now()).days
print(" - " + svc + "/" + key + ": expira em " + str(days_left) + " dias")
conn.close()
if __name__ == "__main__":
main()
Esse script é intencionalmente idempotente: se rodar dez vezes, o resultado é o mesmo. Não altera estado — apenas informa. A rotação real você faz manualmente (ou automatiza com a API do serviço, quando disponível). Essa separação é crucial: notificação é automática, rotação é deliberada.
Boas Práticas de Segurança Que Separam Amadores de Profissionais
Depois de montar esse sistema, aqui vão lições que aprendi na marra — e que economizam horas de dor de cabeça:
1. Nunca logue valores de segredos
Parece óbvio, mas já vi log com print da chave inteira em produção. Se você precisa debugar, logue o hash ou os últimos 4 caracteres: api_key[-4:]. Nada mais.
2. Rode o vault sob um usuário dedicado
Se o script roda como root, qualquer vulnerabilidade em qualquer processo do servidor dá acesso aos seus segredos. Crie um usuário secrets-runner com permissões mínimas e rode tudo por lá.
3. Faça backup do banco (encriptado, claro)
Se o servidor queimar, você perde o banco SQLite e a chave mestre — e com eles, o acesso a todos os serviços. Backup automático do diretório ~/.secrets_vault/ num volume externo resolve.
4. Teste a restauração
Backup sem teste de restore é como não ter backup. Uma vez por mês, restore o banco num ambiente de teste e verifique se consegue decriptar um segredo. Se não consegue, seu backup é inútil.
5. Documente o processo de emergência
Quando tudo dá errado às 3h da manhã, ninguém quer ler código fonte. Tenha um EMERGENCY.md no diretório do vault com: como gerar uma nova chave mestre, como restaurar do backup, como invalidar todas as chaves de uma vez.

Comparação Rápida: Por Que Não Usar Variáveis de Ambiente?
Muita gente vai dizer: “Ué, mas .env já resolve.” E funciona — para três chaves e um projeto. Quando você escala, o .env vira:
| Critério | .env files | Secret Vault SQLite |
|---|---|---|
| Encriptação em repouso | Não | Sim (Fernet) |
| Histórico de rotação | Não | Log completo |
| Alerta de expiração | Manual | Automático |
| Acesso centralizado | Espalhado | Único ponto |
| Permissão por processo | Mesmo arquivo | Query seletiva |
| Complexidade | Zero | Baixa |
O vault não é overkill — é o mínimo viável quando você passa de “brincando com automação” para “dependo dessas automações pra viver”.
Conclusão: Segurança Não Precisa Ser Complexa
O maior erro que vejo gente cometer é pensar que segurança exige infraestrutura enterprise. Na prática, o que protege seus dados é consistência — e um sistema simples que roda todo dia vale mais que um vault caro que ninguém configurou direito.
Esse gerenciador de segredos resolve o problema real de quem automa: chaves que expiram, que vazam, que se multiplicam sem controle. Com SQLite, Python e systemd, você tem rotação automática, histórico de alterações e notificações. Sem depender de ninguém.
Agora me diz aqui nos comentários: qual automação de segurança você quer ver no próximo artigo? Monitoramento de portas abertas? Scan de vulnerabilidades automatizado? Detecção de intrusão leve com logs do SSH? Sugere aí que eu monto.
Se esse post te ajudou, dá uma olhada na nossa categoria Fortaleza Digital — tem mais conteúdo sobre proteger suas automações sem depender de serviço caro. E se curtiu, compartilha com aquele dev que ainda guarda chaves de API em comentário no código.
