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
STATE_DIR default "/data/doorbell-listener"
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 math
import os
import sys
import threading
import time
import urllib.parse
from pathlib import Path
import paho.mqtt.client as mqtt
@@ -40,6 +45,7 @@ import requests
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_TOKEN = os.environ.get("NTFY_TOKEN", "")
MQTT_HOST = os.environ.get("MQTT_HOST", "mosquitto")
MQTT_PORT = int(os.environ.get("MQTT_PORT", "1883"))
@@ -72,6 +78,14 @@ def log(msg: str) -> None:
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:
safe = topic.replace("/", "_").replace(" ", "_")
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:
"""Poll one topic. Returns the latest message id seen (or last_id)."""
url = f"https://ntfy.sh/{topic}/json?poll=1"
if auth_param():
url += f"&auth={urllib.parse.quote(auth_param())}"
if last_id:
url += f"&since={last_id}"
log(f"[{topic}] polling {url}")
@@ -8,6 +8,7 @@ CONTAINER="${DB_CONTAINER:-doorbell-listener}"
[ -s "$TOKEN_FILE" ] || exit 1
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=""
@@ -25,7 +26,7 @@ else
# caused false "down" alerts. The subscribe/deliver leg is continuously
# exercised by the production listener consuming real alerts.
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"
fi
fi