Skip to content

Feature request: puppetserver ca extend to re-sign an expiring or expired CA certificate in place #56

Description

@miharp

Problem

The CA certificate that puppetserver ca setup creates is valid for 15 years by default, or for ca_ttl if that is set before setup runs. When it expires, every TLS connection in the deployment fails at once. Recovering does not require a new CA: re-signing the existing CA certificate with the same key, subject, and serial gives it a new validity period, and every host certificate the CA ever issued stays valid because the signing key has not changed. Only the CA certificate file changes, and the new copy then has to reach agents and any service that pins the CA bundle.

Today there is no supported way to do this with OpenVox tooling. The options are:

  • A hand-run openssl x509 -x509toreq / openssl x509 -req -signkey sequence, which the OpenVox docs describe for the single-certificate layout only. The default layout is a two-certificate bundle (intermediate signing cert plus root), where the root must be re-signed with root_key.pem and the intermediate must be re-signed by the root, then the bundle rebuilt in order. That is easy to get wrong, and a mistake breaks authentication for the whole deployment.
  • The puppetlabs/ca_extend module of plans and tasks. It works with open source servers and handles both bundle layouts, but it is GPL-2.0-only, its metadata caps Puppet below 9.0.0, its last release was v3.5.0 in October 2023, and it is built around Puppet Enterprise (its primary configuration task runs puppet infrastructure configure). It also requires an orchestration runner on an operator workstation with SSH access to the CA host, which is more machinery than the actual re-signing step needs.

This gem is the right home for the re-signing step. It already builds the root and intermediate layout in LocalCertificateAuthority#create_root_cert and #create_intermediate_cert, holds CA_EXTENSIONS, knows every file location from the loaded Puppet settings (cacert, cakey, rootkey, cacrl, localcacert), and already has the offline pattern with the check_server_online guard that generate --ca-client uses. A subcommand here reaches every install with no extra dependency.

Proposed behaviour

puppetserver ca extend [--config PATH] [--ttl DURATION] [--force]
  • Refuses to run while the CA service is online, using the same HttpClient.check_server_online check and --force escape hatch as generate --ca-client.
  • Loads ca_crt.pem and identifies each certificate in the bundle by matching its public key against ca_key.pem and, if present, root_key.pem. Refuses when the bundle has a certificate whose key is not on disk, which is the externally issued CA case, with a message saying the external CA must be extended by its owner and imported with puppetserver ca import.
  • For each certificate it owns, builds a new certificate with the same subject, issuer, serial, public key, and extensions (including the existing subjectKeyIdentifier and authorityKeyIdentifier values), not_before set the same way the gem sets it for new certificates, and not_after set to now plus the TTL. Signs the root with the root key and the intermediate with the root key, in that order, so the intermediate's issuer chain still verifies.
  • TTL defaults to the same value setup uses for the CA certificate (ca_ttl, falling back to 15 years), overridable with --ttl in the existing ca_ttl duration format.
  • Re-signs any CRL in the bundle whose next_update has passed, preserving revoked entries and incrementing crlNumber, since a CA that has expired usually has an expired CRL alongside it and the server refuses to start with one. Covers ca_crl.pem and infra_crl.pem.
  • Backs up every file it will overwrite to <name>.pem.<timestamp>.bak in cadir before writing anything, and writes atomically.
  • Writes the new bundle to cacert and localcacert, and the new CRLs to cacrl and hostcrl, with the same modes setup uses.
  • Prints the new expiry of each certificate and a reminder of what is still manual: restart the server, refresh OpenVoxDB's copies (puppetdb ssl-setup -f), and distribute the new bundle to agents and to anything else that pins the CA.
  • Exits non-zero without writing if the CA key does not match any certificate in the bundle, if the bundle has more than two certificates, or if the server is online and --force was not given.

Out of scope

  • Distributing the new CA certificate to agents. That needs reach into the fleet, which a CLI on the CA host does not have. The subcommand prints what still has to happen and leaves the how to the operator.
  • Rotating the CA key. That is a new CA and is already covered by clearing the CA directory and running setup again.
  • Extending an externally issued CA.

Notes for implementation

  • LocalCertificateAuthority#self_signed_ca and #sign_intermediate are close to what is needed but always mint a new serial and a fresh key. The extend path must copy serial and public key from the existing certificate, so it is probably a new method that takes an OpenSSL::X509::Certificate and a signing key rather than a CSR.
  • The Puppet Server Internal Certificate Netscape comment and the other CA_EXTENSIONS entries are already present on every certificate the gem created, so copying the existing extension list is enough. Copying rather than regenerating also keeps a CA that was created by an older release or by puppet cert intact.
  • Worth a --dry-run that prints what would change, given the blast radius.
  • Docs: the "Extend an expired CA certificate" section of the OpenVox Server certificate renewal page currently walks through the openssl sequence and points at ca_extend. It should switch to this subcommand once released, and can then drop the single-layout caveat.

Assisted by Claude.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions