fix(tls): leaf key must be readable by the daemon, not just root

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) <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-08-06 16:25:03 -04:00
co-authored by Claude Opus 5
parent f7bde19860
commit 1dfd9e720b
+21
View File
@@ -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"