Gerenciador de segredos API e rotação automática de chaves em Python - automação de segurança

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.

Infraestrutura de servidor para armazenamento seguro de segredos e chaves de API
Servidor protegido: onde seus segredos de API realmente moram

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:

  1. Um banco SQLite armazena os segredos com metadata (serviço, data de criação, TTL, status)
  2. Um módulo Python faz CRUD + validação + rotação automática
  3. Um systemd timer dispara a verificação de expiração a cada X horas
  4. 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 valor
  • rotation_log manté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 600 na 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)
Rotina de rotação de chaves de API automatizada com Python e systemd timer
Automação em ação: systemd timer executando rotação de chaves

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.

Infraestrutura de servidor para armazenamento seguro de segredos e chaves de API
Servidor protegido: onde seus segredos de API realmente moram

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.

Posts Similares