diff --git a/generic.md b/generic.md index fb99c35..9e4216d 100644 --- a/generic.md +++ b/generic.md @@ -462,11 +462,46 @@ This is the environment these apps deploy into. Claude Code should assume it. - Internal DNS split-horizon via `.internal` domains (`hanzalova.internal`, `kosherinata.internal`, etc.). ### TLS / PKI -- Internal PKI via Smallstep `step-ca` at `ca.internal`. -- Host certs renewed via systemd timers. -- mTLS everywhere internal services talk to each other. +- Internal PKI via Smallstep `step-ca` at `https://ca.internal`. +- Every host runs `step.service` (the Smallstep renewer) which keeps the host's cert fresh. **Certs are issued with a 24-hour expiry** and renewed continuously — services must tolerate cert rotation, not assume certs are stable for the life of the process. +- **mTLS everywhere** internal services talk to each other. - **Quantum-safe** SSH (sntrup761x25519 KEX) and TLS (X25519MLKEM768 where peers support it) are the default. External peers that don't support PQ fall back to classical curves — document the fallback explicitly in nginx config. +**Standard cert paths on every host:** + +| Path | Contents | Mode | +| --- | --- | --- | +| `/etc/pki/ca-trust/source/anchors/root-internal.pem` | Internal root CA bundle | world-readable | +| `/etc/pki/tls/misc/$(hostname -f).pem` | Host cert (public) | world-readable | +| `/etc/pki/tls/private/$(hostname -f).pem` | Host private key | ACL grants read to service-account users | + +Application code and systemd units should reference these paths directly — they're the same on every host, so config templates don't need to bake in a hostname. The key file is not world-readable; each app's service account is granted read access via `setfacl` (e.g., `setfacl -m u::r /etc/pki/tls/private/$(hostname -f).pem`) as part of deploy. This happens in `deploy.sh` alongside the `systemd-sysusers` step (§8). + +**Reacting to cert rotation:** + +Services that hold cert state in memory (most Rust daemons using `rustls` or `openssl`) must reload when the host cert changes. Ship a pair of systemd units alongside the service unit: + +```ini +# /etc/systemd/system/-api-cert.path +[Path] +PathChanged=/etc/pki/tls/misc/.pem +Unit=-api-cert-reload.service + +[Install] +WantedBy=multi-user.target +``` + +```ini +# /etc/systemd/system/-api-cert-reload.service +[Service] +Type=oneshot +ExecStart=/bin/systemctl reload -api.service +``` + +The service unit itself needs an `ExecReload=` that causes the daemon to re-read its certs without dropping in-flight requests (typically `SIGHUP` handling in the Rust binary). If the daemon can't reload gracefully, `ExecStart=/bin/systemctl restart -api.service` is the fallback — but prefer graceful reload. + +Ship these `.path` and cert-reload `.service` units from `asset/systemd/` the same way as the main unit. + ### Ingress - Per-site nginx reverse proxy terminates all WAN inbound 443. - Public DNS via Cloudflare, **unproxied by default** (CF's mTLS origin-pull has been unreliable). Revisit if/when that changes.