Commit Graph
4354 Commits
Author SHA1 Message Date
k.rennerandClaude Opus 4.7 9db5ac19de ingress: drop public hostnames before adding gated public Ingress (step 1/2)
Splitting the home/docs gating change into two commits because Flux
dry-run rejects the new *-public Ingress as long as the old Ingress
still defines the same host (validating webhook checks against live
state, not the post-apply state).

Step 1: remove home.renner.uno / docs.renner.uno from the existing
Ingresses. Step 2 (next commit): add home-assistant-public /
paperless-public Ingresses with forward-auth annotations.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:42:52 +02:00
k.rennerandClaude Opus 4.7 5096056306 expose home + docs publicly via cloudflared, gate with Authentik
home.renner.uno and docs.renner.uno now traverse the cloudflared
tunnel and are forced through an Authentik nginx proxy outpost
before reaching the HA / paperless pods.

  * apps/authentik/blueprints-configmap.yaml: new "nginx" proxy
    outpost. Worker provisions Deployment + Service named
    ak-outpost-nginx in the authentik namespace.

  * apps/home-assistant/ingress.yaml: split. Existing ingress keeps
    home.k.renner.uno (LAN un-gated). New `home-assistant-public`
    Ingress for home.renner.uno carries the auth-url / auth-signin /
    auth-response-headers / auth-snippet annotations pointing at
    ak-outpost-nginx.authentik.svc.cluster.local:9000.

  * apps/paperless/paperless.yaml: same split. Existing ingress keeps
    paperless.k.renner.uno + docs.k.renner.uno + paperless.renner.uno
    (LAN un-gated). New `paperless-public` Ingress for docs.renner.uno
    with the same forward-auth annotations.

  * infrastructure/cloudflared/configmap.yaml: hostnames home.renner.uno
    and docs.renner.uno added; both terminate at ingress-nginx.

Per-app Authentik Application + Proxy Provider (mode =
forward_auth_single, hostname = home.renner.uno / docs.renner.uno)
needs to be created in the Authentik UI; not in the blueprint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:38:38 +02:00
k.rennerandClaude Opus 4.7 f42f3266b9 authentik: outposts dial server via in-cluster Service
Outpost pods can't hairpin to the public ingress-nginx LB IP
(`auth.renner.uno` → `10.2.40.80`); Cilium times out the pod →
LB → ingress flow. Switch each outpost's `authentik_host` to
`http://authentik-server.authentik.svc.cluster.local` and carry the
public URL in `authentik_host_browser` for browser-facing redirects.

Apply the same shape to the LDAP outpost while we're touching it
(its config previously left `authentik_host` at the public URL too).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:29:29 +02:00
k.rennerandClaude Opus 4.7 e8c19fbfc7 authentik: pin Caddy/Radius outposts to authentik namespace
Caddy and Local Radius outposts came over from the Proxmox deployment
with `kubernetes_namespace: default` in their config. The chart-rendered
Role only grants permissions in `authentik`, so the K8s controller's
first GET against `secrets/ak-outpost-{name}` in `default` returned
403. LDAP Outpost worked because its config never had the field set
(falls back to the running pod's namespace).

Pin both to `authentik` via the existing blueprint so the controller
spawns them next to authentik-server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:24:21 +02:00
k.rennerandClaude Opus 4.7 1a090ba3a3 authentik: switch outposts from Local Docker to in-cluster Kubernetes
The Authentik DB carried over a `Local Docker connection`
ServiceConnection from the Proxmox deployment, and all four outposts
(Caddy proxy, LDAP, Local Radius, embedded) still pointed at it. The
worker's `outpost_service_connection_monitor` task crashed every cycle
in-pod with `DockerException: ('Connection aborted.',
FileNotFoundError(...))` because the pod has no /var/run/docker.sock.

Ship a Blueprint via `blueprints.configMaps` that reassigns every
Outpost to the auto-created `Local Kubernetes Cluster` connection and
deletes the Docker connection. Authentik's chart-rendered Role already
grants the deploy/svc/ingress/secret/configmap perms needed to spawn
outposts via the K8s API.

While here:
- Grafana OIDC token_url/api_url were still hitting
  `http://authentik-server.authentik.svc.cluster.local`, violating the
  public-hostname-everywhere rule (split iss claim). Point them at
  https://auth.renner.uno/ alongside auth_url.
- seed-authentik comment still described the dropped pg_dump→pg_restore
  Job; replace with the current initdb + Garage CNPG recovery model.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:16:10 +02:00
k.rennerandClaude Opus 4.7 52452e0eb9 seed: drop migration Jobs and seed-only credentials
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
Cutover is complete; seed Jobs would re-overwrite live cluster state
on every rebuild (e.g. CNPG pg_dump|pg_restore from legacy, rsync
data import, InfluxDB backup-restore from legacy). Remove them along
with the SSH keys and legacy tokens that only existed to feed them.

Kept per app:
  audiobookshelf, music-assistant: pvcs only (library lives there).
  authentik, immich, paperless:    CNPG cluster + objectstore +
                                   scheduled-backup + barman creds
                                   + (where present) pvcs.
  influxdb:                        chart helmrelease, bootstrap admin
                                   secret, ha-token Job (must run
                                   every cluster lifetime to mint
                                   the HA-readable bucket-scoped
                                   token).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:50:51 +02:00
k.rennerandClaude Opus 4.7 b0b4c38e0b matter-server: set accept_ra_rt_info_max_plen via CNI tuning chain
Earlier ra-rio-sysctl init container failed with read-only /proc/sys —
NET_ADMIN alone doesn't grant write, and privileged is off the table by
standing rule. Move the sysctl into the macvlan NAD as a chained
`tuning` plugin: applied during interface creation, before any pod
container sees net1. Drops the failed init from the Deployment.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:25:08 +02:00
k.rennerandClaude Opus 4.7 85b8d2acb1 matter-server: accept RA RIOs so Thread OMR prefix lands on net1
Default kernel `accept_ra_rt_info_max_plen=0` drops every RA route-info
entry. OTBR advertises the Thread OMR prefix (fd5f:…/64) to v30iot as
an RIO, so matter-server's net1 silently rejected it and had no route
to Thread devices — CASE sessions hit UDP retransmission timeouts
despite mDNS discovery succeeding. Add an initContainer with NET_ADMIN
that bumps the limit to 64 on net1/all/default before the main
container starts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:19:18 +02:00
k.rennerandClaude Opus 4.7 c1d4507674 matter-server: pin to HAOS-imported fabric (vendor 0x134B / fabric 2)
Default --vendorid=0xFFF1 / --fabricid=1 caused matter-server to allocate
a fresh fabric on first start and load nodes from
16583007271343510012.json (empty), reporting "Loaded 0 nodes" even though
chip.json's caList plus the 425 KB 10489438502500755905.json fabric file
on disk hold the original HAOS device bonds. Pin to the imported fabric
identity so the per-fabric file matches.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 13:04:24 +02:00
k.rennerandClaude Opus 4.7 303e50f751 home-assistant-zwavejs: enable Z-Wave-JS WS server + reconcile settings
Pod was listening only on 8091 (UI), not 3000 (Z-Wave-JS server) — so
HA's "ws://zwave-js…:3000" submit failed with "Failed to connect".
zwave-js-ui's WS server is gated by zwave.serverEnabled — added it
(plus serverPort: 3000 explicit) into the managed Secret.

Init container now does jq deep-merge instead of skip-if-present:
managed keys (port + serverEnabled/serverPort + security keys) get
overlaid on every start, but UI-side fields (gateway/mqtt/ui) survive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 12:10:26 +02:00
k.rennerandClaude Opus 4.7 f7fabf7f7f home-assistant-zwavejs: bootstrap settings.json on first start
Pod's PVC is empty after a rebuild and zwave-js-ui needs port +
security keys to start the driver. Add an init container that copies
settings.json from a SOPS-encrypted Secret into the store PVC if
absent — first-start bootstrap, idempotent on re-runs, and won't
clobber UI edits once the user touches the file.

Settings.json carries the ZWA-2 port (/dev/zwave) plus the four S2
keys and the two long-range S2 keys, taken from HA's existing stored
zwave_js entry so the new driver resumes the existing Z-Wave network.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:59:02 +02:00
k.rennerandClaude Opus 4.7 45b5596f82 home-assistant(reconciler): scope down to trusted_proxies only
The per-integration reconfigure-flow driving was a bad fit. Removing
all of it: Spec/specs(), drive_form_chain, apply_spec/apply_recreate,
DisableRule, the WS client, the .storage drift-detection read, and
the MQTT_PASSWORD / DEV_MODE / INFLUX_TOKEN env plumbing.

What remains: trusted_proxies sync from ingress-nginx Endpoints into
configuration.yaml + reload_core_config REST call. Every integration
that had a Spec is now configured manually via the HA UI and persists
in HA's stored state across restarts/backups.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 11:53:41 +02:00
k.rennerandClaude Opus 4.7 8fc75c9b13 home-assistant: drop influxdb from reconciler
Reconfigure path was failing with HA's generic invalid_config and
the recreate path doesn't help when the underlying issue is on the
InfluxDB side. Manual UI setup is the simpler path: paste the token
once and HA's stored entry survives subsequent reconciles. Drops
INFLUX_TOKEN env from the reconciler container too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:34:57 +02:00
k.rennerandClaude Opus 4.7 7aa7c8b0b0 home-assistant: DEV_MODE=false (cutover)
Legacy HA VM is off, ZWA-2 + stromzaehler USB are now wired into
k8s-edge, zwave-js scaled to 1. Drop the DEV_MODE guard so the
reconciler stops force-disabling esphome, edl21, roomba, philips,
siku, zwave_js.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:11:24 +02:00
k.rennerandClaude Opus 4.7 de100491e2 home-assistant-zwavejs: add SESSION_SECRET secret manifest
Deployment referenced Secret zwave-js-session/session-secret but the
manifest was never authored — under replicas:0 the gap was invisible,
under replicas:1 the pod CreateContainerConfigError'd. SOPS-encrypted
random 32-byte hex secret added to the kustomization.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:59:35 +02:00
k.rennerandClaude Opus 4.7 4c01246850 home-assistant: wire ZWA-2 + stromzaehler USB to k8s-edge
Pinned-edge cutover step. Both USB devices are now physically attached
to k8s-edge (HA legacy VM stopped, USB mappings released).

- zwave-js: replicas 0 → 1, hostPath the ZWA-2 by-id symlink at
  /dev/zwave inside the container.
- home-assistant: hostPath the stromzaehler FTDI by-id symlink at
  /dev/stromzaehler. edl21 integration stays disabled until DEV_MODE
  flips (separate commit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:55:04 +02:00
k.rennerandClaude Opus 4.7 30990fe1a7 cluster-settings: flip to LE prod ACME endpoint
Cutover step. CLUSTER_IN_DEVELOPMENT=false and ACME_SERVER points at
the LE production endpoint; cert-manager re-issues all 16 Certificates
with prod-CA-signed leaves on the next renewal trigger.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:41:32 +02:00
k.rennerandClaude Opus 4.7 201e97223b external-dns(unifi): manage renner.uno alongside k.renner.uno
ci/woodpecker/manual/rebuild-k8s Pipeline failed
Cutover prerequisite: external-dns now pushes A records for
*.renner.uno to UniFi DNS, so LAN clients resolve the canonical
hostnames directly to the cluster ingress LB. k.renner.uno stays
for the migration-temp alternate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 08:49:44 +02:00
security-bot fb960de3e5 chore(image-automation): bump ghcr.io/home-assistant/home-assistant:2026.5.1
Updates managed by image-automation-controller.
2026-05-08 20:31:51 +00:00
security-bot 932f736215 chore(image-automation): bump ghcr.io/music-assistant/server:2.8.7
ci/woodpecker/cron/e2e Pipeline was successful
ci/woodpecker/cron/security-scan Pipeline was successful
Updates managed by image-automation-controller.
2026-05-08 11:30:58 +00:00
k.rennerandClaude Opus 4.7 1c821e110d home-assistant(reconciler): DEV_MODE-disable vicare
ci/woodpecker/cron/e2e Pipeline was successful
ci/woodpecker/cron/security-scan Pipeline was successful
Viessmann's ViCare cloud API rate-limits per account. With both legacy
HA and k8s HA polling against the same account the budget is exhausted
quickly. Disable on the k8s side until cutover, then flip on once the
legacy HA stops polling.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:52:01 +02:00
k.rennerandClaude Opus 4.7 e92a58e68e home-assistant(reconciler): fix drift detection + handle external_step + drop music_assistant
Three fixes after the live test revealed real bugs:

1. Drift detection always fired. Both REST list (?domain=…) and WS
   config_entries/get_single use ConfigEntry.as_json_fragment, which
   intentionally omits .data to keep secrets off the API surface.
   Without seeing .data we couldn't compare against `desired`, so
   has_drift({}, …) was True every iteration and every spec triggered
   a full recreate every 30 s. Switch drift-side reads to a direct
   read of /config/.storage/core.config_entries (atomic via HA's
   storage.py tempfile+rename, debounced ~1 s; iteration interval is
   30 s so reads are always consistent). All WRITE paths stay on the
   documented APIs.

2. drive_form_chain didn't recognize external_step / show_progress /
   menu — it would treat them as "unexpected" but only after the
   delete had already happened. Add explicit early-detect: log "manual
   UI setup required" and abort the flow. Won't help on the recreate
   path (the entry is already gone), but prevents future damage if a
   reconfigure flow ever returns one of these.

3. music_assistant intentionally removed from specs. Its user_flow's
   first step is a single-field form {url} that LOOKS reconcilable —
   but submission triggers external_step (OAuth: /login?return_url=
   my.home-assistant.io/redirect/oauth?state=…). external_step is
   browser-only per HA dev docs (developers.home-assistant.io/docs/
   data_entry_flow_index). Manual UI setup is the only path. The
   long-lived token MA mints during OAuth survives backup-restore so
   this is one-time per cluster lifetime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:42:16 +02:00
k.rennerandClaude Opus 4.7 c29bf71c14 influxdb(ha-token): grant read+write on bucket, not write-only
Direct probe of the in-cluster InfluxDB confirmed `POST /api/v2/write
?org=renner&bucket=suedstr28` returns 403 "insufficient permissions
for write" with the existing token. InfluxDB resolves the bucket name
to an id at request time, and that resolution requires read scope on
the bucket — a write-only auth fails it.

That meant HA's reconfigure-flow validation (which performs a
test-write-of-empty-bytes against the live server) returned errors=
{base: invalid_config}, AND runtime telemetry writes were also
failing silently.

Add --read-bucket alongside --write-bucket. Bump the description
string so the Job's idempotency check doesn't reuse the prior
write-only auth (existing one is left orphaned in the auth db; admin
can clean up later — harmless until then).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:33:31 +02:00
k.rennerandClaude Opus 4.7 5f25acae92 ingress: enforce dual-hostname convention across apps
Add the missing .k.renner.uno or .renner.uno host to loki, gatus,
and home-assistant Ingresses so every app is reachable on both the
internal LAN-side name and the public name. Drop the authentik test
Ingress; auth stays on auth.renner.uno only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:25:06 +02:00
k.rennerandClaude Opus 4.7 c5fc5b2b3e home-assistant(reconciler): fix form-chain looping, mqtt path, wyoming/MA matchers
Live test of the recreate path exposed three real bugs:

1. drive_form_chain re-submitted on validation errors. mqtt
   reconfigure returned form/step=broker/errors={"base":"cannot_connect"}
   and influxdb returned form/step=reconfigure_v2/errors={"base":
   "invalid_config"}; the loop kept re-POSTing the same payload until
   the 8-step cap. A non-empty `errors` field is a hard validation
   failure — log the payload and abort instead of resubmitting.

2. mqtt reconfigure_flow MERGES user_input into entry.data, so the
   restored backup's TLS leftovers (certificate="auto", tls_insecure,
   transport residue from mqtt.renner.uno:8883) survived the merge and
   HA validated against TLS-on-:1883 — cannot_connect. The reconfigure
   schema doesn't let us null those keys cleanly. Switch mqtt to
   via_recreate: delete the entry first (apply_recreate handles it),
   then user_flow lands on a clean broker step.

3. After a successful recreate, wyoming entries pick up the wyoming
   server's announced lowercase name (faster-whisper, openwakeword,
   speech-to-phrase) rather than the HAOS-supervisor capitalized title
   (Whisper, openWakeWord, Speech-to-Phrase) the backup carried.
   Previously match() keyed on the capitalized title only, so the next
   iteration saw "no matching entry". Match either form going forward.

4. music_assistant has three config_entries on the restored backup
   (one real + two setup-flow-retry orphans). The REST list endpoint
   sanitizes out .data, so the prior url-based match returned false for
   all three and the recreate never fired. Match on state="loaded"
   instead — orphans stay "not_loaded".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:20:14 +02:00
k.rennerandClaude Opus 4.7 6173be0af0 home-assistant(reconciler): delete-and-recreate for integrations without reconfigure_flow
Probing the deployed HA 2026.5.0 confirmed wyoming, matter,
music_assistant, remote_calendar, and otbr all return "Handler X
doesn't support step reconfigure" on POST /api/config/config_entries/
flow with entry_id. Their entry data is just a URL/host pointer (no
integration-managed state to preserve), so reconcile via:

  1. DELETE /api/config/config_entries/entry/<entry_id>
  2. POST /api/config/config_entries/flow without entry_id (= SOURCE_USER)
  3. Walk the form chain, submitting fields from spec.desired that
     match each step's data_schema. Probed user-step schemas:
       wyoming         {host, port}
       matter          {url}                  (step is "manual")
       music_assistant {url}
       remote_calendar {calendar_name, url, verify_ssl}
       otbr            {url}                  (optional)

The form-chain driver caps at 8 steps, refuses to drive menu nodes,
and aborts the flow id on any HARestError so we don't leak open flows.

zwave_js stays on the reconfigure path (multi-step menu, not yet
driven). Delete-and-recreate would be unsafe — entry.data carries the
s2_*/s0 keys that own the Z-Wave network. The entry is DEV_MODE-
disabled today and its URL is already correct from the restored
backup, so no reconcile fires.

Removed the reconcile_otbr_json standalone path; otbr now goes through
the spec/recreate path. The only remaining file edit is configuration
.yaml http.trusted_proxies, which has no API surface.

ha_rest now wraps urllib HTTPError in a HARestError that carries the
response body, so 400s log HA's actual complaint instead of a bare
status code.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:12:24 +02:00
k.rennerandClaude Opus 4.7 21a5e12678 home-assistant(reconciler): pass HA YAML tags through PyYAML
configuration.yaml uses HA-specific tags (!include, !include_dir_*,
!secret, …) that yaml.SafeLoader rejects with ConstructorError. Register
a generic multi-constructor for the "!" prefix that wraps the value in
a small _Tagged dataclass, plus a matching SafeDumper representer that
emits the original tag back. Net effect: yaml.safe_load and safe_dump
become round-trip-safe for HA's configuration.yaml shape.

Without this the trusted_proxies reconcile crashed on the first read,
which short-circuited the iteration before the token pre-flight could
even run.

Verified by round-tripping a sample with !include / !include_dir_merge_
list / !secret tags: all three survive parse + dump unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:40:02 +02:00
k.rennerandClaude Opus 4.7 60b00eebfa home-assistant(reconciler): graceful degradation on token mismatch
Two failure modes the first deploy hit:

  1. Authenticated readiness probe (GET /api/) carried the bad token,
     each call counted as a login attempt, and homeassistant.components
     .http.ban hit its threshold after 5 polls — banning ::1 and
     blocking even unauthenticated traffic. Move ha_ready() to
     /manifest.json with no auth header so token problems can't burn
     the ban budget.

  2. The pasted long-lived access token came from a backup that
     pre-dates the token's creation, so its iss claim isn't in this
     HA's /config/.storage/auth. Pre-flight token_registered() now
     decodes the JWT, looks the iss up against the auth db, and on
     mismatch logs a loud actionable message and falls back to a
     degraded mode that does only the file-side ops (trusted_proxies
     write, otbr JSON patch). The reload-core-config and WS-driven
     reconfigures are skipped until the user mints a token from this
     HA's UI and updates the Secret.

Defensive AuthFatal still raises if WS auth somehow fails despite the
pre-check (jwt_key tampering, off-by-one in the iss match) — main loop
sys.exits, kubelet applies restart backoff.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:35:38 +02:00
k.rennerandClaude Opus 4.7 eda1171e41 home-assistant(reconciler-cm): set namespace on configMapGenerator
Flux's first apply of the API-rewrite commit rejected the generated
ConfigMap because no namespace was attached. Setting the kustomization
root namespace would have re-namespaced the cross-namespace Role +
RoleBinding in rbac.yaml (which intentionally live in the `ingress`
namespace), so scope the namespace to the generator entry instead.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:25:32 +02:00
k.rennerandClaude Opus 4.7 44f7b7dcc1 home-assistant: replace bash sidecar with API-driven Python reconciler
Rewrites the config-reconciler sidecar from ~340 lines of inline shell
that edited /config/.storage/core.config_entries via jq to a Python
script (mounted via configMapGenerator) that drives HA's documented
APIs:

  REST  /api/services/homeassistant/reload_core_config
        /api/config/config_entries/entry?domain=<domain>
        /api/config/config_entries/flow              (init reconfigure)
        /api/config/config_entries/flow/<flow_id>    (configure step)
  WS    config_entries/get_single                    (drift detection)
        config_entries/disable, config_entries/update

Behaviour changes:

  - Drift fixes apply via reconfigure_flow + reload_core_config; the
    pod no longer self-deletes to roll over. Convergence in seconds
    instead of ~3 minutes.
  - YAML influxdb: stanza dropped (deprecated upstream); influxdb is
    now reconfigured via the v2 reconfigure flow, which validates
    against the live server before persisting — a stronger guarantee
    than the prior YAML-import path.
  - Authenticated via a long-lived access token (home-assistant-api-
    token Secret, manually minted in the UI; survives backup-restore
    via .storage/auth).
  - otbr stays as the lone .storage edit — upstream does not expose a
    reconfigure flow for it. Documented inline.

RBAC: home-assistant-self-delete Role + RoleBinding removed; the SA
keeps only ingress-nginx Endpoints read access for the trusted_proxies
reconcile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 21:19:46 +02:00
security-bot e068098b3b chore(image-automation): bump ghcr.io/home-assistant/home-assistant:2026.5.0
Updates managed by image-automation-controller.
2026-05-06 16:30:27 +00:00
k.rennerandClaude Opus 4.7 ae4f169fee home-assistant: reconcile music_assistant url to in-cluster Service
Adds a reconcile_entry in the config-reconciler sidecar that rewrites
the music_assistant config_entry's data.url from https://music.renner.uno
(hairpin: Cloudflare Tunnel → ingress-nginx → cluster) to the cluster-
internal Service music-assistant.music-assistant.svc.cluster.local. The
token in .data is preserved by the existing shallow-merge — same MA
instance issued it, no re-auth needed.

The .data.url filter pins reconciliation to the real entry; two empty-
data orphan entries (leftovers from an HA setup-flow retry) live
alongside it on the live HA and would otherwise be over-written by the
shallow merge. CNP-wise, MA's hostNetwork=true means the Service backs
to the node IP, identified as host/remote-node by Cilium — both already
allowed in HA's CNP egress, so no CNP change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:16:47 +02:00
k.rennerandClaude Opus 4.7 d882895d87 import-cluster-access: default AWS_PROFILE to "secrets"
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:49:25 +02:00
k.rennerandClaude Opus 4.7 fbf1011349 image-automation(home-assistant,cloudflared): drop annual calver bump
Both ImagePolicy ranges were pinned to "2026.x", silently freezing on
the previous year's top tag every January until manually widened. Switch
to ">=2026.0.0" — the filterTags regex already gates candidates to
bare-numeric calver triplets, so removing the upper bound stays safe
while eliminating the annual maintenance step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:47:56 +02:00
k.rennerandClaude Opus 4.7 3788cdd85a e2e-test: replace umbrella seed KS with per-app seed-* in flux-ks loop
ci/woodpecker/manual/rebuild-k8s Pipeline was successful
The umbrella `seed` Kustomization has been `suspend: true` since
bfc8857 split it into per-app `seed-<app>` KSes; the prior assertion
of `kustomization/seed Ready=True` therefore fired a phantom failure
on every rebuild post-split.

Iterating the six `seed-<app>` KSes (audiobookshelf, authentik,
immich, influxdb, music-assistant, paperless) also means the test
now actually validates that the split-out KSes converge — which the
previous loop did not exercise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:20:57 +02:00
k.rennerandClaude Opus 4.7 bfc885762c flux(seed): split monolithic seed KS into per-app seed-<app> KSes
ci/woodpecker/manual/rebuild-k8s Pipeline failed
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
Each `<app>` KS now dependsOn its own `seed-<app>` instead of the
shared `seed` KS, so a slow sub-tree (immich-library-seed at ~26m)
no longer blocks unrelated apps. Audiobookshelf / paperless /
music-assistant / Authentik unblock at their own seed-Job
completion (T+~13–17 vs T+~38–39 in pipeline #884).

Old `seed` KS is suspended + prune:false during the migration
window so SSA-driven ownership transfer to the new KSes doesn't
risk pruning Cluster CRs (which are not prune:disabled-annotated).
A follow-up commit deletes seed.yaml + seed/kustomization.yaml.

bootstrap.sh wait-apps drops the monolithic `wait_kustomization
seed`; per-app waits absorb the seed runtime (immich 1800s,
audiobookshelf 900s, paperless/authentik 600s, music-assistant
300s) and a `seed-influxdb` wait (600s) covers the InfluxDB HR
+ token Jobs since InfluxDB has no separate top-level KS.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 07:46:44 +02:00
k.rennerandClaude Opus 4.7 b4eaa5e2e4 k8s-edge: pin to neptune + declare USB passthrough mappings (cutover prep)
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/manual/rebuild-k8s Pipeline was successful
target_node flips from the `saturn` placeholder to `neptune`: VM 103
(legacy HAOS) currently holds the ZWA-2 + stromzaehler USBs on neptune,
so co-locating k8s-edge VM 153 on the same host means at cutover the
USBs rebind from VM 103 → VM 153 in-place with no physical move.

USB block declared on VM 153 ahead of the cutover apply. Until VM 103
is destroyed, the USB devices are still claimed there; this declaration
is therefore not safe to apply in isolation — it lands at the final-
migration `tofu apply` after VM 103 destroy releases the devices.

k8s-edge also stays a Proxmox VM (no bare-metal pivot) — decision
recorded inline + in wiki note k8s-edge-target-node-neptune-decision.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 23:42:43 +02:00
k.rennerandClaude Opus 4.7 bbb4bb1ff5 cert-manager: introduce letsencrypt-cluster + CLUSTER_IN_DEVELOPMENT flag
Adds a global cluster-settings ConfigMap (CLUSTER_IN_DEVELOPMENT,
ACME_SERVER) consumed via Flux postBuild substitution on the
infrastructure-crds Kustomization. A new letsencrypt-cluster
ClusterIssuer reads ACME_SERVER from the ConfigMap at reconcile time;
every Certificate / Ingress in the repo now references letsencrypt-
cluster instead of letsencrypt-{prod,staging} directly.

While CLUSTER_IN_DEVELOPMENT=true, ACME_SERVER points at acme-staging-
v02, so the only certs the cluster can issue are LE staging — sidesteps
the recurring 5-cert/168h prod rate-limit hit during back-to-back
rebuilds (latest occurrence: 2026-05-05 alloy-syslog + loki-ingress).
Flip both keys together when leaving development.

Per-app KSes intentionally do NOT enable postBuild — Grafana dashboards
and HA's deployment shell scripts contain unrelated \${VAR} markers
that would be clobbered. Substitution stays scoped to post-crds/ which
has no other variable expansions.

The standalone letsencrypt-prod / letsencrypt-staging ClusterIssuers
remain defined as escape hatches but are no longer referenced.
e2e-test.sh now also asserts letsencrypt-cluster Ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:51:33 +02:00
k.rennerandClaude Opus 4.7 088bd7b7bb flux(home-assistant): close cross-KS Secret race + relocate ServiceMonitor
matter-server's seed initContainer mounts garage-migration-creds, owned
by the home-assistant Kustomization. Without an explicit dependsOn the
init container CreateContainerConfigError-loops on a fresh rebuild while
HA is still gated upstream — observed during the 2026-05-05 cold start.

home-assistant's ServiceMonitor consumes a CRD installed by the
kube-prometheus-stack chart (monitoring KS), so applying it from the HA
KS races the CRD on first reconcile. Move it under monitoring-rules,
which already exists for monitoring.coreos.com/* resources after the
chart lands its CRDs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:51:11 +02:00
k.renner 7c39deed70 home-assistant(seed): match both home-assistant/ and home-assistant S3 prefixes
ci/woodpecker/manual/rebuild-k8s Pipeline failed
The HACS `s3_compatible` uploader on legacy HA writes nightly backups
to keys like `home-assistantAutomatic_backup_2026.4.4_2026-05-05_…tar`
— prefix + filename concatenated *without* a `/` separator. The first
upload (April 24) used the with-slash form `home-assistant/Automatic_…`
but every nightly since has been the no-slash form. The seed's
`--prefix "$S3_PREFIX/"` filter only matched the with-slash form, so
selection always came back as the April 24 backup; nightlies from
April 25 → May 5 were invisible to the seed.

Drop the trailing slash on the prefix. Both forms now match. The
loosened filter doesn't introduce false matches — no other objects
in this bucket start with `home-assistant`.

This is forward-fixing only (next time the seed runs on a fresh
PVC, it'll pick the latest). The currently-staged April 24 backup
+ sentinel stay in place; refreshing requires deleting both and
restarting HA.
2026-05-05 20:26:57 +02:00
k.renner 31b855bec7 home-assistant: patch influxdb config_entry .title alongside .data
`reconcile_entry` shallow-merges into the entry's `.data` (url, token,
bucket, etc.) but doesn't touch `.title`. HA constructs the title as
`{bucket} ({url})` at first-setup time and never refreshes it when
.data.url is later updated, so the UI's "Integration entries" list
keeps showing the stale legacy URL even when writes are correctly
going to the cluster.

Add a separate jq patch to set the title to
`suedstr28 (http://influxdb-influxdb2.influxdb.svc.cluster.local:8086)`
when the entry's current title doesn't match. Triggers drift=1 → pod
self-delete on rollover. Cosmetic only — the underlying connection
was already pointed at the cluster via the .data patch, but the UI
should reflect that rather than confuse the operator.
2026-05-05 19:32:34 +02:00
k.renner d4a961ffe8 home-assistant: write configuration.yaml influxdb: block + keep entry sync
The HA core influxdb integration is YAML-config-only. Its
`core.config_entries` entry has `source: "import"` and HA only loads
the integration on boot if a `influxdb:` block exists in
configuration.yaml (HA then imports YAML → entry → setup). HAOS
restore brought the entry but no YAML block, so the entry was
orphaned and the integration never started — confirmed by zero
"influx*" log lines from HA.

Re-add the yq-on-configuration.yaml patch step that 0efe616 removed
(the assumption that the integration was UI-config / HACS was wrong;
domain "influxdb" with source "import" = HA core's YAML-import path).
Keep the core.config_entries patch in place so the entry stays
consistent with the YAML — otherwise HA's import on next boot would
overwrite the entry with whatever the YAML says, fighting the
reconciler's earlier patch.

Drift detection is split into two: yaml block + entry. Either drifting
triggers `drift=1` and the pod self-deletes for a clean rollover.
2026-05-05 18:50:26 +02:00
k.renner d7d00d0af8 cnp(home-assistant): allow egress to in-cluster InfluxDB
home-assistant's CNP egress allowlist named "default-deny-ingress"
(misleadingly — it's bidirectional) didn't include the influxdb
namespace, so HA's HACS InfluxDB integration's writes to the in-
cluster InfluxDB silently timed out at TCP connect (Cilium drops
without RST). The legacy http://influxdb.srv.renner.uno path was
reachable via the existing `toEntities: world` rule, but cluster-
internal traffic needs its own toEndpoints rule.

Add influxdb namespace as a permitted egress target. Pairs with
68482f2 (the ha-influxdb-token Job creating the write-bucket auth)
and 0efe616 (the config-reconciler patching HA's core.config_entries
URL/token to the cluster).
2026-05-05 18:36:15 +02:00
security-bot ed2cbe3419 chore(image-automation): bump ghcr.io/zwave-js/zwave-js-ui:11.17.0
Updates managed by image-automation-controller.
2026-05-05 16:25:59 +00:00
k.renner 0efe616288 home-assistant: patch core.config_entries for InfluxDB, not yaml
HA writes to InfluxDB via the HACS community integration, which is a
UI-config integration stored in `.storage/core.config_entries` with
`domain: "influxdb"` — not the built-in yaml `influxdb:` block I was
patching in configuration.yaml. The `core.config_entries` entry has
`url`/`token`/`bucket`/`organization`/`api_version`/`verify_ssl`/
`ssl_ca_cert` (and was pointed at the legacy
`http://influxdb.srv.renner.uno:8086` with the legacy
"Write buckets home_assistant" token).

Replace the configuration.yaml yq-patch with a reconcile_entry call
matching the existing pattern used for matter / otbr / zwave_js /
wyoming. Shallow-merge preserves ssl_ca_cert and any other HA-managed
fields. api_version is a string here ("2"), matching the shape HA
serializes in core.config_entries.
2026-05-05 18:23:40 +02:00
k.renner 68482f2d3d home-assistant: split Role for InfluxDB-token Secret RBAC
Token Job hit `secrets is forbidden: ... cannot create resource` even
though the Role listed `create` in verbs. Kubernetes RBAC rejects
`create` verbs combined with `resourceNames` — at creation time the
resource doesn't yet exist by name, so the rule never matches.

Standard pattern: split into two rules — blanket `create` on Secrets
in the namespace (creating any other Secret is the only collateral
damage, and there's no other actor with this SA), plus resourceNames-
restricted `get/update/patch` on the single named `ha-influxdb-token`
Secret. Job now creates on first run and updates in place on later
runs.
2026-05-05 18:18:24 +02:00
k.rennerandClaude Opus 4.7 1daaa6f59f home-assistant: write to in-cluster InfluxDB
Add a Job that creates an HA-write-scoped auth on the in-cluster
InfluxDB on every fresh deploy and stashes the token in a Secret in
the home-assistant namespace; HA's config-reconciler reads it and
patches configuration.yaml's `influxdb:` block to point at
`influxdb-influxdb2.influxdb.svc.cluster.local:8086`, bucket
`suedstr28`, org `renner`, with the new token.

Two pieces:

1. kubernetes/seed/influxdb/ha-token-job.yaml — Job in influxdb ns
   that, after `wait-for-influxd`:
   - resolves suedstr28's bucket id,
   - reuses an existing matching auth (description "home-assistant
     write") if one's there, else creates a fresh write-bucket auth,
   - kubectl-applies a generic Secret named `ha-influxdb-token` into
     the home-assistant namespace with the resulting token.
   ServiceAccount lives in influxdb; Role+RoleBinding in
   home-assistant grant just get/create/update/patch on the single
   named Secret. Tokens don't survive a fresh InfluxDB bolt store, so
   a static SOPS Secret won't do — must be re-created per deploy.

2. kubernetes/apps/home-assistant/deployment.yaml — config-reconciler
   sidecar gets `INFLUX_TOKEN` env from `ha-influxdb-token` (with
   `optional: true` so HA boots even before the Job finishes), plus a
   new step in the reconcile loop that drift-detects and patches the
   `influxdb:` block. Only connection+auth keys are touched; HA-
   managed include/exclude/tags filters stay intact.

Lives in seed/ for now (matches CNPG and the chart's own location);
moves to apps/influxdb/ at cutover when the seed phase is removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:08:59 +02:00
k.rennerandClaude Opus 4.7 2d22a52526 woodpecker: remove unused per-app migration pipelines
The migrate-{audiobookshelf,authentik-db,immich,influxdb,music-assistant,
paperless} pipelines are not used anymore. Per-app seed Jobs are
applied by the seed Flux KS at rebuild time (kubernetes/seed/<app>/
seed-job.yaml etc.), so the manual trigger-and-wait shape these
Woodpecker pipelines provided is no longer needed.

migrate-influxdb.yml in particular pointed at a deleted path
(kubernetes/migrations/influxdb/seed-job.yaml from a refactor) and
assumed the old scale-STS-to-0+rsync-into-PVC shape, which is
incompatible with the current backup→restore-via-API design — keeping
it around would have meant either fixing it or leaving it broken.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 18:03:26 +02:00
k.rennerandClaude Opus 4.7 92e5281abe ceph(qlc): tune BlueStore for Crucial P2 sustained writes
Enable async TRIM (bdev_enable_discard, bdev_async_discard) so the
QLC FTL keeps a clean-block reserve as data is deleted; route ≤64 KiB
writes through the RocksDB WAL (bluestore_prefer_deferred_size_ssd
65536, batch ops 64) so random small writes coalesce into sequential
WAL bursts before flushing to QLC extents.

Baseline pre-tune from the 2026-05-05 rebuild: 71.6 MB/s avg / 0.89 s
avg latency on a 5-min replicated 4 MiB write bench. Discard requires
a rolling OSD restart to take effect (init-time); the BlueStore
deferred params are runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:35:59 +02:00
k.rennerandClaude Opus 4.7 fd82b45f81 influxdb(seed): use real operator token for legacy backup
Previous legacy-token (re-encrypted from monitoring/grafana-influxdb-token)
appeared to have system-wide permissions but had `orgID:
1b264ff9ba729136` on the auth itself. InfluxDB's `/api/v2/backup/*`
middleware gates on `auth.OrgID == ""` (true operator identity), not
on permission strings — so the seed Job's `influx backup` always hit
401 with the misleading "read:authorizations is unauthorized" error.

Generated a real operator token on legacy via:

  influxd recovery auth create-operator \
    --bolt-path /var/lib/influxdb/influxd.bolt \
    --username "local admin" \
    --org renner

That writes a system-scoped auth (`OrgID == ""`) into the bolt store,
described as "local admin's Recovery Token". Verified: GET
/api/v2/backup/kv with this token returns HTTP 200 +
application/octet-stream.

Re-encrypted the new value into legacy-token.sops.yaml. Existing seed
Job spec (8a12b10) is unchanged — it just needed an actually-operator
token to reach /backup/kv.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 17:06:56 +02:00