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, includingany: 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¶
- The operator collects all SigningPolicies for a CertificateAuthority
- It renders a policy config YAML into a Secret, mounted into the CA pod
- puppet.conf always points to the
openvox-autosignbinary, so puppet.conf itself never changes when policies change - 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.