The two binaries
| Binary | Role |
|---|---|
certplane-broker | Central HTTPS API. Authenticates agents over mTLS, enforces policy, talks to the configured ACME issuer, caches issued certificates in a SQLite or file store, and records audit events. |
certplane-agent | One-shot binary on each managed host. Enrolls with step-ca once, then on every invocation renews the identity certificate (if needed), generates or reuses service keys locally, submits CSRs to the broker, installs returned bundles, and runs reload hooks. |
-c / --config.
Phase 1: Enrollment
Enrollment happens once per host. It binds the host to its machine identity.Place a bootstrap token
An operator (or an Ansible play) writes a short-lived
step-ca bootstrap token to the path set in identity.bootstrap_token.Run `certplane-agent ... enroll`
The agent acquires a file lock at
state_dir/agent.lock, then:- Generates a local ECDSA private key at
identity.key(if missing). - Builds a CSR with
CN = identity.name. - Calls the
step-caprovisioner endpoint atidentity.step_ca.url, presenting the CSR and the bootstrap token. The server’s TLS certificate is verified using eitheridentity.step_ca.fingerprintoridentity.step_ca.root_ca_bundle. - Writes the returned certificate to
identity.certand deletes the bootstrap token file so it cannot be reused.
Phase 2: Renewal loop
Everycertplane-agent ... run invocation does the following:
Identity renewal
If the identity certificate expires within
identity.renew_before (default 8h), the agent rekeys (or reuses, depending on configuration) with step-ca and writes a fresh identity.crt. If less than identity.warn_before remains but no renewal is yet due, the agent logs a warning.For each service certificate
For every entry under
certificates[]:- Ensure a local service key at
key. The agent generates an ECDSA P-256 key if the file is missing and reuses it otherwise. Keys never leave the host. - Check the existing certificate at
cert. If it parses, matches the key, and is not yet withinrenew_beforeof expiry (default720h), the agent logscertificate skipped, not in renewal windowand moves on. - Build a CSR containing exactly the configured
dns_names. - Call the broker. The agent opens an mTLS connection to
broker.url, presentingidentity.certas the client certificate and trustingbroker.server_ca_bundlefor the broker’s TLS. It POSTs{profile, csr_pem}to/v1/certificates. - Validate the bundle. When the broker returns
cert_pem,chain_pem, andfullchain_pem, the agent verifies the leaf matches the local key and contains exactly the requested DNS names — never trusting the broker blindly. - Install atomically. The agent writes
cert,chain, andfullchainto disk. - Reload. If
reload_commandis set, the agent runs it with timeoutreload_timeout(default30s) and logs the output.
What the broker does on /v1/certificates
When the broker receives an issuance request:
- Authenticate. TLS terminates with
tls.RequireAndVerifyClientCertagainstserver.mtls.agent_ca_bundle. The identity is the client certificate’s CN. - Authorize. The broker looks up the identity in the current policy (
policy.path), confirms the requestedprofileis listed underhosts.<key>.profiles, and validates the CSR’s DNS names against the profile’sdns_names. - Rate-limit. Per-identity and per-(identity, profile) hourly limits from
rate_limitsare enforced. - Serve from cache or issue. If a cached certificate for
(profile, CSR fingerprint)exists and is not within the profile’srenew_beforewindow, it is returned. Otherwise the broker calls the configured ACME directory (Let’s Encrypt by default) with the profile’s challenge type, fetches the certificate, stores the bundle, and returns it. - Audit. Every decision — allow or deny, plus broker lifecycle events — is recorded via the audit recorder. Inspect with
certplane-broker ... audit tail.
What never crosses the wire
- Service private keys. Always generated and rotated on the host using them.
- DNS provider credentials. Stay at the broker. Agents have no knowledge of how the broker satisfies an ACME challenge.
- ACME account keys. Live on the broker only.