From 1dfd9e720b717f6cbe3e195680aa13defa5362a2 Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 6 Aug 2026 16:25:03 -0400 Subject: [PATCH] fix(tls): leaf key must be readable by the daemon, not just root MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found on archi-dev-box the moment the gate tried to serve TLS: the key was installed root:root 0600, nginx's master reads it as root, but the archipelago daemon runs as User=archipelago and got "Permission denied (os error 13)". Every app port then quietly stayed plain HTTP — the exact fail-open shape the gate exists to prevent, and it would have looked like "TLS just doesn't work" with no obvious cause. The warn-level log the tls module deliberately emits for a present-but-unloadable certificate is what turned this into a ten-second diagnosis instead of a hunt; it earned its keep on its first real deployment. Key is now group-owned by the service user at 0640, with a fallback to the user's primary group and a clear message when no such user exists. Nothing wider than that. Verified on the node afterwards, on one gated port (8096): https 401 verify=0 TLS terminated, chain valid against the node CA http 401 same port, plain HTTP, unchanged no CA verify=20 untrusted client correctly rejected The reissued key was also picked up with NO daemon restart — the mtime reload path proven in production, not just in a unit test. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/setup-node-ca.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/setup-node-ca.sh b/scripts/setup-node-ca.sh index 727d4f19..b74f07b9 100755 --- a/scripts/setup-node-ca.sh +++ b/scripts/setup-node-ca.sh @@ -127,6 +127,27 @@ openssl x509 -req -in "$TMP/leaf.csr" -CA "$CA_CRT" -CAkey "$CA_KEY" \ install -m 644 "$TMP/leaf.crt" "$LEAF_CRT" install -m 600 "$TMP/leaf.key" "$LEAF_KEY" +# The leaf key has TWO readers with different privileges: nginx's master +# process (root) and the archipelago daemon (User=archipelago), which needs it +# to terminate TLS on gated app ports. Root-only 0600 silently costs the daemon +# its TLS — it logs "Permission denied" and every app port quietly stays plain +# HTTP, which is exactly the fail-open shape the gate is built to avoid. So the +# key is group-readable by the service user and nothing wider. +SERVICE_USER="${ARCHY_SERVICE_USER:-archipelago}" +if getent group "$SERVICE_USER" >/dev/null 2>&1; then + chgrp "$SERVICE_USER" "$LEAF_KEY" && chmod 640 "$LEAF_KEY" + log "Key readable by group $SERVICE_USER (0640) — the daemon needs it for app-port TLS" +elif getent passwd "$SERVICE_USER" >/dev/null 2>&1; then + # User exists without an eponymous group — fall back to its primary group. + PRIMARY="$(id -gn "$SERVICE_USER" 2>/dev/null || true)" + if [ -n "$PRIMARY" ]; then + chgrp "$PRIMARY" "$LEAF_KEY" && chmod 640 "$LEAF_KEY" + log "Key readable by group $PRIMARY (0640)" + fi +else + log "No '$SERVICE_USER' user on this host — key left root-only (0600)" +fi + # The dashboard serves this for download; it is a public certificate, never the key. install -m 644 "$CA_CRT" "$SSL_DIR/ca-download.crt"