zester
Reference

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-master

zester ca init prints the root SPKI pin — distribute it to peels as enroll_ca_pin for verified first-contact enrollment.


Command Group Flags

FlagDefaultDescription
--dir/var/lib/zester/auth/caCA directory (persistent flag, applies to every ca verb)

The CA directory holds four files after init:

FileContentsMode
root.crtRoot CA certificate0644
root.keyRoot private key0600
intermediate.crtSigning intermediate certificate0644
intermediate.keyIntermediate 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>]
FlagDefaultDescription
--orgZesterOrganization name on the CA subjects
--cn<org> Root CARoot common name
--root-validity87600h (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 pinsha256:<hex> over the root's SubjectPublicKeyInfo. This is the value peels pin via enroll_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>]
FlagDefaultDescription
--dnsDNS SANs (repeatable / comma-separated)
--ipIP SANs (repeatable / comma-separated)
--out. (current directory)Output directory for the cert/key pair
--validity8760h (365 days)Certificate validity

Profiles

ProfileOutput filesCNSAN requirements
nats-servernats-server.crt, nats-server.key, nats-ca.crtnatsAt 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)
enrollenroll.crt, enroll.keyzester-masterDefaults 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 connections

You 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]
KeyFlagDefaultDescription
ca.mode--ca-modeautoCA 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-validity2160h (90 days)Validity of the self-issued enrollment certificate
ca.enroll_sans--ca-enroll-sansExtra DNS/IP SANs for the self-issued enrollment certificate; the machine hostname and localhost are always included

Mode semantics:

ModeBehavior
autoEmbedded iff <ca.dir>/root.crt exists, otherwise external. The default — dropping a CA directory in place is the only switch needed.
embeddedLoad 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).
externalNever 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 GetCertificate from an in-memory leaf — enroll.tls_cert and enroll.tls_key are 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_urls to ca.enroll_sans — peels verify the enrollment endpoint against this SAN set.

The ca readiness check (embedded mode only, on GET /readyz):

StatusWhen
okEnrollment leaf issued, outside the renewal window
degradedLeaf within its renewal window (remaining validity < 1/3) — renewal is failing; check master logs
downNo enrollment certificate issued

nats_advertise_urls (Discovery)

# /etc/zester/master.yaml
nats_advertise_urls:
  - tls://nats1.example.com:4222
  - tls://nats2.example.com:4222
KeyFlagDefaultDescription
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/ca

Response: 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"
}
FieldTypeDescription
vintEnvelope version (currently 1)
ca_bundle_pemstringCA trust bundle (root certificate PEM); empty in external mode
fingerprintstringRoot SPKI pin (sha256:<hex>)
nats_urls[]stringValidated fleet-facing NATS endpoints (nats_advertise_urls)
issued_atstringTimestamp; 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_info

This 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..."]
KeyFlagDefaultDescription
enroll_ca--enroll-caCA certificate file for enrollment TLS verification (strict; highest rung)
enroll_ca_pin--enroll-ca-pinsha256:<hex> SPKI pin(s) of the master CA root; accepts a list to cover root rollover
enroll_trust--enroll-trusttofuFallback 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:

RungSourceBehavior
1enroll_ca fileStrict: only that CA verifies
2enroll_ca_pinStrict: exactly the pinned root becomes the handshake anchor — never a whole served bundle, so a poisoned bundle cannot smuggle in extra authorities
3Persisted anchor <auth_dir>/enroll-ca.crtStrict: written on first successful pin/TOFU contact; later mismatch = fatal ("the master key changed", the Salt minion_master.pub equivalent)
4System trust storeStandard WebPKI verification
5TOFUGenuine 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

KeyFlagDefaultDescription
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:

  1. Explicit nats_url (non-empty) — no discovery at all.
  2. Bootstrap cache <data_dir>/nats-bootstrap.msgpack — the persisted endpoint list from the last discovery, keyed to the current master_urls + pins identity (a changed master/pin set invalidates it). Enables offline-first boot with no reachable master.
  3. Enrollment discovery — GET /api/v1/enroll/ca over anchor/pin-verified TLS.
  4. 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_info applies 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

OperationPeel impact
Intermediate rotationInvisible — peels trust the root; re-issue the intermediate and the certificates it signs
Root rolloverTwo-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

CommandDescription
zester ca init [--org] [--cn] [--root-validity]Generate the root + signing-intermediate hierarchy (refuses overwrite)
zester ca fingerprintPrint the root SPKI pin, one line
zester ca printShow 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.

On this page