# 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 (`.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 `/.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:`, `DNS:.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 `.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 `.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 `.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.