Skip to content

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:

/etc/punix/tls/<domain>/fullchain.pem
/etc/punix/tls/<domain>/privkey.pem

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:

punix service deploy WebStack --file pkgs/ \
  --target ssh://deploy@host \
  --tls-certs ./certs

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 domains and email (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 configFiles entry, never in canonical_json or gen-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.pyTlsProvider, PreSuppliedCertProvider, tls_target_dir.