103 lines
5.2 KiB
Markdown
103 lines
5.2 KiB
Markdown
# 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, default `ssl` →
|
|
`/etc/cups/ssl`), `ServerName` (default common name) and `CreateSelfSignedCerts`
|
|
(`scheduler/conf.c`). `ServerCertificate`/`ServerKey` are dead: in
|
|
`cupsd.conf` CUPS logs "Please move … to the cups-files.conf file", and in
|
|
`cups-files.conf` it 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 with `httpAddrLookup()`; 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 as `fd26:…` is kept.
|
|
- The certificate file is built with `http_make_path()`: it keeps only
|
|
alphanumerics, `-` and `.`, turning everything else into `_`. An IPv6 literal
|
|
`fd26:182:833e:0:be24:11ff:fe21:341c` therefore becomes the file name
|
|
`fd26_182_833e_0_be24_11ff_fe21_341c.crt`.
|
|
- With host networking, Podman writes the host's address into the container's
|
|
`/etc/hosts` as `host.containers.internal`. A client connecting to the LAN
|
|
address therefore makes cupsd look for `host.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 `.local` name validate that name; IP-based URLs remain covered
|
|
by `CERT_EXTRA_NAMES`.
|
|
- Removing `ServerCertificate`/`ServerKey` entirely; 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 -I` does 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 `.local` name, 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.
|