doorbell: add ntfy token auth to listener poll + authenticated kuma canary

- doorbell-listener.py: poll private ntfy topics with auth= (Bearer token) to stop 403/429; add probe-tag routing to DoorbellProbe
- scripts/kuma-heartbeat.sh: publish health canary to ntfy.sh with Authorization: Bearer (was anonymous -> rate limited -> false down in Kuma); read token from .secrets/ntfy-token
This commit is contained in:
2026-08-28 22:14:02 -07:00
parent 0bc38431b9
commit f4d6be0364
2 changed files with 18 additions and 1 deletions
@@ -26,13 +26,18 @@ Env vars (all optional):
POLL_INTERVAL seconds between polls, default 30 POLL_INTERVAL seconds between polls, default 30
STATE_DIR default "/data/doorbell-listener" STATE_DIR default "/data/doorbell-listener"
NTFY_TOPICS comma-separated override, default "ALERT_klubhaus_topic_test,ALERT_klubhaus_topic" NTFY_TOPICS comma-separated override, default "ALERT_klubhaus_topic_test,ALERT_klubhaus_topic"
NTFY_TOKEN ntfy access token (tk_...). Adds the auth= query param the
ntfy.sh topics require; without it, polling the private
topics gets 403/429 rate-limited by ntfy.sh.
""" """
import base64
import json import json
import math import math
import os import os
import sys import sys
import threading import threading
import time import time
import urllib.parse
from pathlib import Path from pathlib import Path
import paho.mqtt.client as mqtt import paho.mqtt.client as mqtt
@@ -40,6 +45,7 @@ import requests
DEFAULT_TOPICS = "ALERT_klubhaus_topic_test,ALERT_klubhaus_topic" DEFAULT_TOPICS = "ALERT_klubhaus_topic_test,ALERT_klubhaus_topic"
NTFY_TOPICS = [t.strip() for t in os.environ.get("NTFY_TOPICS", DEFAULT_TOPICS).split(",") if t.strip()] NTFY_TOPICS = [t.strip() for t in os.environ.get("NTFY_TOPICS", DEFAULT_TOPICS).split(",") if t.strip()]
NTFY_TOKEN = os.environ.get("NTFY_TOKEN", "")
MQTT_HOST = os.environ.get("MQTT_HOST", "mosquitto") MQTT_HOST = os.environ.get("MQTT_HOST", "mosquitto")
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883")) MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
@@ -72,6 +78,14 @@ def log(msg: str) -> None:
print(msg, flush=True) print(msg, flush=True)
def auth_param() -> str:
"""Raw base64 of 'Bearer <token>' with no trailing '=' — mirrors the
doorbell frontend's buildAuthParam(). Empty string when no token set."""
if not NTFY_TOKEN:
return ""
return base64.b64encode(f"Bearer {NTFY_TOKEN}".encode()).decode().rstrip("=")
def last_id_path(topic: str) -> Path: def last_id_path(topic: str) -> Path:
safe = topic.replace("/", "_").replace(" ", "_") safe = topic.replace("/", "_").replace(" ", "_")
return STATE_DIR / f"last_id_{safe}" return STATE_DIR / f"last_id_{safe}"
@@ -112,6 +126,8 @@ def start_flash(client: mqtt.Client, count: int, interval: float, topic: str) ->
def poll_topic(client: mqtt.Client, topic: str, last_id: str) -> str: def poll_topic(client: mqtt.Client, topic: str, last_id: str) -> str:
"""Poll one topic. Returns the latest message id seen (or last_id).""" """Poll one topic. Returns the latest message id seen (or last_id)."""
url = f"https://ntfy.sh/{topic}/json?poll=1" url = f"https://ntfy.sh/{topic}/json?poll=1"
if auth_param():
url += f"&auth={urllib.parse.quote(auth_param())}"
if last_id: if last_id:
url += f"&since={last_id}" url += f"&since={last_id}"
log(f"[{topic}] polling {url}") log(f"[{topic}] polling {url}")
@@ -8,6 +8,7 @@ CONTAINER="${DB_CONTAINER:-doorbell-listener}"
[ -s "$TOKEN_FILE" ] || exit 1 [ -s "$TOKEN_FILE" ] || exit 1
TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")" TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
NTFY_TOKEN="${NTFY_TOKEN:-$(tr -d '[:space:]' < /root/nr-flow-validator/infra/.secrets/ntfy-token 2>/dev/null || true)}"
reason="" reason=""
@@ -25,7 +26,7 @@ else
# caused false "down" alerts. The subscribe/deliver leg is continuously # caused false "down" alerts. The subscribe/deliver leg is continuously
# exercised by the production listener consuming real alerts. # exercised by the production listener consuming real alerts.
url="https://ntfy.sh/${CANARY_TOPIC}" url="https://ntfy.sh/${CANARY_TOPIC}"
if ! curl -fsS -m 10 -o /dev/null -X POST "$url" -d "canary-$(date +%s)" 2>/dev/null; then if ! curl -fsS -m 10 -o /dev/null -X POST "$url" -H "Authorization: Bearer ${NTFY_TOKEN}" -d "canary-$(date +%s)" 2>/dev/null; then
reason="${reason}ntfy_publish_failed" reason="${reason}ntfy_publish_failed"
fi fi
fi fi