TLS certificates¶
A stack declares the domains it terminates TLS for; at deploy time a provider ensures a cert and key exist on the target at a predictable path. Issuance is an effect (it talks to a CA, or copies operator-managed files) — so, like a secret, the cert bytes never enter the canonical hash, the store, or gen-NNN.json. Only the declared domains and email are config.
Declaring TLS¶
module WebStack {
backend = "systemd"
tls = {
acme = true
email = "ops@example.com"
domains = ["app.example.com", "docs.example.com"]
}
reverseProxies = [
{ serverName = "app.example.com" proxyPass = "http://127.0.0.1:8080" tls = true }
]
}
Compose validates every domain as a plain hostname (it becomes an on-target filesystem path — an unvalidated ../../etc/cron.d/x would be an arbitrary-file write). acme = true with no email, or an empty domains, is a compose error.
On-target paths¶
Each domain's cert lands at a predictable location:
The nginx generator references these automatically for any reverseProxies entry with tls = true (see Web & config generators). A reverse proxy that sets tls = true must name a domain in tls.domains — compose cross-checks it, so a TLS vhost can never reference a cert that won't be provisioned.
Providers¶
Pre-supplied certs (ships today)¶
If you already hold certs — from an external ACME run, a corporate CA, or certbot elsewhere — point the deploy at a directory laid out as <dir>/<domain>/{fullchain,privkey}.pem:
The provider copies each cert to the on-target path via the same Transport as everything else, and re-validates the domain before any write (defense-in-depth). Fully hermetic to test — no network.
ACME (deferred)¶
A network AcmeProvider (shell out to an ACME client, HTTP-01) is the deferred adapter — the same shape as the realise dry-run/real split. Renewal, when it lands, is an ACME provider plus a timer; no new concept.
What is and isn't recorded¶
- Config: the declared
domainsandemail(pure data). - Effect, never recorded: the cert and key bytes. They are written out of band of the generation record — never a store path, never a
configFilesentry, never incanonical_jsonorgen-NNN.json.
Provisioning runs before config files are written (so a unit or vhost can reference the cert path) and is skipped under --dry-run, like the closure push.
Where in the code¶
src/punix/deploy/tls.py—TlsProvider,PreSuppliedCertProvider,tls_target_dir.
Related¶
- Web & config generators — reverse proxies wire to these cert paths.
- Secrets at deploy — the same "reference is config, value is effect" shape.
- Reference: decisions — ADR-013.