Files
juergen 50497780c7
ci/woodpecker/push/build Pipeline was successful
fix ipps certificate lookup
2026-10-19 20:39:02 +02:00

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, 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.