Files
printserver/openspec/changes/archive/2026-10-18-advertise-valid-ipps-certificate/design.md
T
juergen 548ae918ea
ci/woodpecker/push/build Pipeline was successful
Archive the advertise-valid-ipps-certificate change
Moves the completed change to
openspec/changes/archive/2026-10-18-advertise-valid-ipps-certificate/ and syncs
the printserver/cups capability into openspec/specs/: the image dependency
requirement now mentions the TLS tooling, and two requirements are added for the
name-valid _ipps certificate and for starting cleanly on stale runtime state.
2026-10-18 22:39:16 +02:00

6.3 KiB

Design

Context

See proposal.md — Why.

The relevant facts about the running system, established by inspecting a live container of this image (CUPS 2.4.2 on Debian bookworm):

  • CUPS advertises the queue on both _ipp._tcp and _ipps._tcp; macOS and iOS prefer the latter and check the certificate's subject alternative names against the name they connected to. CUPS' own clients (lpadmin, ipptool) do not perform that check.
  • The name clients use is the one Avahi advertises (<host>.local, queryable over D-Bus as GetHostNameFqdn), not the container host name. The two can differ: observed example, host name host.containers.internal served while Avahi advertised podp.local.
  • CUPS generates a certificate on demand for the host name, so that certificate cannot cover names the host name does not imply (in the example above, podp.local was absent, and so was a .local form, because CUPS only appends .local to dot-less host names).
  • In CUPS 2.4, ServerCertificate/ServerKey are accepted and preserved by cupsctl (which rewrites cupsd.conf), but cupsd still served the certificate kept at <certificate dir>/<host name>.crt. Writing our certificate to that path is what changed the served certificate.
  • /run survives a container restart (podman restart, systemd restart), and /etc/cups is a named volume that survives container replacement.

Goals / Non-Goals

Goals:

  • The certificate CUPS presents for _ipps validates for every name a client may use, including the Avahi-advertised one.
  • Restarting the container is safe and does not reissue the certificate.
  • Everything stays self-contained: no external CA, no configuration file edits by the user, no new volumes.

Non-Goals:

  • Obtaining a publicly trusted certificate or running a private CA.
  • Obtaining the printer's own (non-AirPrint) capabilities over IPP.
  • Changing how jobs are converted or delivered to the printer.

Decisions

Generate the certificate in the entrypoint rather than shipping one. A committed certificate would have a fixed key (shared by every deployment), a fixed expiry, and no knowledge of the local host name. Generating at first start keeps the key per deployment and lets the SANs reflect the actual names.

Collect SANs from several sources instead of one "server name" setting. Clients may reach the server by the Avahi FQDN, the container host name, a .local form, an IP address or a DNS name users add themselves. Adding all of them is cheap and only ever widens acceptance: DNS:<host name>, DNS:<host name>.local (when the host name has no dot), the Avahi FQDN, localhost, localhost.localdomain, host.containers.internal, IP:127.0.0.1, and the comma-separated CERT_EXTRA_NAMES. SANs are used because modern clients ignore the common name; the common name is set to the host name deliberately (see next decision).

Keep the common name equal to the host name. CUPS' certificate handling is name-based; keeping the common name equal to the host name keeps it consistent with CUPS' own notion of its identity, while the SANs carry the names clients actually use. basicConstraints=CA:TRUE is set so the file can also be imported as a trust anchor on macOS/iOS.

Install the certificate in two places. As ServerCertificate/ServerKey (the documented way, added to cupsd.conf when the directives are absent — which also repairs a cupsd.conf that cupsctl rewrote), and as <name>.crt/.key for both the host name and the Avahi FQDN, because that is where cupsd 2.4 actually loaded the certificate from in testing. Belt and braces: either path alone would be enough on some versions, together they hold on the version in the image. The copies are rewritten on every start, since cupsd may replace them.

Reuse the certificate whenever it already covers the required names. Reissuing forces every client that trusted the old certificate to trust a new one, so regeneration is limited to genuinely missing names, and the recorded SAN list is compared as a set (sorted, deduplicated) so the order in which names are discovered does not matter. A transient failure to query Avahi therefore does not churn the certificate. Consequence: names are never removed from an existing certificate; that requires deleting it.

Treat TLS failures as non-fatal, but fix the restart defect properly. If the certificate directory is not writable (for example a read-only bind mount), the entrypoint warns and lets CUPS fall back to its own certificate, because plain IPP printing still works. For startup, the stale-state problem is fixed at its root: D-Bus and Avahi are started based on whether their process is running, and their leftover socket/PID files are removed first. Avahi failing to start is reported as a warning rather than ending the entrypoint, so a discovery problem cannot take the whole print server down.

Risks / Trade-offs

  • [Self-signed certificate is rejected by validating clients] → Documented: trust the certificate on macOS/iOS, or keep using plain IPP. The entrypoint cannot influence a client's trust store.
  • [Only the common name is known on first start if Avahi is not up yet] → The name is re-checked on every start and the certificate is reissued when a required name is missing, so the next start heals it. Host networking keeps this rare.
  • [A name that later disappears stays in the certificate] → Accepted: extra SANs are harmless and churn would invalidate client trust.
  • [Destroying server.san loses the record of covered names] → Worst case the certificate is reissued once, which is the same as losing the certificate.
  • [Restarting rewrites <name>.crt, overwriting a certificate the user placed there by hand] → Accepted; this image owns the certificate directory.
  • [Host name changes are not picked up until a restart] → Accepted; the certificate is inspected at startup only.

Migration Plan

Deploy by rebuilding the image; no data migration. An existing /etc/cups volume is repaired on first start: the ServerCertificate/ServerKey directives are appended to the existing cupsd.conf and the stale <host name>.crt is replaced. Clients that already added the queue as a Generic PostScript printer must remove and re-add it, because macOS caches the driver choice. Rollback is redeploying the previous image; a certificate written by this version is ignored by it.