Enrollment e Identidade
A operacao mais sensivel do Tatuzim e o bootstrap de um novo agent: ele chega sem nada, e precisa receber uma identidade criptograficamente verificavel pra participar do sistema.
O problema
Como dar uma identidade autenticada pra um VPS recem-provisionado, sem:
- Pre-distribuir certificados (logistica complexa, replay)
- Compartilhar senhas (vazam, ficam em logs)
- Confiar em hostnames ou IPs (spoofable)
A solucao: token + step-ca
O fluxo combina token one-time (autenticacao no enrollment) com CSR localmente gerado (chave privada nunca sai do agent).
Fluxo detalhado
Admin no hub:
tatuzim token create --hostname vps-mautic-01 --role mautic --ttl 1h
┌─────────────────────────────────────────────┐
│ 1. Gera 32 bytes random URL-base64 │
│ Token = "RIAHMgn_HSp5...kdzZUvQ" │
│ │
│ 2. Hash = SHA256(token) │
│ │
│ 3. INSERT enrollment_tokens ( │
│ token_hash, │
│ expected_hostname="vps-mautic-01", │
│ expected_role="mautic", │
│ expires_at=now()+1h │
│ ) │
│ │
│ 4. Imprime token (cleartext mostrado 1x) │
└─────────────────────────────────────────────┘
│
▼
Admin entrega token via canal seguro
(Implant, scp, password manager, etc.)
│
▼
Operador no VPS:
export TATUZIM_ENROLL_TOKEN="RIAHMgn_HSp5...kdzZUvQ"
export TATUZIM_AGENT_HOSTNAME="vps-mautic-01"
export TATUZIM_AGENT_ROLE="mautic"
tatuzim-agent enroll
┌─────────────────────────────────────────────┐
│ 1. Gera keypair ECDSA P-256 (local) │
│ 2. Cria CSR (CN=vps-mautic-01) │
│ 3. POST https://hub/v1/enroll │
│ Body: {token, hostname, role, csr_pem} │
└─────────────────────────────────────────────┘
│
▼
Server recebe:
┌─────────────────────────────────────────────┐
│ 1. hash = SHA256(req.token) │
│ 2. SELECT enrollment_tokens WHERE hash=... │
│ 3. Valida: │
│ - Token existe? → 401 │
│ - consumed_at IS NULL? → 401 │
│ - expires_at > now()? → 401 │
│ - expected_hostname == req? → 403 │
│ - expected_role == req? → 403 │
│ │
│ 4. POST step-ca /1.0/sign │
│ Body: { csr, ott (JWT do JWK provis.) } │
│ → cert assinado │
│ │
│ 5. INSERT agentes (id, hostname, cert, ...) │
│ 6. UPDATE enrollment_tokens │
│ SET consumed_at=now() │
│ WHERE id=... │
│ │
│ 7. Response: {agent_id, cert_pem, ca_pem} │
└─────────────────────────────────────────────┘
│
▼
Agent persiste:
┌─────────────────────────────────────────────┐
│ /var/lib/tatuzim-agent/identity/ │
│ agent.crt (leaf cert, 0644) │
│ agent.key (private key local, 0600) │
│ ca.pem (intermediate CA, 0644) │
└─────────────────────────────────────────────┘Por que cada decisao
| Decisao | Por que |
|---|---|
| Token = 32 bytes random | 256 bits entropia — bruteforce infeasivel |
| URL-base64 (sem padding) | Copiavel em URLs/env sem escape |
| Apenas SHA256 no DB | Logs vazando token = ainda nao usavel |
| Single-use | Replay zero — mesmo token nao funciona 2x |
| TTL curto (1h default) | Janela curta pra atacante interceptar |
| Vinculacao a hostname+role | Token de "vps-01" nao serve pra "vps-02" |
| CSR localmente gerado | Chave privada nunca sai do agent |
| step-ca emite o cert | PKI hierarquica, root offline |
Identidade resultante
Apos enroll bem-sucedido, o agent tem tres arquivos:
/var/lib/tatuzim-agent/identity/
├── agent.crt # leaf cert: CN=hostname, signed by intermediate CA
├── agent.key # ECDSA P-256 private key
└── ca.pem # intermediate CA cert (pra chain mTLS)E uma entrada no DB do server:
SELECT * FROM agentes WHERE hostname = 'vps-mautic-01';
-- id: 00000000-0000-...
-- hostname: vps-mautic-01
-- role: mautic
-- identity_cert_pem: -----BEGIN CERTIFICATE-----...
-- identity_serial: 0123456789abcdef0123456789abcdef
-- enrollment_token_id: 00000000-0000-...
-- enrolled_at: 2026-05-23 22:14:38mTLS apos enrollment
Da segunda chamada em diante, o agent usa o cert recebido pra mTLS:
curl --cert agent.crt --key agent.key --cacert root_ca.pem \
https://hub:8443/v1/manifestNa porta 8443, o server tem WebPkiClientVerifier que:
- Verifica chain do cert do cliente termina na CA configurada
- Extrai CN do client cert
- Middleware busca
find_agente_by_hostname(cn)no DB - Se nao existe ou revogado → 401
- Se ok → injeta
Agentecomoaxum::Extensionno handler
Handlers entao recebem o agente autenticado e podem fazer logica baseada na identidade.
Renovacao
Cert do agent vai expirar (24h por default do step-ca). O loop tatuzim-agent run faz check periodico:
// pseudo-codigo
loop {
let validity = read_cert_validity(cert_path);
if validity.not_after - now() < renewal_threshold { // default 6h
let (old, new) = perform_renewal(stepca_via_mtls);
rebuild_mtls_client(); // proximas chamadas usam novo cert
emit_event("cert_renewed", {old_serial, new_serial});
}
poll_manifest();
sleep(poll_interval);
}Renovacao tambem disponivel manualmente:
tatuzim-agent rotateRevogacao
Server pode marcar agente como revogado:
UPDATE agentes SET revoked_at = now() WHERE hostname = 'vps-comprometido';(Endpoint admin pra isso ainda nao implementado — fazer via CLI offline por enquanto.)
Apos revogacao:
- Proxima request mTLS do agent retorna 401
agent_revoked - Cert atual continua valido criptograficamente (TLS aceita), mas server rejeita logicamente
- Agente revogado deve ser reenrolado com novo cert
Pra revogacao mais forte (rejeitar no TLS handshake), precisaria CRL ou OCSP — fora de escopo do MVP.
Diferenca: identidade de longo prazo vs uso (planejado v0.4)
Atualmente o cert do enroll e o cert de uso (lifetime ~24h). Pra alta seguranca, padrao SPIFFE:
| Tipo | Lifetime | Uso |
|---|---|---|
| Identidade-raiz | Longa (validade do agent) | Usada apenas pra requisitar identidades-de-uso |
| Identidade-de-uso | Curta (minutos) | Usada em todas as chamadas mTLS |
Vazamento de identidade-de-uso = janela de minutos. Vazamento de identidade-raiz = re-enroll necessario.
MVP nao implementa essa separacao; v0.4 planejada.