Skip to content

Add mutual-TLS publisher reusing Puppet CA material#5

Merged
miharp merged 1 commit into
mainfrom
feat/mtls
Jul 23, 2026
Merged

Add mutual-TLS publisher reusing Puppet CA material#5
miharp merged 1 commit into
mainfrom
feat/mtls

Conversation

@miharp

@miharp miharp commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Completes the serving half of phase 2, and resolves the open question that was blocking it.

The blocker is resolved: no second PKI

I inspected ovadm::add_compiler and its configure_compiler_ssl task. Every compiler already ends up with, at Puppet's standard paths:

/etc/puppetlabs/puppet/ssl/certs/<certname>.pem
/etc/puppetlabs/puppet/ssl/private_keys/<certname>.pem
/etc/puppetlabs/puppet/ssl/certs/ca.pem
/etc/puppetlabs/puppet/ssl/crl.pem

codavox reuses them and issues nothing. Nothing to provision, distribute, or rotate — and revoking a compiler's Puppet certificate revokes its access to code as a side effect, which is what an operator would expect.

CA membership is not authorization

A certificate signed by the Puppet CA proves only that the peer is some enrolled node. Every agent in the estate clears that bar, and Puppet manifests routinely reference internal hostnames, credential paths, and topology — a compromised leaf node should not be able to read the whole estate's code.

The publisher additionally requires a pp_role extension matching --allow-role. ovadm already writes pp_role: openvox_compiler into csr_attributes.yaml, so compilers carry their role with no extra configuration.

The OID 1.3.6.1.4.1.34380.1.1.13 was verified against openvox lib/puppet/ssl/oids.rb rather than taken from memory.

A security fix gosec caught

The role check initially used VerifyPeerCertificate. gosec flagged that it is skipped entirely on resumed TLS sessions — a peer that completed one handshake could keep reconnecting without the role ever being rechecked.

Moved to VerifyConnection, which runs on resumed connections too. ServerTLS now also refuses to build a configuration with no allowed roles, so the constraint cannot be omitted by accident rather than being something a caller bolts on.

Verified end to end, not just in-process

TestPublishBinaryEndToEnd builds the real binary, lays out SSL material the way ovadm leaves it on a node, starts the publisher, and checks over an actual TLS connection:

sealed production 778ae27f648b4d86d823a51a6ddb5816d237a6ba692f452b6f8a5fa684d3174f
listening on 127.0.0.1:18151 as puppet.example.com (roles: openvox_compiler)
--- PASS: TestPublishBinaryEndToEnd
    --- PASS: TestPublishBinaryEndToEnd/ordinary_agent_is_refused_by_the_running_binary

That covers argument parsing, certificate discovery from certname and ssldir, and the TLS wiring — none of which an in-process handler test would reach.

Design decisions worth noting

Sealing happens on Reseal, not per request. It walks and hashes a whole environment; doing it per request would also let two compilers polling either side of an r10k run observe different ids for one deploy.

The version map is no-store, because polling is the correctness mechanism and a cached response would pin a compiler to a stale version. Artifacts are immutable, because they are content-addressed by the code_id in the URL.

Only the current code_id is servable. Serving arbitrary history would mean keeping every past tree on the publisher; compilers retain old versions themselves, which is what in-flight agent runs actually need.

codavox stages nothing — it reads a directory r10k already populated, keeping the trust boundary small.

Known gap

The publisher seals once at startup. Triggering a reseal after each r10k run is the operator's job for now; a watch mode is planned and noted in the docs.

🤖 Generated with Claude Code

Resolves the open question that blocked this phase. Inspecting
ovadm::add_compiler and its configure_compiler_ssl task confirms every
compiler already holds a certificate signed by the primary's CA, its private
key, the CA certificate and a CRL at Puppet's standard paths. codavox reuses
them and issues nothing: no second PKI to provision, distribute or rotate,
and revoking a compiler's Puppet certificate revokes its access to code as a
side effect.

Authorization needs more than CA membership. A certificate signed by the
Puppet CA proves only that the peer is some enrolled node, and every agent in
the estate clears that bar, while Puppet manifests routinely reference
internal hostnames, credential paths and topology. The publisher therefore
requires a pp_role extension matching --allow-role. ovadm already writes
pp_role: openvox_compiler into csr_attributes.yaml, so no extra configuration
is needed. The OID 1.3.6.1.4.1.34380.1.1.13 was verified against openvox
lib/puppet/ssl/oids.rb rather than taken from memory.

The role check runs in VerifyConnection, not VerifyPeerCertificate. gosec
flagged that the latter is skipped entirely on resumed sessions, so a peer
that completed one handshake could keep reconnecting without the role ever
being rechecked. ServerTLS also refuses to build a configuration with no
allowed roles, so the constraint cannot be omitted by accident.

Sealing happens on Reseal rather than per request: it walks and hashes a whole
environment, and doing it per request would let two compilers polling either
side of an r10k run observe different ids for one deploy. The version map is
served no-store because polling is the correctness mechanism; artifacts are
served immutable because they are content-addressed by the code_id in the URL.
Only the current code_id is servable, since compilers retain old versions
themselves for in-flight runs.

Verified by an end-to-end test that builds the real binary, lays out SSL
material the way ovadm leaves it, and checks over an actual TLS connection
that a compiler is admitted and an ordinary agent is refused.

Signed-off-by: Michael Harp <mike@mikeharp.com>
@miharp
miharp merged commit e2768f1 into main Jul 23, 2026
7 checks passed
@miharp
miharp deleted the feat/mtls branch July 23, 2026 16:54
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.

1 participant