Compare commits

...
Author SHA1 Message Date
atlas-botandClaude Opus 5.5 843e21dce7 feat(openproject): deploy OpenProject Community (REN-1363)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
New openproject namespace and Flux KS: web + worker pod (17.9.1-slim,
digest-pinned), op-pg CNPG 17 with Barman to Hetzner, attachments PVC
with a Velero schedule, ESO-generated SECRET_KEY_BASE and admin password,
public Ingress on openproject.renner.uno via the NetBird edge, default-deny
CNP, op-pg PodMonitor and a blackbox health probe.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 21:00:14 +00:00
security-bot bcb6f1cb76 chore(image-automation): bump gitea.renner.uno/konairius/paperclip:build.1791487850@sha256:ff81da300e6c35581f34fad61a55da771b0e991d2fb2dcb624a0361263f01a37
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-08 19:47:38 +00:00
argus-bot (Paperclip service account) 46f25c2990 Merge pull request 'feat(paperclip): bridge allows the agent's own secret routes (REN-1345)' (#1204) from atlas/ren-1345-bridge-agent-secrets into master
ci/woodpecker/push/build-ci-images Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/push/build-paperclip Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-08 19:29:16 +00:00
argus-bot (Paperclip service account) f5360590b7 Merge pull request 'feat(agent-runtime-ops): add jq (REN-1345)' (#1206) from atlas/ren-1345-ops-image-jq into master
ci/woodpecker/push/build-ci-images Pipeline was successful
ci/woodpecker/push/build-agent-runtime Pipeline was successful
2026-10-08 19:29:07 +00:00
argus-bot (Paperclip service account) fa25d6b7b1 Merge pull request 'feat(paperclip): agent-hermes reads Prometheus via the proxy Role (REN-1345)' (#1205) from atlas/ren-1345-hermes-prometheus-read into master 2026-10-08 19:29:05 +00:00
atlas-botandClaude Opus 5.5 1154d918ae feat(agent-runtime-ops): add jq (REN-1345)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:17:17 +00:00
atlas-botandClaude Opus 5.5 afd11b81b0 feat(paperclip): agent-hermes reads Prometheus via the proxy Role (REN-1345)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:17:08 +00:00
atlas-botandClaude Opus 5.5 b77cd8dbae feat(paperclip): bridge allows the agent's own secret routes (REN-1345)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Sandbox pods got 403 Route not allowed on /api/agents/me/secrets*.
Adds the five /api/agents/me secret routes to the callback bridge
allowlist; company-wide secret routes stay denied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:16:47 +00:00
argus-bot (Paperclip service account) de4fa6b44f Merge pull request 'chore(edge): retire sml2mqtt for good (REN-802)' (#1203) from atlas/ren-802-retire-sml2mqtt into master
ci/woodpecker/push/deploy-edge-mqtt-bridge Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-08 19:11:34 +00:00
atlas-botandClaude Opus 5.5 4daee5fcea chore(edge): retire sml2mqtt for good (REN-802)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
The SML power-meter reader was retired by owner decision on 2026-09-19:
grid data comes from the E380 on the CAN bus (wiki
sml-to-e380-grid-meter-migration). Its reader has been unplugged since then.

- register: edge-1 loses write sml2mqtt/# (live, no broker restart)
- bridge templates: no sml2mqtt topic, no legacy sml2mqtt bridge
- remove role power-meter-sml, play install-power-meter.yml, inventory
  group power_meter and the vault indirection
- edge-mqtt-bridge play: pre_tasks stop and remove the leftover compose
  project on edge-1 on the next pipeline run (idempotent)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 19:04:10 +00:00
argus-bot (Paperclip service account) 527c90ee9b Merge pull request 'feat(mqtt): register edge-1, controller main-cb96b1a pushes its bridge password (REN-1307)' (#1202) from atlas/ren-1307-edge1-register into master
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-08 10:35:04 +00:00
atlas-botandClaude Opus 5.5 8ce3afcf88 feat(mqtt): register edge-1, controller main-cb96b1a pushes its bridge password (REN-1307)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
The register account `edge-1` replaces the legacy password users open3e
and sml2mqtt for edge-1's local mosquitto bridge. Controller main-cb96b1a
(REN-1308) delivers the password to edge-mqtt-cred over HTTPS with its
client certificate and pins edge-1's server certificate on first contact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:29:07 +00:00
argus-bot (Paperclip service account) b2c3c1dc3e Merge pull request 'feat(edge): bridge credential pushed by mqtt-account-controller (REN-1307)' (#1201) from atlas/ren-1307-edge1-cred-receiver into master
ci/woodpecker/push/deploy-edge-mqtt-bridge Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-08 10:08:38 +00:00
atlas-botandClaude Opus 5.5 1b3c73dbe4 feat(edge): bridge credential pushed by mqtt-account-controller (REN-1307)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
edge-1's bridge password moves out of ansible-vault. A small receiver,
edge-mqtt-cred, speaks the controller's esp32 API over HTTPS with mTLS
(only CN mqtt-account-controller from the MQTT CA) and writes one bridge
`edge-1` for open3e and sml2mqtt. Until the first push the two
vault-password bridges stay in bridge.d/legacy.conf, so nothing changes
on the cluster side yet.

A Woodpecker pipeline runs the role-only play on merge, so no manual play
run is needed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:02:39 +00:00
argus-bot (Paperclip service account) f28edc6ac6 Merge pull request 'feat(mqtt): controller main-a5e1d98 keeps HA's MQTT entry on its client certificate (REN-953)' (#1200) from atlas/ren-953-controller-ha-cert into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-08 07:12:37 +00:00
atlas-botandClaude Opus 5.5 91b8b9ab8b feat(mqtt): controller main-a5e1d98 keeps HA's MQTT entry on its client certificate (REN-953)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
mqtt-account-controller (REN-1284) now pushes the CA and the client
certificate of the register account home-assistant into HA's MQTT config
entry via HA's reconfigure flow, only when tls.crt changed and only inside
02:00-05:00 Europe/Berlin. Mounts mqtt-home-assistant-tls and the HA token
(OpenBao apps/home-assistant/mqtt-cert-sync-token, service user MQTT Cert
Sync), sets the HA env and adds Secret mqtt-ha-cert-sync to the Role.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 07:02:34 +00:00
argus-bot (Paperclip service account) 745d96e304 Merge pull request 'chore(home-assistant): prune orphaned Secret mosquitto-shellies-credentials (REN-1256, PR 2/2)' (#1199) from atlas/ren-1256-prune-shellies-secret into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-08 04:52:07 +00:00
atlas-botandClaude Opus 5.5 4c5660e447 chore(home-assistant): prune orphaned Secret mosquitto-shellies-credentials (REN-1256)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Step 2 of adopt-then-prune. #1196 put the Secret in the home-assistant
Flux inventory (Ready at 67bc773a). Removing the manifest lets prune
delete it. Nothing reads it since #1191.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 04:45:48 +00:00
argus-bot (Paperclip service account) 67bc773a9c Merge pull request 'chore(home-assistant): adopt orphaned Secret mosquitto-shellies-credentials (REN-1256, PR 1/2)' (#1196) from atlas/ren-1256-adopt-shellies-secret into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-08 04:43:39 +00:00
atlas-botandClaude Opus 5.5 235809766a chore(home-assistant): adopt orphaned Secret mosquitto-shellies-credentials (REN-1256)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Step 1 of adopt-then-prune. The removed shellies bootstrap Job created
this Secret outside Git; nothing reads it. An empty manifest with the
same name and type puts it in the Flux inventory; the follow-up PR
removes the file so prune deletes the Secret.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 04:17:34 +00:00
security-bot 4a503b887b chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1791426752
ci/woodpecker/cron/wiki-maintenance Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
Updates managed by image-automation-controller.
2026-10-08 02:51:33 +00:00
security-bot 5c9d6bacf3 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1791424302
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-08 02:21:33 +00:00
argus-bot (Paperclip service account) d72be453db Merge pull request 'feat(mqtt): remove legacy broker users shellies and open3e-int (REN-1017 step 4, window)' (#1191) from atlas/ren-1017-remove-legacy-shellies into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-08 00:48:49 +00:00
argus-bot (Paperclip service account) a9d8be344e Merge pull request 'feat(authentik): 2026.8.3 chart + samba-bridge 2026.8.3-872f754 (REN-723)' (#1148) from atlas/ren-723-authentik-2026.8.3 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-08 00:29:50 +00:00
argus-bot (Paperclip service account) 69d30e3599 Merge pull request 'feat(home-assistant): ghostunnel sidecar for MQTT by client certificate (REN-953)' (#1169) from atlas/ren-953-ha-mqtt-tls into master 2026-10-08 00:18:45 +00:00
security-bot 8c01eb5389 chore(image-automation): bump ghcr.io/usetrmnl/terminus:0.77.0
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 23:42:39 +00:00
security-bot 60df4538e5 chore(image-automation): bump ghcr.io/immich-app/immich-server:v3.3.0
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 19:11:23 +00:00
security-bot 25fda555a2 chore(image-automation): bump woodpeckerci/woodpecker-server:v3.19.0
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 18:48:37 +00:00
security-bot 210506beb1 chore(image-automation): bump woodpeckerci/woodpecker-agent:v3.19.0
Updates managed by image-automation-controller.
2026-10-07 18:48:36 +00:00
security-bot 7755fccdd4 chore(image-automation): bump ghcr.io/immich-app/immich-machine-learning:v3.3.0
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 18:44:05 +00:00
argus-bot (Paperclip service account) 93d42166f9 Merge pull request 'fix(eso): allow the gitea namespace on the openbao store (REN-1203)' (#1195) from atlas/ren-1203-eso-gitea-namespace into master
ci/woodpecker/cron/issue-fix Pipeline was successful
2026-10-07 18:32:53 +00:00
atlas-botandClaude Opus 5.5 de64c87c99 fix(eso): allow the gitea namespace on the openbao store (REN-1203)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
homelab#1194 added ExternalSecret gitea/gitea-package-retention, but the
openbao ClusterSecretStore only admits listed namespaces. ESO denies it
("denied by spec.condition") and the gitea Kustomization health check
waits on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:27:02 +00:00
argus-bot (Paperclip service account) 47760079df Merge pull request 'feat(gitea): nightly retention for CI image builds (REN-1203)' (#1194) from atlas/ren-1203-gitea-package-retention into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 18:21:14 +00:00
atlas-botandClaude Opus 5.5 9ee1526f88 feat(gitea): nightly retention for CI image builds (REN-1203)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Nothing pruned old CI builds in Gitea's registry, so data-gitea-0 was
down to 24% free four days after the grow to 60Gi (~2.2 GB/day).
Gitea 1.21 has no API for cleanup rules and the package owners are
human accounts, so the rule runs as a CronJob in the gitea namespace,
with the write:package-only token Hermes seeded in REN-1204.

Keeps the 5 newest images per CI build package, main/latest, and any
tag a pod runs now. Skips gitea_admin/*, date-tagged packages and
agent-runtime images.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:12:59 +00:00
security-bot 7687133aa0 chore(image-automation): bump ghcr.io/home-assistant/home-assistant:2026.10.0
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 17:44:47 +00:00
argus-bot (Paperclip service account) 0411e56cc7 Merge pull request 'fix(ci): tzdata in the go and ops agent-runtime images (REN-1196)' (#1192) from atlas/ren-1196-agent-runtime-tzdata into master
ci/woodpecker/push/build-ci-images Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/push/build-agent-runtime Pipeline was successful
2026-10-07 17:39:04 +00:00
atlas-botandClaude Opus 5.5 156d93948d fix(ci): tzdata in the go and ops agent-runtime images (REN-1196)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The stock agent-runtime-claude base has no /usr/share/zoneinfo:
TZ=Europe/Berlin date prints UTC and Python ZoneInfo raises
ZoneInfoNotFoundError (REN-1195). k8s-base and k8s-go run these images.
Install tzdata and fail the build if Europe/Berlin does not resolve.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:33:34 +00:00
atlas-botandClaude Opus 5.5 4be0c53053 feat(mqtt): remove legacy broker users shellies and open3e-int (REN-1017 step 4)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
All Shellies log in with their own Dynamic Security account; the ESP32
open3e-int is decommissioned (owner, REN-1109). Drops both users from the
passwd/ACL, their env and Secret refs, the shellies bootstrap Job, the
open3e-int ExternalSecret, rotator and policy file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 17:18:00 +00:00
argus-bot (Paperclip service account) d9bfaf0a65 Merge pull request 'feat(mqtt): re-land H&T push on wake with controller main-a5b299d (REN-1017, REN-1171)' (#1190) from atlas/ren-1017-ht-wake-reland into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 17:00:05 +00:00
argus-bot (Paperclip service account) 728e917bd4 Merge pull request 'feat(mqtt): Shelly device 30 (Plus 2PM) on its own account (REN-1017)' (#1189) from atlas/ren-1017-shelly-30 into master 2026-10-07 16:47:03 +00:00
atlas-botandClaude Opus 5.5 8c377a2cfd feat(mqtt): re-land H&T push on wake with controller main-a5b299d (REN-1017, REN-1171)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:41:22 +00:00
atlas-botandClaude Opus 5.5 a4ee9f81ac feat(mqtt): Shelly device 30 (Plus 2PM) on its own account (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:33:54 +00:00
argus-bot (Paperclip service account) efb3fc2ec9 Merge pull request 'revert(home-assistant): mqtt-account-controller back to main-1d03144 (REN-1167)' (#1188) from atlas/ren-1167-revert-1183 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 16:28:12 +00:00
security-bot 954218767e chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1791389286
Updates managed by image-automation-controller.
2026-10-07 16:21:32 +00:00
atlas-botandClaude Opus 5.5 6ddb81961f revert(home-assistant): mqtt-account-controller back to main-1d03144 (REN-1167)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Reverts homelab#1183 (merge 2f20f214). Controller main-982231a adds the
wake role to its own dynsec client; the broker kicks the client on that
("disconnected by administrative action" 45 ms after every connect), so
the controller never holds a connection and /healthz stays 503.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 16:05:10 +00:00
security-bot f9dc7ea526 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1791387785
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 15:51:31 +00:00
argus-bot (Paperclip service account) fc535074c0 Merge pull request 'fix: [alert] KubePersistentVolumeFillingUp (paperclip) (#1184)' (#1185) from alert-fix/issue-1184 into master 2026-10-07 15:50:11 +00:00
issue-fix-bot f529698a99 fix: [alert] KubePersistentVolumeFillingUp (paperclip) (#1184)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
2026-10-07 15:42:50 +00:00
security-bot 60e4ba4d68 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1791385485
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 15:21:31 +00:00
argus-bot (Paperclip service account) 2f20f2141f Merge pull request 'feat(mqtt): controller main-982231a (push on wake), register sleeping H&Ts 35-38 (REN-1017)' (#1183) from atlas/ren-1017-ht-wake into master 2026-10-07 15:19:42 +00:00
atlas-botandClaude Opus 5.5 099aea3807 feat(mqtt): controller main-982231a (push on wake), register sleeping H&Ts 35-38 (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 15:04:18 +00:00
security-bot ee8ae7a8e8 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1791382755
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 14:51:31 +00:00
security-bot a760364556 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1791378441
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 13:21:31 +00:00
security-bot c1ae5dc8d3 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1791374125
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 12:21:31 +00:00
argus-bot (Paperclip service account) f90dc1847f Merge pull request 'feat(mqtt): HiluX DS8 unit 44 (toilet) on its own account (REN-1017)' (#1182) from atlas/ren-1017-shelly-group5c into master
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 11:43:48 +00:00
atlas-botandClaude Opus 5.5 64e7fd1fd2 feat(mqtt): HiluX DS8 unit 44 (toilet) on its own account (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:37:31 +00:00
argus-bot (Paperclip service account) 52b320db38 Merge pull request 'feat(mqtt): HiluX DS8 units 9 and 31 (bathroom mesh) on own accounts (REN-1017)' (#1181) from atlas/ren-1017-shelly-group5b into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
2026-10-07 11:21:43 +00:00
atlas-botandClaude Opus 5.5 18b6fa858c feat(mqtt): HiluX DS8 units 9 and 31 (bathroom mesh) on own accounts (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 11:08:10 +00:00
argus-bot (Paperclip service account) 241e10cdc8 Merge pull request 'feat(mqtt): controller main-1d03144 and rotate: false on shelly-2 (REN-1080)' (#1180) from atlas/ren-1080-controller-1d03144-rotate-hold into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 11:01:27 +00:00
argus-bot (Paperclip service account) 49ece400f3 Merge pull request 'feat(mqtt): HiluX DS8 unit 12 on its own account, one unit first (REN-1017)' (#1179) from atlas/ren-1017-shelly-group5 into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 10:51:52 +00:00
security-bot 67d2a09e0c chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1791369788
Updates managed by image-automation-controller.
2026-10-07 10:51:31 +00:00
atlas-botandClaude Opus 5.5 9b149feaba feat(mqtt): controller main-1d03144 and rotate: false on shelly-2 (REN-1080)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
main-1d03144 adds the per-account register field `rotate: false`
(mqtt-account-controller PR #6, reviewed in REN-1073). Set it on shelly-2
only. Both changes go in one commit: main-a307693 rejects a register with
the unknown field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:31:33 +00:00
atlas-botandClaude Opus 5.5 4526c963d0 feat(mqtt): HiluX DS8 unit 12 on its own account, one unit first (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:30:27 +00:00
argus-bot (Paperclip service account) 599e9ffe5e Merge pull request 'feat(mqtt): Shelly group 4 (Pro 4PM, Pro 3) on own accounts (REN-1017)' (#1178) from atlas/ren-1017-shelly-group4 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 10:07:38 +00:00
atlas-botandClaude Opus 5.5 08467a1b7e feat(mqtt): Shelly group 4 (Pro 4PM, Pro 3) on own accounts (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:01:11 +00:00
security-bot 1a1cdee39e chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1791365067
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 09:51:31 +00:00
security-bot 9ab53f0621 chore(image-automation): bump lscr.io/linuxserver/lidarr:3.1.0.4875-ls43
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-10-07 07:53:01 +00:00
argus-bot (Paperclip service account) c12aaffad7 Merge pull request 'feat(mqtt): controller main-a307693, retry failed push (REN-1053)' (#1177) from atlas/ren-1053-controller-a307693 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 07:23:15 +00:00
atlas-botandClaude Opus 5.5 6a17cbc913 feat(mqtt): controller main-a307693, retry failed push (REN-1053)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 07:10:49 +00:00
argus-bot (Paperclip service account) bbf767628a Merge pull request 'feat(mqtt): Shelly group 3 (Plus 2PM, Wall Display, Power Strip) on own accounts (REN-1017)' (#1176) from atlas/ren-1017-shelly-group3 into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 06:42:06 +00:00
atlas-botandClaude Opus 5.5 e1eb919198 feat(mqtt): Shelly group 3 (Plus 2PM, Wall Display, Power Strip) on own accounts (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 06:32:54 +00:00
argus-bot (Paperclip service account) dc40b0e084 Merge pull request 'feat(mqtt): Shelly group 2 (Gen1/G3 plugs, 1PM Mini) on own accounts (REN-1017)' (#1175) from atlas/ren-1017-shelly-group2 into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
2026-10-07 06:23:08 +00:00
atlas-botandClaude Opus 5.5 5f7cbcfff3 feat(mqtt): Shelly group 2 (Gen1/G3 plugs, 1PM Mini) on own accounts (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 06:18:09 +00:00
argus-bot (Paperclip service account) 297d068faf Merge pull request 'feat(mqtt): Shelly group 1 on own MQTT accounts, drop device-list Job (REN-1017)' (#1174) from atlas/ren-1017-shelly-group1 into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 06:10:13 +00:00
atlas-botandClaude Opus 5.5 84e3843e96 feat(mqtt): Shelly group 1 (5 devices without switched load) on own accounts, drop device-list Job (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 06:02:37 +00:00
argus-bot (Paperclip service account) 1cc08a1757 Merge pull request 'chore(mqtt): one-shot Job listing shelly-manager device ids (REN-1017)' (#1173) from atlas/ren-1017-device-list into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 05:58:57 +00:00
atlas-botandClaude Opus 5.5 55725ea6e8 chore(mqtt): one-shot Job listing shelly-manager device ids (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 05:45:08 +00:00
argus-bot (Paperclip service account) 16203265a2 Merge pull request 'feat(mqtt): controller main-4adc9a6, Shelly device 5 on its own account (REN-1017)' (#1172) from atlas/ren-1017-shelly-accounts into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 05:21:29 +00:00
argus-bot (Paperclip service account) f52af8d0da Merge pull request 'network-policies: paperclip pod -> mosquitto :8884 for agents-read (REN-953)' (#1171) from atlas/ren-953-agents-read-paperclip-pod into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 05:13:00 +00:00
atlas-botandClaude Opus 5.5 f8c83347cc feat(mqtt): controller main-4adc9a6 and Shelly device 5 on its own account (REN-1017)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Bump mqtt-account-controller to main-4adc9a6 (shelly-manager#93 client),
give it the shelly-manager API key and URL, and register shelly-5 (Plus i4)
as the first auth: password account. The legacy user shellies stays.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 05:06:40 +00:00
atlas-botandClaude Opus 5.5 19996ebdad network-policies: paperclip pod -> mosquitto :8884 for agents-read (REN-953)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Agent runs mostly execute in the paperclip pod, not in a sandbox (REN-1001:
TCP timeout to :8884 from paperclip-b65cc7768). Adds the egress rule in
paperclip-egress and the paperclip pod as a source in mosquitto-agent-ingress.
:8884 only; it requires an mqtt-ca client certificate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 05:02:17 +00:00
argus-bot (Paperclip service account) a65387e1b8 Merge pull request 'feat(mqtt): read-only cert account agents-read + agent path :8884 (REN-953)' (#1170) from atlas/ren-953-agents-read into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 03:45:29 +00:00
argus-bot (Paperclip service account) e2e51fd01a Merge pull request 'feat(mqtt): register cert account home-assistant (REN-953)' (#1168) from atlas/ren-953-ha-account into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
2026-10-07 03:28:46 +00:00
atlas-botandClaude Opus 5.5 68dd5ad132 feat(mqtt): read-only cert account agents-read for agents (REN-953)
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Register entry agents-read (read open3e/#, zigbee2mqtt/#, homeassistant/#),
delivered to the paperclip namespace; agent kubectl identities may get that
one Secret; dedicated CNPs open agent sandbox pods -> mosquitto :8884.
Nothing restarts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 03:07:20 +00:00
atlas-botandClaude Opus 5.5 519552187a feat(home-assistant): ghostunnel sidecar for MQTT by client certificate (REN-953)
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
HA keeps MQTT client certificates only as PEMs inside its config entry, so a
renewal could not reach it. A ghostunnel client sidecar listens on
127.0.0.1:11883 and forwards to mosquitto :8884 with the home-assistant
certificate, reloading it on renewal. HA's config entry is pointed there in a
separate HA-side step.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 03:05:53 +00:00
atlas-botandClaude Opus 5.5 55e2313114 feat(mqtt): register cert account home-assistant (REN-953)
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Register entry for Home Assistant (ACL readwrite # as the legacy user
homeassistant, different name so it cannot shadow that login) and its Secret
on the controller's revocation list. Nothing restarts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 03:05:15 +00:00
argus-bot (Paperclip service account) faec2f72f2 Merge pull request 'feat(evcc): MQTT login by client certificate (REN-953)' (#1167) from atlas/ren-953-evcc-cert into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/cron/wiki-maintenance Pipeline was successful
2026-10-07 02:58:13 +00:00
argus-bot (Paperclip service account) 8469d99cfd Merge pull request 'chore(deps): pin dependencies' (#1053) from renovate/pin-image-digests into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 02:31:37 +00:00
argus-bot (Paperclip service account) 2913723bb3 Merge pull request 'feat(mqtt): register cert account evcc-client + :8884 path (REN-953)' (#1166) from atlas/ren-953-evcc-account into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
2026-10-07 02:22:29 +00:00
renovate-bot dbccd4cfd7 chore(deps): pin dependencies
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
2026-10-07 02:01:16 +00:00
atlas-botandClaude Opus 5.5 024eb97292 feat(authentik): samba-bridge images 2026.8.3-872f754 (REN-723)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Server and LDAP outpost images built from k.renner/authentik-samba-bridge
872f7546 (Authentik 2026.8.3), in lockstep with the 2026.8.3 chart.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 08:37:50 +00:00
atlas-botandClaude Opus 5.5 ca003e9cd8 feat(authentik): 2026.8.3 chart + trusted proxy CIDRs (REN-723)
2026.8 honours forwarded headers only from AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS.
Set it to the pod CIDRs Traefik connects from. Bridge image tags follow
once authentik-samba-bridge#4 is merged and built (lockstep pin).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-06 08:36:33 +00:00
89 changed files with 2070 additions and 496 deletions
+2 -2
View File
@@ -199,7 +199,7 @@ steps:
# separately rather than one this step quietly implies is covered.
- name: shellcheck
depends_on: [] # see note above `steps:`
image: koalaman/shellcheck-alpine:v0.11.0
image: koalaman/shellcheck-alpine:v0.11.0@sha256:9955be09ea7f0dbf7ae942ac1f2094355bb30d96fffba0ec09f5432207544002
commands:
- |
set -eu
@@ -319,7 +319,7 @@ steps:
# writes its stripped mirror under mktemp and never into the workspace.
- name: promtool
depends_on: [] # see note above `steps:`
image: prom/prometheus:v3.15.0
image: prom/prometheus:v3.15.0@sha256:efd719c99d83b060d9daefdcf00360461adf279f45ef5391f8d111892118753e
commands:
- sh scripts/ci/promtool-rules.sh
+36
View File
@@ -0,0 +1,36 @@
when:
# Deploy only on master (merge) + manual — never auto-deploy from a branch.
- event: push
branch: master
path:
- "ansible/roles/mqtt-bridge/**"
- "ansible/plays/edge-mqtt-bridge.yml"
- "ansible/inventory/group_vars/edge_host/**"
- ".woodpecker/deploy-edge-mqtt-bridge.yml"
- event: deployment
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "deploy-edge-mqtt-bridge"'
timeout: 30
steps:
- name: deploy-edge-mqtt-bridge
image: gitea.renner.uno/gitea_admin/ci-ansible:2026-04-29
environment:
ANSIBLE_FORCE_COLOR: "true"
ANSIBLE_HOST_KEY_CHECKING: "false"
ANSIBLE_ROLES_PATH: "../roles:./roles"
SSH_PRIVATE_KEY:
from_secret: ssh_private_key
VAULT_PASS:
from_secret: vault_pass
commands:
- mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | base64 -d > ~/.ssh/id_ed25519
- chmod 600 ~/.ssh/id_ed25519
- printf "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null\n" > ~/.ssh/config
- cd ansible
- echo "$VAULT_PASS" > .vault_pass && chmod 600 .vault_pass
- export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
# Local MQTT bridge on the edge hosts only (REN-1307); restarts only the
# local mosquitto on edge-1, never the cluster broker.
- ansible-playbook -i inventory/ plays/edge-mqtt-bridge.yml -v
+2 -2
View File
@@ -18,12 +18,12 @@ when:
steps:
- name: iac-scan
image: aquasec/trivy:latest
image: aquasec/trivy:latest@sha256:af6acf9a6b85dfe389a1941505c0ce9efef52a4719635e1a962f022a3d855daa
commands:
- trivy fs --format json -o trivy-iac.json --severity CRITICAL,HIGH --ignore-unfixed .
- name: image-scans
image: aquasec/trivy:latest
image: aquasec/trivy:latest@sha256:af6acf9a6b85dfe389a1941505c0ce9efef52a4719635e1a962f022a3d855daa
commands:
- trivy image --download-db-only
- |
+2 -1
View File
@@ -58,7 +58,8 @@ target `build-agent-runtime`), versions bumped by Renovate. Both also
carry `claude-agent-acp` for the ACP engine, pinned and patched like
Paperclip ships it, SDK override included
(`ci/images/agent-runtime-common/`); bump it by hand
together with Paperclip. All tools are on the default `PATH`.
together with Paperclip. All tools are on the default `PATH`. Both add
`tzdata` (the base has no zoneinfo); the default zone stays UTC.
| Image | `imageRegistry` | Adds |
|---|---|---|
+15 -2
View File
@@ -62,7 +62,6 @@ inventory change.
|---|---|---|
| Innomaker USB2CAN (Vitocal/VX3 + E380) | `heating_can` | `plays/install-heating-can.yml` |
| Nabu Casa ZWA-2 Z-Wave radio | `zwave_gateway` | `plays/install-zwave-gateway.yml` |
| FTDI serial power-meter head | `power_meter` | `plays/install-power-meter.yml` |
Add the host to the matching group, then run that playbook. They depend on
the base play having run: all three need the local MQTT bridge, and two of
@@ -87,7 +86,6 @@ kubectl -n home-assistant get secret zwave-js-session -o jsonpath='{.data.sessio
```yaml
# vault.yml
vault_edge_sml2mqtt_mqtt_password: "..."
vault_edge_open3e_mqtt_password: "..."
vault_edge_zwavejs_session_secret: "..."
vault_edge_loki_password: "..." # dedicated edge-1 Loki push cred (see "Logging & eMMC")
@@ -238,6 +236,21 @@ cluster-side change was needed**: one authenticates as the `open3e` user
out). The local broker is `allow_anonymous` on loopback — the trust boundary
is the host; nothing off-host can reach it.
**Bridge credential (REN-1307).** The two vault-password bridges are being
replaced by one bridge as the register account `edge-1`
(`kubernetes/apps/home-assistant/mqtt-accounts.yaml`). Its password is pushed
by mqtt-account-controller to `edge-mqtt-cred` (`:8443`, the controller's
esp32 API over HTTPS). That endpoint accepts only a client certificate from
the MQTT CA with CN `mqtt-account-controller`; edge-1 holds the public CA and
its own server key, nothing else. On a push it writes
`/etc/mosquitto/bridge.d/edge-1.conf`, deletes `bridge.d/legacy.conf` and
restarts the local mosquitto. The controller sets a new password whenever the
bridge reports "not connected" (also after a reinstall), so no play run is
needed for a rotation. `.woodpecker/deploy-edge-mqtt-bridge.yml` runs
`plays/edge-mqtt-bridge.yml` on merge. Diagnosis: `journalctl -u
edge-mqtt-cred`, and `mosquitto_sub -t '$SYS/broker/connection/edge-1/state'`
(retained `1` = connected).
**eMMC:** the local broker writes **nothing** to flash — `persistence false`
(retained/queued messages in RAM) and `log_dest stdout` (to the volatile
journal → Loki). Do not add persistence or log-file settings to
@@ -2,7 +2,6 @@
# Secret indirection — real values live in vault.yml (ansible-vault) next
# to this file; see docs/edge-host.md for how to populate it from
# the cluster secrets.
edge_sml2mqtt_mqtt_password: "{{ vault_edge_sml2mqtt_mqtt_password }}"
edge_open3e_mqtt_password: "{{ vault_edge_open3e_mqtt_password }}"
edge_zwavejs_session_secret: "{{ vault_edge_zwavejs_session_secret }}"
# Loki push credential (dedicated 'edge-1' user, Traefik basic-auth on the
@@ -39,8 +38,8 @@ chrony_hwtimestamp: false
# --- Local MQTT contract -------------------------------------------------
# Shared between the broker and its clients, so it belongs to the inventory
# rather than to either side's role. mqtt-bridge listens here; heating-can and
# power-meter-sml publish here; and the bridge authenticates to the cluster
# rather than to either side's role. mqtt-bridge listens here; heating-can
# publishes here; and the bridge authenticates to the cluster
# broker AS the open3e user, so it needs that identity too.
#
# Keeping these in one role's defaults only worked while a single play loaded
-4
View File
@@ -41,13 +41,9 @@ edge-1.iot.renner.uno
[heating_can]
edge-1.iot.renner.uno
[power_meter]
edge-1.iot.renner.uno
[edge_host:children]
zwave_gateway
heating_can
power_meter
# NTP servers. edge-1 was added here so the role would survive the PVE hosts
# being retired for bare-metal Talos, which cannot run chrony or keepalived
+33
View File
@@ -0,0 +1,33 @@
---
# Local MQTT bridge and its credential receiver only (REN-1307). Run by
# .woodpecker/deploy-edge-mqtt-bridge.yml on merge, so a change to this role
# reaches edge-1 without a play run by hand. The full base play
# (install-edge-host.yml) carries the same role.
- name: Converge the edge MQTT bridge
hosts: edge_host
become: true
# The SML power-meter reader (sml2mqtt) is retired for good: owner decision
# 2026-09-19, grid data comes from the E380 on the CAN bus. Its role and
# play are gone; this removes the leftover compose project from edge-1 on
# the next pipeline run. Idempotent: a no-op once the directory is gone.
pre_tasks:
- name: Check for the retired sml2mqtt compose project
ansible.builtin.stat:
path: /opt/edge-host/sml2mqtt/docker-compose.yml
register: edge_sml2mqtt_compose
- name: Stop the retired sml2mqtt container
ansible.builtin.command:
cmd: docker compose down --remove-orphans
chdir: /opt/edge-host/sml2mqtt
when: edge_sml2mqtt_compose.stat.exists
changed_when: true
- name: Remove the retired sml2mqtt directory
ansible.builtin.file:
path: /opt/edge-host/sml2mqtt
state: absent
roles:
- mqtt-bridge
-1
View File
@@ -9,7 +9,6 @@
#
# plays/install-zwave-gateway.yml [zwave_gateway] ZWA-2 radio
# plays/install-heating-can.yml [heating_can] Innomaker USB2CAN
# plays/install-power-meter.yml [power_meter] FTDI meter head
#
# Splitting the roles without splitting the plays would have kept the coupling:
# one play applying every role to one group is the monolith again, a layer up.
-14
View File
@@ -1,14 +0,0 @@
---
# Power-meter reader (sml2mqtt) for hosts with the FTDI serial meter head
# attached.
#
# Targets [power_meter]: membership means the meter head is physically present.
#
# Requires the base play (plays/install-edge-host.yml) to have run: it needs
# docker and publishes to the local MQTT bridge.
- name: Install power-meter reader
hosts: power_meter
become: true
roles:
- power-meter-sml
@@ -10,3 +10,14 @@
edge_mqtt_cluster_host: mqtt.renner.uno
edge_mqtt_cluster_tls_port: 8883
edge_mqtt_bridge_cafile: /etc/ssl/certs/ca-certificates.crt
# --- Bridge credential from mqtt-account-controller (REN-1307) ------------
# The cluster account the bridge logs in as (register entry in
# kubernetes/apps/home-assistant/mqtt-accounts.yaml). The controller pushes
# its password to edge-mqtt-cred and rotates it when the bridge reports
# "not connected"; there is no copy in ansible-vault.
edge_mqtt_bridge_account: edge-1
# HTTPS port of edge-mqtt-cred (esp32 device API, mTLS). Only a client
# certificate from the MQTT CA with this CN is accepted.
edge_mqtt_cred_port: 8443
edge_mqtt_cred_allowed_cn: mqtt-account-controller
@@ -0,0 +1,141 @@
#!/usr/bin/env python3
# Managed by ansible (roles/mqtt-bridge). Receives the cluster bridge password
# from mqtt-account-controller (REN-1307) so no plaintext copy lives in
# ansible-vault and no play run is needed to rotate it.
#
# Speaks the controller's esp32 device API over HTTPS:
# GET /api/status -> {"mqtt": {"connected": bool}}
# PUT /api/settings <- {"mqtt": {"user": "...", "pass": "..."}}
# A client certificate from the MQTT CA with CN mqtt-account-controller is
# required; nothing else may call it. The server key is generated on this
# host and never leaves it (the controller pins it on first contact).
#
# PUT writes the bridge file (template + credentials), removes the legacy
# vault-password bridges and restarts the local mosquitto. The password is
# kept only in that 0640 root:mosquitto file.
import grp
import json
import os
import socket
import ssl
import subprocess
import sys
import tempfile
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
CONF = json.load(open(sys.argv[1]))
WRITE = threading.Lock()
def connected():
try:
out = subprocess.run(
["mosquitto_sub", "-h", "127.0.0.1", "-p", str(CONF["local_port"]),
"-t", CONF["state_topic"], "-C", "1", "-W", "3"],
capture_output=True, text=True, timeout=10).stdout
except (OSError, subprocess.TimeoutExpired):
return False
return out.strip() == "1"
def write_bridge(user, password):
with open(CONF["template"]) as f:
body = f.read()
body += f"remote_username {user}\nremote_password {password}\n"
d = os.path.dirname(CONF["bridge_file"])
fd, tmp = tempfile.mkstemp(dir=d, prefix=".edge-mqtt-cred.")
with os.fdopen(fd, "w") as f:
f.write(body)
os.chown(tmp, 0, grp.getgrnam("mosquitto").gr_gid)
os.chmod(tmp, 0o640)
os.replace(tmp, CONF["bridge_file"])
for legacy in CONF["legacy_files"]:
if os.path.exists(legacy):
os.remove(legacy)
subprocess.run(["systemctl", "restart", "mosquitto"], check=True, timeout=60)
def valid(s):
# Controller passwords are random URL-safe tokens; anything with
# whitespace or a control character would break the config line.
return isinstance(s, str) and 0 < len(s) <= 256 and s.isprintable() and not any(c.isspace() for c in s)
class Handler(BaseHTTPRequestHandler):
def authorized(self):
cert = self.connection.getpeercert() or {}
cns = [v for rdn in cert.get("subject", ()) for k, v in rdn if k == "commonName"]
if cns != [CONF["allowed_cn"]]:
self.send_error(403)
return False
return True
def reply(self, code, obj=None):
data = b"" if obj is None else json.dumps(obj).encode()
self.send_response(code)
if data:
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def do_GET(self):
if not self.authorized():
return
if self.path != "/api/status":
return self.send_error(404)
self.reply(200, {"mqtt": {"connected": connected()}})
def do_PUT(self):
if not self.authorized():
return
if self.path != "/api/settings":
return self.send_error(404)
try:
n = int(self.headers.get("Content-Length", "0"))
if not 0 < n <= 4096:
raise ValueError("body size")
m = json.loads(self.rfile.read(n))["mqtt"]
user, password = m["user"], m["pass"]
except (ValueError, KeyError, TypeError):
return self.send_error(400)
if user != CONF["user"] or not valid(password):
return self.send_error(400)
with WRITE:
write_bridge(user, password)
self.log_message("bridge credentials for %s replaced, mosquitto restarted", user)
self.reply(204)
def log_message(self, fmt, *args):
sys.stderr.write("%s %s\n" % (self.address_string(), fmt % args))
class Server(ThreadingHTTPServer):
address_family = socket.AF_INET6
def server_bind(self):
self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
super().server_bind()
def get_request(self):
# The TLS handshake runs in the request thread, with a timeout, so a
# stalled peer cannot block the accept loop.
sock, addr = self.socket.accept()
sock.settimeout(10)
return self.ctx.wrap_socket(sock, server_side=True, do_handshake_on_connect=False), addr
def main():
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
ctx.load_cert_chain(CONF["cert"], CONF["key"])
ctx.load_verify_locations(CONF["ca"])
ctx.verify_mode = ssl.CERT_REQUIRED
srv = Server(("::", CONF["port"]), Handler)
srv.ctx = ctx
srv.serve_forever()
if __name__ == "__main__":
main()
@@ -0,0 +1,10 @@
-----BEGIN CERTIFICATE-----
MIIBdTCCARygAwIBAgIUN8ne4k4Wif9O83GDyKiBP0Ma7EIwCgYIKoZIzj0EAwIw
GTEXMBUGA1UEAxMOUmVubmVyIE1RVFQgQ0EwHhcNMjYxMDA2MTUzMDE1WhcNMzYx
MDAzMTUzMDE1WjAZMRcwFQYDVQQDEw5SZW5uZXIgTVFUVCBDQTBZMBMGByqGSM49
AgEGCCqGSM49AwEHA0IABOZhZF2JDoyvVg2oIXSbJAuAWDcoG22RxPA4oMKRMA/b
XzIY566D2k6cVnWqbil9s/q4O2mj+Epo7plBw8NwuOOjQjBAMA4GA1UdDwEB/wQE
AwICpDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQcqSDDiLZzhrvqq9f16Y08
CO7QfzAKBggqhkjOPQQDAgNHADBEAiBFkxd9pXnqIcjReEcOTvCBZ3HxKu2jNQGL
/le7gunTGgIgZapWAcu4awa+wmZcR/z2luOjD2GhIT15um14Z+sstJk=
-----END CERTIFICATE-----
@@ -5,3 +5,9 @@
ansible.builtin.service:
name: mosquitto
state: restarted
- name: Restart edge-mqtt-cred
ansible.builtin.systemd:
name: edge-mqtt-cred
state: restarted
daemon_reload: true
+84 -2
View File
@@ -11,13 +11,39 @@
name: mosquitto
state: present
- name: Deploy local mosquitto bridge config
- name: Create the bridge directory
ansible.builtin.file:
path: /etc/mosquitto/bridge.d
state: directory
owner: root
group: mosquitto
mode: "0750"
- name: Check for the controller-managed bridge file
ansible.builtin.stat:
path: "/etc/mosquitto/bridge.d/{{ edge_mqtt_bridge_account }}.conf"
register: edge_mqtt_bridge_managed
# Until mqtt-account-controller has pushed the first password, keep the
# vault-password bridges so the cluster never loses edge-1's data. Once the
# managed file exists, never render them again (edge-mqtt-cred removed them).
- name: Deploy legacy vault-password bridges
ansible.builtin.template:
src: edge-mqtt-bridge-legacy.conf.j2
dest: /etc/mosquitto/bridge.d/legacy.conf
owner: root
group: mosquitto
mode: "0640" # carries the legacy cluster bridge credentials
when: not edge_mqtt_bridge_managed.stat.exists
notify: Restart mosquitto
- name: Deploy local mosquitto config
ansible.builtin.template:
src: edge-mqtt-bridge.conf.j2
dest: /etc/mosquitto/mosquitto.conf
owner: root
group: mosquitto
mode: "0640" # carries the cluster bridge credentials
mode: "0640"
notify: Restart mosquitto
- name: Enable local mosquitto bridge
@@ -25,3 +51,59 @@
name: mosquitto
enabled: true
state: started
# --- edge-mqtt-cred (REN-1307): receives the bridge password from
# mqtt-account-controller. edge-1 holds only the public MQTT CA and its own
# server key, generated here and never copied off the host.
- name: Create edge-mqtt-cred config directory
ansible.builtin.file:
path: /etc/edge-mqtt-cred
state: directory
owner: root
group: root
mode: "0700"
- name: Generate edge-mqtt-cred server key and certificate
ansible.builtin.command:
cmd: >-
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1
-nodes -days 36500 -subj "/CN={{ inventory_hostname }}"
-keyout /etc/edge-mqtt-cred/tls.key -out /etc/edge-mqtt-cred/tls.crt
creates: /etc/edge-mqtt-cred/tls.key
notify: Restart edge-mqtt-cred
- name: Deploy edge-mqtt-cred files
ansible.builtin.copy:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: root
group: root
mode: "{{ item.mode }}"
loop:
- {src: edge-mqtt-cred.py, dest: /usr/local/sbin/edge-mqtt-cred, mode: "0755"}
# Public CA of the cluster's MQTT client certificates
# (kubernetes/infrastructure/post-crds/mqtt-ca.yaml), valid until 2036-10-03.
- {src: mqtt-ca.crt, dest: /etc/edge-mqtt-cred/mqtt-ca.crt, mode: "0644"}
notify: Restart edge-mqtt-cred
- name: Deploy edge-mqtt-cred config and bridge template
ansible.builtin.template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
owner: root
group: root
mode: "{{ item.mode }}"
loop:
- {src: edge-mqtt-cred.json.j2, dest: /etc/edge-mqtt-cred/config.json, mode: "0600"}
# Read only on the controller's next push: a change here reaches the
# running bridge when the password is next set.
- {src: edge-1-bridge.conf.in.j2, dest: /etc/edge-mqtt-cred/bridge.conf.in, mode: "0600"}
- {src: edge-mqtt-cred.service.j2, dest: /etc/systemd/system/edge-mqtt-cred.service, mode: "0644"}
notify: Restart edge-mqtt-cred
- name: Enable edge-mqtt-cred
ansible.builtin.systemd:
name: edge-mqtt-cred
enabled: true
state: started
daemon_reload: true
@@ -0,0 +1,21 @@
# Managed by roles/mqtt-bridge (REN-1307). edge-mqtt-cred appends
# remote_username/remote_password to this and writes the result to
# /etc/mosquitto/bridge.d/{{ edge_mqtt_bridge_account }}.conf. One bridge for
# open3e, as the register account `{{ edge_mqtt_bridge_account }}`
# (ACL readwrite open3e/#). sml2mqtt is retired (SML reader, 2026-09-19).
connection {{ edge_mqtt_bridge_account }}
address {{ edge_mqtt_cluster_host }}:{{ edge_mqtt_cluster_tls_port }}
bridge_cafile {{ edge_mqtt_bridge_cafile }}
bridge_insecure false
remote_clientid edge1-bridge
cleansession false
try_private true
restart_timeout 5 30
queue_qos0_messages true
# The bridge state goes to the LOCAL broker only (retained 1/0); edge-mqtt-cred
# reads it to answer the controller's "connected?".
notifications true
notifications_local_only true
notification_topic $SYS/broker/connection/{{ edge_mqtt_bridge_account }}/state
topic open3e/# out 0
topic open3e/cmnd in 0
@@ -0,0 +1,21 @@
# Managed by roles/mqtt-bridge. Legacy vault-password bridges, rendered only
# while /etc/mosquitto/bridge.d/{{ edge_mqtt_bridge_account }}.conf does not
# exist; edge-mqtt-cred deletes this file on the controller's first push
# (REN-1307). Remove with the vault keys once edge-1 is on the register account.
# --- Bridge 1: open3e datapoints + command topic, as the cluster `open3e`
# user (its ACL is `readwrite open3e/#`, which covers publishing datapoints
# and subscribing to open3e/cmnd). e3oncan also publishes under open3e/#.
connection edge1-open3e
address {{ edge_mqtt_cluster_host }}:{{ edge_mqtt_cluster_tls_port }}
bridge_cafile {{ edge_mqtt_bridge_cafile }}
bridge_insecure false
remote_username {{ edge_open3e_mqtt_user }}
remote_password {{ edge_open3e_mqtt_password }}
remote_clientid edge1-bridge-open3e
cleansession false
try_private true
notifications false
restart_timeout 5 30
queue_qos0_messages true
topic open3e/# out 0
topic open3e/cmnd in 0
@@ -22,36 +22,9 @@ log_type notice
listener 1883 127.0.0.1
allow_anonymous true
# --- Bridge 1: open3e datapoints + command topic, as the cluster `open3e`
# user (its ACL is `readwrite open3e/#`, which covers publishing datapoints
# and subscribing to open3e/cmnd). e3oncan also publishes under open3e/#.
connection edge1-open3e
address {{ edge_mqtt_cluster_host }}:{{ edge_mqtt_cluster_tls_port }}
bridge_cafile {{ edge_mqtt_bridge_cafile }}
bridge_insecure false
remote_username {{ edge_open3e_mqtt_user }}
remote_password {{ edge_open3e_mqtt_password }}
remote_clientid edge1-bridge-open3e
cleansession false
try_private true
notifications false
restart_timeout 5 30
queue_qos0_messages true
topic open3e/# out 0
topic open3e/cmnd in 0
# --- Bridge 2: power-meter topics, as the cluster `sml2mqtt` user (ACL
# `write sml2mqtt/#`). Publish-only, so no `in` topics.
connection edge1-sml2mqtt
address {{ edge_mqtt_cluster_host }}:{{ edge_mqtt_cluster_tls_port }}
bridge_cafile {{ edge_mqtt_bridge_cafile }}
bridge_insecure false
remote_username sml2mqtt
remote_password {{ edge_sml2mqtt_mqtt_password }}
remote_clientid edge1-bridge-sml2mqtt
cleansession false
try_private true
notifications false
restart_timeout 5 30
queue_qos0_messages true
topic sml2mqtt/# out 0
# --- Bridges to the cluster broker (REN-1307). bridge.d holds either
# {{ edge_mqtt_bridge_account }}.conf, written by edge-mqtt-cred with the password
# mqtt-account-controller pushed, or, until that first push, legacy.conf with
# the two vault-password bridges. edge-mqtt-cred deletes legacy.conf when it
# writes the new file.
include_dir /etc/mosquitto/bridge.d
@@ -0,0 +1,13 @@
{{ {
"port": edge_mqtt_cred_port,
"allowed_cn": edge_mqtt_cred_allowed_cn,
"user": edge_mqtt_bridge_account,
"cert": "/etc/edge-mqtt-cred/tls.crt",
"key": "/etc/edge-mqtt-cred/tls.key",
"ca": "/etc/edge-mqtt-cred/mqtt-ca.crt",
"template": "/etc/edge-mqtt-cred/bridge.conf.in",
"bridge_file": "/etc/mosquitto/bridge.d/" ~ edge_mqtt_bridge_account ~ ".conf",
"legacy_files": ["/etc/mosquitto/bridge.d/legacy.conf"],
"local_port": edge_mqtt_port,
"state_topic": "$SYS/broker/connection/" ~ edge_mqtt_bridge_account ~ "/state",
} | to_nice_json }}
@@ -0,0 +1,19 @@
# Managed by roles/mqtt-bridge (REN-1307). See /usr/local/sbin/edge-mqtt-cred.
[Unit]
Description=Receives the MQTT bridge credential from mqtt-account-controller
After=network-online.target mosquitto.service
Wants=network-online.target
[Service]
ExecStart=/usr/bin/python3 /usr/local/sbin/edge-mqtt-cred /etc/edge-mqtt-cred/config.json
Restart=always
RestartSec=5
# Root only to write the bridge file and restart mosquitto.
ProtectSystem=strict
ReadWritePaths=/etc/mosquitto/bridge.d
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
[Install]
WantedBy=multi-user.target
@@ -1,23 +0,0 @@
---
# Power-meter reader: sml2mqtt against the FTDI serial meter head.
# Service root. Kept per-role so the role is usable on its own.
edge_base_dir: /opt/edge-host
# Own compose project directory. Unchanged from the path used under the
# previous shared project.
sml_base_dir: "{{ edge_base_dir }}/sml2mqtt"
# The compose file that used to hold both this service and zwave-js. Removed
# once, so the fixed container_name is released for the new projects.
edge_legacy_compose: "{{ edge_base_dir }}/docker-compose.yml"
# Stable by-id symlink -- see the zwave-gateway role.
edge_stromzaehler_device: /dev/serial/by-id/usb-FTDI_FT230X_Basic_UART_DK0E1GP5-if00-port0
# Hand-pinned on purpose: critical meter bridge, deliberately excluded from
# image automation.
edge_sml2mqtt_image: spacemanspiff2007/sml2mqtt:3.6.0
# Secret from inventory/group_vars/edge_host/vault.yml:
# edge_sml2mqtt_mqtt_password
@@ -1,6 +0,0 @@
---
- name: Restart sml2mqtt
ansible.builtin.command:
cmd: docker compose up -d --force-recreate
chdir: "{{ sml_base_dir }}"
changed_when: true
@@ -1,55 +0,0 @@
---
# Power-meter reader: sml2mqtt against the FTDI serial meter head.
#
# Its own compose project. It shared a compose file with the Z-Wave gateway,
# which meant a change to either recreated both -- two unrelated services bound
# to different USB devices. Data paths are unchanged.
- name: Create sml2mqtt directory
ansible.builtin.file:
path: "{{ sml_base_dir }}"
state: directory
mode: "0755"
# sml2mqtt reads only a config file and has no env override for the password,
# so the rendered config carries it -- 0600, root-owned, same trust boundary as
# the k8s init-container approach it replaced.
- name: Deploy sml2mqtt config
ansible.builtin.template:
src: sml2mqtt-config.yml.j2
dest: "{{ sml_base_dir }}/config.yml"
mode: "0600"
notify: Restart sml2mqtt
- name: Deploy sml2mqtt compose project
ansible.builtin.template:
src: docker-compose.yml.j2
dest: "{{ sml_base_dir }}/docker-compose.yml"
mode: "0644"
notify: Restart sml2mqtt
- name: Pull sml2mqtt image
ansible.builtin.command:
cmd: docker compose pull
chdir: "{{ sml_base_dir }}"
changed_when: false
# Tolerates a missing USB device, as for the Z-Wave gateway.
- name: Start sml2mqtt
ansible.builtin.command:
cmd: docker compose up -d
chdir: "{{ sml_base_dir }}"
register: sml_compose_up
failed_when: >-
sml_compose_up.rc != 0
and 'error gathering device information' not in sml_compose_up.stderr
changed_when: >-
'Started' in sml_compose_up.stderr or 'Created' in sml_compose_up.stderr
or 'Started' in sml_compose_up.stdout or 'Creating' in sml_compose_up.stdout
- name: Warn if sml2mqtt is waiting for its USB device
ansible.builtin.debug:
msg: >-
sml2mqtt could not start -- the FTDI meter head is not present:
{{ sml_compose_up.stderr_lines | select('search', 'device') | list }}
when: sml_compose_up.rc != 0
@@ -1,21 +0,0 @@
# Managed by ansible (roles/power-meter-sml).
#
# Its own compose project -- see the role tasks for why it no longer shares a
# file with the Z-Wave gateway.
#
# Outbound-only (serial in, MQTT out). Host networking sidesteps the docker
# NAT66 gap: the broker LB is IPv6-first and bridge-network v6 SYNs leave
# unmasqueraded and die. The host stack reaches it fine (open3e proves it
# continuously).
services:
sml2mqtt:
image: {{ edge_sml2mqtt_image }}
container_name: sml2mqtt
restart: unless-stopped
network_mode: host
# Serial exclusivity: this is the ONLY reader of the meter port --
# HA's edl21 integration stays disabled.
devices:
- "{{ edge_stromzaehler_device }}:/dev/stromzaehler"
volumes:
- ./config.yml:/sml2mqtt/config.yml:ro
@@ -1,32 +0,0 @@
# Managed by ansible (roles/power-meter-sml). Ported 1:1 from the k8s ConfigMap
# sml2mqtt-config; only host and password delivery differ (broker is now
# reached via its LoadBalancer hostname, password rendered by ansible
# instead of an init container).
logging:
level: INFO
file: stdout # 'stdout' -> docker logs (NOT '' — that opens the dir)
mqtt:
connection:
identifier: sml2mqtt-meter
host: {{ edge_mqtt_host }}
port: {{ edge_mqtt_port }}
user: sml2mqtt
password: {{ edge_sml2mqtt_mqtt_password }}
topic prefix: sml2mqtt
defaults:
qos: 0
retain: false
last will:
topic: status
general:
Wh in kWh: false # keep raw Wh to match edl21 history — HA long-term
# stats continue with zero unit discontinuity
republish after: 120 # re-emit unchanged values every 120 s
inputs:
- type: serial
url: /dev/stromzaehler # FTDI FT230X, mapped by docker compose
timeout: 3
# 9600 8N1 are sml2mqtt defaults for SML/EDL21 — no override needed.
+12 -1
View File
@@ -30,6 +30,15 @@ RUN ln -s /usr/local/go/bin/go /usr/local/bin/go \
&& ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt
USER 1000:1000
# Time zones (REN-1196): the base has no /usr/share/zoneinfo, so
# TZ=Europe/Berlin silently prints UTC and Python ZoneInfo() raises. The
# default zone stays UTC; agents set TZ where they need local time.
USER root
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata \
&& rm -rf /var/lib/apt/lists/*
USER 1000:1000
# ACP server the Paperclip ACP engine starts in the pod (REN-302). Version and
# patch match what Paperclip ships: packages/adapters/claude-local/package.json
# pins 0.73.0 and applies patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch
@@ -63,6 +72,8 @@ ENV PATH=/usr/local/go/bin:$PATH \
# Base user (1000:1000), entrypoint (tini) and cmd (agent shim) are inherited.
# The PATH check uses the base image's default PATH, not the ENV above.
RUN go version && golangci-lint version \
RUN TZ=Europe/Berlin date +%Z | grep -Eq '^CES?T$' \
&& python3 -c "from zoneinfo import ZoneInfo; ZoneInfo('Europe/Berlin')" \
&& go version && golangci-lint version \
&& env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
sh -ec 'for t in go gofmt golangci-lint govulncheck claude-agent-acp; do command -v "$t"; done'
+14 -2
View File
@@ -21,6 +21,16 @@ COPY --from=flux /usr/local/bin/flux /usr/local/bin/flux
COPY --from=kustomize /app/kustomize /usr/local/bin/kustomize
COPY --from=helm /usr/bin/helm /usr/local/bin/helm
# Time zones (REN-1196): the base has no /usr/share/zoneinfo, so
# TZ=Europe/Berlin silently prints UTC and Python ZoneInfo() raises. The
# default zone stays UTC; agents set TZ where they need local time.
# jq (REN-1345): the agents' duty lists and the Paperclip skill use it.
USER root
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends tzdata jq \
&& rm -rf /var/lib/apt/lists/*
USER 1000:1000
# ACP server the Paperclip ACP engine starts in the pod (REN-302). Version and
# patch match what Paperclip ships: packages/adapters/claude-local/package.json
# pins 0.73.0 and applies patches/@agentclientprotocol__claude-agent-acp@0.73.0.patch
@@ -46,6 +56,8 @@ USER 1000:1000
# write only under $HOME, which the sandbox pod mounts writable. No ENV needed.
# Base user (1000:1000), entrypoint (tini) and cmd (agent shim) are inherited.
RUN kubectl version --client && flux version --client && kustomize version && helm version \
RUN TZ=Europe/Berlin date +%Z | grep -Eq '^CES?T$' \
&& python3 -c "from zoneinfo import ZoneInfo; ZoneInfo('Europe/Berlin')" \
&& kubectl version --client && flux version --client && kustomize version && helm version && jq --version \
&& env -i PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
sh -ec 'for t in kubectl flux kustomize helm claude-agent-acp; do command -v "$t"; done'
sh -ec 'for t in kubectl flux kustomize helm jq claude-agent-acp; do command -v "$t"; done'
+8
View File
@@ -52,6 +52,14 @@ Job (and, on destroy, the pod) reads 404, polling for up to 20 s, below the
server's 30 s plugin RPC timeout. If that check times out or fails, the hook
returns no receipt, as before. No upstream PR yet.
Agents in k8s pods cannot read their own secrets (REN-1345): the sandbox
callback bridge answers `403 Route not allowed` for `/api/agents/me/secrets`,
although upstream's own Paperclip skill documents those routes for runs.
`ren-1345-bridge-agent-secrets.patch` adds the five `/api/agents/me` secret
routes (list, read one value, list/create/withdraw proposals) to the bridge
allowlist. The server still resolves the agent from the run JWT and checks
the binding; company-wide secret routes stay denied. No upstream PR yet.
## Files
- `upstream.env` — pinned commit and the version string stamped into the
@@ -0,0 +1,48 @@
diff --git a/packages/adapter-utils/src/sandbox-callback-bridge.test.ts b/packages/adapter-utils/src/sandbox-callback-bridge.test.ts
index 4419bee..d57de8d 100644
--- a/packages/adapter-utils/src/sandbox-callback-bridge.test.ts
+++ b/packages/adapter-utils/src/sandbox-callback-bridge.test.ts
@@ -1452,6 +1452,11 @@ describe("sandbox callback bridge", () => {
{ method: "POST", path: "/api/routines/r-1/triggers" },
{ method: "PATCH", path: "/api/routine-triggers/t-1" },
{ method: "DELETE", path: "/api/routine-triggers/t-1" },
+ { method: "GET", path: "/api/agents/me/secrets" },
+ { method: "POST", path: "/api/agents/me/secrets/github_token/value" },
+ { method: "GET", path: "/api/agents/me/secret-proposals" },
+ { method: "POST", path: "/api/agents/me/secret-proposals" },
+ { method: "DELETE", path: "/api/agents/me/secret-proposals/prop-1" },
];
for (const request of allowed) {
expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request)).toBeNull();
@@ -1491,6 +1496,11 @@ describe("sandbox callback bridge", () => {
{ method: "POST", path: "/api/companies/co-1/logo" },
{ method: "GET", path: "/api/companies/co-1/secrets" },
{ method: "PATCH", path: "/api/secrets/secret-1" },
+ { method: "GET", path: "/api/agents/agent-1/secrets" },
+ { method: "POST", path: "/api/agents/me/secrets/a/b/value" },
+ { method: "GET", path: "/api/agents/me/secrets/github_token/value" },
+ { method: "GET", path: "/api/companies/co-1/secret-proposals" },
+ { method: "POST", path: "/api/companies/co-1/secret-proposals/prop-1/approve" },
];
for (const request of denied) {
expect(authorizeSandboxCallbackBridgeRequestWithRoutes(request)).toBe(
diff --git a/packages/adapter-utils/src/sandbox-callback-bridge.ts b/packages/adapter-utils/src/sandbox-callback-bridge.ts
index 40776a3..e825197 100644
--- a/packages/adapter-utils/src/sandbox-callback-bridge.ts
+++ b/packages/adapter-utils/src/sandbox-callback-bridge.ts
@@ -133,6 +133,15 @@ export const DEFAULT_SANDBOX_CALLBACK_BRIDGE_ROUTE_ALLOWLIST: readonly SandboxCa
{ method: "POST", path: /^\/api\/agents\/[^/]+\/skills\/sync$/ },
{ method: "PATCH", path: /^\/api\/agents\/[^/]+\/instructions-path$/ },
+ // Run-scoped secrets of the calling agent (`/agents/me` only). The server
+ // resolves the agent from the run JWT and checks bindings; values are
+ // registered for run-log redaction. Company-wide secret routes stay denied.
+ { method: "GET", path: /^\/api\/agents\/me\/secrets$/ },
+ { method: "POST", path: /^\/api\/agents\/me\/secrets\/[^/]+\/value$/ },
+ { method: "GET", path: /^\/api\/agents\/me\/secret-proposals$/ },
+ { method: "POST", path: /^\/api\/agents\/me\/secret-proposals$/ },
+ { method: "DELETE", path: /^\/api\/agents\/me\/secret-proposals\/[^/]+$/ },
+
// Read-only schema discovery for validated control-plane requests.
{ method: "GET", path: /^\/api\/openapi\.json$/ },
@@ -1591,7 +1591,7 @@ data:
# return entries instead of short-circuiting. Built from
# the samba-bridge repo at
# gitea.renner.uno/k.renner/authentik-samba-bridge .
container_image: gitea.renner.uno/k.renner/authentik-samba-bridge-ldap-outpost:2026.5.3-f02b945
container_image: gitea.renner.uno/k.renner/authentik-samba-bridge-ldap-outpost:2026.8.3-872f754
object_naming_template: "ak-outpost-%(name)s"
kubernetes_namespace: authentik
kubernetes_replicas: 1
+9 -2
View File
@@ -20,7 +20,7 @@ spec:
# Pinned to match the samba-bridge fork's app image (server.Dockerfile
# AUTHENTIK_VERSION). Bump in lockstep with the bridge rebuild — a
# floating range drifts the chart ahead of the pinned app image.
version: "2026.5.3"
version: "2026.8.3"
sourceRef:
kind: HelmRepository
name: authentik
@@ -58,7 +58,7 @@ spec:
# 2. Pinning means the deployed binary is traceable to a
# specific repo commit. Bump the tag whenever a new build
# is published from gitea.renner.uno/k.renner/authentik-samba-bridge .
tag: "2026.5.3-f02b945"
tag: "2026.8.3-872f754"
envFrom:
# SAMBA_BRIDGE_DOMAIN_SID / _ENABLED_GROUP / _UID_OFFSET
# consumed by the bridge module at Django app startup. Missing
@@ -100,6 +100,13 @@ spec:
secretKeyRef:
name: ldap-outpost-tls
key: tls.key
# 2026.8 honours X-Forwarded-Proto/-Host/-For only from these
# networks. Traefik reaches authentik-server (IPv4 single-stack
# Service) from a pod IP; same ranges Traefik itself trusts for
# forwarded headers. Without them, HTTPS is seen as HTTP and SSO
# breaks. Comma-separated, no spaces (authentik list parsing).
- name: AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRS
value: "10.244.0.0/16,fd10:244::/48,127.0.0.0/8,::1/128"
authentik:
log_level: info
+7
View File
@@ -12,3 +12,10 @@ resources:
- ingressroute-ssh.yaml
- ingress.yaml
- networkpolicy.yaml
# Nightly prune of old CI image builds (REN-1203).
- package-retention.yaml
configMapGenerator:
- name: gitea-package-retention
namespace: gitea
files:
- package-retention.py
+23
View File
@@ -49,3 +49,26 @@ spec:
- ports:
- port: "3000"
protocol: TCP
---
# Package retention CronJob (package-retention.yaml, REN-1203). It lists pods
# to find the running image tags. The namespace baseline does not open the API
# server: `kube-apiserver` is its own entity in Cilium (see
# network-policies/paperclip-renner-ns.yaml); port is the post-DNAT 6443. Its
# Gitea calls go to the in-cluster Service, which the baseline already allows
# inside the namespace.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: gitea-package-retention-kube-apiserver
namespace: gitea
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: gitea-package-retention
egress:
- toEntities:
- kube-apiserver
toPorts:
- ports:
- port: "6443"
protocol: TCP
+162
View File
@@ -0,0 +1,162 @@
"""Prune old CI image builds from Gitea's container registry (REN-1203).
See package-retention.yaml for what is pruned, what is kept, and why.
Standard library only.
"""
import json
import os
import re
import ssl
import sys
import urllib.parse
import urllib.request
from datetime import datetime
GITEA = os.environ["GITEA_URL"].rstrip("/")
TOKEN = os.environ["GITEA_TOKEN"]
REGISTRY_HOST = os.environ["REGISTRY_HOST"]
OWNERS = os.environ["OWNERS"].split(",")
KEEP = int(os.environ["KEEP_IMAGES"])
GROUP_SECONDS = int(os.environ["GROUP_SECONDS"])
MAX_DELETES = int(os.environ["MAX_DELETES"])
EXCLUDE = re.compile(os.environ["EXCLUDE_PACKAGES"])
DRY_RUN = os.environ.get("DRY_RUN", "false") == "true"
# A package is a CI build package if at least one of its tags looks like this.
BUILD_TAG = re.compile(r"^(build\.[0-9]+|main-[0-9a-f]{7,40})$")
ALWAYS_KEEP = {"main", "latest"}
SA = "/var/run/secrets/kubernetes.io/serviceaccount"
def log(msg):
print(msg, flush=True)
def gitea(method, path):
req = urllib.request.Request(
GITEA + path, method=method, headers={"Authorization": "token " + TOKEN}
)
with urllib.request.urlopen(req, timeout=60) as resp:
body = resp.read()
return json.loads(body) if body else None
def ts(s):
# Gitea returns RFC 3339, e.g. 2026-10-07T18:07:32Z or +02:00.
return datetime.fromisoformat(s.replace("Z", "+00:00")).timestamp()
def running_images():
"""Tags in use by any pod, per package. Packages referenced by digest only
cannot be matched to a tag, so they are returned in `unsafe` and skipped."""
ctx = ssl.create_default_context(cafile=SA + "/ca.crt")
with open(SA + "/token") as f:
sa_token = f.read().strip()
req = urllib.request.Request(
"https://kubernetes.default.svc/api/v1/pods",
headers={"Authorization": "Bearer " + sa_token},
)
with urllib.request.urlopen(req, timeout=60, context=ctx) as resp:
pods = json.load(resp)["items"]
in_use, unsafe = {}, set()
prefix = REGISTRY_HOST + "/"
for pod in pods:
spec = pod["spec"]
for c in (
spec.get("containers", [])
+ spec.get("initContainers", [])
+ spec.get("ephemeralContainers", [])
):
image = c["image"]
if not image.startswith(prefix):
continue
ref = image[len(prefix):].split("@", 1)[0]
repo, _, tag = ref.rpartition(":")
if not repo or "/" in tag:
unsafe.add(ref)
continue
in_use.setdefault(repo, set()).add(tag)
return in_use, unsafe
def list_packages(owner):
out, page = [], 1
while True:
batch = gitea(
"GET", f"/api/v1/packages/{owner}?type=container&limit=50&page={page}"
)
if not batch:
return out
out.extend(batch)
page += 1
def plan(versions, protected):
"""Group tags into images (one push writes all tags of an image within a
few seconds) and return the tags of the images to delete."""
tags = sorted(
(v for v in versions if not v["version"].startswith("sha256:")),
key=lambda v: ts(v["created_at"]),
reverse=True,
)
images, last = [], None
for v in tags:
t = ts(v["created_at"])
if last is None or last - t > GROUP_SECONDS:
images.append([])
images[-1].append(v["version"])
last = t
delete = []
for i, image in enumerate(images):
if i < KEEP or set(image) & (ALWAYS_KEEP | protected):
continue
delete.extend(image)
return images, delete
def main():
in_use, unsafe = running_images()
log(f"running tags from {REGISTRY_HOST}: {sum(len(t) for t in in_use.values())}")
work = []
for owner in OWNERS:
by_name = {}
for v in list_packages(owner):
by_name.setdefault(v["name"], []).append(v)
for name, versions in sorted(by_name.items()):
full = f"{owner}/{name}"
if EXCLUDE.search(full):
log(f"skip {full}: excluded")
continue
if not any(BUILD_TAG.match(v["version"]) for v in versions):
continue
if full in unsafe:
log(f"skip {full}: a pod pins it by digest only")
continue
protected = in_use.get(full, set())
images, delete = plan(versions, protected)
log(f"{full}: {len(images)} images, {len(delete)} tags to delete, running {sorted(protected)}")
work += [(owner, name, tag) for tag in delete]
if len(work) > MAX_DELETES:
log(f"abort: {len(work)} deletes exceed MAX_DELETES={MAX_DELETES}; nothing deleted")
return 1
failed = 0
for owner, name, tag in work:
if DRY_RUN:
log(f"dry-run: would delete {owner}/{name}:{tag}")
continue
path = "/api/v1/packages/{}/container/{}/{}".format(
owner, urllib.parse.quote(name, safe=""), urllib.parse.quote(tag, safe="")
)
try:
gitea("DELETE", path)
log(f"deleted {owner}/{name}:{tag}")
except Exception as e: # keep going; report at the end
failed += 1
log(f"FAILED {owner}/{name}:{tag}: {e}")
log(f"done: {len(work) - failed} deleted, {failed} failed, dry_run={DRY_RUN}")
return 1 if failed else 0
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,165 @@
# Nightly retention for CI image builds in Gitea's container registry (REN-1203).
#
# Why: nothing pruned old builds, so the data volume filled up (REN-580, at
# 40Gi) and was down to 24% free again four days after the grow to 60Gi
# (REN-1203, about 2.2 GB/day). Gitea 1.21 has no API for per-owner cleanup
# rules, and the owners are human accounts, so the rules cannot live in Gitea.
# This job applies the rule Hermes ran by hand in REN-1204.
#
# What it prunes, per package of the owners in OWNERS:
# - only CI build packages: at least one tag is `build.<n>` or `main-<sha>`.
# Date-tagged utility images (alpine-jq, cni-plugins, ...) are not touched.
# - one image carries several tags (build.N, short and long sha, sometimes
# main/latest). Tags pushed within GROUP_SECONDS are one image, and the job
# deletes all tags of an image, because a sha tag left behind keeps the
# blobs alive.
# - keeps the KEEP_IMAGES newest images, any image tagged main or latest, and
# any image whose tag a pod in the cluster runs now. A package that a pod
# pins by digest only is skipped as a whole.
# - never touches gitea_admin/* (CI base images) or EXCLUDE_PACKAGES. The
# agent-runtime images are excluded because Paperclip pins their tag in its
# own config, not in a pod that exists between runs.
# - aborts without deleting if the plan exceeds MAX_DELETES.
# Freed blobs are removed by Gitea's own `cleanup_packages` cron (daily).
#
# Token: `gitea-hermes-bot-package-retention-token` on hermes-bot (site admin),
# scope `write:package` only (REN-1204). Admin and repo routes return 403 with
# it. Hermes owns the account and the token.
#
# Kubernetes access: list pods, cluster-wide, to find the running tags. Nothing
# else.
#
# Dry run: set DRY_RUN to "true"; the job then only logs the plan.
# Stop it: `spec.suspend: true` in git. Revert: delete this file, the
# configMapGenerator entry and the policy in networkpolicy.yaml. Deleted images
# cannot be restored; CI rebuilds one from its commit.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: gitea-package-retention
namespace: gitea
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: openbao
target:
name: gitea-package-retention
data:
- secretKey: GITEA_TOKEN
remoteRef:
key: infrastructure/gitea/hermes-bot
property: gitea-hermes-bot-package-retention-token
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: gitea-package-retention
namespace: gitea
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: gitea-package-retention
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: gitea-package-retention
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: gitea-package-retention
subjects:
- kind: ServiceAccount
name: gitea-package-retention
namespace: gitea
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: gitea-package-retention
namespace: gitea
labels:
app.kubernetes.io/name: gitea-package-retention
spec:
# Quiet hour for CI pushes. Gitea's own daily cleanup_packages cron frees
# the blobs afterwards.
schedule: "30 22 * * *"
timeZone: Etc/UTC
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 3
startingDeadlineSeconds: 3600
jobTemplate:
spec:
backoffLimit: 0
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 604800
template:
metadata:
labels:
app.kubernetes.io/name: gitea-package-retention
spec:
restartPolicy: Never
enableServiceLinks: false
serviceAccountName: gitea-package-retention
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: retention
image: docker.io/library/python:3.13-alpine@sha256:2d9aefe2fef018a7eb2c13064c89c71929800fd2e5dccdbf52ea5da5bb8d929a
command: ["python3", "/app/package-retention.py"]
env:
# In-cluster Service; the public name hairpins off the ingress
# LB from a pod (see networkpolicy.yaml).
- name: GITEA_URL
value: http://gitea.gitea.svc:3000
- name: REGISTRY_HOST
value: gitea.renner.uno
- name: OWNERS
value: k.renner,konairius
- name: KEEP_IMAGES
value: "5"
# Tags of one push land within seconds; the smallest gap
# between two images was 379 s (REN-1204).
- name: GROUP_SECONDS
value: "60"
- name: MAX_DELETES
value: "300"
- name: EXCLUDE_PACKAGES
value: "^konairius/agent-runtime"
- name: DRY_RUN
value: "false"
- name: PYTHONDONTWRITEBYTECODE
value: "1"
envFrom:
- secretRef:
name: gitea-package-retention
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: script
mountPath: /app
readOnly: true
volumes:
- name: script
configMap:
name: gitea-package-retention
+1 -1
View File
@@ -37,7 +37,7 @@ spec:
# both handled as explicit steps in MIGRATION-gitea-woodpecker.md. Pinned
# to match the LXC for a clean SQLite lift; no imagepolicy marker (an
# auto-bump across versions on live SQLite data must never be applied).
image: gitea/gitea:1.21.4-rootless
image: gitea/gitea:1.21.4-rootless@sha256:600f1f1e1d090c763866db16882b5eac5c6a80b7664ee53bec0806c656c0033f
env:
- name: GITEA_WORK_DIR
value: /var/lib/gitea
+59 -2
View File
@@ -95,7 +95,7 @@ spec:
memory: 32Mi
containers:
- name: home-assistant
image: ghcr.io/home-assistant/home-assistant:2026.9.4 # {"$imagepolicy": "flux-system:home-assistant"}
image: ghcr.io/home-assistant/home-assistant:2026.10.0 # {"$imagepolicy": "flux-system:home-assistant"}
env:
- name: TZ
value: Europe/Berlin
@@ -187,6 +187,55 @@ spec:
memory: 32Mi
limits:
memory: 64Mi
# MQTT client certificate for Home Assistant (REN-953). HA keeps
# client certificates only as PEMs uploaded into its MQTT config
# entry, so a cert-manager renewal (~60 days) could never reach it
# without a reconfigure flow and an admin token. Instead HA's config
# entry points at 127.0.0.1:11883 (plain, loopback only) and this
# sidecar forwards to the broker's mTLS listener :8884 with the
# certificate of the register account `home-assistant`. The broker
# takes the username from the certificate CN; whatever user/password
# HA sends is ignored. --timed-reload picks up a renewed certificate
# for new connections without a restart; an open connection stays up
# (checked locally with ghostunnel v1.11.3 and mosquitto 2.0.22).
#
# Probes: liveness only, on the HTTPS status port (process alive). No
# readiness probe on purpose: a broker outage must not take HA out of
# its Service endpoints.
- name: mqtt-tls
image: ghostunnel/ghostunnel:v1.11.3@sha256:2599b8a04bae16d70a4209495618dea61b46a00a9a05fcf74da282688ad3517f
args:
- client
- --listen=127.0.0.1:11883
- --target=mosquitto.home-assistant.svc.cluster.local:8884
- --cert=/mqtt-tls/tls.crt
- --key=/mqtt-tls/tls.key
- --cacert=/mqtt-tls/ca.crt
- --timed-reload=10m
- --status=0.0.0.0:16060
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
livenessProbe:
tcpSocket:
port: 16060
periodSeconds: 30
failureThreshold: 4
volumeMounts:
- name: mqtt-tls
mountPath: /mqtt-tls
readOnly: true
resources:
requests:
cpu: 5m
memory: 16Mi
limits:
memory: 64Mi
# Browser-based VS Code editing /config in-place. Sidecar (not
# separate Deployment) because the config PVC is RWO ceph-block.
# Exposed LAN-only on vscode.home.renner.uno; --auth=none means
@@ -253,7 +302,7 @@ spec:
# in the PVC's remote URL; reload auth uses a dedicated long-lived token.
# Runs as root to match HA's root-owned /config files. See config-sync.sh.
- name: config-sync
image: alpine/git:2.54.0
image: alpine/git:2.54.0@sha256:a4bb51f1a3553df194ce679fc1db721d8bfba2046fa1a88fe9d4ac551ffbce25
command: ["/bin/sh", "/scripts/sync.sh"]
env:
- name: SYNC_BRANCH
@@ -297,6 +346,14 @@ spec:
- name: proxy-config
configMap:
name: ha-proxy-config
# Client certificate of the register account home-assistant, issued
# by mqtt-account-controller. Not subPath, so renewals land in place.
# 0444 because the sidecar runs as 65534; only this pod mounts it.
# Not in the Reloader annotation: a renewal must not restart HA.
- name: mqtt-tls
secret:
secretName: mqtt-home-assistant-tls
defaultMode: 0444
# Writable emptyDir for the nginx entrypoint's envsubst output
# (/etc/nginx/conf.d/default.conf rendered from the .template).
- name: proxy-conf-d
@@ -16,9 +16,6 @@ resources:
# probes use the cert account mosquitto-probe, REN-953); removed with its
# rotator in the REN-802 cleanup step.
- mosquitto-health-credentials-externalsecret.yaml
# open3e-int broker credential: open3e-esp32 gateway on the Vitocal internal
# CAN; OpenBao-backed + Class-A rotated.
- mosquitto-open3e-int-credentials-externalsecret.yaml
- mosquitto-config.yaml
- mosquitto-certificate.yaml
- mosquitto-service.yaml
@@ -177,77 +177,6 @@ spec:
limits:
memory: 64Mi
---
# Second broker user: `shellies`. Separate Secret (not a second key on
# mosquitto-credentials) so each credential's lifecycle is independent —
# rotating one never touches the other, and existing clusters whose
# mosquitto-credentials was already generated for `homeassistant` only
# don't end up with a half-populated Secret.
apiVersion: batch/v1
kind: Job
metadata:
name: mosquitto-shellies-credentials-bootstrap
namespace: home-assistant
annotations:
# Job pod templates are immutable; force-recreate so image bumps
# apply on Flux re-reconcile. Safe: the Job is idempotent (see above).
kustomize.toolkit.fluxcd.io/force: enabled
spec:
backoffLimit: 4
template:
metadata:
labels:
app.kubernetes.io/name: mosquitto-bootstrap
spec:
serviceAccountName: mosquitto-bootstrap
restartPolicy: OnFailure
containers:
- name: bootstrap
image: alpine:3.24@sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6
command:
- sh
- -c
- |
set -eu
apk add --no-cache curl jq >/dev/null
NS=home-assistant
NAME=mosquitto-shellies-credentials
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
API=https://kubernetes.default.svc
if curl -fsSL --cacert "$CA" -H "Authorization: Bearer $TOKEN" \
"$API/api/v1/namespaces/$NS/secrets/$NAME" >/dev/null 2>&1; then
echo "$NAME already exists; nothing to do."
exit 0
fi
PW=$(tr -dc 'A-Za-z0-9' </dev/urandom | head -c 32)
PW_B64=$(printf '%s' "$PW" | base64 | tr -d '\n')
body=$(jq -nc \
--arg name "$NAME" \
--arg ns "$NS" \
--arg pw "$PW_B64" \
'{apiVersion: "v1",
kind: "Secret",
metadata: {name: $name, namespace: $ns},
type: "Opaque",
data: {password: $pw}}')
curl -fsSL --cacert "$CA" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" -X POST \
--data "$body" \
"$API/api/v1/namespaces/$NS/secrets" >/dev/null
echo "generated $NAME"
resources:
requests:
cpu: 10m
memory: 32Mi
limits:
memory: 64Mi
---
# Fourth broker user: `sml2mqtt` — the power-meter bridge. Publish-only
# under `sml2mqtt/#` (ACL in mosquitto-deployment.yaml). Own credential so
# its lifecycle is independent of the others. The sml2mqtt pod reads this
@@ -392,7 +321,7 @@ spec:
---
# Broker user `zigbee2mqtt` — the Zigbee2MQTT bridge (ZHA replacement). Scoped
# to its own `zigbee2mqtt/#` subtree plus `write homeassistant/#` (so it can
# publish HA MQTT-discovery configs, exactly like the `shellies` user) and
# publish HA MQTT-discovery configs) and
# `read homeassistant/status` (HA's birth/will topic; on `online` Z2M
# republishes cached device states, NOT discovery configs — ACL and the full
# reasoning in mosquitto-deployment.yaml). Own credential so its lifecycle is
@@ -5,14 +5,6 @@ metadata:
namespace: home-assistant
labels:
app.kubernetes.io/name: mosquitto
annotations:
# REQUIRED for credential rotation to take effect. The startup shim rebuilds
# the password file only on pod start, so without this a rotated Secret sits
# inert while the old password stays valid indefinitely.
#
# Listed per-Secret rather than with the blanket auto annotation, so that
# enrolling another broker user is a visible edit to this line.
secret.reloader.stakater.com/reload: mosquitto-open3e-int-credentials
spec:
replicas: 1
strategy:
@@ -74,23 +66,14 @@ spec:
set -eu
rm -f /mosquitto/auth/passwd
mosquitto_passwd -c -b /mosquitto/auth/passwd homeassistant "$HA_PASSWORD"
mosquitto_passwd -b /mosquitto/auth/passwd shellies "$SHELLY_PASSWORD"
mosquitto_passwd -b /mosquitto/auth/passwd admin "$ADMIN_PASSWORD"
mosquitto_passwd -b /mosquitto/auth/passwd sml2mqtt "$SML2MQTT_PASSWORD"
mosquitto_passwd -b /mosquitto/auth/passwd open3e "$OPEN3E_PASSWORD"
mosquitto_passwd -b /mosquitto/auth/passwd open3e-int "$OPEN3E_INT_PASSWORD"
mosquitto_passwd -b /mosquitto/auth/passwd evcc "$EVCC_PASSWORD"
mosquitto_passwd -b /mosquitto/auth/passwd zigbee2mqtt "$ZIGBEE2MQTT_PASSWORD"
# ACL: HA keeps full access (subscribes to everything); the
# shellies user is `readwrite shellies/#` (devices publish state
# and subscribe to their command topics) + `write homeassistant/#`
# so Shelly Gen2 firmwares can publish HA MQTT-discovery configs.
# It is WRITE-only on homeassistant/# on purpose: a publish-only
# client never needs to read the whole discovery/state tree, and
# denying read removes device-inventory recon for a compromised
# Shelly. (Full per-device write scoping isn't expressible with
# the flat discovery topic layout; that would need a dynamic-auth
# plugin — out of scope.)
# ACL: HA keeps full access (subscribes to everything). Shellies
# are not legacy users any more: each has its own Dynamic Security
# account from mqtt-accounts.yaml (REN-802, REN-1017).
# `admin` is a workstation/debugging credential — full
# readwrite, kept on its own credential so its password can
# be rotated independently of the HA session.
@@ -111,10 +94,6 @@ spec:
user homeassistant
topic readwrite #
user shellies
topic readwrite shellies/#
topic write homeassistant/#
user admin
topic readwrite #
@@ -124,12 +103,6 @@ spec:
user open3e
topic readwrite open3e/#
# open3e-esp32 gateway on the Vitocal internal CAN: own subtree
# under open3e/ so edge-1's listener on open3e/cmnd never sees
# its commands.
user open3e-int
topic readwrite open3e/int/#
user evcc
topic read open3e/#
topic readwrite evcc/#
@@ -137,9 +110,8 @@ spec:
# `zigbee2mqtt` is the Zigbee2MQTT bridge (the ZHA replacement).
# Owns its own `zigbee2mqtt/#` subtree; may WRITE `homeassistant/#`
# to publish HA MQTT-discovery configs (write-only there, like the
# shellies user, to deny discovery-tree recon from a compromised
# bridge); and READs `homeassistant/status`, HA's birth/will topic.
# to publish HA MQTT-discovery configs (write-only there, to deny
# discovery-tree recon from a compromised bridge); and READs `homeassistant/status`, HA's birth/will topic.
#
# That read grant does NOT buy a discovery re-publish: on
# `online`, Z2M 2.12.1 republishes bridge/state and cached device
@@ -176,11 +148,6 @@ spec:
secretKeyRef:
name: mosquitto-credentials
key: homeassistant-password
- name: SHELLY_PASSWORD
valueFrom:
secretKeyRef:
name: mosquitto-shellies-credentials
key: password
- name: ADMIN_PASSWORD
valueFrom:
secretKeyRef:
@@ -196,13 +163,6 @@ spec:
secretKeyRef:
name: mosquitto-open3e-credentials
key: password
# open3e-int: OpenBao-backed + Class-A rotated (see
# mosquitto-open3e-int-credentials-externalsecret.yaml).
- name: OPEN3E_INT_PASSWORD
valueFrom:
secretKeyRef:
name: mosquitto-open3e-int-credentials
key: password
- name: EVCC_PASSWORD
valueFrom:
secretKeyRef:
@@ -1,21 +0,0 @@
# Projects the mosquitto `open3e-int` broker credential (open3e-esp32 gateway
# on the Vitocal internal CAN) from OpenBao into the Secret the broker
# consumes. Rotator: kubernetes/infrastructure/openbao/rotation/mosquitto-open3e-int.yaml.
#
# creationPolicy is implicitly Owner: deleting this ExternalSecret deletes the
# live Secret. Set creationPolicy: Orphan first to detach without destroying.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mosquitto-open3e-int-credentials
namespace: home-assistant
spec:
refreshInterval: 10m
secretStoreRef:
kind: ClusterSecretStore
name: openbao
target:
name: mosquitto-open3e-int-credentials
dataFrom:
- extract:
key: apps/home-assistant/mosquitto-open3e-int-credentials
@@ -47,8 +47,8 @@ subjects:
---
# Per deliver.namespace: manage the account Certificates there (server-side
# apply = patch, which creates through `create`). One Role + RoleBinding per
# namespace that appears as `deliver.namespace` in the register: home-assistant
# and evcc.
# namespace that appears as `deliver.namespace` in the register: home-assistant,
# evcc and paperclip.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
@@ -85,6 +85,8 @@ subjects:
# that creates, because the name is in the request path).
# mqtt-<name>-tls: delete on revocation, one entry per cert account
# delivered to this namespace. Add the name with the register entry.
# mqtt-ha-cert-sync: sha256 of the client certificate last accepted by
# Home Assistant's MQTT config entry (HA cert sync, REN-953/REN-1284).
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
@@ -95,7 +97,7 @@ metadata:
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["mqtt-account-hashes", "mqtt-dynsec-seed"]
resourceNames: ["mqtt-account-hashes", "mqtt-dynsec-seed", "mqtt-ha-cert-sync"]
verbs: ["get", "create", "patch"]
- apiGroups: [""]
resources: ["secrets"]
@@ -103,6 +105,7 @@ rules:
- mqtt-test-cert-tls
- mqtt-mosquitto-probe-tls
- mqtt-zigbee2mqtt-bridge-tls
- mqtt-home-assistant-tls
verbs: ["delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
@@ -156,6 +159,41 @@ subjects:
name: mqtt-account-controller
namespace: home-assistant
---
# deliver.namespace paperclip (account agents-read): its Certificate, and its
# Secret by name for revocation.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: mqtt-account-controller-certificates
namespace: paperclip
labels:
app.kubernetes.io/name: mqtt-account-controller
rules:
- apiGroups: ["cert-manager.io"]
resources: ["certificates"]
verbs: ["get", "create", "patch", "delete"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames:
- mqtt-agents-read-tls
verbs: ["delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: mqtt-account-controller-certificates
namespace: paperclip
labels:
app.kubernetes.io/name: mqtt-account-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: mqtt-account-controller-certificates
subjects:
- kind: ServiceAccount
name: mqtt-account-controller
namespace: home-assistant
---
# The controller's own MQTT identity: CN = Dynamic Security admin client
# (ADMIN_USER). Not labelled managed-by, so the controller never treats it
# as an account certificate and never revokes it. Re-read on every TLS
@@ -187,6 +225,10 @@ metadata:
namespace: home-assistant
labels:
app.kubernetes.io/name: mqtt-account-controller
annotations:
# The controller reads the shelly-manager API key once at start; the
# quarterly rotation of shelly-manager-auth must restart it too.
secret.reloader.stakater.com/reload: shelly-manager-auth
spec:
replicas: 1
# One writer at a time: two controllers would race on rotations and the
@@ -212,10 +254,33 @@ spec:
- name: controller
# Woodpecker builds main-<sha> on every merge to main; no sortable
# tag, so bumps are manual (same as ical-proxy).
image: gitea.renner.uno/konairius/mqtt-account-controller:main-b3d5cbb
image: gitea.renner.uno/konairius/mqtt-account-controller:main-cb96b1a@sha256:9d9b9e83a9c0f9f567e57edacab11cf4c58e8c0e49e38e8d7b51421cd1242911
env:
- name: BROKER_URL
value: ssl://mosquitto.home-assistant.svc.cluster.local:8884
# Password push to Shellies (accounts with deliver.shelly-manager).
# The API-key-only Service (api-proxy sidecar, :8081), like the
# agents; same namespace, so the default-deny allows it.
- name: SHELLY_MANAGER_URL
value: http://shelly-manager-api.home-assistant.svc.cluster.local
- name: SHELLY_MANAGER_API_KEY_FILE
value: /etc/shelly-manager/api-key
# HA cert sync (REN-953, REN-1284): keeps Home Assistant's MQTT
# config entry on mosquitto:8884 with the CA and client
# certificate of the register account `home-assistant`. After
# each loop the controller compares tls.crt (mounted below) with
# the last certificate HA accepted (Secret mqtt-ha-cert-sync) and,
# only inside 02:00-05:00 Europe/Berlin, runs HA's reconfigure
# flow. HA tests the connection before it saves. Defaults:
# broker mosquitto.home-assistant.svc.cluster.local:8884, cert dir
# /etc/mqtt-home-assistant-tls. Off while HA_MQTT_ENTRY_ID is unset.
- name: HA_MQTT_ENTRY_ID
value: 01KRFXD4046FEBWQ0EX0GNE418
# HA itself (:8123), not the :8124 XFF proxy; same namespace.
- name: HA_URL
value: http://home-assistant-bgp.home-assistant.svc.cluster.local:8123
- name: HA_TOKEN_FILE
value: /etc/ha-token/token
# Not :8080: several :8080 ingress grants in the home-assistant
# default-deny apply to every pod in the namespace.
- name: LISTEN
@@ -243,6 +308,15 @@ spec:
- name: tls
mountPath: /etc/mqtt-tls
readOnly: true
- name: shelly-manager-key
mountPath: /etc/shelly-manager
readOnly: true
- name: ha-mqtt-tls
mountPath: /etc/mqtt-home-assistant-tls
readOnly: true
- name: ha-token
mountPath: /etc/ha-token
readOnly: true
resources:
requests:
cpu: 5m
@@ -258,3 +332,42 @@ spec:
- name: tls
secret:
secretName: mqtt-account-controller-tls
# Only the api-key item, not the shelly-manager admin password.
- name: shelly-manager-key
secret:
secretName: shelly-manager-auth
items:
- key: api-key
path: api-key
# Client certificate HA gets (cert-manager renews it in place; not a
# subPath mount, so the next loop sees the new file).
- name: ha-mqtt-tls
secret:
secretName: mqtt-home-assistant-tls
# HA long-lived token of the service user `MQTT Cert Sync`
# (system-admin; config flows need admin). REN-1268.
- name: ha-token
secret:
secretName: mqtt-account-controller-ha-token
---
# HA admin token for the HA cert sync (see the Deployment env). Minted by
# Hermes on a dedicated HA service user, not the owner's account (REN-1268).
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: mqtt-account-controller-ha-token
namespace: home-assistant
labels:
app.kubernetes.io/name: mqtt-account-controller
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: openbao
target:
name: mqtt-account-controller-ha-token
data:
- secretKey: token
remoteRef:
key: apps/home-assistant/mqtt-cert-sync-token
property: token
@@ -41,3 +41,184 @@
auth: cert
deliver: {namespace: evcc}
acl: ["read open3e/#", "readwrite evcc/#", "read energy/#"]
# Home Assistant, via the ghostunnel sidecar `mqtt-tls` in its pod (HA keeps
# client certificates only as uploaded PEMs in its config entry, so a renewal
# could not reach it without a reconfigure). Same ACL as the legacy user
# `homeassistant`; not named like it, so it cannot shadow that login (REN-953).
- name: home-assistant
auth: cert
deliver: {namespace: home-assistant}
acl: ["readwrite #"]
# Read-only account for agents (REN-953). One account for all of them: the
# agents share sandboxes, so per-agent separation would be nominal. The
# Secret lands in `paperclip`; agents fetch it per run with their kubectl
# identity (Role mqtt-agents-read in apps/paperclip/agent-kubectl-rbac.yaml),
# so a renewal needs no restart. Network path: agent sandbox pods ->
# mosquitto :8884 only.
- name: agents-read
auth: cert
deliver: {namespace: paperclip}
acl: ["read open3e/#", "read zigbee2mqtt/#", "read homeassistant/#"]
# edge-1's local mosquitto bridge (REN-1307): one bridge for open3e/e3oncan
# and sml2mqtt, replacing the legacy password users `open3e` and `sml2mqtt`
# (different name, so it cannot shadow their logins). The controller pushes
# the password to edge-mqtt-cred on edge-1 (ansible role mqtt-bridge) over
# HTTPS with its own client certificate, and pins edge-1's self-generated
# server certificate on first contact (key edge-1.pin in mqtt-account-hashes;
# delete it after a reinstall of edge-1).
- name: edge-1
auth: password
deliver: {esp32: "https://edge-1.iot.renner.uno:8443"}
# No sml2mqtt/#: the SML meter reader is retired for good (owner,
# 2026-09-19; grid data comes from the E380 on the CAN bus).
acl: ["readwrite open3e/#"]
# Shellies (REN-1017): one password account per device, pushed by the
# controller through shelly-manager's per-device API. Name shelly-<id> =
# shelly-manager device id. ACL as the former shared user `shellies`
# (removed in REN-1017 step 4). Gen2+ devices reboot once when their password
# is pushed. A new Shelly needs its entry here first: shelly-manager's MQTT
# template user no longer exists on the broker.
- name: shelly-5 # Plus i4, the API test device of REN-929
auth: password
deliver: {shelly-manager: 5}
acl: ["readwrite shellies/#", "write homeassistant/#"]
# Only devices that log in as `shellies` today. A device with MQTT off never
# reports connected, so the controller would push (and reboot it) again
# every ROTATE_GRACE.
- name: shelly-6
auth: password
deliver: {shelly-manager: 6} # Plus i4 90380c3808b8
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-29
auth: password
deliver: {shelly-manager: 29} # Plus i4 b8d61a8c7f24
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-3
auth: password
deliver: {shelly-manager: 3} # Pro 3EM ac15186d43c0
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-42
auth: password
deliver: {shelly-manager: 42} # 3EM-63 G3 d885ac1c5ab4
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-43
auth: password
deliver: {shelly-manager: 43} # 3EM-63 G3 d885ac1ab274
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-10
auth: password
deliver: {shelly-manager: 10} # Plug S Gen1 4382e5
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-22
auth: password
deliver: {shelly-manager: 22} # Plug S Gen1 87e961
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-25
auth: password
deliver: {shelly-manager: 25} # Plug S Gen1 1c1a8f
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-15
auth: password
deliver: {shelly-manager: 15} # Plug S G3 8cbfea910578
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-16
auth: password
deliver: {shelly-manager: 16} # Plug S G3 b08184a63ef0
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-14
auth: password
deliver: {shelly-manager: 14} # Plus 1PM Mini 348518e0be40
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-27
auth: password
deliver: {shelly-manager: 27} # Plus 1PM Mini 348518e06c04
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-13
auth: password
deliver: {shelly-manager: 13} # Plus 2PM 48551997e624
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-19
auth: password
deliver: {shelly-manager: 19} # Plus 2PM 48551999a790
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-21
auth: password
deliver: {shelly-manager: 21} # Plus 2PM 0cdc7efa8d00
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-34
auth: password
deliver: {shelly-manager: 34} # Plus 2PM 3ce90e2f2d7c
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-2
auth: password
deliver: {shelly-manager: 2} # Wall Display 00082266e106
# Checked but never rotated on "device not connected" (REN-1066).
rotate: false
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-26
auth: password
deliver: {shelly-manager: 26} # Power Strip G4 206ef102a6cc
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-1
auth: password
deliver: {shelly-manager: 1} # Pro 4PM 9454c5b9c954
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-4
auth: password
deliver: {shelly-manager: 4} # Pro 4PM ac15186ca578
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-32
auth: password
deliver: {shelly-manager: 32} # Pro 4PM 30c6f7834ec4
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-33
auth: password
deliver: {shelly-manager: 33} # Pro 3 fce8c0db5f28
acl: ["readwrite shellies/#", "write homeassistant/#"]
# HiluX DS8: HA drives them over MQTT only, and their firmware can hang in
# the MQTT reconnect path until a power cycle (wiki
# shelly-hilux-ds8-mqtt-reconnect-hang-bug). One unit at a time; 12 is a
# bathroom mesh receiver, so its wall switch works without MQTT.
- name: shelly-12
auth: password
deliver: {shelly-manager: 12} # HiluX DS8 34b7da890cfc, Bad Spiegel Rechts
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-9
auth: password
deliver: {shelly-manager: 9} # HiluX DS8 34b7da8c03d0, Bad Badewanne
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-31
auth: password
deliver: {shelly-manager: 31} # HiluX DS8 34b7da888050, Bad Spiegel Links
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-44
auth: password
deliver: {shelly-manager: 44} # HiluX DS8 34b7da894604, Toilette (standalone, no mesh)
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-30
auth: password
deliver: {shelly-manager: 30} # Plus 2PM b48a0a226c28 (reachable again, REN-1108)
acl: ["readwrite shellies/#", "write homeassistant/#"]
# Sleeping H&Ts (REN-1107): awake only 8-13 s every 1-2 h, so the
# controller pushes when the device publishes `online` on its wake topic
# (shelly-manager topic prefix `shellies/<hostname>`), not in the 2-min loop.
- name: shelly-35
auth: password
deliver: {shelly-manager: 35} # Plus H&T 08b61fcea690
wake: shellies/shellyplusht-08b61fcea690/online
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-36
auth: password
deliver: {shelly-manager: 36} # Plus H&T 08b61fce90dc
wake: shellies/shellyplusht-08b61fce90dc/online
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-37
auth: password
deliver: {shelly-manager: 37} # H&T G3 84fce63ab4d8
wake: shellies/shellyhtg3-84fce63ab4d8/online
acl: ["readwrite shellies/#", "write homeassistant/#"]
- name: shelly-38
auth: password
deliver: {shelly-manager: 38} # H&T G3 ecda3bc0b6a0
wake: shellies/shellyhtg3-ecda3bc0b6a0/online
acl: ["readwrite shellies/#", "write homeassistant/#"]
@@ -61,7 +61,7 @@ spec:
type: RuntimeDefault
containers:
- name: shelly-manager
image: gitea.renner.uno/k.renner/shelly-manager:build.1791337222 # {"$imagepolicy": "flux-system:shelly-manager"}
image: gitea.renner.uno/k.renner/shelly-manager:build.1791426752 # {"$imagepolicy": "flux-system:shelly-manager"}
imagePullPolicy: IfNotPresent
ports:
- name: http
+1 -1
View File
@@ -32,7 +32,7 @@ spec:
type: RuntimeDefault
containers:
- name: immich-machine-learning
image: ghcr.io/immich-app/immich-machine-learning:v3.2.4 # {"$imagepolicy": "flux-system:immich-machine-learning"}
image: ghcr.io/immich-app/immich-machine-learning:v3.3.0 # {"$imagepolicy": "flux-system:immich-machine-learning"}
ports:
- name: http
containerPort: 3003
+1 -1
View File
@@ -87,7 +87,7 @@ spec:
mountPath: /ca
containers:
- name: immich-server
image: ghcr.io/immich-app/immich-server:v3.2.4 # {"$imagepolicy": "flux-system:immich-server"}
image: ghcr.io/immich-app/immich-server:v3.3.0 # {"$imagepolicy": "flux-system:immich-server"}
ports:
- name: http
containerPort: 3001
+1 -1
View File
@@ -54,7 +54,7 @@ spec:
type: RuntimeDefault
containers:
- name: llama-server
image: ghcr.io/ggml-org/llama.cpp:server-vulkan-b10731
image: ghcr.io/ggml-org/llama.cpp:server-vulkan-b10731@sha256:0262343f64f030e8efff15ec05ee8180b69b7f6745de2caa3af5148e0dcb8aba
args:
# The one place this model is named; llama-server resolves the repo
# and downloads it itself, so there is nothing to keep in sync.
+1 -1
View File
@@ -43,7 +43,7 @@ spec:
type: RuntimeDefault
containers:
- name: llama-embed
image: ghcr.io/ggml-org/llama.cpp:server-vulkan-b10731
image: ghcr.io/ggml-org/llama.cpp:server-vulkan-b10731@sha256:0262343f64f030e8efff15ec05ee8180b69b7f6745de2caa3af5148e0dcb8aba
args:
- --hf-repo
- gpustack/bge-m3-GGUF:Q8_0
+1 -1
View File
@@ -32,7 +32,7 @@ spec:
type: RuntimeDefault
containers:
- name: wiki-embed
image: ghcr.io/ggml-org/llama.cpp:server-vulkan-b10731
image: ghcr.io/ggml-org/llama.cpp:server-vulkan-b10731@sha256:0262343f64f030e8efff15ec05ee8180b69b7f6745de2caa3af5148e0dcb8aba
args:
- --hf-repo
- gpustack/bge-m3-GGUF:Q8_0
+1 -1
View File
@@ -72,7 +72,7 @@ spec:
type: RuntimeDefault
containers:
- name: element-web
image: ghcr.io/element-hq/element-web:v1.12.30
image: ghcr.io/element-hq/element-web:v1.12.30@sha256:3e7dbd4424de9e11c812bc740acf2725b2abe66ea0d0802023b56fd85d3fa09a
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
+1 -1
View File
@@ -137,7 +137,7 @@ spec:
type: RuntimeDefault
containers:
- name: mas
image: ghcr.io/element-hq/matrix-authentication-service:1.26.0
image: ghcr.io/element-hq/matrix-authentication-service:1.26.0@sha256:e089f1048a1d4a9a492ed17b9fe759100f1bd619407b001f5927928d88b780c4
# The server applies database migrations and syncs the upstream
# providers from the config on start.
args: ["server", "--config", "/config/config.yaml"]
+2 -2
View File
@@ -152,7 +152,7 @@ spec:
# first. The marker on the data PVC makes later starts skip it.
# Debug image: the plain one has no shell for the marker check.
- name: migrate-to-mas
image: ghcr.io/element-hq/matrix-authentication-service:1.26.0-debug
image: ghcr.io/element-hq/matrix-authentication-service:1.26.0-debug@sha256:46f7480f5ea4243ccb6fd2c447ea859e0230c8b3c0e676ee285d4ae6ca344ee3
command:
- /busybox/sh
- -ec
@@ -207,7 +207,7 @@ spec:
mountPath: /data
containers:
- name: synapse
image: ghcr.io/element-hq/synapse:v1.162.0
image: ghcr.io/element-hq/synapse:v1.162.0@sha256:6b84a7bbac36f080b2d2e51e0289cf1b08b349598ea44a558df38d558f2c2311
# Bypass /start.py: it expects to run as root and template a config
# from env. Default mode is generate-missing-and-run, which creates
# the signing key on first start.
@@ -123,3 +123,23 @@ spec:
interval: 30s
podTargetLabels:
- role
---
# op-pg (OpenProject, REN-1363). Added with the cluster, like ha-pg, so the
# CNPG backup/WAL rules see it from the first moment.
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: cnpg-op-pg
namespace: openproject
labels:
app.kubernetes.io/name: cloudnative-pg
release: kube-prometheus-stack
spec:
selector:
matchLabels:
cnpg.io/cluster: op-pg
podMetricsEndpoints:
- port: metrics
interval: 30s
podTargetLabels:
- role
@@ -71,6 +71,11 @@ spec:
# health, not reachability — the root path returns 404 by design, since
# the service only answers on calendar paths.
- https://ical-proxy.renner.uno/health
# OpenProject (REN-1363). Ungated (the app has its own login), so the
# public name reaches the app: /health_checks/default answers 200 only
# when the web process and op-pg are up. App health and the public
# edge path in one target.
- https://openproject.renner.uno/health_checks/default
# APP-HEALTH probes for the gated monitoring UIs, from the correct
# in-cluster vantage (monitoring-vantage-point-rule): the gate is
# only on the ingress, so the ClusterIP /-/healthy endpoints are
@@ -27,7 +27,7 @@ spec:
effect: NoSchedule
containers:
- name: smartctl-exporter
image: quay.io/prometheuscommunity/smartctl-exporter:v0.14.0
image: quay.io/prometheuscommunity/smartctl-exporter:v0.14.0@sha256:cfe22c36d7d2fac48ebf619707305acb65eb0fb670656eb80f356e606d782bc1
# No --smartctl.device: the exporter scans and rescans on its own.
# Device names are not stable across these machines.
args:
+2 -2
View File
@@ -93,7 +93,7 @@ spec:
type: RuntimeDefault
containers:
- name: lidarr
image: lscr.io/linuxserver/lidarr:3.1.0.4875-ls42 # {"$imagepolicy": "flux-system:lidarr"}
image: lscr.io/linuxserver/lidarr:3.1.0.4875-ls43 # {"$imagepolicy": "flux-system:lidarr"}
env:
- name: PUID
value: "1000"
@@ -140,7 +140,7 @@ spec:
drop: ["ALL"]
add: ["CHOWN", "DAC_OVERRIDE", "FOWNER", "SETGID", "SETUID"]
- name: reconcile
image: lscr.io/linuxserver/lidarr:3.1.0.4875-ls42 # {"$imagepolicy": "flux-system:lidarr"}
image: lscr.io/linuxserver/lidarr:3.1.0.4875-ls43 # {"$imagepolicy": "flux-system:lidarr"}
command: ["sh", "/scripts/reconcile.sh"]
volumeMounts:
- name: config
+71
View File
@@ -0,0 +1,71 @@
# openproject
OpenProject Community Edition, the owner's project board (REN-1346,
REN-1363). <https://openproject.renner.uno>, public through the NetBird
proxy like immich and matrix; OpenProject's own login guards it.
## Layout
- `openproject` Deployment, one pod: init `wait-for-db` and `seeder`
(schema load or migrations, then seed), then `web` (puma, :8080) and
`worker` (GoodJob). One pod because both need the RWO attachments PVC.
- `op-pg` CNPG cluster (PostgreSQL 17, 2 instances), WAL archive and daily
base backup to Hetzner `s3://renner-uno-k8s-cnpg/op-pg`, 30 days.
- `openproject-assets` PVC (attachments), Velero `openproject-daily` at
10:15 local, 30 days.
- `openproject-secrets` is generated once in-cluster by ESO:
`SECRET_KEY_BASE` and `OPENPROJECT_SEED_ADMIN_USER_PASSWORD` (login
`admin`). The password only seeds the first start; a change in the UI is
not reverted.
## Image updates
The image is pinned by tag and digest (`openproject/openproject:<x.y.z>-slim`)
and Renovate opens the bump. The `seeder` init container migrates the schema
before the new web container starts. A major bump (17 -> 18) gets its own
card and a check of the upstream upgrade notes.
## Monitoring
- Blackbox `https://openproject.renner.uno/health_checks/default`: public
edge, Traefik, web and database in one target (catch-all
`BlackboxProbeFailed`).
- `cnpg-op-pg` PodMonitor: the CNPG backup and WAL-archive rules cover op-pg.
- Velero schedule rules cover `openproject-daily`.
## Restore
Database (physical restore of the latest base backup plus WAL):
1. Check a backup exists: `kubectl -n openproject get backups.postgresql.cnpg.io`
and the bucket path `op-pg/`.
2. Follow `kubernetes/RECOVERY.md` (cold rebuild) with this stanza in
`cluster.yaml` in place of `bootstrap.initdb`:
```yaml
bootstrap:
recovery:
source: op-pg-store
database: openproject
owner: openproject
externalClusters:
- name: op-pg-store
plugin:
name: barman-cloud.cloudnative-pg.io
parameters:
barmanObjectName: op-pg-store
serverName: op-pg
```
Scale the `openproject` Deployment to 0 first, so nothing writes to an
empty database while the cluster rebuilds. Revert to `initdb` afterwards.
3. A non-destructive test restore into a side cluster: "CNPG restore-test"
in `kubernetes/RECOVERY.md`.
Attachments: scale the Deployment to 0, delete the `openproject-assets` PVC,
then `velero restore create --from-backup <openproject-daily-...>
--include-namespaces openproject`. Restore database and attachments from the
same day, so attachment rows and files match.
`openproject-secrets`: if lost, ESO generates a new one. Sessions end; the
admin password in the database stays what it was.
+42
View File
@@ -0,0 +1,42 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: op-pg
namespace: openproject
spec:
instances: 2
smartShutdownTimeout: 10
stopDelay: 60
# PG 17, not the repo-standard 16: OpenProject 17 ships its all-in-one
# image and packages on 17 and its system requirements point there. Bare
# major tag like the other clusters (minor bumps flow, majors stay an
# explicit decision).
imageName: ghcr.io/cloudnative-pg/postgresql:17
storage:
storageClass: ceph-block
size: 5Gi
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: op-pg-store
bootstrap:
initdb:
database: openproject
owner: openproject
postgresql:
parameters:
shared_buffers: "128MB"
max_connections: "100"
# Same write-reduction settings as the other clusters; see
# apps/trmnl/cluster.yaml for the reasoning and the RPO trade.
wal_compression: "zstd"
archive_timeout: "30min"
checkpoint_timeout: "15min"
max_wal_size: "2GB"
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
@@ -0,0 +1,15 @@
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: hetzner-s3-creds
namespace: openproject
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: openbao
target:
name: hetzner-s3-creds
dataFrom:
- extract:
key: infrastructure/hetzner-s3
+31
View File
@@ -0,0 +1,31 @@
# Public, same edge path as immich and matrix: the dns.renner.uno/public
# label makes netbird-service-sync publish the host through the NetBird proxy
# (Cloudflare wildcard -> proxy.renner.uno -> TLS passthrough to Traefik) and
# external-dns-unifi skip it; LAN clients follow the public answer and the UDM
# hairpins back in. OpenProject's own login guards it.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: openproject
namespace: openproject
labels:
dns.renner.uno/public: "true"
annotations:
cert-manager.io/cluster-issuer: letsencrypt-cluster
spec:
ingressClassName: traefik
tls:
- hosts:
- openproject.renner.uno
secretName: openproject-tls
rules:
- host: openproject.renner.uno
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: openproject
port:
number: 80
@@ -0,0 +1,13 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- secrets.yaml
- pvcs.yaml
- openproject.yaml
- ingress.yaml
# op-pg CNPG Postgres + offsite Barman backup (PostgreSQL 17, the version
# OpenProject 17 targets — see cluster.yaml).
- cluster.yaml
- objectstore.yaml
- scheduled-backup.yaml
- hetzner-creds-externalsecret.yaml
@@ -0,0 +1,30 @@
---
apiVersion: barmancloud.cnpg.io/v1
kind: ObjectStore
metadata:
name: op-pg-store
namespace: openproject
spec:
configuration:
# Offsite Hetzner Object Storage (fsn1), same bucket as the other CNPG
# clusters. No SSE: Hetzner supports only SSE-C, unusable by barman-cloud
# (plugin #646), so at-rest encryption is unavailable; transit is TLS.
destinationPath: s3://renner-uno-k8s-cnpg/op-pg
endpointURL: https://fsn1.your-objectstorage.com
s3Credentials:
accessKeyId:
name: hetzner-s3-creds
key: ACCESS_KEY_ID
secretAccessKey:
name: hetzner-s3-creds
key: ACCESS_SECRET_KEY
# Region is required (REGION=fsn1) — without it boto3's HeadBucket
# (barman-cloud-check-wal-archive during bootstrap.recovery) 400s.
region:
name: hetzner-s3-creds
key: REGION
wal:
compression: gzip
data:
compression: gzip
retentionPolicy: "30d"
@@ -0,0 +1,224 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: openproject-config
namespace: openproject
data:
OPENPROJECT_HOST__NAME: "openproject.renner.uno"
# TLS ends at Traefik; this makes OpenProject build https:// links and set
# secure cookies.
OPENPROJECT_HTTPS: "true"
# Log in with the generated password directly instead of being forced to
# change it on first login (see secrets.yaml).
OPENPROJECT_SEED_ADMIN_USER_PASSWORD_RESET: "false"
# One user base and a small team: two puma workers instead of the
# per-CPU default keep the pod inside its memory limit.
OPENPROJECT_WEB_WORKERS: "2"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: openproject
namespace: openproject
labels:
app.kubernetes.io/name: openproject
annotations:
# Env vars are read once at container start; roll the pod when the
# ConfigMap or the generated Secret changes.
configmap.reloader.stakater.com/reload: openproject-config
secret.reloader.stakater.com/reload: openproject-secrets
spec:
replicas: 1
# Web and worker share the RWO attachments PVC, and the seeder init
# container migrates the schema; two pods would race on both.
strategy:
type: Recreate
selector:
matchLabels:
app.kubernetes.io/name: openproject
template:
metadata:
labels:
app.kubernetes.io/name: openproject
spec:
enableServiceLinks: false
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
# `app` in the official image.
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
initContainers:
# Retries until op-pg accepts connections; on a fresh install the
# cluster comes up in the same Flux apply as this pod.
- name: wait-for-db
image: openproject/openproject:17.9.1-slim@sha256:2beeecac788d4f423c46a95ffbd04e6479491938b9924b610e9f47d81a38303c
args: ["/app/docker/prod/wait-for-db"]
envFrom:
- configMapRef:
name: openproject-config
- secretRef:
name: openproject-secrets
env:
# CNPG generates this Secret; `uri` carries the rotated password
# and points at op-pg-rw.
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: op-pg-app
key: uri
resources:
requests:
cpu: 50m
memory: 256Mi
limits:
memory: 1Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
# Loads the schema on an empty database, else runs pending
# migrations, then seeds (idempotent; creates `admin` on first run).
# Runs on every pod start, so an image bump migrates before the new
# web container serves.
- name: seeder
image: openproject/openproject:17.9.1-slim@sha256:2beeecac788d4f423c46a95ffbd04e6479491938b9924b610e9f47d81a38303c
args: ["/app/docker/prod/seeder"]
envFrom:
- configMapRef:
name: openproject-config
- secretRef:
name: openproject-secrets
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: op-pg-app
key: uri
volumeMounts:
- name: assets
mountPath: /var/openproject/assets
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 2Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
containers:
- name: web
image: openproject/openproject:17.9.1-slim@sha256:2beeecac788d4f423c46a95ffbd04e6479491938b9924b610e9f47d81a38303c
args: ["/app/docker/prod/web"]
ports:
- name: http
containerPort: 8080
envFrom:
- configMapRef:
name: openproject-config
- secretRef:
name: openproject-secrets
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: op-pg-app
key: uri
volumeMounts:
- name: assets
mountPath: /var/openproject/assets
# /health_checks/default checks the database and the web process.
# The Host header is required: the request goes to the pod IP,
# which OpenProject's host check would answer with 404.
startupProbe:
httpGet:
path: /health_checks/default
port: http
httpHeaders:
- name: Host
value: localhost
periodSeconds: 10
# ~5 min for puma to boot and preload the app.
failureThreshold: 30
livenessProbe:
httpGet:
path: /health_checks/default
port: http
httpHeaders:
- name: Host
value: localhost
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health_checks/default
port: http
httpHeaders:
- name: Host
value: localhost
periodSeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 250m
memory: 1Gi
limits:
memory: 2Gi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
# Background jobs (GoodJob): mails, notifications, exports, and the
# cron-like jobs. Needs the attachments volume for exports/imports.
- name: worker
image: openproject/openproject:17.9.1-slim@sha256:2beeecac788d4f423c46a95ffbd04e6479491938b9924b610e9f47d81a38303c
args: ["/app/docker/prod/worker"]
envFrom:
- configMapRef:
name: openproject-config
- secretRef:
name: openproject-secrets
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: op-pg-app
key: uri
volumeMounts:
- name: assets
mountPath: /var/openproject/assets
resources:
requests:
cpu: 100m
memory: 512Mi
limits:
memory: 1536Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumes:
- name: assets
persistentVolumeClaim:
claimName: openproject-assets
---
apiVersion: v1
kind: Service
metadata:
name: openproject
namespace: openproject
labels:
app.kubernetes.io/name: openproject
spec:
selector:
app.kubernetes.io/name: openproject
ports:
- name: http
port: 80
targetPort: http
+19
View File
@@ -0,0 +1,19 @@
# Attachments (OPENPROJECT_ATTACHMENTS__STORAGE__PATH defaults to
# $APP_DATA_PATH/files = /var/openproject/assets/files). Web and worker share
# one pod, so RWO is enough and the Deployment uses `Recreate`. Backed up by
# the Velero schedule openproject-daily (selected by these labels).
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: openproject-assets
namespace: openproject
labels:
app.kubernetes.io/name: openproject
app.kubernetes.io/component: assets
spec:
accessModes:
- ReadWriteOnce
storageClassName: ceph-block
resources:
requests:
storage: 10Gi
@@ -0,0 +1,17 @@
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: op-pg-daily
namespace: openproject
spec:
# 03:00 local, same slot as the other CNPG clusters. `immediate` takes the
# first base backup as soon as the cluster is up. The attachments PVC is
# covered by the Velero schedule openproject-daily.
schedule: "0 0 3 * * *"
immediate: true
backupOwnerReference: self
cluster:
name: op-pg
method: plugin
pluginConfiguration:
name: barman-cloud.cloudnative-pg.io
+42
View File
@@ -0,0 +1,42 @@
# Generated once in-cluster by ESO; nothing here is seeded by hand.
#
# SECRET_KEY_BASE Rails session/cookie key.
# OPENPROJECT_SEED_ADMIN_USER_PASSWORD Password of the `admin` account the
# seeder creates on the first start.
# Only read at seed time: changing the
# password in the UI later is not
# reverted, and this value goes stale.
#
# Deleting the Secret regenerates both; that logs everyone out and does NOT
# change an existing admin password.
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
metadata:
name: openproject-secrets
namespace: openproject
spec:
length: 64
digits: 16
symbols: 0
allowRepeat: true
noUpper: false
secretKeys:
- SECRET_KEY_BASE
- OPENPROJECT_SEED_ADMIN_USER_PASSWORD
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: openproject-secrets
namespace: openproject
spec:
# Generate once; Periodic would rotate the key every refresh.
refreshPolicy: CreatedOnce
target:
name: openproject-secrets
dataFrom:
- sourceRef:
generatorRef:
apiVersion: generators.external-secrets.io/v1alpha1
kind: Password
name: openproject-secrets
+1 -1
View File
@@ -48,7 +48,7 @@ spec:
#
# Hence socat is installed at startup and run inside this container.
- name: otbr
image: openthread/otbr:latest
image: openthread/otbr:latest@sha256:49b352ce39dd6514b486af7397356c5cf07ed3f090a48ca3006cb72ec45a46f8
command:
- bash
- -c
@@ -5,8 +5,9 @@
# `paperclip-prometheus-read`. Atlas, Iris, Hestia and Argus get exactly what
# they hold today through that shared account: built-in `view` cluster-wide
# (no Secrets, no write verb) plus the `monitoring/paperclip-prometheus-read`
# Role (see prometheus-read-rbac.yaml). Hermes gets only `get` on the five
# token Secrets below, to issue and rotate the kubeconfigs.
# Role (see prometheus-read-rbac.yaml). Hermes gets `get` on the five token
# Secrets below, to issue and rotate the kubeconfigs, plus that Prometheus
# Role (REN-1345).
#
# Token Secrets carry no data in git; kube-controller-manager fills
# `data.token`. Flux applies server-side and does not own `data`, so it does
@@ -159,7 +160,9 @@ roleRef:
# Existing Role in prometheus-read-rbac.yaml, unchanged.
name: paperclip-prometheus-read
# Subjects live in `paperclip`, the binding in `monitoring`: `namespace:` is
# load-bearing.
# load-bearing. agent-hermes is bound here too (REN-1345): its duty list reads
# Prometheus, and this Role is `get` on three named read-only proxies only.
# It does not get `view`.
subjects:
- kind: ServiceAccount
name: agent-atlas
@@ -173,6 +176,9 @@ subjects:
- kind: ServiceAccount
name: agent-argus
namespace: paperclip
- kind: ServiceAccount
name: agent-hermes
namespace: paperclip
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
@@ -212,3 +218,47 @@ subjects:
- kind: ServiceAccount
name: agent-hermes
namespace: paperclip
---
# MQTT read access for agents (REN-953): `get` on the one Secret that holds
# the agents-read client certificate (issued by mqtt-account-controller,
# renewed by cert-manager). Agents fetch it per run, so a renewal needs no
# restart. The certificate can only read open3e/#, zigbee2mqtt/# and
# homeassistant/# on the broker (register mqtt-accounts.yaml). Hermes is not
# bound: it issues kubeconfigs and has no MQTT task.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: mqtt-agents-read
namespace: paperclip
labels:
app.kubernetes.io/name: paperclip
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["mqtt-agents-read-tls"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: mqtt-agents-read
namespace: paperclip
labels:
app.kubernetes.io/name: paperclip
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: mqtt-agents-read
subjects:
- kind: ServiceAccount
name: agent-atlas
namespace: paperclip
- kind: ServiceAccount
name: agent-iris
namespace: paperclip
- kind: ServiceAccount
name: agent-hestia
namespace: paperclip
- kind: ServiceAccount
name: agent-argus
namespace: paperclip
@@ -74,6 +74,18 @@ spec:
- ports:
- port: "8443"
protocol: TCP
# mosquitto mTLS listener (REN-953): agents that run in this pod read MQTT
# with the agents-read client certificate. :8884 only; the listener
# rejects any connection without an mqtt-ca certificate. Paired with
# mosquitto-agent-ingress in network-policies/home-assistant-ns.yaml.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": home-assistant
app.kubernetes.io/name: mosquitto
toPorts:
- ports:
- port: "8884"
protocol: TCP
# Notes wiki API, authenticated by the paperclip API key.
- toEndpoints:
- matchLabels:
+1 -1
View File
@@ -34,7 +34,7 @@ spec:
type: RuntimeDefault
containers:
- name: paperclip
image: gitea.renner.uno/konairius/paperclip:build.1791297080@sha256:0764e882521410d8ce8ed1359f8a1aa64eaa7746aa909653ef70050c239ca195 # {"$imagepolicy": "flux-system:paperclip"}
image: gitea.renner.uno/konairius/paperclip:build.1791487850@sha256:ff81da300e6c35581f34fad61a55da771b0e991d2fb2dcb624a0361263f01a37 # {"$imagepolicy": "flux-system:paperclip"}
ports:
- name: http
containerPort: 3100
+1 -1
View File
@@ -10,4 +10,4 @@ spec:
storageClassName: ceph-block
resources:
requests:
storage: 20Gi
storage: 40Gi # ceph-block expands online; 20Gi filled in ~12 days (#1184)
+2 -2
View File
@@ -134,7 +134,7 @@ spec:
# is a device-bricking class of risk. Bumps are a reviewed, manual
# act — see kubernetes/infrastructure/image-automation/README.md.
- name: terminus
image: ghcr.io/usetrmnl/terminus:0.76.0 # {"$imagepolicy": "flux-system:terminus"}
image: ghcr.io/usetrmnl/terminus:0.77.0 # {"$imagepolicy": "flux-system:terminus"}
ports:
- name: http
containerPort: 2345
@@ -197,7 +197,7 @@ spec:
capabilities:
drop: ["ALL"]
- name: sidekiq
image: ghcr.io/usetrmnl/terminus:0.76.0 # {"$imagepolicy": "flux-system:terminus"}
image: ghcr.io/usetrmnl/terminus:0.77.0 # {"$imagepolicy": "flux-system:terminus"}
command: ["bundle", "exec", "sidekiq", "-r", "./config/sidekiq.rb"]
envFrom:
- configMapRef:
@@ -57,7 +57,7 @@ spec:
# Pinned, and deliberately NOT under Flux image automation. rbac.yaml
# is derived from what this exact version calls; an automatic bump
# could add a call the Role does not cover, silently.
image: ghcr.io/stakater/ingressmonitorcontroller:v2.2.13
image: ghcr.io/stakater/ingressmonitorcontroller:v2.2.13@sha256:3301afb61c10abc4e334deab9cd8601cef069e352b4d7f4dd0897787fca32eac
command: ["/manager"]
args:
- --health-probe-bind-address=:8081
+1 -1
View File
@@ -34,7 +34,7 @@ spec:
# Image automation, range 3.x (see image-automation/woodpecker.yaml).
# MUST stay version-matched with woodpecker-server (gRPC parity) — both
# track the same range/filter so they bump together on each release.
image: woodpeckerci/woodpecker-agent:v3.18.1 # {"$imagepolicy": "flux-system:woodpecker-agent"}
image: woodpeckerci/woodpecker-agent:v3.19.0 # {"$imagepolicy": "flux-system:woodpecker-agent"}
env:
- name: WOODPECKER_SERVER
value: woodpecker.woodpecker.svc.cluster.local:9000 # in-cluster gRPC, replaces LXC :9000
+1 -1
View File
@@ -40,7 +40,7 @@ spec:
# minor + patch auto-update within 3.x; only the 4.0 major is held for
# a reviewed bump. Safe now the DB is postgres (the old sqlite-surprise
# -bump hazard is gone). Stays in lockstep with the agent.
image: woodpeckerci/woodpecker-server:v3.18.1 # {"$imagepolicy": "flux-system:woodpecker-server"}
image: woodpeckerci/woodpecker-server:v3.19.0 # {"$imagepolicy": "flux-system:woodpecker-server"}
env:
- name: WOODPECKER_HOST
value: https://ci.renner.uno
@@ -0,0 +1,28 @@
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: openproject
namespace: flux-system
spec:
# OpenProject Community (REN-1363): web + worker, the op-pg CNPG Cluster +
# ObjectStore + ScheduledBackup and the attachments PVC. Own KS so a
# failure here never holds up the shared apps KS.
interval: 10m
retryInterval: 1m
timeout: 30m
sourceRef:
kind: GitRepository
name: flux-system
path: ./kubernetes/apps/openproject
prune: true
wait: true
dependsOn:
- name: infrastructure
- name: infrastructure-crds
- name: infrastructure-ceph-cluster
- name: infrastructure-cnpg-barman
healthCheckExprs:
- apiVersion: postgresql.cnpg.io/v1
kind: Cluster
failed: "has(status.phase) && status.phase.startsWith('Failed')"
current: "has(status.phase) && (status.phase == 'Cluster in healthy state' || status.readyInstances > 0)"
@@ -131,7 +131,7 @@ spec:
# to run on every node (and re-run if the node reboots / the binary
# is wiped). Pause image keeps memory near zero.
- name: pause
image: registry.k8s.io/pause:3.10
image: registry.k8s.io/pause:3.10@sha256:ee6521f290b2168b6e0935a181d4cff9be1ac3f505666ef0e3c98fae8199917a
terminationGracePeriodSeconds: 5
volumes:
- name: cnibin
@@ -167,7 +167,7 @@ spec:
SLEEP=false source /install-cni.sh
/token-watcher.sh &
/ip-control-loop -log-level debug
image: ghcr.io/k8snetworkplumbingwg/whereabouts:v0.9.4
image: ghcr.io/k8snetworkplumbingwg/whereabouts:v0.9.4@sha256:f279fd7dc1124d7819330fb2e07357990b2b4c5d8733aef1b028573c29f05e14
env:
- name: NODENAME
valueFrom:
@@ -51,7 +51,7 @@ spec:
args:
- -log-level
- debug
image: ghcr.io/k8snetworkplumbingwg/whereabouts:v0.9.4
image: ghcr.io/k8snetworkplumbingwg/whereabouts:v0.9.4@sha256:f279fd7dc1124d7819330fb2e07357990b2b4c5d8733aef1b028573c29f05e14
env:
- name: WHEREABOUTS_NAMESPACE
valueFrom:
@@ -152,6 +152,12 @@ kind: Namespace
metadata:
name: matrix
---
# OpenProject Community, the owner's project board (REN-1363).
apiVersion: v1
kind: Namespace
metadata:
name: openproject
---
# Terminus (TRMNL BYOS server) driving the entrance-way e-ink panel.
apiVersion: v1
kind: Namespace
@@ -439,3 +439,39 @@ spec:
- ports:
- port: "8081"
protocol: TCP
---
# Agent sandbox pods -> mosquitto :8884 (REN-953). The listener requires a
# client certificate from mqtt-ca; agents hold only agents-read, which may
# read open3e/#, zigbee2mqtt/# and homeassistant/# and write nothing.
# DEDICATED policy selecting mosquitto alone, like shelly-manager-agent-ingress.
#
# Revert: delete this document and agent-egress-mosquitto.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: mosquitto-agent-ingress
namespace: home-assistant
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: mosquitto
ingress:
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": paperclip-renner
paperclip.io/role: agent
toPorts:
- ports:
- port: "8884"
protocol: TCP
# The paperclip pod itself: most agent runs execute there, not in a
# sandbox (REN-1001). Egress side: paperclip-egress in
# apps/paperclip/network-policy.yaml.
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": paperclip
app.kubernetes.io/name: paperclip
toPorts:
- ports:
- port: "8884"
protocol: TCP
@@ -27,6 +27,7 @@ resources:
- gitea-ns.yaml
- netbird-service-sync-ns.yaml
- trmnl-ns.yaml
- openproject-ns.yaml
- matrix-ns.yaml
- wordpress-ns.yaml
- ai-ns.yaml
@@ -0,0 +1,90 @@
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: default-deny-ingress
namespace: openproject
spec:
endpointSelector: {}
ingress:
# Intra-namespace — openproject → op-pg.
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": openproject
# openproject.renner.uno via Traefik.
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": ingress
app.kubernetes.io/name: traefik
# Prometheus scrape.
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": monitoring
# Velero backups via PVC mount.
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": velero
# CNPG operator (in `database` ns) reconciles op-pg — needs to reach
# the postgres pods for health, failover, role checks.
- fromEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": database
- fromEntities:
- host
- remote-node
# ICMP — all standard ICMPv4 + ICMPv6 from any cluster pod. See
# home-assistant-ns.yaml for the rationale. INGRESS ONLY.
- fromEndpoints:
- matchExpressions:
- key: io.kubernetes.pod.namespace
operator: Exists
icmps:
- fields:
- { type: 0, family: IPv4 }
- { type: 3, family: IPv4 }
- { type: 8, family: IPv4 }
- { type: 11, family: IPv4 }
- { type: 12, family: IPv4 }
- { type: 1, family: IPv6 }
- { type: 2, family: IPv6 }
- { type: 3, family: IPv6 }
- { type: 4, family: IPv6 }
- { type: 128, family: IPv6 }
- { type: 129, family: IPv6 }
- { type: 133, family: IPv6 }
- { type: 134, family: IPv6 }
- { type: 135, family: IPv6 }
- { type: 136, family: IPv6 }
- { type: 143, family: IPv6 }
---
# Egress for the OpenProject pod only, so the op-pg CNPG pods keep
# unrestricted egress for the barman-cloud WAL archive to Hetzner (same trade
# as trmnl-ns.yaml).
#
# OpenProject needs nothing outside the namespace today: no SMTP, no OIDC, no
# file-storage integration. Adding one of those needs a rule here, or it
# fails closed.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: openproject-egress
namespace: openproject
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: openproject
egress:
# Intra-namespace — op-pg-rw.
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": openproject
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
@@ -166,3 +166,28 @@ spec:
- ports:
- port: "8081"
protocol: TCP
---
# Agent pods -> mosquitto mTLS listener :8884 (REN-953), for the read-only
# certificate account agents-read. Not :1883/:8883: those take passwords, and
# agents hold none. Matching ingress: mosquitto-agent-ingress in
# home-assistant-ns.yaml. Not labelled `paperclip.io/managed-by`.
#
# Revert: delete this document and mosquitto-agent-ingress.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: agent-egress-mosquitto
namespace: paperclip-renner
spec:
endpointSelector:
matchLabels:
paperclip.io/role: agent
egress:
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": home-assistant
app.kubernetes.io/name: mosquitto
toPorts:
- ports:
- port: "8884"
protocol: TCP
@@ -1,30 +0,0 @@
# OpenBao policy: rotator-mosquitto-open3e-int
#
# Least-privilege policy for the mosquitto `open3e-int` broker-credential
# rotation CronJob (kubernetes auth role `rotator-mosquitto-open3e-int`).
# Per-secret isolation, same template as rotator-mosquitto-health.
#
# NOT applied by Flux. Apply ONCE with an admin token:
#
# bao policy write rotator-mosquitto-open3e-int \
# kubernetes/infrastructure/openbao/policies/rotator-mosquitto-open3e-int.hcl
#
# bao write auth/kubernetes/role/rotator-mosquitto-open3e-int \
# bound_service_account_names=mosquitto-open3e-int-secret-rotator \
# bound_service_account_namespaces=openbao \
# policies=rotator-mosquitto-open3e-int ttl=15m
#
# Do NOT set token_no_default_policy=true (kv-v2 patch preflight needs default).
#
# BOOTSTRAP: `random` mode rotates an existing field and never creates one.
# Seed the path once with a `password` field before the first rotation:
#
# bao kv put -mount=homelab apps/home-assistant/mosquitto-open3e-int-credentials \
# password=<generated>
#
# The consumer is the open3e-esp32 gateway on the heat pump's internal CAN
# (NVS key mq_pass); a rotation takes effect on the device only after its
# MQTT settings are updated (web UI or NVS image).
path "homelab/data/apps/home-assistant/mosquitto-open3e-int-credentials" {
capabilities = ["read", "patch"]
}
@@ -48,7 +48,6 @@ resources:
- review-bot.yaml
- claude-oauth.yaml
- mosquitto-health.yaml
- mosquitto-open3e-int.yaml
# NOT ROTATED, deliberately — recorded here because the whole point of this
# directory is that unrotated credentials should be a visible choice:
#
@@ -1,83 +0,0 @@
---
# Class-A rotation of the mosquitto `open3e-int` broker credential: the
# open3e-esp32 gateway on the Vitocal internal CAN (ECU 0x6e0). Same
# rotator -> OpenBao -> ESO -> Reloader chain as mosquitto-health.yaml.
#
# The consumer is an embedded device: after a rotation its MQTT password must
# be updated on the device (web UI Einstellungen, or an NVS image over USB).
#
# ON-DEMAND: kubectl -n openbao create job --from=cronjob/mosquitto-open3e-int-secret-rotate mosquitto-open3e-int-secret-rotate-now
apiVersion: v1
kind: ServiceAccount
metadata:
name: mosquitto-open3e-int-secret-rotator
namespace: openbao
labels:
app.kubernetes.io/name: mosquitto
app.kubernetes.io/component: secret-rotator
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: mosquitto-open3e-int-secret-rotate
namespace: openbao
labels:
app.kubernetes.io/name: mosquitto
app.kubernetes.io/component: secret-rotator
spec:
# Quarterly, own month offset (paperless: 1,4,7,10; health: 2,5,8,11).
schedule: "0 4 1 3,6,9,12 *"
# Suspended (REN-802): the ESP32 only gets its password by hand, so a
# rotation would take the gateway offline. MQTT credentials move to the
# git-defined account register; this rotator is removed in that cleanup.
suspend: true
timeZone: Europe/Berlin
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 2
failedJobsHistoryLimit: 5
startingDeadlineSeconds: 3600
jobTemplate:
spec:
backoffLimit: 2
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 259200 # 3 d
template:
metadata:
labels:
app.kubernetes.io/name: mosquitto
app.kubernetes.io/component: secret-rotator
spec:
serviceAccountName: mosquitto-open3e-int-secret-rotator
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 65534
runAsGroup: 65534
seccompProfile:
type: RuntimeDefault
containers:
- name: rotate
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-07-30
args: ["random"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
env:
- name: BAO_ADDR
value: http://openbao.openbao.svc.cluster.local:8200
- name: BAO_ROLE
value: rotator-mosquitto-open3e-int
- name: BAO_PATH
value: homelab/apps/home-assistant/mosquitto-open3e-int-credentials
- name: BAO_FIELDS
value: "password"
- name: RANDOM_BYTES
value: "32"
resources:
requests:
cpu: 25m
memory: 16Mi
limits:
memory: 48Mi
@@ -22,12 +22,14 @@ spec:
- crowdsec
- evcc
- flux-system # flux-system-git-auth: syncs the rotated flux gitea PAT into Flux's git secret
- gitea # gitea-package-retention token (apps/gitea/package-retention.yaml, REN-1203)
- home-assistant
- immich
- influxdb
- ingress
- matrix # synapse-secrets, mx-pg barman creds (apps/matrix)
- monitoring
- openproject # op-pg barman creds (apps/openproject)
- paperclip
- paperless
- renovate # renovate-bot token (apps/renovate/externalsecret.yaml)
@@ -368,3 +368,30 @@ spec:
labelSelector:
matchLabels:
app.kubernetes.io/name: synapse
---
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: openproject-daily
namespace: velero
labels:
app.kubernetes.io/part-of: homelab-backup
spec:
# Daily at 10:15 local (next free stagger slot after matrix-daily at
# 09:45). openproject-assets only: the attachments. Selected by the label
# this repo sets, like wordpress-daily; the op-pg database is covered by
# Barman.
schedule: "15 10 * * *"
useOwnerReferencesInBackup: false
template:
includedNamespaces:
- openproject
includedResources:
- persistentvolumeclaims
storageLocation: default
ttl: 720h0m0s
snapshotMoveData: true
labelSelector:
matchLabels:
app.kubernetes.io/name: openproject
app.kubernetes.io/component: assets