feat(server): enroll agents over the mTLS port via a dedicated enrollment sub-CA (VS-68) #87
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/VS-68-enroll-over-mtls"
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?
Lets agents bootstrap and enroll over the single agent mTLS port, never touching the browser/HTTPS frontend. Prerequisite for VA-67. Greenfield, so VS-63's browser-path token POST is replaced outright.
Design
A dedicated enrollment sub-CA (
vervain-pki/src/enrollment_ca.rs) signed by the agent CA, so an enrollment-issued client cert chains to the same root the agent listener'sWebPkiClientVerifieralready trusts (no verifier change needed). Real agent leaves are issued directly by the agent CA (AGENT_CA_CN=VervainAgentCA); bootstrap leaves by the sub-CA (ENROLLMENT_CA_CN=VervainEnrollmentCA). The leaf issuer is what separates them; single-use + short TTL contains a leaked bootstrap cert.Flow
POST /api/v1/enroll-tokens, site-admin session, browser listener) returns a bootstrap bundle{enroll_cert_pem, enroll_key_pem, enrollment_ca_pem, server_ca_pem, expires_at}and records a single-use token (carried in the cert CNenroll//<token>) inenroll_tokens. Admins mint from the browser; agents never do.POST /api/v1/enroll) moves onto the agent mTLS listener (build_agent_router). Body is{csr_pem}only; the token rides in the presenting bootstrap cert's CN. The handler requires the client cert to be issued by the enrollment sub-CA, atomically consumes the token, signs the CSR with the agent CA, returns{agent_cert_pem, ca_chain}.Walls
agent_ws_handlerrejects any leaf issued by the enrollment sub-CA (a bootstrap cert cannot be an agent identity).AgentClientCert::issuer_cnbacks both. The enrollment CA is threaded throughServerConfig/AppStateand ensured at boot inmain.rs.Agent contract (for VA-67)
The agent's whole bootstrap input is a domain (host:port) + the bundle above. It connects to the mTLS port with the bootstrap cert (presenting
enrollment_ca_pemin its chain), POSTs its CSR (CN =node//<hash>), persists the returned cert +ca_chain, then reconnects with its real cert and sends oneAuthInfo. It never reaches the browser/HTTPS path.Tests
enroll.rs: mint-bundle contract (200 + bundle shape +enroll//CN; 401/403 gates).agent_mtls.rsend to end: real mTLS enroll (bootstrap cert in, agent cert out signed by the agent CA), single-use rejection, a real agent cert refused at enroll, and a bootstrap cert refused on the control channel.just pre-commitgreen (fmt, clippy-D warnings, build, 411 lib tests + integration).Follow-ups
Server-pushed agent config / group assignment is VS-67; the dead
insecureSkipHashCheckknob is VS-66.🤖 Generated with Claude Code
Lets agents bootstrap and enroll over the single agent mTLS port, never touching the browser/HTTPS frontend. Replaces VS-63's browser-path token POST (greenfield, no compatibility kept). New enrollment sub-CA in vervain-pki (`enrollment_ca.rs`): a dedicated CA signed by the agent CA, so an enrollment-issued client cert chains to the same root the agent listener's `WebPkiClientVerifier` already trusts. `ensure_enrollment_ca` persists it (`enrollment-ca-cert-*`); `mint_enroll_cert` issues a short-lived single-use ClientAuth leaf whose CN is `enroll//<token>`. Real agent leaves are issued directly by the agent CA (`AGENT_CA_CN`); bootstrap leaves by the sub-CA (`ENROLLMENT_CA_CN`). The leaf issuer is what separates the two. Mint (`POST /api/v1/enroll-tokens`, site-admin, browser listener) now returns a bootstrap bundle `{enroll_cert_pem, enroll_key_pem, enrollment_ca_pem, server_ca_pem, expires_at}` instead of an opaque token, and records the single-use token in `enroll_tokens`. Admins mint from the browser; agents never do. Enroll (`POST /api/v1/enroll`) moves onto the agent mTLS listener (`build_agent_router`). The body is `{csr_pem}` only; the token rides in the presenting bootstrap cert's CN. The handler requires the client cert to be issued by the enrollment sub-CA, atomically consumes the token, signs the CSR with the agent CA, and returns `{agent_cert_pem, ca_chain}`. Walls: `agent_ws_handler` rejects any leaf issued by the enrollment sub-CA (a bootstrap cert cannot be an agent identity); the enroll endpoint rejects any cert not issued by the sub-CA (a real agent cert cannot re-enroll). `AgentClientCert::issuer_cn` backs both checks. The enrollment CA is threaded through `ServerConfig`/`AppState` and ensured at boot in `main.rs` alongside the agent CA. Tests: pki unit tests (sub-CA signed by agent CA / persisted / mints a token-CN ClientAuth leaf). `enroll.rs` covers the mint-bundle contract (200 + bundle shape + `enroll//` CN, 401/403 gates). A new `agent_mtls.rs` end-to-end drives a real mTLS enroll (bootstrap cert in, agent cert out signed by the agent CA), single-use rejection, a real agent cert refused at enroll, and a bootstrap cert refused on the control channel. #VS-68 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>