image-automation commits tag bumps straight to master with no CI gate, and
the broker is a single pod — so an image that fails to come up would be
silent. Two changes make that safe:
- gatus TCP-checks the broker's ClusterIP on :1883, so a pod that never comes
up alerts within one interval. Reachability needed a scoped egress (apps ns)
+ ingress (home-assistant ns, gatus pod on :1883 only), mirroring the
existing evcc -> mosquitto:1883 allow.
- strategy Recreate -> RollingUpdate (maxUnavailable:0, maxSurge:1). With the
PVC gone there is no RWO volume to fence, so the new broker comes up and
passes its readiness probe before the old one is torn down — no full-down
window on a bump/reschedule; clients on the old pod just reconnect. The brief
two-pod overlap is two independent stateless brokers behind the LB, fine with
no shared state + self-healing discovery.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The broker ran on a floating `eclipse-mosquitto:2` tag — no reproducibility,
and a reschedule could pull a changed/broken image. Rather than hand-pin a
digest (which would then never get patched), add it to the repo's Flux
image-automation like redis et al.:
- ImageRepository + ImagePolicy tracking eclipse-mosquitto, semver range 2.x
(auto-bump minor+patch; a 3.0 major stays a deliberate migration), matching
plain-semver tags only (excludes -openssl/latest/bare 2).
- The deployment image carries the pinned tag + the $imagepolicy marker, so
each bump lands as a git commit and rolls the broker (brief restart;
discovery self-heals via HA).
Net: the broker is both pinned (reproducible) and auto-patched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The broker is now load-bearing (heat pump, energy meters, EV charging, HA all
depend on it) but was a slow-to-recover SPOF: single replica on a Ceph RWO PVC
with no priority and default 5-min fault tolerations. mosquitto can't cluster,
so instead of faking HA this minimizes recovery time and removes the storage
coupling.
- persistence false + drop the Ceph PVC. The db was only ~178 KB of retained
messages (evcc state, HA discovery, Gen1 Shelly announce/state) — all of
which clients re-publish on reconnect. Removing it means the broker no
longer depends on Ceph to start, and reschedules with no RWO volume to
fence/detach/reattach. Trade-off: a restart briefly empties retained state
until clients reconnect (self-healing).
- PriorityClass `home-critical` (1e6, below system-cluster-critical) so the
broker is evicted last and reschedules ahead of ordinary workloads.
- not-ready/unreachable tolerations at 30s (was default 300s) → node-failure
reschedule in ~30s instead of ~5min.
- terminationGracePeriodSeconds 10 for faster Recreate rollovers.
Single replica + Recreate retained (two brokers would split clients/state).
Apply notes:
- Recreates the pod (brief MQTT reconnect; retained state re-populates).
- The old PVC has prune:disabled, so Flux won't delete it — reclaim manually
with `kubectl -n home-assistant delete pvc mosquitto-data` once happy.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The broker had no resource limits at all. It's now load-bearing and reachable
by ~30 untrusted IoT devices (the Shelly fleet) + the edge host on a single
256Mi replica, so a misbehaving/compromised client is a real DoS vector.
Add:
- message_size_limit 1048576 — MQTT permits up to 256 MB per publish; one such
message would OOM the broker. Every legit payload here (HA discovery,
Shelly/open3e/evcc telemetry) is <10 KB.
- persistent_client_expiration 1d — reap abandoned persistent sessions (the
edge bridges reconnect within seconds, so they never hit this).
- max_connections 200 per listener — bound a connection flood (~40 clients
today; generous headroom, not the previous unlimited).
All values chosen well above real usage, so no legit client is affected.
Apply: the ConfigMap has no checksum annotation, so Flux won't restart the pod
on its own — needs `kubectl -n home-assistant rollout restart deploy/mosquitto`
(brief reconnect; clients auto-reconnect) after reconcile.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Security review #sec-1 (edge-1 leg). open3e/e3oncan/sml2mqtt publish to the
cluster broker in plaintext over the untrusted IoT VLAN, and open3e/e3oncan
have no native MQTTS (their MQTT code does a bare connect()), so "just use
8883" isn't possible for them.
Run a local mosquitto on edge-1 that the clients reach over loopback
(127.0.0.1:1883), bridged to the cluster broker over TLS (mqtt.renner.uno:8883,
public LE cert validated via the system CA bundle). Plaintext stays on
loopback; the wire hop is encrypted. Two bridge connections preserve the
existing cluster identities (open3e user carries open3e/# out + open3e/cmnd
back; sml2mqtt user carries sml2mqtt/# out), so NO cluster-side change is
needed and the existing ACLs already suffice.
Bonus (availability, #avail-4): the local broker buffers telemetry in RAM and
flushes on reconnect, so edge-1 no longer hard-depends on cluster/network
uptime for every publish.
eMMC: the local broker writes NOTHING to flash — persistence false (queue in
RAM) + log_dest stdout (volatile journal -> Loki).
Verified before building: edge-1 -> :8883 TLS validates (return code 0);
mosquitto 2.1.2 in Arch extra; existing open3e/sml2mqtt cluster ACLs cover the
bridge; #125 netpol already allows :8883.
Rollback: point edge_mqtt_host back to mqtt.renner.uno and re-run.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Security review finding #sec-3/#sec-4.
- shellies: `readwrite homeassistant/#` -> `write homeassistant/#`. Shelly Gen2
firmwares self-publish HA MQTT-discovery configs (so the grant must stay),
but a publish-only client never needs to READ the whole discovery/state
tree. Write-only removes device-inventory recon for a compromised Shelly
while keeping discovery working. (`readwrite shellies/#` stays — devices
publish state and subscribe to their command topics.)
- sml2mqtt: `readwrite sml2mqtt/#` -> `write sml2mqtt/#`. The bridge is
publish-only (verified: its config has no subscribe/command topics, only
LWT + value publishes), so it needs write only.
Left as-is deliberately: open3e keeps `readwrite open3e/#` (it subscribes to
open3e/cmnd in listener mode); homeassistant/admin keep full access; evcc is
already least-privilege. Per-device Shelly credentials were considered and
rejected — Mosquitto's static passwd/acl model can't scope them without a
dynamic-auth plugin, which isn't worth it at this device count.
Applying rolls the (single-replica, Recreate) broker -> a brief MQTT
reconnect. kustomize build clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The home-assistant default-deny-ingress CNP granted `fromEntities: world`
namespace-wide (endpointSelector:{}), so the untrusted IoT VLAN / any
BGP-routed source could reach EVERY pod in the namespace on every port —
vscode (:8080), zone-configurator (:42069), otbr, whisper, matter-server,
speech-to-phrase, etc. — when only two pods are actually fronted by
world-facing LoadBalancers.
Replace the blanket rule with two endpoint+port-scoped CiliumNetworkPolicies:
- world -> mosquitto :1883, :8883
- world -> home-assistant :8123
Verified against live Services: home-assistant-bgp (targetPort 8123) and
mosquitto-iot (1883/8883) are the only LoadBalancers in the namespace;
matter-server's IoT VIP rides a multus net1 interface that bypasses Cilium
policy entirely, so it is unaffected.
Security review finding #sec-2. Additive-safe (removes a blanket allow, adds
scoped allows covering the same two real ingress paths); revert = restore the
namespace-wide world rule.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gegenstück zum WW-Dashboard, für die Puffer-/Estrich-Strategie:
Scatter Heiz-Ladeleistung vs. Außentemp + Zeitreihen (inkl. Puffer-
temperatur) + Ø-Kennzahl, auf Basis sensor.vitocal_heating_loading_power.
Datenquelle Prometheus. Füllt sich ab der ersten Heizperiode.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Neues Dashboard heatpump-dhw-loading (uid heatpump-dhw-loading):
Scatter WP-Ladeleistung vs. Außentemperatur + Zeitreihen + Ø-Kennzahl,
auf Basis von sensor.vitocal_dhw_loading_power (nur während WW-Ladung
verfügbar) und der Außentemperatur. Datenquelle Prometheus (HA-
prometheus-Integration; diese HA schreibt nicht nach InfluxDB).
Gegenstück zum HA-Sensor-PR im homeassistant-Repo.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Immich hat am 2026-07-02 v3.0.0/v3.0.1 released; die ImagePolicy war
bewusst auf semver 2.x gegated, daher blieb v2.7.5 stehen.
Preflight fuer den Major-Bump:
- pgvecto.rs-Removal betrifft uns nicht: DB_VECTOR_EXTENSION unset,
v2.7.5 laeuft bereits auf VectorChord (dual-extension CNPG-Image)
- keine der entfernten ML-Env-Vars (MACHINE_LEARNING_PRELOAD__*,
IMMICH_MACHINE_LEARNING_PING_TIMEOUT) in den Manifests
- oauth.issuerUrl ist valide HTTPS-URL (strengere v3-Validierung ok)
- Gatus probt /api/server/ping — nicht auf der Removed-Endpoints-Liste
Policy-Gate wandert auf 3.x; 4.x-Cutover braucht wieder manuellen Review.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The zwave-js settings backup guard in #120 checked `tail -c1 == "}"`, but
settings.json ends with "}\n" — so the last byte is a newline and the guard
always skipped ("not }-terminated"), producing no backup. Strip trailing
whitespace before the check so a valid, newline-terminated file is backed up
while a genuinely truncated one is still rejected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
With logs off flash, the dominant remaining eMMC writer is zwave-js-ui: its
value DB is fsync'd on every value change (jsonl-db; zwave-js-ui doesn't
expose the driver throttle). On a busy network that fsync storm churned
~2.6 GiB/day for ~0.1 KiB/s of real data — near-pure write amplification
(each fsync forces a block write + ext4 journal commit).
Preload libeatmydata into the zwave-js container (musl .so extracted from the
image's Alpine base, mounted ro, LD_PRELOAD) so fsync/fdatasync become no-ops
and the kernel batches writes via normal writeback. The WS/MQTT value stream
to HA and the on-disk cache are unchanged — full data resolution, no
re-interview on restart. No ext4 mount option can do this (fsync is a
durability contract; commit=/data=writeback/nobarrier don't batch it, and the
only mount-level alternative — tmpfs — makes the whole store volatile).
Also:
- noatime -> noatime,lazytime on root: batches the inode-timestamp writeback
(e.g. the zwave-js lockfile heartbeat) without weakening data durability.
- edge-zwave-settings-backup.timer: hourly validated rolling backup of
settings.json (Z-Wave network keys), since neutralized fsync means a
power-loss mid settings-write could truncate it. Value-cache loss on crash
is harmless (devices re-report; jsonl-db tolerates a partial trailing line).
Verify the drop on the eMMC-write monitor; rollback = drop the LD_PRELOAD env
+ .so mount from the compose file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beide Provider (nur k.renner via Infrastructure Admins, 4 Passkeys
enrolled) an den Passwordless- und Step-up-Flow gebunden; Live-Stand
der Pflichtfelder gespiegelt. PVE, PBS und paperless-gate bleiben
bewusst auf Passwort+MFA.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bindet die OAuth2-Provider Audiobookshelf und immich-oauth-provider
an den Passwordless-Authentication-Flow (Passkey-Autofill inklusive)
und den Step-up-Authorization-Flow — gleiche Mechanik wie Home
Assistant und NetBird (#115/#118). Pflichtfelder (invalidation_flow,
client_type, redirect_uris) spiegeln den Live-Stand vom 2026-07-07.
Nutzerabdeckung geprüft: Audiobookshelf Users (k.renner, s.renner)
haben Passkeys; Immich Users enthält g.renner ohne Passkey — sie
bekommt beim ersten Zugriff das Inline-Enrollment über die Step-up-
Stage (configure), kein Lockout.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Setzt webauthn_stage auf der Identification-Stage des Passwordless-
Flows (Conditional WebAuthn, authentik >= 2025.12): der Browser bietet
enrollte Passkeys direkt im Autofill des Username-Felds an — ein Tap,
kein Username. Username-Tippen bleibt als Fallback für nicht-
discoverable Credentials erhalten.
last_auth_threshold der Passwordless-Validation-Stage von seconds=0
auf minutes=5: verhindert Doppel-Challenge derselben Flow-Ausführung
nach Autofill-Validierung; der Skip ist session-gebunden.
Validation-Stage-Entry vor den Identification-Entry gezogen, weil
!KeyOf nur rückwärts auflöst.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The role README, install play, and a defaults comment described edge-1 as a
"bare-metal Arch N100" partitioning "the NVMe". The deployed hardware is a
Dell Wyse 3040 (Atom x5-Z8350, 2 GB RAM) on a 7.3 GB internal eMMC. Correct
the docs to reflect reality, note the actual hardware up front, and keep the
installer's dual NVMe/eMMC autodetect accurately described. Docs/comments
only — no runtime change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Ergänzt den Step-up um den eigentlichen Kern: einen passwortlosen
Authentication-Flow (Identification ohne Password-Stage → WebAuthn →
Login), per authentication_flow an beide Provider gebunden. Frische
Logins in HA/NetBird zeigen damit kein Passwortfeld mehr; der
Authentik-eigene Login (Default-Brand-Flow) bleibt unverändert.
Sicherheitsdetail: die Validation-Stage im Authentication-Flow ist
eine SEPARATE Stage mit not_configured_action: deny — `configure`
wäre hier eine Account-Übernahme (User ist nur identifiziert, nicht
authentifiziert). Inline-Enrollment bleibt exklusiv im Authorization-
Step-up mit require_authenticated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR #114 pointed the edge-alloy.service unit at /usr/bin/alloy, but the Arch
`grafana-alloy` package installs the binary as /usr/bin/grafana-alloy. The
unit crash-looped with status=203/EXEC ("Unable to locate executable
/usr/bin/alloy"). Make the path a variable (edge_alloy_bin) and default it to
the real path. Verified on edge-1: the binary runs (v1.13.2), the alloy user
exists (in systemd-journal), and the deployed config parses cleanly — only
the ExecStart path was wrong.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Neuer Authorization-Flow `passkey-stepup-authorization` mit einer
WebAuthn-only Authenticator-Validation-Stage, per Blueprint auf die
OAuth2-Provider von Home Assistant und NetBird gebunden. Login in
diese Apps erfordert damit einen Passkey — auch mit bestehender
SSO-Session aus einem Passwort-Login.
- not_configured_action: configure — g.renner (HA-Zugriff, kein
Passkey enrolled) wird beim ersten Login zur Enrollment geführt
statt ausgesperrt
- last_auth_threshold hours=8 — kein Doppel-Touch direkt nach
Passkey-Login; NetBirds /silent-auth (prompt=none) bleibt innerhalb
des Fensters funktionsfähig
- Provider-Pflichtfelder (invalidation_flow, redirect_uris,
client_type) spiegeln den Live-Stand vom 2026-07-07; ab jetzt
besitzt Git diese Felder
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
edge-1 is a Dell Wyse 3040 on a 7.3 GB internal eMMC (not the N100/NVMe the
README assumed). Unbounded docker json logs (a single zwave-js log had grown
to 214 MB) plus a persistent journald were churning the flash. Move all
logging off local flash and ship it to Loki instead:
- journald Storage=volatile (RAM), size-capped; purge the on-disk store
- docker json-file -> journald log driver (no container json logs on disk)
- Grafana Alloy (Arch pkg) tails the RAM journal -> Loki, WAL on /run tmpfs
Loki endpoint: the unauthenticated srv-network Loki is firewalled off the IoT
VLAN, so Alloy pushes to the authenticated cluster ingress
(https://loki.renner.uno) with a dedicated 'edge-1' basic-auth user — its
htpasswd line is added to the loki-basic-auth cluster secret, plaintext in the
edge_host ansible vault. Delivery activates once Flux reconciles that secret;
until then Alloy 401s and buffers in RAM.
Flash longevity: noatime on root, weekly fstrim.timer, and a weekly
edge-emmc-health check of the JEDEC life-time / pre-EOL estimators.
The Alloy config is purpose-built in the edge-host role rather than reusing
the shared monitoring-agent role: that role is Debian-oriented (apt + GitHub
binary), force-enables a docker log source that would double-ship against the
journald driver, and always configures a Prometheus remote-write whose
endpoint is unreachable from the IoT VLAN.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
host_key_checking=False entfernt (zurück auf Default = aktiv) und im
Kommentar festgehalten, warum das nie wieder abgeschaltet werden darf:
Der DNS-Drift-Vorfall vom 2026-07-06 (edge-1.iot.renner.uno zeigte per
public Wildcard auf den Hetzner-Proxy) wurde nur deshalb nicht vom
Hostkey-Wechsel-Alarm gestoppt, weil diese Einstellung ihn unterdrückt
hat — ein Produktions-Play lief dadurch gegen die falsche Maschine.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vitocal.543 (SmartGridReadyConsolidator), 2350 (EMS ResultingControl-
State), 2560 (SmartGridFeatureSelection) und 2543 (SmartGrid-Offsets)
— alle read-only. Zweck: beobachten, ob die Eigenstromoptimierung
(EMS) bei PV-Überschuss von selbst den WW-Boost auslöst; der Offset
2543.2 steht laut DID-Scan vom 2026-07-02 bereits auf +10 K. Klärt
Option B (natives EMS) vor SG-Ready-Verdrahtung (Option A) und ohne
CAN-Writes (Serviceanleitung 6200041: EMS und SG-Kontakte bedienen
dieselben 2543er-Parameter; SG-/EMS-Zustand ist auf der DID-Ebene nur
als ro-Status sichtbar).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds DID 266 (ErrorDtcHistory, O3EList) for the VX3 to the open3e poll
set so the FailureCommunicationEnergyMeter fault — the E380 energy-meter
link the internal PV-surplus EMS relies on — is continuously visible in
MQTT/HA, catching recurrences beyond the transient 2026-07-02 event.
Decodes via the master O3EList codec inherited through the vx3plus
overlay (the vx3 base list keeps 266). Read-only; applied via the
edge-host ansible pipeline.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The grid price was published to evcc/tariff/gridprice, under evcc's own
retained-topic namespace — evcc manages evcc/# and wiped the price
(observed: retained value cleared with no broker/evcc restart), which
would hang evcc on its next restart until the HA automation republishes.
Move it to a neutral topic evcc only READS: grant the evcc Mosquitto user
'read energy/#', and point the grid tariff at energy/tariff/grid. The HA
automation is repointed in the homeassistant repo.
Note: applying this restarts the Mosquitto pod once (ACL change).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a commented, documented heating-device block to evcc.yaml: an
sgready-relay charger + heating loadpoint (surplus-threshold driven) plus
a heat-pump power meter (open3e DID 2488). Left INACTIVE so evcc doesn't
try to reach a relay that isn't wired yet. To activate: wire a Shelly to
the Vitocal SG-Ready terminals (143.1/143.4/143.5), enable SG in the
service menu, set host, uncomment. evcc drives boost/normal via SG-Ready;
the heat pump firmware owns the smart-grid logic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>