van-router-mqtt: broker credentials in a separate 0600 file; log refused connects
config.json is re-rendered every deploy, so credentials go in /etc/van-router-mqtt/credentials.json (seeded empty if absent, never overwritten). The service logs MQTT connect/refuse/disconnect state changes, and --post-check warns when the broker refuses the service. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
This commit is contained in:
@@ -711,4 +711,78 @@ for k in 'VANLINK_CONF=' 'VANLINK_HOSTNAME=' '--apply-preflight'; do
|
||||
done
|
||||
rm -rf "$t20"
|
||||
|
||||
# 21. van-router-mqtt: broker credentials (separate 0600 file) and refusal logging
|
||||
t21=$(mktemp -d)
|
||||
if python3 -c 'import ast,sys; ast.parse(open(sys.argv[1]).read())' router-status/van-router-mqtt; then ok "21a: van-router-mqtt parses"; else bad "21a: van-router-mqtt does not parse"; fi
|
||||
cat > "$t21/t.py" <<'PY'
|
||||
import importlib.machinery, importlib.util, io, json, os, sys, tempfile, contextlib
|
||||
ldr = importlib.machinery.SourceFileLoader("vrm", "router-status/van-router-mqtt")
|
||||
spec = importlib.util.spec_from_loader("vrm", ldr)
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
buf = io.StringIO()
|
||||
res = []
|
||||
def check(name, cond): res.append((name, bool(cond)))
|
||||
with contextlib.redirect_stdout(buf):
|
||||
ldr.exec_module(m) # must not start the service or connect
|
||||
d = tempfile.mkdtemp(); cfgp = os.path.join(d, "config.json"); cp = os.path.join(d, "credentials.json")
|
||||
base = {"broker": "b", "username": "old", "password": "oldpw"}
|
||||
r = m.load_credentials(cfgp, dict(base)); check("missing file -> unchanged", r == base)
|
||||
open(cp, "w").write('{"username": "homeassistant", "password": "s3cretPW"}')
|
||||
r = m.load_credentials(cfgp, {"broker": "b"}); check("valid merged", r == {"broker": "b", "username": "homeassistant", "password": "s3cretPW"})
|
||||
r = m.load_credentials(cfgp, dict(base)); check("credentials override config", r["username"] == "homeassistant")
|
||||
open(cp, "w").write("{not json"); r = m.load_credentials(cfgp, {"broker": "b"}); check("invalid JSON -> unchanged", r == {"broker": "b"})
|
||||
open(cp, "w").write("[1]"); r = m.load_credentials(cfgp, {"broker": "b"}); check("non-object -> unchanged", r == {"broker": "b"})
|
||||
open(cp, "w").write('{"username": "", "password": "x"}'); r = m.load_credentials(cfgp, {"broker": "b"}); check("empty username -> anonymous", "username" not in r and "password" not in r)
|
||||
os.chmod(cp, 0); r = m.load_credentials(cfgp, {"broker": "b"}); check("unreadable -> unchanged", r == {"broker": "b"} or os.geteuid() == 0)
|
||||
os.chmod(cp, 0o600); open(cp, "w").write('{"username": "u", "password": "topsecretpw"}')
|
||||
m.load_credentials(cfgp, {})
|
||||
check("credentials path beside config", m.credentials_path("/etc/van-router-mqtt/config.json") == "/etc/van-router-mqtt/credentials.json")
|
||||
check("reason rc4", "bad username or password" in m.refusal_reason(4))
|
||||
check("reason rc5", "not authorised" in m.refusal_reason(5))
|
||||
check("reason unknown", "unknown" in m.refusal_reason(99) and "99" in m.refusal_reason(99))
|
||||
mark = buf.tell()
|
||||
m.note_connect(5); m.note_connect(5); m.note_disconnect(5); m.note_connect(5)
|
||||
refused = buf.getvalue()[mark:]
|
||||
check("refusal logged once, with hint", refused.count("MQTT connect refused by broker (not authorised)") == 1 and "credentials.json" in refused)
|
||||
mark = buf.tell()
|
||||
m.note_connect(0); m.note_connect(0); m.note_disconnect(7); m.note_disconnect(7); m.note_connect(0)
|
||||
conn = buf.getvalue()[mark:]
|
||||
check("connected once per connect", conn.count("MQTT connected to") == 2)
|
||||
check("disconnect logged once per outage", conn.count("MQTT disconnected (rc=7)") == 1)
|
||||
out = buf.getvalue()
|
||||
check("password/username never printed", "s3cretPW" not in out and "topsecretpw" not in out and "homeassistant" not in out)
|
||||
check("import printed nothing service-like", "started" not in out)
|
||||
for n, c in res: print(("ok " if c else "FAIL ") + n)
|
||||
PY
|
||||
if o=$(python3 "$t21/t.py" 2>&1); then
|
||||
echo "$o" | grep -q '^FAIL' && { bad "21b: behavioural checks"; echo "$o" | sed 's/^/ /'; } || ok "21b: credentials merge and refusal logging ($(echo "$o" | grep -c '^ok') checks)"
|
||||
else bad "21b: test harness crashed"; echo "$o" | sed 's/^/ /'; fi
|
||||
# 21c. post_check probe against a stub journalctl
|
||||
mkdir -p "$t21/stub" "$t21/nojd"
|
||||
for f in /usr/bin/*; do [ "${f##*/}" = journalctl ] || ln -s "$f" "$t21/nojd/${f##*/}" 2>/dev/null || true; done
|
||||
mkstub() { printf '#!/bin/sh\ncat <<'"'"'EOF'"'"'\n%s\nEOF\n' "$1" > "$t21/stub/journalctl"; chmod +x "$t21/stub/journalctl"; }
|
||||
pc() { PATH="$1:/usr/bin:/bin" POSTCHECK_WAIT=0 ./deploy.sh --post-check hosts/van.conf 2>&1 || true; }
|
||||
R='MQTT connect refused by broker (not authorised) — if the broker requires authentication, set username/password in /x'
|
||||
C='MQTT connected to localhost:1883'
|
||||
mkstub "$R"; o=$(pc "$t21/stub")
|
||||
echo "$o" | grep -qF 'van-router-mqtt is refused by the MQTT broker' && ok "21c: refused last -> warn" || bad "21c: refused last: no warn"
|
||||
mkstub "$R
|
||||
$C"; o=$(pc "$t21/stub")
|
||||
{ echo "$o" | grep -qF 'van-router-mqtt: MQTT connection ok' && ! echo "$o" | grep -qF 'refused by the MQTT broker'; } && ok "21c: refused then connected -> ok" || bad "21c: refused then connected"
|
||||
mkstub "$C
|
||||
$R"; o=$(pc "$t21/stub")
|
||||
echo "$o" | grep -qF 'refused by the MQTT broker' && ok "21c: connected then refused -> warn" || bad "21c: connected then refused"
|
||||
mkstub ""; o=$(pc "$t21/stub")
|
||||
{ ! echo "$o" | grep -qF 'MQTT'; } && ok "21c: no journal lines -> nothing" || bad "21c: empty journal produced MQTT output"
|
||||
o=$(PATH="$t21/nojd" POSTCHECK_WAIT=0 ./deploy.sh --post-check hosts/van.conf 2>&1 || true)
|
||||
{ echo "$o" | grep -qF '== post-deploy check ==' && ! echo "$o" | grep -qF 'MQTT'; } && ok "21c: journalctl missing -> nothing" || bad "21c: journalctl missing"
|
||||
# 21d. example file, seeding
|
||||
python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if d.get("username")=="" and d.get("password")=="" else 1)' router-status/credentials.json.example && ok "21d: credentials.json.example is valid JSON with an empty username" || bad "21d: credentials.json.example"
|
||||
! grep -q CHANGE_ME router-status/credentials.json.example && ! grep -q CHANGE_ME deploy.sh && ok "21d: no CHANGE_ME in the example or deploy.sh" || bad "21d: CHANGE_ME present"
|
||||
grep -qE '^ +install -D -m0600 router-status/credentials\.json\.example /etc/van-router-mqtt/credentials\.json' deploy.sh && ! grep -E 'credentials\.json' deploy.sh | grep -q -- '-m0644' && ok "21d: deploy.sh seeds credentials.json 0600, never 0644" || bad "21d: deploy.sh credentials seeding"
|
||||
grep -B1 'install -D -m0600 router-status/credentials.json.example' deploy.sh | grep -qF '[ ! -f /etc/van-router-mqtt/credentials.json ]' && ok "21d: seed only if absent" || bad "21d: seed not guarded"
|
||||
d=$(./deploy.sh --diff hosts/van.conf 2>&1 || true)
|
||||
! echo "$d" | grep -q 'credentials.json' && ok "21d: --diff never lists credentials.json" || bad "21d: --diff mentions credentials.json"
|
||||
rm -rf "$t21"
|
||||
|
||||
exit "$fail"
|
||||
|
||||
Reference in New Issue
Block a user