feat(server): agent CA, enrollment token, CSR-signing endpoint (VS-63) #83
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/VS-63-agent-ca-enrollment"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Add the server side of token-bootstrapped agent enrollment, replacing the
*.vagentmint + cert-pin handshake with a dedicated agent CA that issues mTLS client certs from agent CSRs.vervain-pki: new
agent_camodule.ensure_agent_cagenerates (or reuses) a self-signed ECDSA P-256 agent CA (IsCa,KeyCertSign), persisted under<datapath>/agent-ca-cert-{public.crt,private.key}, distinct from the web-serving root fromensure_chain.AgentCa::sign_csrcertifies the CSR's OWN public key (it does NOT regenerate a throwaway keypair, which is roci'ssign_csrbug) and pins the leaf authoritatively: CN/SAN = node id from the CSR subject,ExtendedKeyUsage = ClientAuth, not a CA, validity bounded to 1 year.vervain-db: new
enroll_tokenstable (schema v2) plusinsert_enroll_tokenandconsume_enroll_token. Consumption is a single atomicUPDATE ... WHERE consumed_at IS NULL AND expires_at > nowwhose affected-row count is the accept/reject decision, so a consumed, expired, or replayed token cannot succeed twice.vervain-web: new
enrollmodule wiring two routes.POST /api/v1/enroll-tokens(site-admin session) mints a random opaque single-use token with a 1-hour TTL and persists it.POST /api/v1/enroll(public, no client cert, since the enrolling agent has none yet) takes{ token, csr_pem }, atomically consumes the token, signs the CSR with the agent CA, and returns{ agent_cert_pem, ca_chain }whereca_chainis the server root the agent must trust to verify the server's TLS cert. The agent CA is threaded throughServerConfig/AppStateand ensured at boot inmain.rs.Tests: agent-CA unit tests (self-signed/IsCa, persistence/reuse, distinct-from-web-root, certifies-the-CSR's-own-key, bounded validity, garbage rejected); db single-use/expired/unknown-token tests; and an end-to-end
tests/enroll.rscovering admin mint, public redeem returning both PEM blocks, single-use rejection, and the 401/403 auth gates.Scope note: acceptance criterion 5 (remove the
AuthRequest/AuthVerify/ServerIdhandshake from the server) is intentionally NOT done here. It is mutually coupled with VS-62 (the agent mTLS listener), which is still open: VS-62 builds itsWebPkiClientVerifierfrom this CA, and the handshake removal needs VS-62's listener to supply agent identity (the mTLS client cert) once the handshake is gone. Removing the handshake now would leave agent authentication with no replacement and break currently-working agent connections, since VA-67 (the agent client) also still speaks the handshake. The removal should land together with VS-62; that dependency is already linked from this issue.#VS-63
Add the server side of token-bootstrapped agent enrollment, replacing the `*.vagent` mint + cert-pin handshake with a dedicated agent CA that issues mTLS client certs from agent CSRs. vervain-pki: new `agent_ca` module. `ensure_agent_ca` generates (or reuses) a self-signed ECDSA P-256 agent CA (`IsCa`, `KeyCertSign`), persisted under `<datapath>/agent-ca-cert-{public.crt,private.key}`, distinct from the web-serving root from `ensure_chain`. `AgentCa::sign_csr` certifies the CSR's OWN public key (it does NOT regenerate a throwaway keypair, which is roci's `sign_csr` bug) and pins the leaf authoritatively: CN/SAN = node id from the CSR subject, `ExtendedKeyUsage = ClientAuth`, not a CA, validity bounded to 1 year. vervain-db: new `enroll_tokens` table (schema v2) plus `insert_enroll_token` and `consume_enroll_token`. Consumption is a single atomic `UPDATE ... WHERE consumed_at IS NULL AND expires_at > now` whose affected-row count is the accept/reject decision, so a consumed, expired, or replayed token cannot succeed twice. vervain-web: new `enroll` module wiring two routes. `POST /api/v1/enroll-tokens` (site-admin session) mints a random opaque single-use token with a 1-hour TTL and persists it. `POST /api/v1/enroll` (public, no client cert, since the enrolling agent has none yet) takes `{ token, csr_pem }`, atomically consumes the token, signs the CSR with the agent CA, and returns `{ agent_cert_pem, ca_chain }` where `ca_chain` is the server root the agent must trust to verify the server's TLS cert. The agent CA is threaded through `ServerConfig`/`AppState` and ensured at boot in `main.rs`. Tests: agent-CA unit tests (self-signed/IsCa, persistence/reuse, distinct-from-web-root, certifies-the-CSR's-own-key, bounded validity, garbage rejected); db single-use/expired/unknown-token tests; and an end-to-end `tests/enroll.rs` covering admin mint, public redeem returning both PEM blocks, single-use rejection, and the 401/403 auth gates. Scope note: acceptance criterion 5 (remove the `AuthRequest`/`AuthVerify`/`ServerId` handshake from the server) is intentionally NOT done here. It is mutually coupled with VS-62 (the agent mTLS listener), which is still open: VS-62 builds its `WebPkiClientVerifier` from this CA, and the handshake removal needs VS-62's listener to supply agent identity (the mTLS client cert) once the handshake is gone. Removing the handshake now would leave agent authentication with no replacement and break currently-working agent connections, since VA-67 (the agent client) also still speaks the handshake. The removal should land together with VS-62; that dependency is already linked from this issue. #VS-63