Split the one TLS certificate so a caddy compromise cannot impersonate mail #40

Open
opened 2026-10-15 21:09:03 +00:00 by hutao · 0 comments
Owner

modules/acme.nix issues one certificate, certs.${domain}, with every name in infra.certSubdomains as a SAN: the public sites, mail and smtp, and the tailnet-only names (dozzle, grafana, syncthing). Caddy and the mailserver both mount the same /var/lib/acme/<domain> directory.

So one private key impersonates everything. Caddy is the most exposed process on the box, since it parses every public byte of HTTP, TLS and QUIC. Code execution in caddy therefore yields a key that is also valid for the mail server, which caddy never serves. No sandbox helps there, because reading that key is caddy's legitimate job.

Proposal

Split into certificates by who uses them:

  • web: the public caddy sites
  • mail: mail, smtp, read only by the mailserver
  • tailnet: dozzle, grafana, syncthing, served by caddy's second listener

Each is its own security.acme.certs entry, with group and reloadServices scoped to its users. Caddy then mounts only web and tailnet, and the mailserver only mail. A compromised caddy can impersonate the web names until revocation, and nothing else.

infra.certSubdomains is currently the single list that feeds the one cert; it becomes one list per cert, or a name-to-cert map. Caddy's certDir and the mailserver's mount follow.

Also worth a look

  • Shorter-lived certificates. Let's Encrypt has been rolling out short-lived profiles (days, not 90). Check whether it's generally available, and whether the NixOS acme module (lego) can request a profile, before relying on it.
  • Certificate Transparency monitoring for hu-tao.dev, so an issuance nobody here asked for is noticed.

Check

  • Each cert's SANs (openssl x509 -noout -ext subjectAltName) match its list, and no name appears in two.
  • Every public site, every tailnet vhost, IMAP (993) and SMTP submission (465/587) present the right certificate.
  • A renewal reloads only that cert's users.
`modules/acme.nix` issues one certificate, `certs.${domain}`, with every name in `infra.certSubdomains` as a SAN: the public sites, `mail` and `smtp`, and the tailnet-only names (`dozzle`, `grafana`, `syncthing`). Caddy and the mailserver both mount the same `/var/lib/acme/<domain>` directory. So one private key impersonates everything. Caddy is the most exposed process on the box, since it parses every public byte of HTTP, TLS and QUIC. Code execution in caddy therefore yields a key that is also valid for the mail server, which caddy never serves. No sandbox helps there, because reading that key is caddy's legitimate job. ## Proposal Split into certificates by who uses them: - **web**: the public caddy sites - **mail**: `mail`, `smtp`, read only by the mailserver - **tailnet**: `dozzle`, `grafana`, `syncthing`, served by caddy's second listener Each is its own `security.acme.certs` entry, with `group` and `reloadServices` scoped to its users. Caddy then mounts only web and tailnet, and the mailserver only mail. A compromised caddy can impersonate the web names until revocation, and nothing else. `infra.certSubdomains` is currently the single list that feeds the one cert; it becomes one list per cert, or a name-to-cert map. Caddy's `certDir` and the mailserver's mount follow. ## Also worth a look - **Shorter-lived certificates.** Let's Encrypt has been rolling out short-lived profiles (days, not 90). Check whether it's generally available, and whether the NixOS acme module (lego) can request a profile, before relying on it. - **Certificate Transparency monitoring** for `hu-tao.dev`, so an issuance nobody here asked for is noticed. ## Check - Each cert's SANs (`openssl x509 -noout -ext subjectAltName`) match its list, and no name appears in two. - Every public site, every tailnet vhost, IMAP (993) and SMTP submission (465/587) present the right certificate. - A renewal reloads only that cert's users.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
hutao/vps#40
No description provided.