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:38

mTLS 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/manifest

Na porta 8443, o server tem WebPkiClientVerifier que:

  1. Verifica chain do cert do cliente termina na CA configurada
  2. Extrai CN do client cert
  3. Middleware busca find_agente_by_hostname(cn) no DB
  4. Se nao existe ou revogado → 401
  5. Se ok → injeta Agente como axum::Extension no 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 rotate

Revogacao

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.

Referencias

By Borlot.com.br on 23/05/2026