Skip to content

Certificate

A Certificate manages the lifecycle of a single X.509 certificate signed by a CertificateAuthority.

Example

apiVersion: openvox.voxpupuli.org/v1alpha1
kind: Certificate
metadata:
  name: production-cert
spec:
  authorityRef: production-ca
  certname: puppet
  dnsAltNames:
    - puppet
    - production-ca

Spec

Field Type Default Description
authorityRef string required Reference to the CertificateAuthority
certname string required Certificate common name (CN)
dnsAltNames []string - DNS subject alternative names
renewBefore string 60d Duration before expiration when the certificate should be renewed (e.g. 60d, 720h)
csrExtensions CSRExtensionsSpec - Puppet CSR extension attributes to embed in the CSR

Status

Field Type Description
phase string Current lifecycle phase
secretName string Name of the Secret containing cert.pem and key.pem
notAfter time Expiry time of the signed certificate
conditions []Condition CertSigned

Deletion

Deleting a Certificate revokes it on the CA before the finalizer (openvox.voxpupuli.org/certificate-cleanup) is released, so the certificate cannot be used again. If the CA cannot be reached the controller retries a few times and then releases the finalizer anyway, rather than leaving the resource stuck in Terminating.

Revocation is skipped when the CertificateAuthority is itself being deleted -- there is nothing left to revoke against, and its Service is on its way out. This is what lets kubectl delete namespace finish promptly: every object is marked for deletion at the same moment.

Phases

Phase Description
Pending Waiting for CertificateAuthority to reach Ready or External
Requesting Certificate signing in progress
WaitingForSigning CSR submitted, waiting for CA to sign (backoff polling in progress)
Signed TLS Secret created, Servers can mount it
Renewing A renewal attempt failed and will be retried
Error Certificate signing failed

Phases are a human-readable summary of the observed state. The controller never reads them back as input: whether a certificate needs renewing is recomputed on every reconcile from status.notAfter and spec.renewBefore, so editing or losing the phase does not change what the operator does.

Immutable fields

certname cannot be changed after creation. The name is baked into the issued certificate and into the entry the CA keeps for it; changing it would orphan that entry under the old name, so the finalizer could no longer revoke it on deletion.

To use a different certname, delete the Certificate and create a new one -- the finalizer cleans up the old entry on the CA on the way out.

Re-signing on spec changes

status.signedSpecHash records the spec fields the current certificate was issued for: certname, dnsAltNames and csrExtensions. When any of them changes, the certificate is re-signed on the next reconcile -- adding a DNS alt name takes effect immediately instead of waiting for the next renewal.

renewBefore is not part of the hash. It only moves the point in time at which renewal happens and does not change the certificate itself.

Certificates issued before this field existed carry an empty hash. The controller adopts the current spec as the baseline for them rather than re-signing every certificate after an operator upgrade.

CSR Poll Backoff

When the CA does not immediately sign the CSR (e.g. autosigning is disabled), the controller enters WaitingForSigning after 10 unsuccessful poll attempts and retries with exponential backoff:

Attempts Interval
0-2 5s
3-5 30s
6-9 2m
10+ 5m

The poll attempt count is tracked via the annotation openvox.voxpupuli.org/csr-poll-attempts on the pending Secret {name}-tls-pending. To resolve manually, sign the pending CSR on the CA server (for example via a matching SigningPolicy or the CA's signing API); the controller picks it up on the next poll.

CSR Extensions

The csrExtensions field allows embedding Puppet CSR extension attributes in the certificate signing request. These extensions are used by the Puppet CA for authorization and metadata.

Field Type Description
ppCliAuth bool Adds the pp_cli_auth extension, granting CA API signing rights
ppRole string Sets the pp_role extension value
ppEnvironment string Sets the pp_environment extension value
customExtensions map[string]string Arbitrary pp_* extensions as name/value pairs

Keys in customExtensions must be known Puppet extension names (e.g. pp_cost_center, pp_department). The dedicated fields (ppCliAuth, ppRole, ppEnvironment) must not be duplicated in customExtensions.

Example

apiVersion: openvox.voxpupuli.org/v1alpha1
kind: Certificate
metadata:
  name: operator-signing
spec:
  authorityRef: production-ca
  certname: production-ca-operator
  csrExtensions:
    ppCliAuth: true
    ppRole: compiler
    ppEnvironment: production
    customExtensions:
      pp_cost_center: IT
      pp_department: Engineering

Signing Strategy

The controller uses two paths to obtain a signed certificate:

Strategy Condition How it works
CA setup export Certificate created before/with CA CA setup Job creates the CA AND exports the server cert+key as a TLS Secret. The Certificate controller adopts the Secret.
HTTP signing Certificate created after CA is Ready Operator generates an RSA key pair in-process, submits a CSR to the Puppet CA HTTP API, and polls for the signed certificate. No Jobs or shell scripts involved.
flowchart TD
    Start["Certificate created"] --> CheckCA{"CA Secret exists?"}
    CheckCA -->|No| Wait["Phase: Pending<br/>Wait for CA"]
    Wait --> CheckCA

    CheckCA -->|Yes| CheckTLS{"TLS Secret<br/>already exists?"}
    CheckTLS -->|Yes| Adopt["Adopt Secret<br/>(CA setup export path)"]
    Adopt --> Signed

    CheckTLS -->|No| FindSvc["Discover CA Service<br/>(Server ca:true → Pool → Service)"]
    FindSvc --> GenKey["Generate RSA 4096 key pair"]
    GenKey --> CSR["Submit CSR via<br/>PUT /puppet-ca/v1/certificate_request"]
    CSR --> Poll["Poll for signed cert<br/>GET /puppet-ca/v1/certificate"]
    Poll --> CreateSecret["Create TLS Secret<br/>cert.pem + key.pem"]
    CreateSecret --> Signed["Phase: Signed"]

The controller discovers the CA Service automatically by finding Servers with ca: true in the same Config and the Pools whose selector matches them.

Certname uniqueness

A certname identifies exactly one entry on the CertificateAuthority. Two Certificates that claim the same certname against the same CA are indistinguishable to it, with two consequences: the second CSR is rejected, and deleting either one revokes the entry both rely on.

The operator therefore refuses the collision in three places:

  • the admission webhook rejects a duplicate at creation, naming the other resource
  • the controller refuses to sign and reports CertnameConflict, since webhooks are disabled by default
  • a certificate returned by the CA is verified against the private key it was requested for, so a foreign certificate under the same name is never stored

certname is required and has no default. It used to default to puppet, which made two Certificates created without one collide by default rather than by mistake, and puppet is only the right identity for the main server anyway -- PuppetDB and any further certificate need their own.

The names agents connect through, such as a load balancer or a service address, belong in dnsAltNames rather than here. The chart derives the Service names of every Pool a server joins into that list automatically.

Created Resources

Resource Name Description
Secret {name}-tls Certificate data: cert.pem, key.pem