ci/woodpecker/push/build Pipeline was successful
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.
119 lines
6.3 KiB
Markdown
119 lines
6.3 KiB
Markdown
# 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.
|