CA CLI & Configuration Reference
The zester ca command group manages Zester's embedded certificate authority — a root + signing-intermediate hierarchy (stdlib crypto/x509, ECDSA P-256) that backs the master's enrollment HTTPS certificate, the external NATS server certificate, and the peel-side trust ladder (pins, persisted anchors, TOFU).
All ca verbs are deliberately offline: pure local file I/O, no NATS connection, no running master. CA creation must work before any bus exists — the external NATS server needs its certificate first — so these run on the master host (typically as root) during initial provisioning.
Typical bootstrap on the first master host:
zester ca init --dir /var/lib/zester/auth/ca
zester ca issue nats-server --dns nats.example.com --out /etc/nats/tls
# install the issued cert/key on the NATS host, then start zester-masterzester ca init prints the root SPKI pin — distribute it to peels as enroll_ca_pin for verified first-contact enrollment.
Command Group Flags
| Flag | Default | Description |
|---|---|---|
--dir | /var/lib/zester/auth/ca | CA directory (persistent flag, applies to every ca verb) |
The CA directory holds four files after init:
| File | Contents | Mode |
|---|---|---|
root.crt | Root CA certificate | 0644 |
root.key | Root private key | 0600 |
intermediate.crt | Signing intermediate certificate | 0644 |
intermediate.key | Intermediate private key (issues all leaves) | 0600 |
In a multi-master deployment, replicate the CA directory to every master — exactly like account.seed.
zester ca init
Generates the root + signing-intermediate hierarchy into the CA directory. Refuses to run if CA material already exists there (never overwrites).
zester ca init [--dir <dir>] [--org <name>] [--cn <name>] [--root-validity <dur>]| Flag | Default | Description |
|---|---|---|
--org | Zester | Organization name on the CA subjects |
--cn | <org> Root CA | Root common name |
--root-validity | 87600h (10 years) | Root certificate validity (intermediate defaults to 5 years) |
Example:
$ zester ca init --dir /var/lib/zester/auth/ca
Embedded CA initialized in /var/lib/zester/auth/ca
Root subject: CN=Zester Root CA,O=Zester
Valid until: 2036-07-08T10:00:00Z
Fingerprint: sha256:4f2a...
SPKI pin: sha256:9b1c...
Next steps:
1. zester ca issue nats-server --dns <every name peels dial> --out <dir>
and install the cert/key on the NATS server host(s).
2. Distribute the SPKI pin to peels (enroll_ca_pin in peel.yaml).
3. Multi-master: replicate this CA directory to every master,
exactly like account.seed.Two identifiers are printed:
- Fingerprint — the SHA-256 hash of the root certificate (changes on re-issue).
- SPKI pin —
sha256:<hex>over the root's SubjectPublicKeyInfo. This is the value peels pin viaenroll_ca_pin; it survives certificate re-issuance under the same key.
zester ca fingerprint
Prints the root SPKI pin as a single line — designed for provisioning templates (cloud-init, Terraform, Ansible) that inject enroll_ca_pin into peel.yaml.
$ zester ca fingerprint
sha256:9b1c...zester ca print
Shows the CA hierarchy details and the PEM trust bundle:
$ zester ca print
Root: CN=Zester Root CA,O=Zester
Valid: 2026-07-08T10:00:00Z – 2036-07-08T10:00:00Z
Fingerprint: sha256:4f2a...
SPKI pin: sha256:9b1c...
Intermediate: CN=Zester Signing CA,O=Zester
Valid: 2026-07-08T10:00:00Z – 2031-07-08T10:00:00Z
-----BEGIN CERTIFICATE-----
...If the root private key is not present in the CA directory (taken offline after issuing the intermediate), print notes Root key: offline (not present in the CA directory) — day-to-day issuance needs only the intermediate key.
zester ca issue <profile>
Issues a TLS server certificate signed by the embedded CA. The certificate file contains the leaf + intermediate chain (verifiers need only the root); the key is written with mode 0600.
zester ca issue <nats-server|enroll> [--dns a,b] [--ip 1.2.3.4] [--out <dir>] [--validity <dur>]| Flag | Default | Description |
|---|---|---|
--dns | — | DNS SANs (repeatable / comma-separated) |
--ip | — | IP SANs (repeatable / comma-separated) |
--out | . (current directory) | Output directory for the cert/key pair |
--validity | 8760h (365 days) | Certificate validity |
Profiles
| Profile | Output files | CN | SAN requirements |
|---|---|---|---|
nats-server | nats-server.crt, nats-server.key, nats-ca.crt | nats | At least one --dns or --ip required — SANs must cover every name or IP peels dial (nats_url / nats_advertise_urls, plus localhost for a colocated watchdog) |
enroll | enroll.crt, enroll.key | zester-master | Defaults to the machine hostname + localhost when no SANs are given |
The nats-server profile also drops nats-ca.crt — the CA root — next to the cert. That is the trust anchor peels verify the NATS TLS chain against (servers present leaf+intermediate; clients anchor on the root), and it is the path the packaged peel config expects. Distribute it to peels as their nats_ca; no separate "export the root" step is needed.
Example — NATS server certificate:
$ zester ca issue nats-server --dns nats.example.com,nats --ip 10.0.0.5 --out /etc/nats/tls
Issued nats-server certificate (valid until 2027-07-08T10:00:00Z)
Certificate: /etc/nats/tls/nats-server.crt (leaf + intermediate chain)
Private key: /etc/nats/tls/nats-server.key
NATS CA: /etc/nats/tls/nats-ca.crt (root — distribute to peels as nats-ca.crt)
SANs: dns=[nats.example.com nats] ip=[10.0.0.5]
Install on the NATS host, point the tls{} block at the cert + key, then:
nats-server --signal reload # hitless for live connectionsYou rarely need `issue enroll`
In embedded mode the master self-issues its enrollment HTTPS certificate from the CA in-process and hot-renews it — zester ca issue enroll is the manual/offline variant for external-mode setups or debugging.
Master Configuration
The ca Block
# /etc/zester/master.yaml
ca:
mode: auto # auto | embedded | external
dir: /var/lib/zester/auth/ca # default: <auth_dir>/ca
enroll_cert_validity: 2160h # 90 days
enroll_sans: [master1.example.com, 10.0.0.10]| Key | Flag | Default | Description |
|---|---|---|---|
ca.mode | --ca-mode | auto | CA mode: auto | embedded | external |
ca.dir | --ca-dir | <auth_dir>/ca (/var/lib/zester/auth/ca) | Directory holding the embedded CA material |
ca.enroll_cert_validity | --ca-enroll-cert-validity | 2160h (90 days) | Validity of the self-issued enrollment certificate |
ca.enroll_sans | --ca-enroll-sans | — | Extra DNS/IP SANs for the self-issued enrollment certificate; the machine hostname and localhost are always included |
Mode semantics:
| Mode | Behavior |
|---|---|
auto | Embedded iff <ca.dir>/root.crt exists, otherwise external. The default — dropping a CA directory in place is the only switch needed. |
embedded | Load the embedded CA and self-issue/renew the enrollment certificate. Absent CA material is a fatal startup error (the error message names the zester ca init command to run). |
external | Never touch CA material; the enrollment listener uses the operator-provided enroll.tls_cert / enroll.tls_key files. |
In embedded mode:
- The enrollment HTTPS certificate is served via
GetCertificatefrom an in-memory leaf —enroll.tls_certandenroll.tls_keyare unused and need not exist. - The leaf is hot-renewed in-process at ~2/3 of its validity; no restart, no file management.
- Add every name that appears in peel
master_urlstoca.enroll_sans— peels verify the enrollment endpoint against this SAN set.
The ca readiness check (embedded mode only, on GET /readyz):
| Status | When |
|---|---|
ok | Enrollment leaf issued, outside the renewal window |
degraded | Leaf within its renewal window (remaining validity < 1/3) — renewal is failing; check master logs |
down | No enrollment certificate issued |
nats_advertise_urls (Discovery)
# /etc/zester/master.yaml
nats_advertise_urls:
- tls://nats1.example.com:4222
- tls://nats2.example.com:4222| Key | Flag | Default | Description |
|---|---|---|---|
nats_advertise_urls | --nats-advertise-urls | — (empty disables discovery) | Fleet-facing NATS URLs served to peels via the bootstrap document |
This is the endpoint list peels receive during enrollment discovery — it is intentionally separate from the master's own nats_url, because the master's local view (e.g. tls://localhost:4222) is meaningless to the fleet. Every entry is validated at startup:
tls://scheme only (same rule as everywhere else).- Loopback, unspecified (
0.0.0.0), link-local addresses,localhost, and numeric-loopback forms are rejected — a master's own loopback view can never leak into the fleet-facing list.
All-in-one boxes cannot be zero-config
A single host running NATS + master + peel may have no advertisable non-loopback name. Keep an explicit nats_url in that peel's config instead of relying on discovery.
Bootstrap Document (GET /api/v1/enroll/ca)
The master serves a discovery + trust document on the enrollment listener. It is unauthenticated (public material only), cacheable (Cache-Control: public, max-age=60), and exempt from the strict enrollment rate limiter — it uses the relaxed bucket (120 tokens, 20/s) so mass fleet bring-up is never throttled.
GET /api/v1/enroll/caResponse: 200 OK
{
"v": 1,
"ca_bundle_pem": "-----BEGIN CERTIFICATE-----\n...",
"fingerprint": "sha256:9b1c...",
"nats_urls": ["tls://nats1.example.com:4222", "tls://nats2.example.com:4222"],
"issued_at": "2026-07-08T10:00:00Z"
}| Field | Type | Description |
|---|---|---|
v | int | Envelope version (currently 1) |
ca_bundle_pem | string | CA trust bundle (root certificate PEM); empty in external mode |
fingerprint | string | Root SPKI pin (sha256:<hex>) |
nats_urls | []string | Validated fleet-facing NATS endpoints (nats_advertise_urls) |
issued_at | string | Timestamp; excluded from the document's content identity (ETag / idempotent KV Put), so master restarts don't churn caches |
The document is additive-only: old peels ignore new fields, and a new peel against an old master tolerates absent fields. It is deliberately served on an idempotent, re-fetchable route — never bundled into the single-use creds response — so a peel that lost its cache can always re-fetch it.
Live refresh channel: the same document is republished to the secrets KV bucket under the well-known key _cluster_info. Connected peels watch it and apply endpoint/trust updates without re-enrolling. Peel JWTs carry a corresponding read-only grant:
$KV.secrets._cluster_infoThis is the only bootstrap-related addition to the peel grant set; own-scoped keys are unaffected. Peels with pre-discovery credentials simply lack the live-refresh channel until re-issued — enrollment-time discovery over HTTPS still works.
Peel Configuration
Trust Knobs
# /etc/zester/peel.yaml — zero-config example
master_urls: [https://master1.example.com:8443, https://master2.example.com:8443]
enroll_ca_pin: ["sha256:9b1c..."]| Key | Flag | Default | Description |
|---|---|---|---|
enroll_ca | --enroll-ca | — | CA certificate file for enrollment TLS verification (strict; highest rung) |
enroll_ca_pin | --enroll-ca-pin | — | sha256:<hex> SPKI pin(s) of the master CA root; accepts a list to cover root rollover |
enroll_trust | --enroll-trust | tofu | Fallback when no enroll_ca/enroll_ca_pin is configured: tofu (trust first contact) or strict (fail closed) |
The trust ladder — the first configured/available rung wins, and a higher rung that mismatches is fatal, never a downgrade to a lower rung:
| Rung | Source | Behavior |
|---|---|---|
| 1 | enroll_ca file | Strict: only that CA verifies |
| 2 | enroll_ca_pin | Strict: exactly the pinned root becomes the handshake anchor — never a whole served bundle, so a poisoned bundle cannot smuggle in extra authorities |
| 3 | Persisted anchor <auth_dir>/enroll-ca.crt | Strict: written on first successful pin/TOFU contact; later mismatch = fatal ("the master key changed", the Salt minion_master.pub equivalent) |
| 4 | System trust store | Standard WebPKI verification |
| 5 | TOFU | Genuine first contact only (no credentials, no anchor): trusts the served CA, logs its fingerprint, persists it as the anchor |
An already-enrolled peel that encounters an unknown authority never re-TOFUs — it fails closed. Break-glass after a legitimate CA replacement: rm <auth_dir>/enroll-ca.crt and restart.
Strict YAML parsing
peel.yaml is decoded with unknown-key rejection: a typo like enroll_ca_pins: fails startup loudly instead of silently leaving the peel in the weakest (TOFU) mode while the operator believes it is pinned.
The CA a peel trusted is also signed into its enrollment submission (Ed25519, challenge-bound). The master compares it to its own root; a mismatch shows as MISMATCH! in the zester enroll list TRUST column, zester enroll approve refuses it without --force, and reactor auto-approval refuses unconditionally. See the enrollment security model.
nats_url Discovery Sentinel
| Key | Flag | Default | Description |
|---|---|---|---|
nats_url | --nats-url | "" (sentinel) | NATS server URL; empty means discover |
An unset nats_url enables endpoint discovery; a non-empty value (flag or YAML) is an explicit operator override that always wins. Discovery precedence at boot:
- Explicit
nats_url(non-empty) — no discovery at all. - Bootstrap cache
<data_dir>/nats-bootstrap.msgpack— the persisted endpoint list from the last discovery, keyed to the currentmaster_urls+ pins identity (a changed master/pin set invalidates it). Enables offline-first boot with no reachable master. - Enrollment discovery —
GET /api/v1/enroll/caover anchor/pin-verified TLS. - Builtin tail
tls://nats:4222— preserves the historical default.
Every candidate list — fetched, KV-refreshed, or cached — passes the same tls:// + no-loopback validator as nats_advertise_urls.
Two runtime mechanisms keep the endpoint list current without restarts:
- KV watch on
secrets._cluster_infoapplies live updates to the connection's server pool. - Recovery loop: if NATS stays unhealthy for more than 5 minutes, the peel re-fetches the bootstrap document over verified TLS and repoints the live connection (
SetServerPool+ForceReconnect) — a fleet whose NATS moved recovers without any process restart.
Convention Master
With no master_urls/master_url configured, the peel tries https://zester:8443 — the Salt/Puppet-style convention hostname (like salt/puppet). This applies to the enrollment path only; a peel that already has credentials never re-enrolls via the convention name.
Watchdog
The packaged zester-peel.service runs the peel under zester-watchdog --bootstrap-cache <data_dir>/nats-bootstrap.msgpack: instead of a fixed --nats-url, the watchdog reads (and polls for) the peel's discovered endpoint list, so both processes follow the same fleet view.
CA Rotation
| Operation | Peel impact |
|---|---|
| Intermediate rotation | Invisible — peels trust the root; re-issue the intermediate and the certificates it signs |
| Root rollover | Two-phase: serve an overlap bundle (old + new root), roll the NATS server cert to the new chain, then drop the old root. The NATS client re-reads RootCAs on every reconnect, so this is a file/bundle drop — no process restarts |
Never rotate CA material during a self-update rollout soak window: a peel that cannot re-verify NATS during soak fails readiness and rolls back.
Command Summary
| Command | Description |
|---|---|
zester ca init [--org] [--cn] [--root-validity] | Generate the root + signing-intermediate hierarchy (refuses overwrite) |
zester ca fingerprint | Print the root SPKI pin, one line |
zester ca print | Show CA details and the PEM trust bundle |
zester ca issue nats-server --dns a,b [--ip ...] [--out] [--validity] | Issue the NATS server certificate (leaf + intermediate chain) |
zester ca issue enroll [--dns ...] [--ip ...] [--out] [--validity] | Issue the enrollment HTTPS certificate manually (embedded mode self-issues) |
All verbs honor --dir (default /var/lib/zester/auth/ca) and run fully offline.