Skip to content

SigningPolicy

A SigningPolicy defines a policy for automatic CSR signing against a CertificateAuthority. Multiple policies can reference the same CA -- if any policy matches, the CSR is signed (OR logic between policies). Within a single policy, all set fields must match (AND logic).

Signing has two planes:

  • Match plane (any, certnames, csrAttributes) -- decides whether a policy applies to a CSR.
  • Guard plane (dnsAltNames, ipAltNames, uriAltNames, emailAltNames, extensions) -- fail-closed constraints that decide whether the CSR is safe to sign. If a CSR carries a SAN type or a privileged authorization extension the policy does not explicitly allow, it is denied. The guard plane applies to every policy, including any: true, so no policy can implicitly grant a privileged extension.

Example

apiVersion: openvox.voxpupuli.org/v1alpha1
kind: SigningPolicy
metadata:
  name: auto-approve
spec:
  certificateAuthorityRef: production-ca
  any: true

Certname Matching

apiVersion: openvox.voxpupuli.org/v1alpha1
kind: SigningPolicy
metadata:
  name: trusted-hosts
spec:
  certificateAuthorityRef: production-ca
  certnames:
    allow:
      - "*.example.com"
      - "web-*"

DNS SAN Validation

apiVersion: openvox.voxpupuli.org/v1alpha1
kind: SigningPolicy
metadata:
  name: allow-internal-sans
spec:
  certificateAuthorityRef: production-ca
  certnames:
    allow:
      - "*.example.com"
  dnsAltNames:
    allow:
      - "*.internal.example.com"
      - "*.svc.cluster.local"

IP / URI / Email SAN Validation

Each SAN type has its own fail-closed allowlist. ipAltNames uses CIDR ranges; uriAltNames and emailAltNames use wildcard patterns where * spans any characters (including / and @).

apiVersion: openvox.voxpupuli.org/v1alpha1
kind: SigningPolicy
metadata:
  name: gateway-sans
spec:
  certificateAuthorityRef: production-ca
  certnames:
    allow:
      - "gateway-*"
  ipAltNames:
    allow:
      - "10.0.0.0/16"
      - "::1/128"
  uriAltNames:
    allow:
      - "spiffe://example.com/gateway/*"
  emailAltNames:
    allow:
      - "*@example.com"

A CSR carrying a SAN of a type whose allowlist is unset is denied.

Checks that no policy can waive

Two conditions are evaluated before any policy is consulted. They apply to every policy, any: true included.

Reserved certnames. The operator issues itself a certificate under {ca}-operator for mTLS against the CA API, and the CA auth.conf grants admin rights to that name as well as to the pp_cli_auth extension. An agent holding a certificate under it would therefore be a CA admin. The operator renders the name into the generated policy file, and the autosign binary refuses it regardless of what any policy allows.

You do not configure this: the name is derived from the CertificateAuthority and reserved automatically.

Subject binding. puppetserver takes the certname from the request path and passes it to the autosign binary as an argument, while the CN lives in the CSR subject. A CSR whose subject differs from the requested certname is refused, so a policy cannot approve one name while the certificate is issued carrying another.

Authorization Extensions

Privileged authorization extensions (the 1.3.6.1.4.1.34380.1.3 arc: pp_cli_auth, pp_authorization, pp_auth_token) are denied by default, even for any: true policies. A certificate carrying pp_cli_auth=true is granted CA-admin access by the built-in auth.conf rules, so a CSR requesting it must be explicitly allowed:

apiVersion: openvox.voxpupuli.org/v1alpha1
kind: SigningPolicy
metadata:
  name: ca-admin-bootstrap
spec:
  certificateAuthorityRef: production-ca
  certnames:
    allow:
      - "ca-admin.example.com"
  extensions:
    allow:
      - pp_cli_auth

Authorization-arc OIDs with no known Puppet name cannot be allow-listed and are always denied. Trusted-fact extensions (the 1.3.6.1.4.1.34380.1.1 arc, e.g. pp_role, pp_environment) are not gated.

CSR Attribute Matching

Match CSR extension attributes with inline values or Secret references:

apiVersion: openvox.voxpupuli.org/v1alpha1
kind: SigningPolicy
metadata:
  name: bootstrap-key
spec:
  certificateAuthorityRef: production-ca
  csrAttributes:
    - name: pp_preshared_key
      valueFrom:
        secretKeyRef:
          name: signing-psk
          key: psk

Combined (AND within policy)

apiVersion: openvox.voxpupuli.org/v1alpha1
kind: SigningPolicy
metadata:
  name: trusted-with-psk
spec:
  certificateAuthorityRef: production-ca
  certnames:
    allow:
      - "*.example.com"
  csrAttributes:
    - name: pp_preshared_key
      valueFrom:
        secretKeyRef:
          name: signing-psk
          key: psk
    - name: pp_environment
      value: production

This policy requires a matching certname pattern and a valid PSK and the correct pp_environment extension.

Spec

Field Type Default Description
certificateAuthorityRef string required Reference to the CertificateAuthority
any bool false Sign all CSRs unconditionally
certnames PatternSpec - Allowed certname glob patterns; the certname must match at least one
dnsAltNames PatternSpec - Allowed DNS SAN glob patterns. If a CSR carries DNS SANs and this is unset, it is denied
ipAltNames PatternSpec - Allowed IP SAN CIDR ranges. If a CSR carries IP SANs and this is unset, it is denied
uriAltNames PatternSpec - Allowed URI SAN wildcard patterns (* spans /). If a CSR carries URI SANs and this is unset, it is denied
emailAltNames PatternSpec - Allowed email SAN wildcard patterns (* spans @). If a CSR carries email SANs and this is unset, it is denied
extensions PatternSpec - Puppet extension names a CSR may carry. Authorization-arc extensions are denied unless listed here (applies to any: true too)
csrAttributes []CSRAttributeMatch - CSR extension attributes that must all match (AND)

PatternSpec

Field Type Default Description
allow []string required Glob patterns; certname must match at least one

CSRAttributeMatch

Field Type Default Description
name string required CSR extension attribute name (e.g. pp_preshared_key, pp_environment)
value string - Expected value (inline)
valueFrom SecretKeySelector - Expected value from a Secret

Either value or valueFrom must be set.

SecretKeySelector

Field Type Default Description
secretKeyRef.name string required Name of the Secret
secretKeyRef.key string required Key within the Secret

Status

Field Type Description
observedGeneration int64 The .metadata.generation the status was last derived from. A value below .metadata.generation means the rest of this status has not caught up with the current spec yet
phase string Current lifecycle phase
conditions []Condition Ready

Phases

Phase Description
Active Policy is rendered and active
Disabled Deliberately bypassed by an autosignCommand override -- a configuration choice, not a fault
Error Policy is not in effect -- see the Ready condition for which case

The status is derived from the rendered autosign policy Secret, so it reports whether this policy actually reached the CA rather than whether the resource itself is well-formed. The Ready condition carries the reason:

Reason Meaning
Rendered The policy is present in the rendered Secret, at its current generation
CertificateAuthorityRefMissing spec.certificateAuthorityRef is empty, so the policy is bound to no CA
CertificateAuthorityNotFound spec.certificateAuthorityRef points at a CertificateAuthority that does not exist
NoConfig No Config references that CertificateAuthority, so nothing renders the policy
OverriddenByAutosignCommand Every Config referencing the CA sets spec.puppet.autosignCommand, which replaces the built-in binary and bypasses SigningPolicy resources
NotRendered The Secret does not (yet) contain this policy
RenderedConfigStale The Secret contains this policy, but as it was at an earlier generation
RenderedConfigSourceUnknown The Secret predates this mechanism and does not record what it was rendered from; it resolves once the Config controller re-renders

Automation upgrading from an earlier operator version should note that these reasons replace the previous two: PolicyRendered became Rendered, and a single catch-all Error reason was split into the specific cases above.

The Secret's openvox.voxpupuli.org/rendered-from annotation names the policies its content was built from and the generation each was rendered at, which is what separates Rendered from RenderedConfigStale.

Rendering failures -- an unresolvable csrAttributes Secret, for example -- are reported on the Config that owns the Secret, as an AutosignPolicyRenderFailed event, and the failed render leaves the previous Secret untouched.

What the policy reports then depends on whether its own spec changed. If a spec edit triggered the failing render, the generation moved on and the policy reports RenderedConfigStale. If the spec did not change -- the referenced csrAttributes Secret was rotated or deleted underneath it -- the generation is unchanged, the previous Secret still matches it, and the policy keeps reporting Active while the CA signs under the last policy that rendered cleanly. Watch the Config's AutosignPolicyRenderFailed events for that case; the policy's own status cannot see it.

How It Works

  1. The operator collects all SigningPolicies for a CertificateAuthority
  2. It renders a policy config YAML into a Secret, mounted into the CA pod
  3. puppet.conf always points to the openvox-autosign binary, so puppet.conf itself never changes when policies change
  4. When a SigningPolicy changes, the operator rewrites the Secret. It is mounted as a directory, so the kubelet syncs the new policy into the running CA pod, usually within a minute. The CA pod is not restarted. No manual restart needed.

A policy change only affects CSRs submitted after it reached the CA pod. puppetserver evaluates autosign once, when a CSR arrives. A CSR that was denied stays pending and is not re-evaluated, even if a later policy would match it. Sign it manually (puppetserver ca sign --certname <name>), or clean it and let the agent submit a new one.

The openvox-autosign binary shipped in the openvox-server container image evaluates policies at CSR signing time:

flowchart TD
    Start["CSR received<br/>(certname + CSR on stdin)"] --> Load["Load all SigningPolicies"]
    Load --> Any{"Any policies?"}
    Any -->|No| Deny

    Any -->|Yes| Loop["Evaluate next policy"]
    Loop --> CheckExt{"authz extensions<br/>allowed? (guard)"}
    CheckExt -->|No| Next
    CheckExt -->|Yes / none| CheckSAN{"all SAN types<br/>allowed? (guard)"}
    CheckSAN -->|No| Next

    CheckSAN -->|Yes / none| CheckAny{"any: true?"}
    CheckAny -->|Yes| Sign

    CheckAny -->|No| CheckPattern{"certname matches?"}
    CheckPattern -->|No| Next
    CheckPattern -->|Yes / not set| CheckCSR{"csrAttributes match?"}
    CheckCSR -->|No| Next
    CheckCSR -->|Yes / not set| Sign

    Next{"More policies?"} -->|Yes| Loop
    Next -->|No| Deny

    Sign["exit 0 (sign)"]
    Deny["exit 1 (deny)"]
  • Guard plane first: privileged authorization extensions and every SAN type are fail-closed and evaluated for every policy, including any: true
  • Between policies: OR -- any matching policy is sufficient
  • Within a policy: AND -- all set match fields must match
  • No policies → deny all
  • any: true → approve unconditionally after the guard plane passes (it does not waive extension/SAN protection)

OR composition

Adding a restrictive policy never removes permission granted by another policy. A CSR is signed if any policy both matches it and permits its extensions/SANs. Avoid a broad any: true policy alongside restrictive ones unless you intend it.

Supported CSR Attributes

All standard Puppet/OpenVox CSR extension attributes are supported, including:

Attribute OID
pp_preshared_key 1.3.6.1.4.1.34380.1.1.4
pp_environment 1.3.6.1.4.1.34380.1.1.12
pp_role 1.3.6.1.4.1.34380.1.1.13
pp_auth_token 1.3.6.1.4.1.34380.1.3.2
challengePassword 1.2.840.113549.1.9.7

See the Puppet CSR attributes documentation for the full list.