Skip to content

Support wire-compatible auto-renewal (no CSR) on /certificate_renewal#113

Open
bootc wants to merge 1 commit into
mainfrom
feature/auto-renew-wire-compat
Open

Support wire-compatible auto-renewal (no CSR) on /certificate_renewal#113
bootc wants to merge 1 commit into
mainfrom
feature/auto-renew-wire-compat

Conversation

@bootc

@bootc bootc commented Jul 12, 2026

Copy link
Copy Markdown
Member

Real Puppet/OpenVox agents renew their client certificate by default (hostcert_renewal_interval, default 30d) by POSTing an empty body to /certificate_renewal and relying solely on the mTLS-presented client certificate to prove identity and key possession — they never send a CSR, and expect the SAME key reissued with a fresh serial and validity. Our existing CSR-only handler rejected every such request with a 400, so a stock agent's automatic renewal silently never worked against openvox-ca (the agent logs a warning and keeps using its current cert).

This adds CA.AutoRenew, matching OpenVox Server's own Clojure CA (renew-certificate! in certificate_authority.clj): it builds the new certificate directly from the already-issued certificate presented over mTLS — no CSR lookup at all, so it also covers certificates whose CSR was deleted after signing (the normal case) or that arrived via migration import with no CSR ever recorded. It carries forward the original certificate's SANs and Puppet OID extensions, and reissues under the same key.

POST /certificate_renewal now branches on an empty body to this path; a non-empty body keeps using the existing CSR re-key path unchanged.

The internals of signWithDuration were split so the "build template, sign, persist" tail (issueLeafLocked) is shared between the CSR path and AutoRenew.

Real-agent regression test: added Group 3b to test/puppet/puppet-stack.sh, which drives the actual puppet ssl renew_cert CLI action (the same code path a stock agent's automatic renewal takes) against a live openvox-ca instance in the compose-puppet stack, and asserts: exit 0, a new serial, the same public key, and that the renewed cert still works for a normal agent run. This is deliberately end-to-end with the real openvox-agent package rather than a Go-mocked TLS cert, since the bug this PR fixes is specifically a wire-format mismatch that unit tests alone would not catch.

Scope note: OpenVox Server's Clojure CA does not revoke the certificate being replaced during this kind of renewal — both the old and new certs (same key) stay valid until the old one naturally expires. This PR preserves that behaviour to match. A follow-up PR (#114, based on this branch) adds revocation of the superseded certificate as a configurable, secure-by-default option.

🤖 Generated with Claude Code

Real Puppet/OpenVox agents renew their client certificate by default
(hostcert_renewal_interval, default 30d) by POSTing an empty body to
/certificate_renewal and relying solely on the mTLS-presented client
certificate to prove identity and key possession; they expect the SAME
key reissued with a fresh serial and validity. Our CSR-only handler
rejected every such request with 400, so this endpoint silently never
worked against a stock agent.

Add CA.AutoRenew, matching OpenVox Server's own Clojure CA
(renew-certificate! in certificate_authority.clj): it builds the new
certificate straight from the already-issued certificate presented over
mTLS (no CSR lookup at all, so it also covers certs whose CSR was
deleted after signing or that arrived via migration import), carrying
forward SANs and Puppet OID extensions, and reissues under the same
key. Extract the shared "build template, sign, persist" tail out of
signWithDuration into issueLeafLocked so both paths share it.

/certificate_renewal now branches on an empty body to this path,
keeping the existing CSR re-key path for non-empty bodies.

Add a real-agent regression test (Group 3b in
test/puppet/puppet-stack.sh) that drives `puppet ssl renew_cert`
against a live openvox-ca instance, since this is exactly the code path
real agents exercise and unit tests alone would not catch a wire-format
mismatch.
Comment thread internal/ca/signing.go Dismissed
Comment thread internal/ca/signing.go Dismissed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants