The broker crash-looped on every deploy: "Unable to load server certificate /mosquitto/certs/mqtt.crt ... Permission denied". The cert-sync script wrote 600 root:root into a 0700 root:root directory, on the assumption that mosquitto runs as root inside its container. It does not — the stock eclipse-mosquitto entrypoint drops privileges to the in-image mosquitto user, confirmed on the server as uid=1883(mosquitto) gid=1883(mosquitto), and the broker's own log says so on every start. Certs dir is now root:1883 0750 with the cert 0644 and the key 0640, and the data dir is 1883:1883 recursively — recursively because mosquitto WRITES dynamic-security.json there, and a root-owned file left by an earlier deploy would still be unwritable after a directory-only chown. Also drops the "unverified Caddy cert path" note: a real issuance confirmed the path, producing CN=mqtt.drb.cusano.net signed by Let's Encrypt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57 lines
2.6 KiB
Django/Jinja
57 lines
2.6 KiB
Django/Jinja
#!/bin/bash
|
|
# Managed by Ansible — do not edit manually.
|
|
#
|
|
# Copies Caddy's managed TLS cert for mqtt.{{ domain }} out of Caddy's
|
|
# storage (root:caddy, 0700 — nothing else can read it) into a location the
|
|
# mosquitto container can read, then SIGHUPs the broker so it picks up the
|
|
# new cert without a full restart.
|
|
#
|
|
# Triggered by mqtt-cert-sync.path.j2 (a systemd path unit) watching the
|
|
# source cert file for changes — a path unit rather than cron so this fires
|
|
# on the actual write instead of racing a polling interval.
|
|
#
|
|
# CONFIRMED 2026-08-16 against a real issuance on drb-server: this path is
|
|
# correct, and the copied cert came out as CN=mqtt.drb.cusano.net issued by
|
|
# Let's Encrypt. Was previously flagged unverified.
|
|
#
|
|
# UNVERIFIED: mosquitto 2.x reloading TLS certs on SIGHUP without dropping
|
|
# connections is documented upstream but untested here. If listener 8883
|
|
# doesn't pick up the new cert (check `docker compose logs mosquitto` after
|
|
# a sync), replace the `kill -s HUP` line below with a full
|
|
# `docker compose ... restart mosquitto` instead.
|
|
set -euo pipefail
|
|
|
|
DOMAIN="mqtt.{{ domain }}"
|
|
CADDY_CERT_DIR="/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory/${DOMAIN}"
|
|
DEST_DIR="/opt/drb/mosquitto-certs"
|
|
APP_DIR="{{ app_dir }}"
|
|
|
|
SRC_CERT="${CADDY_CERT_DIR}/${DOMAIN}.crt"
|
|
SRC_KEY="${CADDY_CERT_DIR}/${DOMAIN}.key"
|
|
|
|
if [ ! -f "$SRC_CERT" ] || [ ! -f "$SRC_KEY" ]; then
|
|
echo "sync-mqtt-cert: source cert/key not found yet at $CADDY_CERT_DIR — Caddy may not have issued it yet." >&2
|
|
exit 0
|
|
fi
|
|
|
|
mkdir -p "$DEST_DIR"
|
|
# Copy, don't symlink — nothing outside the caddy user can read the
|
|
# originals (0700-owned), so mosquitto (running as a different container/
|
|
# user) needs its own readable copy, not a pointer to an unreadable file.
|
|
cp "$SRC_CERT" "$DEST_DIR/mqtt.crt"
|
|
cp "$SRC_KEY" "$DEST_DIR/mqtt.key"
|
|
# Ownership matters: the stock eclipse-mosquitto entrypoint drops privileges
|
|
# to the in-image `mosquitto` user (uid/gid 1883) — the broker does NOT run
|
|
# as root, despite what an earlier note in DEFERRED.md claimed. Proof from a
|
|
# real deploy: "running mosquitto as user: mosquitto", immediately followed
|
|
# by "Unable to load server certificate ... Permission denied" on a
|
|
# 600 root:root cert. The host has no such user, so use the numeric gid.
|
|
# The cert is public material (0644); the key is group-read only (0640).
|
|
chown root:1883 "$DEST_DIR/mqtt.crt" "$DEST_DIR/mqtt.key"
|
|
chmod 644 "$DEST_DIR/mqtt.crt"
|
|
chmod 640 "$DEST_DIR/mqtt.key"
|
|
|
|
cd "$APP_DIR"
|
|
docker compose -f docker-compose.yml -f docker-compose.prod.yml kill -s HUP mosquitto
|
|
echo "sync-mqtt-cert: copied cert for ${DOMAIN} and sent SIGHUP to mosquitto."
|