5.2 KiB
Design
Context
See proposal.md — Why.
The previous change assumed cupsd served the file named by ServerCertificate,
and only wrote <host name>.crt/<Avahi FQDN>.crt on top of that. Observing a
live deployment (CUPS 2.4.2 on Debian bookworm, host networking) and reading the
sources shows a different mechanism:
- CUPS 2.4.2 reads
ServerKeychain(a directory, defaultssl→/etc/cups/ssl),ServerName(default common name) andCreateSelfSignedCerts(scheduler/conf.c).ServerCertificate/ServerKeyare dead: incupsd.confCUPS logs "Please move … to the cups-files.conf file", and incups-files.confit logs "is no longer supported" and ignores the value. - In
_httpTLSStart()(cups/tls-openssl.c) a server connection takes the local socket address (getsockname), not the SNI/host name. Loopback yields an empty name, so the lookup falls back to the default common name. Any other address is reverse-resolved withhttpAddrLookup(); if that fails the numeric string is used. A result that starts with a digit or[is discarded (numeric IPv4), but an IPv6 literal such asfd26:…is kept. - The certificate file is built with
http_make_path(): it keeps only alphanumerics,-and., turning everything else into_. An IPv6 literalfd26:182:833e:0:be24:11ff:fe21:341ctherefore becomes the file namefd26_182_833e_0_be24_11ff_fe21_341c.crt. - With host networking, Podman writes the host's address into the container's
/etc/hostsashost.containers.internal. A client connecting to the LAN address therefore makes cupsd look forhost.containers.internal.crt.
Observed, same cupsd and port, different certificate depending on the address the client connected to:
| Connected to | cupsd lookup name | Served certificate |
|---|---|---|
127.0.0.1 |
default common name (podp.internal.jacs.at) |
ours (copy existed) |
192.168.100.20 |
host.containers.internal |
cupsd's own, CN=host.containers.internal |
[fd26:…]:631 |
IPv6 literal | cupsd's own, CN=fd26:… |
Goals / Non-Goals
Goals:
- cupsd presents our certificate (with the advertised name among its SANs) no matter which of the server's addresses the client connected to.
- The certificate content and the reuse/regeneration behaviour stay as they are, so already-trusted clients keep working across an image upgrade.
Non-Goals:
- Adding the server's addresses to the certificate's IP SANs. Clients that use
the advertised
.localname validate that name; IP-based URLs remain covered byCERT_EXTRA_NAMES. - Removing
ServerCertificate/ServerKeyentirely; they are kept for CUPS versions before 2.4.
Decisions
Enumerate the names cupsd can look up instead of guessing two.
install_default_cert_copy() now writes a copy for every DNS name in the SAN
list and for each local address (the address literal with %zone written as
+zone and mangled the way http_make_path() does, plus the name the address
reverse-resolves to via getent hosts). This is derived from the running
system, so it also covers names a future Podman version writes into /etc/hosts
and any IPv6 literal, without hard-coding host.containers.internal.
Reuse the existing helpers and regeneration logic.
The SAN list already contains host.containers.internal, localhost and the
advertised names, so enumerating DNS SANs covers the name-based lookups; the
local-address enumeration adds the literal/IPv6 cases. Nothing in
ensure_certificate() changes, so the certificate is not reissued on upgrade.
Locate local addresses with hostname -I, fall back to ip -o addr.
Debian's hostname (present in bookworm-slim) supports -I and prints all
IPv4 and IPv6 addresses; ip from iproute2 is not installed, so it is only a
fallback. Loopback is excluded because cupsd handles it with the default common
name.
Keep the copies' permissions consistent with the certificate.
Each copy gets the same mode/owner (0644 cert, 0640 key, root:lp) as
server.crt/server.key, so cupsd can read them.
Risks / Trade-offs
- [A local address changes while the container runs] → The names are recomputed on every start; a changed address is covered after the next restart. Addresses are stable with host networking.
- [Link-local IPv6 addresses whose scope
hostname -Idoes not print] → Their literal copy may differ from what cupsd computes, so a client connecting over such an address could still get cupsd's certificate. Accepted; clients use the advertised.localname, which typically resolves to a global/ULA address. - [Writing a folder full of certificate copies] → A handful of small files in
/etc/cups/ssl; rewrite is idempotent and cheap. - [Comma-free, space-free names only] →
http_make_path()collapses any other character to_, so the name we write matches what cupsd computes by construction.
Migration Plan
Rebuild and redeploy the image; no volume reset. On first start the missing
<name>.crt/.key copies (notably host.containers.internal) appear and cupsd
serves them from then on. The certificate itself is unchanged, so no client
needs to re-trust it. Rollback is redeploying the previous image; the extra
copies are simply ignored.