Skip to content

Commit 895f7f8

Browse files
authored
Merge pull request #103 from slauger/docs/helper-binaries-reference
docs: add helper binaries reference, mock server guide, and fix outdated defaults
2 parents 1ce9053 + 3936ac9 commit 895f7f8

9 files changed

Lines changed: 492 additions & 2 deletions

File tree

docs/concepts/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ The operator itself runs with its own ServiceAccount (managed by the Helm chart)
101101

102102
## Pod Volumes
103103

104-
Server pods use `readOnlyRootFilesystem: true` for security hardening. All writable paths are explicit emptyDir mounts; all configuration comes from ConfigMaps and Secrets. This section documents every volume mounted into Server and CA pods.
104+
Server pods can optionally use `readOnlyRootFilesystem: true` for security hardening. This is controlled by the `.spec.readOnlyRootFilesystem` field in the Config CRD (default: `false`). When enabled, all writable paths are backed by emptyDir volumes and `server-var-dir` is redirected to `/run/puppetserver`. All configuration comes from ConfigMaps and Secrets. This section documents every volume mounted into Server and CA pods.
105105

106106
### Server Pod Volumes
107107

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Certificate Signing
2+
3+
This guide explains how the operator bootstraps a Certificate Authority, signs certificates, and distributes CRLs.
4+
5+
## CA Bootstrap
6+
7+
When a CertificateAuthority resource is created, the operator runs a setup Job that initializes the CA on a PVC:
8+
9+
```mermaid
10+
sequenceDiagram
11+
participant User
12+
participant Operator
13+
participant Job as CA Setup Job
14+
participant PVC as CA PVC
15+
participant K8s as Kubernetes API
16+
17+
User->>Operator: Create CertificateAuthority
18+
Operator->>K8s: Create PVC ({ca}-data)
19+
Operator->>K8s: Create ServiceAccount + RBAC
20+
Operator->>K8s: Create Job ({ca}-setup)
21+
Job->>PVC: Run puppetserver ca setup
22+
Job->>K8s: Create Secret {ca}-ca (public cert)
23+
Job->>K8s: Create Secret {ca}-ca-key (private key)
24+
Job->>K8s: Create Secret {ca}-ca-crl (CRL)
25+
Job-->>Operator: Job completed
26+
Operator->>Operator: Phase -> Ready
27+
```
28+
29+
The setup Job:
30+
31+
1. Runs `puppetserver ca setup` on the PVC to generate the CA key pair and self-signed certificate
32+
2. Exports three Secrets via the Kubernetes API:
33+
- **`{ca}-ca`** - public CA certificate (`ca_crt.pem`), mounted in all pods
34+
- **`{ca}-ca-key`** - CA private key (`ca_key.pem`), never mounted in pods
35+
- **`{ca}-ca-crl`** - certificate revocation list, mounted in non-CA pods
36+
3. If a Certificate resource already exists for the CA server, the Job also signs and exports its TLS Secret
37+
38+
The Job is idempotent: if the CA is already initialized on the PVC, it skips setup and only ensures the Secrets exist.
39+
40+
## Certificate Signing Strategies
41+
42+
The operator uses two strategies depending on when the Certificate is created relative to the CA:
43+
44+
### Strategy 1: CA Setup Export
45+
46+
**When:** The Certificate exists before or at the same time as the CA setup Job runs.
47+
48+
The CA setup Job signs the certificate as part of the initial `puppetserver ca setup` and exports the cert+key directly as a Kubernetes Secret. The Certificate controller detects the existing Secret, adopts it (sets ownerReference), and marks the Certificate as `Signed`.
49+
50+
This is the typical path for the **CA server's own certificate**.
51+
52+
### Strategy 2: HTTP Signing
53+
54+
**When:** The Certificate is created after the CA is already `Ready`.
55+
56+
This is the typical path for **non-CA compile servers**:
57+
58+
```mermaid
59+
sequenceDiagram
60+
participant Cert as Certificate Controller
61+
participant K8s as Kubernetes API
62+
participant CA as CA Server (Puppetserver)
63+
64+
Cert->>K8s: Generate RSA 4096 key
65+
Cert->>K8s: Store key in {cert}-tls-pending Secret
66+
Cert->>CA: PUT /puppet-ca/v1/certificate_request/{certname}
67+
CA-->>Cert: 200 OK (CSR accepted)
68+
69+
loop Poll every 5s
70+
Cert->>CA: GET /puppet-ca/v1/certificate/{certname}
71+
CA-->>Cert: Signed certificate (when ready)
72+
end
73+
74+
Cert->>K8s: Create {cert}-tls Secret (cert.pem + key.pem)
75+
Cert->>K8s: Delete {cert}-tls-pending Secret
76+
Cert->>Cert: Phase -> Signed
77+
```
78+
79+
The controller:
80+
81+
1. Generates an RSA 4096-bit private key and stores it in a temporary `{cert}-tls-pending` Secret
82+
2. Creates a CSR with the configured `certname` and `dnsAltNames`
83+
3. Submits the CSR via HTTP PUT to the CA server's Puppetserver API
84+
4. Polls for the signed certificate via HTTP GET (every 5 seconds)
85+
5. Once signed, creates the final `{cert}-tls` Secret and deletes the pending Secret
86+
87+
The pending Secret ensures idempotency: if the controller restarts mid-signing, it reuses the same key instead of generating a new one.
88+
89+
### Service Discovery
90+
91+
The Certificate controller discovers the CA server endpoint automatically:
92+
93+
1. Find all Configs with `authorityRef` pointing to the CA
94+
2. Find a Server with `ca: true` referencing one of those Configs
95+
3. Use the first `poolRef` as the Kubernetes Service name
96+
4. Endpoint: `https://{pool-name}.{namespace}.svc:8140`
97+
98+
No manual URL configuration is needed.
99+
100+
## CRL Distribution
101+
102+
The operator periodically fetches the CRL from the CA server and stores it as a Secret:
103+
104+
1. Fetches CRL from `https://{ca-service}:8140/puppet-ca/v1/certificate_revocation_list/ca`
105+
2. Updates the `{ca}-ca-crl` Secret with the fresh CRL
106+
3. Requeues after `spec.crlRefreshInterval` (default: 5 minutes)
107+
108+
Non-CA pods mount the CRL Secret as a **directory volume** (without SubPath), which allows kubelet to auto-sync the content without pod restarts. CA pods read the CRL directly from their PVC.
109+
110+
## Secrets Overview
111+
112+
| Secret | Contents | Created By | Mounted In |
113+
|--------|----------|------------|------------|
114+
| `{ca}-ca` | `ca_crt.pem` | CA setup Job | All pods (trust chain) |
115+
| `{ca}-ca-key` | `ca_key.pem` | CA setup Job | Never (API access only) |
116+
| `{ca}-ca-crl` | `ca_crl.pem` | CA setup Job, then operator refresh | Non-CA pods (directory mount) |
117+
| `{cert}-tls` | `cert.pem`, `key.pem` | CA setup Job or Certificate controller | Server pods (SSL) |
118+
| `{cert}-tls-pending` | `key.pem` | Certificate controller | Never (temporary, deleted after signing) |
119+
120+
## Phase Lifecycle
121+
122+
### CertificateAuthority
123+
124+
```
125+
Pending -> Initializing -> Ready
126+
|
127+
v
128+
Error
129+
```
130+
131+
| Phase | Description |
132+
|-------|-------------|
133+
| `Pending` | Waiting for Config with `authorityRef` pointing to this CA |
134+
| `Initializing` | CA setup Job is running |
135+
| `Ready` | CA Secrets created, certificates can be signed |
136+
| `Error` | Setup Job failed (retried up to 3 times) |
137+
138+
### Certificate
139+
140+
```
141+
Pending -> Requesting -> Signed
142+
|
143+
v
144+
Error
145+
```
146+
147+
| Phase | Description |
148+
|-------|-------------|
149+
| `Pending` | Waiting for CertificateAuthority to reach `Ready` |
150+
| `Requesting` | CSR submitted, polling for signed certificate |
151+
| `Signed` | TLS Secret created, Server can mount it |
152+
| `Error` | Signing failed |

docs/concepts/config-rollout.md

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Configuration Rollout
2+
3+
This guide explains how configuration changes propagate to Server pods and which changes require a pod restart.
4+
5+
## Hash-Based Rolling Restarts
6+
7+
The Server controller adds SHA256 hash annotations to the pod template. When any tracked hash changes, Kubernetes detects a pod template change and triggers a rolling restart (or recreate for CA pods).
8+
9+
Tracked annotations:
10+
11+
| Annotation | Source | Triggers Restart |
12+
|------------|--------|:---:|
13+
| `openvox.voxpupuli.org/config-hash` | ConfigMap (`{config}-config`) | Yes |
14+
| `openvox.voxpupuli.org/ssl-secret-hash` | SSL Secret (`{cert}-tls`) | Yes |
15+
| `openvox.voxpupuli.org/ca-secret-hash` | CA Secret (`{ca}-ca`) | Yes |
16+
| `openvox.voxpupuli.org/enc-secret-hash` | ENC Secret (`{config}-enc`) | Yes |
17+
| `openvox.voxpupuli.org/report-webhook-secret-hash` | Report webhook Secret (`{config}-report-webhook`) | Yes |
18+
| `openvox.voxpupuli.org/code-image` | Code OCI image reference | Yes |
19+
20+
## What Triggers a Restart
21+
22+
Any change to the Config CRD spec fields that affect the ConfigMap causes a rolling restart:
23+
24+
| Changed Field | Affected File | Restart |
25+
|---------------|---------------|:---:|
26+
| `puppet.*` | `puppet.conf` | Yes |
27+
| `puppetdb.*` | `puppetdb.conf` | Yes |
28+
| `puppetserver.*` | `puppetserver.conf`, `webserver.conf`, `auth.conf` | Yes |
29+
| `logging.*` | `logback.xml` | Yes |
30+
| `metrics.*` | `metrics.conf` | Yes |
31+
32+
Changes to CRDs that generate Secrets with tracked hashes also trigger restarts:
33+
34+
| Changed CRD | Generated Secret | Restart |
35+
|--------------|-----------------|:---:|
36+
| NodeClassifier | `{config}-enc` | Yes |
37+
| ReportProcessor | `{config}-report-webhook` | Yes |
38+
39+
## What Does NOT Trigger a Restart
40+
41+
| Changed CRD | Generated Secret | Restart | How It Propagates |
42+
|--------------|-----------------|:---:|-------------------|
43+
| SigningPolicy | `{ca}-autosign-policy` | No | `openvox-autosign` reads the policy file on every CSR signing request. The Secret is not tracked in pod annotations. |
44+
| CRL updates (non-CA) | `{ca}-ca-crl` | No | Mounted as a directory volume (without SubPath), which kubelet auto-syncs every ~60 seconds. |
45+
46+
## Controller Watch Mechanics
47+
48+
The operator uses a chain of watches to propagate changes from sub-resources to the correct Servers:
49+
50+
```mermaid
51+
graph LR
52+
SP[SigningPolicy] -->|enqueueConfigsForSigningPolicy| CFG[Config Controller]
53+
NC[NodeClassifier] -->|enqueueConfigsForNodeClassifier| CFG
54+
RP[ReportProcessor] -->|enqueueConfigsForReportProcessor| CFG
55+
CFG -->|updates ConfigMap| CM[ConfigMap]
56+
CFG -->|updates Secret| SEC[Secret]
57+
SEC -->|enqueueServersForSecret| SRV[Server Controller]
58+
SRV -->|recomputes hashes| DEP[Deployment]
59+
```
60+
61+
1. **SigningPolicy/NodeClassifier/ReportProcessor changes** trigger the Config controller via custom watch handlers
62+
2. The **Config controller** reconciles the ConfigMap and Secrets
63+
3. **Secret changes** trigger the Server controller via a Secret watcher (filtered by `app.kubernetes.io/managed-by: openvox-operator` label)
64+
4. The **Server controller** recomputes all hash annotations on the pod template
65+
5. If any hash changed, Kubernetes performs a **rolling restart**
66+
67+
## Rollout Strategy
68+
69+
| Server Role | Strategy | Reason |
70+
|-------------|----------|--------|
71+
| CA (`ca: true`) | `Recreate` | Only one pod can write to the CA PVC at a time |
72+
| Non-CA | `RollingUpdate` | Zero-downtime updates for stateless catalog compilation |
73+
74+
## Common Scenarios
75+
76+
### Updating a Puppet Setting
77+
78+
Changing `spec.puppet.environmentTimeout` on a Config:
79+
80+
1. Config controller updates the ConfigMap (`puppet.conf` changes)
81+
2. Server controller detects updated `config-hash` annotation
82+
3. Rolling restart of all Servers referencing this Config
83+
84+
### Adding a Signing Policy
85+
86+
Creating or updating a SigningPolicy:
87+
88+
1. Config controller is triggered via SigningPolicy watcher
89+
2. Autosign policy Secret is updated
90+
3. Server controller is triggered but autosign hash is **not tracked** - no restart
91+
4. `openvox-autosign` reads the updated policy at the next CSR signing attempt
92+
93+
### Changing ENC Configuration
94+
95+
Updating a NodeClassifier:
96+
97+
1. Config controller is triggered via NodeClassifier watcher
98+
2. ENC Secret (`{config}-enc`) is updated
99+
3. Server controller detects updated `enc-secret-hash` annotation
100+
4. Rolling restart of all Servers with `server: true` referencing this Config

docs/development/mock-server.md

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# openvox-mock
2+
3+
`openvox-mock` is a lightweight mock server for E2E testing. It provides ENC, report, and PuppetDB endpoints in a single binary with no external dependencies.
4+
5+
## Endpoints
6+
7+
| Method | Path | Purpose |
8+
|--------|------|---------|
9+
| `GET` | `/node/{certname}` | ENC classification (returns Puppet ENC YAML) |
10+
| `POST` | `/reports` | Receive Puppet reports |
11+
| `POST` | `/pdb/cmd/v1` | Receive PuppetDB Wire Format commands |
12+
| `GET` | `/api/reports` | List all received reports (JSON) |
13+
| `GET` | `/api/pdb-commands` | List all received PDB commands (JSON) |
14+
| `GET` | `/api/classifications` | List all served classifications (JSON) |
15+
| `GET` | `/healthz` | Health check |
16+
17+
The `/api/*` endpoints are useful for assertions in E2E tests. They return all data the mock has received or served during its lifetime.
18+
19+
## Environment Variables
20+
21+
| Variable | Default | Description |
22+
|----------|---------|-------------|
23+
| `LISTEN` | `:8080` | Listen address |
24+
| `ENC_CLASSES` | - | Comma-separated list of Puppet classes to return for all nodes |
25+
| `ENC_ENVIRONMENT` | - | Puppet environment to return for all nodes |
26+
| `CLASSIFICATIONS_FILE` | - | Path to a YAML file with per-node classifications |
27+
| `AUTH_TYPE` | - | Authentication method: `bearer`, `basic`, or `token` |
28+
| `AUTH_TOKEN` | - | Token value (for `bearer` and `token` auth) |
29+
| `AUTH_HEADER` | `X-Auth-Token` | Custom header name (for `token` auth) |
30+
| `AUTH_USERNAME` | - | Username (for `basic` auth) |
31+
| `AUTH_PASSWORD` | - | Password (for `basic` auth) |
32+
33+
## Classification
34+
35+
### Static (Environment Variables)
36+
37+
Set `ENC_CLASSES` and `ENC_ENVIRONMENT` to return the same classification for all nodes:
38+
39+
```bash
40+
ENC_CLASSES="role::webserver,profile::base" \
41+
ENC_ENVIRONMENT="production" \
42+
openvox-mock
43+
```
44+
45+
Every `GET /node/{certname}` request returns:
46+
47+
```yaml
48+
---
49+
environment: production
50+
classes:
51+
role::webserver:
52+
profile::base:
53+
```
54+
55+
### File-Based (Per-Node)
56+
57+
Set `CLASSIFICATIONS_FILE` to a YAML file for per-node classifications:
58+
59+
```yaml
60+
# classifications.yaml
61+
webserver01.example.com:
62+
classes:
63+
- role::webserver
64+
- profile::base
65+
environment: production
66+
dbserver01.example.com:
67+
classes:
68+
- role::database
69+
environment: staging
70+
_default:
71+
classes:
72+
- profile::base
73+
environment: production
74+
```
75+
76+
The `_default` key is used as a fallback when a certname is not found. If neither file-based nor env-var classification matches, an empty response is returned.
77+
78+
The classifications file is automatically reloaded every 5 seconds when modified (hot-reload).
79+
80+
## Authentication
81+
82+
When `AUTH_TYPE` is set, all ENC, report, and PDB endpoints require authentication. The `/api/*` and `/healthz` endpoints are always unauthenticated.
83+
84+
=== "Bearer Token"
85+
86+
```bash
87+
AUTH_TYPE=bearer AUTH_TOKEN=my-secret openvox-mock
88+
```
89+
90+
Expects: `Authorization: Bearer my-secret`
91+
92+
=== "Basic Auth"
93+
94+
```bash
95+
AUTH_TYPE=basic AUTH_USERNAME=admin AUTH_PASSWORD=secret openvox-mock
96+
```
97+
98+
Expects: standard HTTP Basic Authentication
99+
100+
=== "Custom Token Header"
101+
102+
```bash
103+
AUTH_TYPE=token AUTH_TOKEN=my-secret AUTH_HEADER=X-Api-Key openvox-mock
104+
```
105+
106+
Expects: `X-Api-Key: my-secret`
107+
108+
## PuppetDB Command Validation
109+
110+
The `/pdb/cmd/v1` endpoint validates the PuppetDB Wire Format envelope:
111+
112+
- `command` field must be present
113+
- `version` field must be non-zero
114+
- `store report` commands must use version 8

docs/getting-started/installation.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,21 @@ NAME READY STATUS AGE
3232
openvox-operator-7b8f9d6c4-x2k9m 1/1 Running 30s
3333
```
3434

35+
## Namespace-Scoped Mode
36+
37+
By default the operator watches all namespaces (cluster-scoped). To restrict it to a single namespace:
38+
39+
```bash
40+
helm install openvox-operator \
41+
oci://ghcr.io/slauger/charts/openvox-operator \
42+
--namespace openvox-system \
43+
--create-namespace \
44+
--set scope.mode=namespace \
45+
--set scope.watchNamespace=my-namespace
46+
```
47+
48+
In namespace mode the operator uses Role/RoleBinding instead of ClusterRole/ClusterRoleBinding and only reconciles resources in the configured namespace.
49+
3550
## Next Steps
3651

3752
Once the operator is running, follow the [Quick Start](quickstart.md) guide to deploy an OpenVox stack.

0 commit comments

Comments
 (0)