Compare commits

..
Author SHA1 Message Date
claude-botandClaude Opus 5.5 9cb8e65d85 fix(crowdsec-edge-bouncer): cache the release outside /tmp
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
The archive was fetched into the controller's /tmp, so every run after a
/tmp cleanup downloaded it again and reported a change. It now lives in
~/.cache/ansible/crowdsec, like the netbird-client download.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:45:57 +01:00
claude-botandClaude Opus 5.5 41cadcb3ac fix(ansible): site.yml runs without warnings or no-op changes
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
- ssh-ca reloads sshd only when the CA key, the Include or the drop-in
  changed; principals and revoked keys are read per login.
- On DSM the sudo rules are checked with visudo -c -f - on the
  controller instead of a temporary file, which reported three changes
  per run.
- Facts are read through ansible_facts (INJECT_FACTS_AS_VARS
  deprecation) in common, qemu-guest-agent and keepalived-ntp.
- common masks systemd-timesyncd on guests only; a KVM host such as a
  NAS running VMM has no such unit.
- netbird-client uses deb822_repository (apt_repository deprecation) and
  removes the old one-line source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 12:04:38 +01:00
claude-botandClaude Opus 5.5 4d97dd6e1f feat(ansible): reach nas-1 as root
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
nas-1 trusts the SSH CA now; root accepts the owner certificate there,
as on nas-2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:54:25 +01:00
claude-botandClaude Opus 5.5 6dede6c422 fix(ansible): reach nas-1 as ladmin until it trusts the SSH CA
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
root on nas-1 accepts no owner key or certificate yet, so the first
run has to go through ladmin with its become password.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:40:38 +01:00
claude-botandClaude Opus 5.5 3b1bd4cd2e feat(ansible): Synology hosts are reached as root
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
With the SSH CA in place, root accepts the owner certificate on DSM
(PermitRootLogin without-password), so runs need no become password,
as on every other host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:29:59 +01:00
claude-botandClaude Opus 5.5 2c22f66539 feat(ansible): nas-1 back in the inventory
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
nas-1 is powered on again. It joins [synology_nas] and [ssh_ca] like
nas-2; the roles and plays dropped with it in 9c9e45d1 stay dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:27:33 +01:00
claude-botandClaude Opus 5.5 c32dd3bdfd feat(ansible): claude-bot has full sudo on nas-2
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
On DSM claude-bot is in administrators, whose sudo rule asks for a
password nobody knows. ssh_ca_claude_sudo_all (default false) adds a
passwordless rule; synology_nas sets it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:20:16 +01:00
claude-botandClaude Opus 5.5 5c416f8578 feat(ansible): nas-2 trusts the SSH user CA
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
nas-2 (Synology DSM 7.3) joins [ssh_ca] under a new [synology_nas] group,
so claude-bot reaches it with its 1h certificate like every other host.

DSM differs from the fleet in ways the ssh-ca role now handles:
- Python 3.8 only; plays/configure-synology-python.yml installs Synology's
  Python3.9 package via raw, and group_vars point the interpreter at it.
- No getent: admin logins come from /etc/passwd on every host.
- No useradd: the account is made with synouser and added to
  administrators (the only group DSM lets in over SSH). synogroup --member
  replaces the list, so the current members are read and passed along,
  and the run stops if the owner login is not among them.
- No visudo: the sudo rules are rendered and checked on the controller.
- sshd_config has no Include; the role puts it first.
- No sftp/scp for ladmin: transfers are piped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:00:51 +01:00
security-bot 0c37b0d757 chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-10-29-0807
Updates managed by image-automation-controller.
2026-10-29 08:35:30 +00:00
security-bot d1eb310673 chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-10-29-0745
Updates managed by image-automation-controller.
2026-10-29 08:05:30 +00:00
security-bot 46f56f1eb8 chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-10-29-0702
Updates managed by image-automation-controller.
2026-10-29 07:05:30 +00:00
security-bot e1558800a0 chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-10-29-0606
Updates managed by image-automation-controller.
2026-10-29 06:35:30 +00:00
security-bot 4be6ad740d chore(image-automation): bump twinproduction/gatus:v5.38.0
Updates managed by image-automation-controller.
2026-10-28 21:51:08 +00:00
security-bot b78b705c6c chore(image-automation): bump ghcr.io/usetrmnl/terminus:0.79.0
Updates managed by image-automation-controller.
2026-10-28 20:50:43 +00:00
security-bot 4cbcebf53f chore(image-automation): bump woodpeckerci/woodpecker-server:v3.20.0
Updates managed by image-automation-controller.
2026-10-28 19:54:06 +00:00
security-bot 256801e481 chore(image-automation): bump woodpeckerci/woodpecker-agent:v3.20.0
Updates managed by image-automation-controller.
2026-10-28 19:53:33 +00:00
security-bot f3513c0ebe chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-10-28-1832
Updates managed by image-automation-controller.
2026-10-28 18:35:28 +00:00
k.renner 41f114de58 Merge pull request 'fix(shelly-manager): allow the IoT VLAN ULA prefix in egress' (#1518) from shelly-manager-iot-ipv6-egress into main
Reviewed-on: #1518
2026-10-28 18:29:51 +00:00
claude-botandClaude Opus 5.5 65ea8521c9 fix(shelly-manager): allow the IoT VLAN ULA prefix in egress
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
shelly-manager reached v30iot over eth0 only via IPv4; devices
advertising fd02:30::/64 addresses were blocked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0185Qb5YG28FfnVi1ujMEV5C
2026-10-28 18:52:41 +01:00
security-bot 5071d34bd5 chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-10-28-1639
Updates managed by image-automation-controller.
2026-10-28 16:42:08 +00:00
k.renner ea8aca23e4 Merge pull request 'fix(shelly-manager): firmware endpoint name and bind to the pod addresses' (#1517) from shelly-manager-net1-no-ra into main
Reviewed-on: #1517
2026-10-28 15:54:05 +00:00
claude-botandClaude Opus 5.5 e6d71ba033 fix(shelly-manager): name the firmware endpoint firmware.shelly-manager.renner.uno
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
App endpoints are <function>.<app>.renner.uno (ftp.paperless.renner.uno,
syncthing.paperless.renner.uno).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 16:51:03 +01:00
claude-botandClaude Opus 5.5 1f835e31b6 fix(shelly-manager): bind the app to the pod addresses, not [::]
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was canceled
[::]:8082 also listened on net1. POD_IPS (status.podIPs) lets the app bind
8080/8082/9090 to the pod IPv4 and IPv6 only; ASPNETCORE_URLS falls back
to the pod IPv4 for images that do not read POD_IPS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 16:44:08 +01:00
k.renner 20fcdfe738 Merge pull request 'fix(shelly-manager): no IPv6 router advertisements on the IoT NIC' (#1516) from shelly-manager-net1-no-ra into main
Reviewed-on: #1516
2026-10-28 15:38:04 +00:00
claude-botandClaude Opus 5.5 a8c1710cab fix(shelly-manager): no IPv6 router advertisements on the IoT NIC
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Captured on k8s-1 (cilium-dbg monitor, endpoint 1708): an IPv6 SYN from
edge-1 (fd02:30::/64) to fd99::8a reached the pod on 8082, but no reply
left via eth0; the pod has on-link and default IPv6 routes on net1 from
RAs. sbr only confines IPv4. Turn RA processing off on net1 with the
tuning plugin (as matter-server already uses it) and roll the pod.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 16:27:32 +01:00
k.renner 4d556c1b4b Merge pull request 'fix(shelly-manager): reachable firmware LoadBalancer on IPv4 and IPv6' (#1515) from shelly-firmware-lb-reachable into main
Reviewed-on: #1515
2026-10-28 14:50:51 +00:00
claude-botandClaude Opus 5.5 371f18c85f fix(shelly-manager): reachable firmware LoadBalancer on IPv4 and IPv6
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
The auto-assigned IPv4 address 10.2.99.0 timed out from the LAN while
the pod answered on 8082 directly; pin 10.2.99.90/fd99::8a like the
other bgp-pool Services. Kestrel listened on 8082 only on the pod IPv4,
so the IPv6 endpoint had no listener; bind 8082 on [::].

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:25:36 +01:00
security-bot 0c56e0dc1a chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-10-28-1412
Updates managed by image-automation-controller.
2026-10-28 14:18:03 +00:00
k.renner 45befc8af8 Merge pull request 'feat(shelly-manager): serve firmware to the fleet and alert on rollout halts' (#1514) from shelly-manager-local-firmware into main
Reviewed-on: #1514
2026-10-28 14:17:18 +00:00
claude-botandClaude Opus 5.5 1a18755025 feat(shelly-manager): serve firmware to the fleet and alert on rollout halts
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
shelly-manager now checks and rolls out Shelly firmware itself
(konairius/shelly-manager#121). Devices download from it on :8082 via a
BGP LoadBalancer named shelly-firmware.renner.uno; it reads Shelly's
index from *.shelly.cloud over HTTPS; /metrics on :9090 is scraped and
alerts on halted rollouts, a stale index and invalid rollout config.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 15:11:19 +01:00
k.renner a2b6456d97 Merge pull request 'feat(music-assistant): Pocket Casts Up Next as a Top Pick' (#1513) from claude/ma-pocketcasts-up-next-top-pick into main
Reviewed-on: #1513
Reviewed-by: Review Bot <review-bot@renner.uno>
2026-10-28 12:06:04 +00:00
security-bot 6da80f7761 chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-10-28-1038
ci/woodpecker/cron/e2e Pipeline failed
ci/woodpecker/cron/security-scan Pipeline was successful
Updates managed by image-automation-controller.
2026-10-28 11:05:11 +00:00
claude-botandClaude Opus 5.5 9bb5490c33 feat(music-assistant): Pocket Casts Up Next as a Top Pick
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Pocket Casts now serves an Up Next recommendation row whose single item is
the Up Next playlist, titled with the episode at the head of the queue
(podcast as subtitle, podcast artwork). The frontend builds Top Picks for
You from the first items of the visible recommendation rows, so the head of
Up Next shows there; clicking or playing it loads the whole playlist. An
empty Up Next yields an empty row, which the frontend hides together with
the pick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 11:39:17 +01:00
security-bot c58631adcd chore(image-automation): bump gitea.renner.uno/gitea_admin/ci-backup-verify:2026-10-28
Updates managed by image-automation-controller.
2026-10-28 07:53:53 +00:00
security-bot 011301fdef chore(image-automation): bump gitea.renner.uno/gitea_admin/cloudflare-lb-sync:2026-10-28-0718
Updates managed by image-automation-controller.
2026-10-28 07:47:13 +00:00
k.renner 4f8443f3e8 Merge pull request 'feat(authentik): Flux image automation tracks the LDAP outpost image' (#1512) from claude/outpost-image-flux into main
Reviewed-on: #1512
2026-10-28 07:38:59 +00:00
claude-botandClaude Opus 5.5 478f39d727 feat(authentik): Flux image automation tracks the LDAP outpost image
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
The k8s-outposts blueprint pinned the patched LDAP outpost image by hand.
Flux setters cannot patch text inside the blueprint ConfigMap string, and
Gitea's cleanup rule deletes a pinned tag once a newer build ages it out.

The image now lives in the authentik Flux Kustomization's
postBuild.substitute, with the authentik-samba-bridge-ldap-outpost
ImagePolicy marker, and the blueprint reads ${SAMBA_BRIDGE_LDAP_OUTPOST_IMAGE}.
kustomize-controller renders the literal before Authentik reads the file.
`flux build kustomization authentik` differs from the unsubstituted render
only in that line; kubernetes/apps/authentik has no other ${...} literals.

The SAMBA_BRIDGE_LDAP_OUTPOST_IMAGE env var in global.env, used by the
earlier !Env attempt, is removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:37:17 +01:00
security-bot 86681c9d5b chore(image-automation): bump gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-28-0712
Updates managed by image-automation-controller.
2026-10-28 07:36:52 +00:00
security-bot 1f3bdb648a chore(image-automation): bump gitea.renner.uno/gitea_admin/secret-rotator:2026-10-28
Updates managed by image-automation-controller.
2026-10-28 07:23:51 +00:00
security-bot 7989113433 chore(image-automation): bump gitea.renner.uno/gitea_admin/kuma-sync:2026-10-28-0712
Updates managed by image-automation-controller.
2026-10-28 07:23:50 +00:00
security-bot 52e935af8b chore(image-automation): bump gitea.renner.uno/konairius/cni-plugins:2026-10-28-0711
Updates managed by image-automation-controller.
2026-10-28 07:23:12 +00:00
security-bot 8be540a8af chore(image-automation): bump gitea.renner.uno/konairius/alpine-jq:2026-10-28-0711 gitea.renner.uno/konairius/alpine-openssl:2026-10-28-0711
Updates managed by image-automation-controller.
2026-10-28 07:23:11 +00:00
k.renner e1092c4c24 Merge pull request 'docs(mosquitto): Shelly MQTT server now set via shelly-manager group settings' (#1508) from docs/mosquitto-shelly-manager-settings-comment into main
Reviewed-on: #1508
2026-10-28 07:18:41 +00:00
k.renner 30fb6dcccd Merge pull request 'fix(authentik): LDAP outpost image back to a literal in the blueprint' (#1511) from claude/outpost-image-literal into main
Reviewed-on: #1511
2026-10-28 07:18:18 +00:00
claude-botandClaude Opus 5.5 0708e7504f fix(authentik): LDAP outpost image back to a literal in the blueprint
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
#1510 set the k8s-outposts blueprint's container_image through
`!Env SAMBA_BRIDGE_LDAP_OUTPOST_IMAGE`. It did not resolve to the patched
image: at 07:07:39Z the outpost controller recreated
ak-outpost-ldap-outpost with Authentik's default
ghcr.io/goauthentik/ldap:2026.8.4, although every worker carries the env
var. The upstream outpost lacks the sambaSamAccount/sambaDomain object
classes DSM's smbd queries for.

container_image is a literal again, on the samba-bridge outpost build that
exists now (2026-10-28-0655).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 08:16:09 +01:00
security-bot 6060ebdfd3 chore(image-automation): bump gitea.renner.uno/gitea_admin/review-bot:2026-10-28-0709
Updates managed by image-automation-controller.
2026-10-28 07:12:27 +00:00
security-bot 19968c16fe chore(image-automation): bump gitea.renner.uno/konairius/hermes:2026-10-28-0704
Updates managed by image-automation-controller.
2026-10-28 07:07:12 +00:00
security-bot 5015d41f93 chore(image-automation): bump gitea.renner.uno/konairius/wiki:2026-10-28-0700
Updates managed by image-automation-controller.
2026-10-28 07:05:13 +00:00
security-bot bdf4defe29 chore(image-automation): bump gitea.renner.uno/konairius/shelly-manager:2026-10-28-0657
Updates managed by image-automation-controller.
2026-10-28 07:05:11 +00:00
security-bot fa9c7ec965 chore(image-automation): bump gitea.renner.uno/konairius/alpine-jq:2026-10-27 2026-10-28-0655 gitea.renner.uno/konairius/authentik-samba-bridge-ldap-outpost:2026-10-28-0655 gitea.renner.uno/konairius/mqtt-account-controller:2026-10-28-0657 gitea.renner.uno/konairius/ical-proxy:2026-10-28-0651 gitea.renner.uno/konairius/alpine-openssl:2026-10-27
Updates managed by image-automation-controller.
2026-10-28 07:04:44 +00:00
security-bot 817e61c66c chore(image-automation): bump gitea.renner.uno/konairius/cni-plugins:2026-10-27
Updates managed by image-automation-controller.
2026-10-28 07:04:43 +00:00
security-bot 8386bde784 chore(image-automation): bump gitea.renner.uno/konairius/vsftpd:2026-10-28-0649
Updates managed by image-automation-controller.
2026-10-28 07:04:39 +00:00
k.renner 3248c01d33 Merge pull request 'feat(images): one date tag per in-house konairius build, rolled out by Flux' (#1510) from claude/date-image-tag into main
ci/woodpecker/push/hermes-image Pipeline was successful
ci/woodpecker/push/build-ci-images Pipeline was successful
Reviewed-on: #1510
2026-10-28 07:03:57 +00:00
claude-botandClaude Opus 5.5 20be3ada61 feat(images): one date tag per in-house konairius build, rolled out by Flux
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
Gitea's konairius package cleanup rule keeps the most recent version and
counts every tag as a version. Builds that push several tags (build.<ts>,
sha, main, latest) keep only the last-pushed one, so the tag a workload
runs gets deleted: vsftpd kept only `main` and ImagePolicy vsftpd fails
with "version list argument cannot be empty" (#1509); ical-proxy's pinned
main-926ead0 is gone too.

Every konairius image is now pushed with exactly one tag, YYYY-MM-DD-HHMM
(UTC), the scheme review-bot and the gitea_admin images use, and every
consumer follows it through an ImagePolicy instead of a hand-written pin:

- hermes-image.yml: date tag only, no :latest.
- build-ci-images.yml: alpine-jq, cni-plugins, alpine-openssl tag
  YYYY-MM-DD-HHMM; the konairius/ and ghcr copies drop :latest.
  gitea_admin/ copies are unchanged.
- ImagePolicies for hermes, wiki, shelly-manager, vsftpd switch to the
  date filter (alphabetical, accepts YYYY-MM-DD for the older utility
  builds). New policies and markers: ical-proxy, mqtt-account-controller,
  authentik-samba-bridge-server (HelmRelease tag), the samba-bridge LDAP
  outpost, alpine-jq, alpine-openssl, cni-plugins.
- The outpost image moves out of the blueprint string, which Flux setters
  cannot patch, into global.env SAMBA_BRIDGE_LDAP_OUTPOST_IMAGE, read by
  the blueprint via !Env (same pattern as LDAP_OUTPOST_TLS_* and
  AUTHENTIK_GATE_SYNC_TOKEN).
- renovate.json: the new Flux-tracked images join the disabled list.
- Hermes homelab-gitea-pr guide: date tag, one tag, no hand pins.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 07:37:21 +01:00
claude-botandClaude Opus 5.5 8d9a6955f9 docs(mosquitto): point Shelly MQTT server note at shelly-manager root group settings
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
shelly-manager's /api/v1/mqtt/push is being removed in favour of
group settings (konairius/shelly-manager group-settings M3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 02:12:33 +00:00
k.renner 2eb943cbf4 Merge pull request 'fix(review-bot): mint the review token with read:issue' (#1507) from review-bot-token-read-issue into main
Reviewed-on: #1507
2026-10-28 00:27:23 +00:00
66 changed files with 799 additions and 190 deletions
+9 -12
View File
@@ -243,7 +243,8 @@ steps:
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- GH_AUTH=$(printf '%s:%s' "$GHCR_USER" "$GHCR_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"},"ghcr.io":{"auth":"%s"}}}' "$AUTH" "$GH_AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
# konairius/ gets one tag per build: Gitea's cleanup rule counts tags.
- TAG=$(date +%Y-%m-%d-%H%M)
- |
/kaniko/executor \
--context . \
@@ -251,9 +252,7 @@ steps:
--destination gitea.renner.uno/gitea_admin/alpine-jq:$TAG \
--destination gitea.renner.uno/gitea_admin/alpine-jq:latest \
--destination gitea.renner.uno/konairius/alpine-jq:$TAG \
--destination gitea.renner.uno/konairius/alpine-jq:latest \
--destination ghcr.io/konairius/alpine-jq:$TAG \
--destination ghcr.io/konairius/alpine-jq:latest
--destination ghcr.io/konairius/alpine-jq:$TAG
# Runtime image, not CI tooling: upstream containernetworking/plugins binaries
# baked in, consumed by the multus cni-plugins-installer DaemonSet. Replaces a
@@ -295,7 +294,8 @@ steps:
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- GH_AUTH=$(printf '%s:%s' "$GHCR_USER" "$GHCR_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"},"ghcr.io":{"auth":"%s"}}}' "$AUTH" "$GH_AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
# konairius/ gets one tag per build: Gitea's cleanup rule counts tags.
- TAG=$(date +%Y-%m-%d-%H%M)
- |
/kaniko/executor \
--context . \
@@ -303,9 +303,7 @@ steps:
--destination gitea.renner.uno/gitea_admin/cni-plugins:$TAG \
--destination gitea.renner.uno/gitea_admin/cni-plugins:latest \
--destination gitea.renner.uno/konairius/cni-plugins:$TAG \
--destination gitea.renner.uno/konairius/cni-plugins:latest \
--destination ghcr.io/konairius/cni-plugins:$TAG \
--destination ghcr.io/konairius/cni-plugins:latest
--destination ghcr.io/konairius/cni-plugins:$TAG
# Runtime image, not CI tooling: alpine + the openssl CLI, for shell scripts
# needing s_client/x509. Named for what it is; first consumer is the paperless
@@ -336,7 +334,8 @@ steps:
- AUTH=$(printf '%s:%s' "$GITEA_USER" "$GITEA_TOKEN" | base64 | tr -d '\n')
- GH_AUTH=$(printf '%s:%s' "$GHCR_USER" "$GHCR_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"},"ghcr.io":{"auth":"%s"}}}' "$AUTH" "$GH_AUTH" > /kaniko/.docker/config.json
- TAG=$(date +%Y-%m-%d)
# konairius/ gets one tag per build: Gitea's cleanup rule counts tags.
- TAG=$(date +%Y-%m-%d-%H%M)
- |
/kaniko/executor \
--context . \
@@ -344,9 +343,7 @@ steps:
--destination gitea.renner.uno/gitea_admin/alpine-openssl:$TAG \
--destination gitea.renner.uno/gitea_admin/alpine-openssl:latest \
--destination gitea.renner.uno/konairius/alpine-openssl:$TAG \
--destination gitea.renner.uno/konairius/alpine-openssl:latest \
--destination ghcr.io/konairius/alpine-openssl:$TAG \
--destination ghcr.io/konairius/alpine-openssl:latest
--destination ghcr.io/konairius/alpine-openssl:$TAG
# Heating CAN readers: open3e (Vitocal/VX3) + E3onCAN (E380 meter), one image
# run as two services on edge-1. Replaces pip-into-venv on the host, where an
+3 -4
View File
@@ -1,5 +1,5 @@
# Builds ci/images/hermes (nousresearch/hermes-agent plus the agent's CLIs) as
# gitea.renner.uno/konairius/hermes:build.<unix-ts> and :latest.
# gitea.renner.uno/konairius/hermes:YYYY-MM-DD-HHMM, one tag per build.
when:
- event: push
branch: main
@@ -20,10 +20,9 @@ steps:
- mkdir -p /kaniko/.docker
- AUTH=$(printf '%s:%s' registry-bot "$GITEA_TOKEN" | base64 | tr -d '\n')
- printf '{"auths":{"gitea.renner.uno":{"auth":"%s"}}}' "$AUTH" > /kaniko/.docker/config.json
- TAG="build.$(date +%s)"
- TAG=$(date +%Y-%m-%d-%H%M)
- |
/kaniko/executor \
--context . \
--dockerfile Containerfile \
--destination gitea.renner.uno/konairius/hermes:$TAG \
--destination gitea.renner.uno/konairius/hermes:latest
--destination gitea.renner.uno/konairius/hermes:$TAG
@@ -0,0 +1,8 @@
---
# DSM's own python3 is 3.8; plays/configure-synology-python.yml installs this.
ansible_python_interpreter: /var/packages/Python3.9/target/usr/bin/python3.9
# DSM offers no sftp/scp.
ansible_ssh_transfer_method: piped
# claude-bot is a DSM administrator here; its password is unknown.
ssh_ca_claude_sudo_all: true
+7 -2
View File
@@ -8,8 +8,6 @@
# pve_cluster (jupiter/neptune/saturn) run Talos on bare metal; no SSH at all
# authentik_db / authentik_app authentik runs in-cluster (CNPG)
# grafana runs in-cluster
# synology_nas (nas-1) powered off; Garage and the PBS client
# it hosted are gone with it
# Their hosts no longer resolve, except the three that resolve to Talos nodes.
[netbird_server]
@@ -25,6 +23,11 @@ proxy-2.renner.uno ansible_host=proxy-2.nb.renner.uno ansible_user=root
[edge_host]
edge-1.iot.renner.uno ansible_user=root
# Synology NAS (DSM): nas-1 RS1221+ on site, nas-2 DS1522+ off site.
[synology_nas]
nas-1.renner.uno ansible_user=root
nas-2.renner.uno ansible_user=root
# Capability groups. Membership states which hardware a host physically has,
# and each service playbook targets the group for its own concern. No flags and
# no `when:` on the roles -- moving a stick to another box is a one-line change
@@ -70,6 +73,8 @@ proxy-1.renner.uno
proxy-2.renner.uno
netbird.renner.uno
edge-1.iot.renner.uno
nas-1.renner.uno
nas-2.renner.uno
# Hosts whose admin logins accept only ssh_admin_authorized_keys (break-glass);
# everything else needs a CA certificate. Members must be in [ssh_ca].
@@ -0,0 +1,19 @@
---
# Ansible needs Python >= 3.9 on the target; DSM ships 3.8. Synology's
# Python3.9 package installs beside it. raw, because the modules cannot run
# before it is there; raw is skipped in check mode.
- name: Install Python 3.9 on the Synology NAS
hosts: synology_nas
become: true
gather_facts: false
tasks:
- name: Install the Python3.9 package
ansible.builtin.raw: >-
if [ -x {{ ansible_python_interpreter }} ]; then echo present;
else /usr/syno/bin/synopkg install_from_server Python3.9; fi
register: synology_python_install
changed_when: "'present' not in synology_python_install.stdout"
- name: Check the interpreter exists
ansible.builtin.raw: test -x {{ ansible_python_interpreter }}
changed_when: false
+5 -6
View File
@@ -8,12 +8,11 @@
lineinfile:
path: /etc/hosts
regexp: '^127\.0\.1\.1\s+{{ inventory_hostname }}'
line: "{{ ansible_default_ipv4.address }} {{ inventory_hostname }} {{ inventory_hostname_short | default(inventory_hostname.split('.')[0]) }}"
line: "{{ ansible_facts['default_ipv4']['address'] }} {{ inventory_hostname }} {{ inventory_hostname_short | default(inventory_hostname.split('.')[0]) }}"
state: present
when:
- ansible_virtualization_type is defined
- ansible_virtualization_type == 'lxc'
- ansible_default_ipv4.address is defined
- ansible_facts['virtualization_type'] == 'lxc'
- ansible_facts['default_ipv4']['address'] is defined
# -------------------------------------------------------
# 1. Install Common Packages
@@ -64,8 +63,8 @@
enabled: no
masked: yes
when:
- ansible_virtualization_type is defined
- ansible_virtualization_type in ['kvm', 'lxc']
- ansible_facts['virtualization_role'] == 'guest'
- ansible_facts['virtualization_type'] in ['kvm', 'lxc']
ignore_errors: yes
# -------------------------------------------------------
@@ -15,6 +15,8 @@
# amd64 asset for exactly this tag.
crowdsec_bouncer_version: v0.0.36
crowdsec_bouncer_sha256: f86e4b72693549d99f40a9402abefb894108f047a3fbe6e72fada25ee17ce88b
# Controller cache for the release archive; it outlives /tmp cleanups.
crowdsec_bouncer_download_dir: "{{ lookup('ansible.builtin.env', 'HOME') }}/.cache/ansible/crowdsec"
# The LAPI over the public path these hosts already serve, see
# kubernetes/infrastructure/crowdsec/lapi-ingress.yaml. The proxies cannot
@@ -32,13 +32,23 @@
# The controller has v4, so it does the fetching. The checksum is verified
# there, before anything is copied to a host. Also in check mode: the fetch
# only writes on the controller, and the copy below needs the file.
- name: Create the controller download directory
ansible.builtin.file:
path: "{{ crowdsec_bouncer_download_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
become: false
run_once: true
check_mode: false
- name: Fetch the pinned bouncer release on the controller
check_mode: false
ansible.builtin.get_url:
url: >-
https://github.com/crowdsecurity/cs-firewall-bouncer/releases/download/{{
crowdsec_bouncer_version }}/crowdsec-firewall-bouncer-linux-amd64.tgz
dest: /tmp/crowdsec-firewall-bouncer-{{ crowdsec_bouncer_version }}.tgz
dest: "{{ crowdsec_bouncer_download_dir }}/crowdsec-firewall-bouncer-{{ crowdsec_bouncer_version }}.tgz"
checksum: sha256:{{ crowdsec_bouncer_sha256 }}
mode: "0644"
delegate_to: localhost
@@ -53,7 +63,7 @@
# host.
- name: Copy the bouncer archive to the host
ansible.builtin.copy:
src: /tmp/crowdsec-firewall-bouncer-{{ crowdsec_bouncer_version }}.tgz
src: "{{ crowdsec_bouncer_download_dir }}/crowdsec-firewall-bouncer-{{ crowdsec_bouncer_version }}.tgz"
dest: /opt/crowdsec-firewall-bouncer-{{ crowdsec_bouncer_version }}.tgz
mode: "0644"
@@ -2,7 +2,7 @@
# Managed by Ansible - Do not edit manually
global_defs {
router_id {{ ansible_hostname }}_ntp
router_id {{ ansible_facts['hostname'] }}_ntp
enable_script_security
script_user root
}
+15 -4
View File
@@ -11,6 +11,8 @@
- curl
- ca-certificates
- gnupg
# deb822_repository
- python3-debian
state: present
update_cache: true
@@ -21,10 +23,19 @@
creates: /usr/share/keyrings/netbird-archive-keyring.gpg
- name: Add NetBird repository
ansible.builtin.apt_repository:
repo: "deb [signed-by=/usr/share/keyrings/netbird-archive-keyring.gpg] https://pkgs.netbird.io/debian stable main"
state: present
filename: netbird
ansible.builtin.deb822_repository:
name: netbird
types: deb
uris: https://pkgs.netbird.io/debian
suites: stable
components: main
signed_by: /usr/share/keyrings/netbird-archive-keyring.gpg
# A one-line netbird.list beside netbird.sources lists the repository twice.
- name: Remove the one-line NetBird repository
ansible.builtin.file:
path: /etc/apt/sources.list.d/netbird.list
state: absent
- name: Install NetBird client
ansible.builtin.apt:
@@ -6,15 +6,15 @@
apt:
name: qemu-guest-agent
state: present
when:
- ansible_os_family is defined and ansible_os_family == 'Debian'
when:
- ansible_facts['os_family'] == 'Debian'
- name: Install QEMU Guest Agent (RHEL/Rocky VMs)
dnf:
name: qemu-guest-agent
state: present
when:
- ansible_os_family is defined and ansible_os_family == 'RedHat'
when:
- ansible_facts['os_family'] == 'RedHat'
- name: Ensure qemu-guest-agent is started and enabled
service:
+3
View File
@@ -16,3 +16,6 @@ ssh_ca_claude_principal: claude-bot
# Units claude-bot may restart/reload via sudo on this host, e.g. [netbird].
ssh_ca_claude_sudo_units: []
# Full passwordless sudo for claude-bot on this host.
ssh_ca_claude_sudo_all: false
+4 -12
View File
@@ -1,23 +1,15 @@
---
# Replaces the admin logins' authorized_keys with ssh_admin_authorized_keys
# (break-glass + CI). Everything else logs in with a CA certificate.
- name: Look up the admin logins
ansible.builtin.getent:
database: passwd
key: "{{ item }}"
fail_key: false
loop: "{{ ssh_ca_owner_users }}"
register: ssh_ca_admin_lookup
# ssh_ca_admin_logins comes from main.yml, which runs first in the same play.
- name: Set the admin logins' authorized_keys to the managed list
ansible.posix.authorized_key:
user: "{{ item.item }}"
user: "{{ item[0] }}"
key: |
{% for k in ssh_admin_authorized_keys %}
{{ (k.options ~ ' ') if (k.options | default('')) else '' }}{{ k.key }}
{% endfor %}
exclusive: true
loop: "{{ ssh_ca_admin_lookup.results }}"
loop: "{{ ssh_ca_admin_logins }}"
loop_control:
label: "{{ item.item }}"
when: (item.ansible_facts.getent_passwd | default({})).get(item.item) is not none
label: "{{ item[0] }}"
+123 -14
View File
@@ -23,6 +23,7 @@
owner: root
group: root
mode: "0644"
register: ssh_ca_trusted_file
# sshd refuses all public key logins when RevokedKeys is set but unreadable,
# so the file is always deployed, even when empty.
@@ -34,6 +35,26 @@
group: root
mode: "0644"
# /etc/passwd, not getent: DSM has no getent binary. The admin logins are
# local accounts. Each entry is the split passwd line (0 name, 5 home).
- name: Read the local accounts
ansible.builtin.slurp:
src: /etc/passwd
register: ssh_ca_passwd_file
- name: Find the existing admin logins
vars:
ssh_ca_passwd: "{{ ssh_ca_passwd_file.content | b64decode | trim | split('\n') | map('split', ':') | list }}"
ansible.builtin.set_fact:
ssh_ca_admin_logins: "{{ ssh_ca_passwd | selectattr('0', 'in', ssh_ca_owner_users) | list }}"
ssh_ca_claude_exists: "{{ ssh_ca_claude_user in ssh_ca_passwd | map('first') }}"
# DSM manages accounts with synouser/synogroup and has no useradd.
- name: Detect DSM
ansible.builtin.stat:
path: /usr/syno/sbin/synouser
register: ssh_ca_synouser
# systemd-journal: read every journal without sudo (see sudoers-claude.j2).
- name: Create the Claude account
ansible.builtin.user:
@@ -43,14 +64,57 @@
password: "!"
groups: systemd-journal
append: true
when: not ssh_ca_synouser.stat.exists
- name: Look up the owner admin logins
ansible.builtin.getent:
database: passwd
key: "{{ item }}"
fail_key: false
loop: "{{ ssh_ca_owner_users }}"
register: ssh_ca_owner_lookup
- name: Create the Claude account on DSM
when: ssh_ca_synouser.stat.exists
block:
# synouser requires a password; nobody knows it and sshd refuses passwords.
- name: Create the DSM account
ansible.builtin.command:
argv:
- /usr/syno/sbin/synouser
- --add
- "{{ ssh_ca_claude_user }}"
- "{{ lookup('ansible.builtin.password', '/dev/null', length=40, chars=['ascii_letters', 'digits']) }}"
- Claude Bot
- "0"
- ""
- "0"
# command runs in check mode here.
when:
- not ssh_ca_claude_exists
- not ansible_check_mode
no_log: true
# Members are listed as "<n>:[<name>]".
- name: Read the administrators members
ansible.builtin.command: /usr/syno/sbin/synogroup --get administrators
register: ssh_ca_dsm_admins_out
changed_when: false
check_mode: false
- name: Find the administrators members
ansible.builtin.set_fact:
ssh_ca_dsm_admins: >-
{{ ssh_ca_dsm_admins_out.stdout_lines | select('match', '^[0-9]+:\[.*\]$')
| map('regex_replace', '^[0-9]+:\[(.*)\]$', '\1') | list }}
# synogroup --member replaces the member list; a parse that lost the owner
# admin login would drop it.
- name: Check the member list holds the owner admin login
ansible.builtin.assert:
that:
- ssh_ca_dsm_admins | intersect(ssh_ca_owner_users) | length > 0
fail_msg: "administrators members lack {{ ssh_ca_owner_users }}: {{ ssh_ca_dsm_admins }}"
# DSM allows SSH for administrators only.
- name: Add the Claude account to administrators
ansible.builtin.command:
argv: "{{ ['/usr/syno/sbin/synogroup', '--member', 'administrators'] + ssh_ca_dsm_admins + [ssh_ca_claude_user] }}"
when:
- ssh_ca_claude_user not in ssh_ca_dsm_admins
- not ansible_check_mode
- name: Create the principals directory
ansible.builtin.file:
@@ -63,14 +127,13 @@
- name: Accept the owner principal for the existing admin logins
ansible.builtin.copy:
content: "{{ ssh_ca_owner_principal }}\n"
dest: "/etc/ssh/auth_principals/{{ item.item }}"
dest: "/etc/ssh/auth_principals/{{ item[0] }}"
owner: root
group: root
mode: "0644"
loop: "{{ ssh_ca_owner_lookup.results }}"
loop: "{{ ssh_ca_admin_logins }}"
loop_control:
label: "{{ item.item }}"
when: (item.ansible_facts.getent_passwd | default({})).get(item.item) is not none
label: "{{ item[0] }}"
- name: Accept the Claude principal for the Claude account
ansible.builtin.copy:
@@ -88,6 +151,48 @@
group: root
mode: "0440"
validate: visudo -cf %s
when: not ssh_ca_synouser.stat.exists
# DSM has no visudo, so the rules are checked on the controller.
- name: Install the Claude sudo rules on DSM
when: ssh_ca_synouser.stat.exists
vars:
ssh_ca_sudoers: "{{ lookup('ansible.builtin.template', 'sudoers-claude.j2') | trim }}"
block:
- name: Check the sudo rules on the controller
ansible.builtin.command:
cmd: visudo -c -f -
stdin: "{{ ssh_ca_sudoers }}"
delegate_to: localhost
become: false
changed_when: false
check_mode: false
- name: Install the checked sudo rules
ansible.builtin.copy:
content: "{{ ssh_ca_sudoers }}\n"
dest: "/etc/sudoers.d/{{ ssh_ca_claude_user }}"
owner: root
group: root
mode: "0440"
- name: Create the sshd drop-in directory
ansible.builtin.file:
path: /etc/ssh/sshd_config.d
state: directory
owner: root
group: root
mode: "0755"
# DSM's sshd_config has no Include. sshd keeps the first value it reads, so
# the Include goes first for the drop-ins to win.
- name: Include the sshd drop-in directory
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
line: Include /etc/ssh/sshd_config.d/*.conf
insertbefore: BOF
validate: sshd -t -f %s
register: ssh_ca_include_line
# A broken drop-in only bites on the next sshd restart, so it is tested here
# and removed again if the full config does not parse.
@@ -100,6 +205,7 @@
owner: root
group: root
mode: "0644"
register: ssh_ca_dropin_file
- name: Test the full sshd config
ansible.builtin.command: sshd -t
@@ -136,10 +242,11 @@
fail_msg: "sshd -T does not show the SSH CA settings; another config line takes precedence"
# The unit is sshd.service on Arch and RedHat, ssh.service on Debian/Ubuntu,
# where sshd.service is an alias that resolves to it. sshd -T only parses the
# files, so the running daemon is reloaded on every run; a reload keeps open
# where sshd.service is an alias that resolves to it. The running daemon is
# reloaded when a file it reads at start changed; a reload keeps open
# sessions. None runs when sshd is socket activated and stopped, or per
# connection (ssh-mesh-only role); the next start reads the config.
# Principals and revoked keys are read per login and need no reload.
# systemctl directly: service_facts took 3-12 s per host.
- name: Find the running sshd unit
ansible.builtin.command: systemctl list-units --state=running --plain --no-legend --type=service sshd.service ssh.service
@@ -161,7 +268,9 @@
ansible.builtin.systemd:
name: "{{ ssh_ca_sshd_units | first }}"
state: reloaded
when: ssh_ca_sshd_units | length == 1
when:
- ssh_ca_sshd_units | length == 1
- ssh_ca_trusted_file is changed or ssh_ca_include_line is changed or ssh_ca_dropin_file is changed
- name: Report the admin logins' authorized keys
ansible.builtin.include_tasks: report.yml
+3 -4
View File
@@ -3,11 +3,10 @@
# [ssh_keys_managed] have this reduced to ssh_admin_authorized_keys.
- name: Find the admin logins' authorized_keys
ansible.builtin.stat:
path: "{{ item.ansible_facts.getent_passwd[item.item][4] }}/.ssh/authorized_keys"
loop: "{{ ssh_ca_owner_lookup.results }}"
path: "{{ item[5] }}/.ssh/authorized_keys"
loop: "{{ ssh_ca_admin_logins }}"
loop_control:
label: "{{ item.item }}"
when: (item.ansible_facts.getent_passwd | default({})).get(item.item) is not none
label: "{{ item[0] }}"
register: ssh_ca_keyfiles
- name: Fingerprint the admin logins' authorized keys
@@ -10,3 +10,6 @@
{% for unit in ssh_ca_claude_sudo_units %}
{{ ssh_ca_claude_user }} ALL=(root) NOPASSWD:NOEXEC: /usr/bin/systemctl restart {{ unit }}, /usr/bin/systemctl reload {{ unit }}
{% endfor %}
{% if ssh_ca_claude_sudo_all %}
{{ ssh_ca_claude_user }} ALL=(ALL) NOPASSWD: ALL
{% endif %}
+2
View File
@@ -8,6 +8,8 @@
# One-off actions are not state and stay separate: plays/update-os.yml,
# plays/update-dist-upgrade.yml, plays/rotate-netbird-secrets.yml.
- name: Synology Python
ansible.builtin.import_playbook: plays/configure-synology-python.yml
- name: Base system
ansible.builtin.import_playbook: plays/common.yml
- name: VM time sync
@@ -705,8 +705,9 @@ data:
# AND (&(sambaDomainName=...)(objectClass=sambaDomain))
# return entries instead of short-circuiting. Built from
# the samba-bridge repo at
# gitea.renner.uno/konairius/authentik-samba-bridge .
container_image: gitea.renner.uno/konairius/authentik-samba-bridge-ldap-outpost:2026.8.4-3c8f413
# gitea.renner.uno/konairius/authentik-samba-bridge ; Flux
# substitutes the image from clusters/homelab/authentik.yaml.
container_image: ${SAMBA_BRIDGE_LDAP_OUTPOST_IMAGE}
object_naming_template: "ak-outpost-%(name)s"
kubernetes_namespace: authentik
kubernetes_replicas: 1
+1 -1
View File
@@ -105,7 +105,7 @@ spec:
type: RuntimeDefault
containers:
- name: controller
image: gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-27-0801 # {"$imagepolicy": "flux-system:authentik-gate-sync"}
image: gitea.renner.uno/gitea_admin/authentik-gate-sync:2026-10-28-0712 # {"$imagepolicy": "flux-system:authentik-gate-sync"}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
+1 -15
View File
@@ -50,15 +50,7 @@ spec:
# https://gitea.renner.uno/konairius/authentik-samba-bridge .
image:
repository: gitea.renner.uno/konairius/authentik-samba-bridge-server
# Pinned to a specific build for two reasons:
# 1. K8s does not pull on every reschedule when tag=latest, so
# a floating tag means we don't actually pick up rebuilds
# without a manual rollout-restart (which is forbidden per
# "no pod invasion" rule).
# 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/konairius/authentik-samba-bridge .
tag: "2026.8.4-3c8f413"
tag: "2026-10-28-0655" # {"$imagepolicy": "flux-system:authentik-samba-bridge-server:tag"}
envFrom:
# SAMBA_BRIDGE_DOMAIN_SID / _ENABLED_GROUP / _UID_OFFSET
# consumed by the bridge module at Django app startup. Missing
@@ -114,7 +106,6 @@ spec:
# 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
error_reporting:
@@ -130,7 +121,6 @@ spec:
password: ""
use_tls: true
from: "olymp@renner.uno"
server:
resources:
requests:
@@ -173,7 +163,6 @@ spec:
- secretName: authentik-tls
hosts:
- auth.renner.uno
worker:
resources:
requests:
@@ -190,13 +179,10 @@ spec:
minReplicas: 2
maxReplicas: 4
targetCPUUtilizationPercentage: 70
postgresql:
enabled: false
redis:
enabled: false
# Mount the homelab blueprints (K8s outpost integration, SSO and
# paperless flows) — every key of the ConfigMap is applied by
# the worker on boot and on each apply cycle. See
@@ -30,7 +30,7 @@ spec:
type: RuntimeDefault
containers:
- name: controller
image: gitea.renner.uno/gitea_admin/cloudflare-lb-sync:2026-10-27-0807 # {"$imagepolicy": "flux-system:cloudflare-lb-sync"}
image: gitea.renner.uno/gitea_admin/cloudflare-lb-sync:2026-10-28-0718 # {"$imagepolicy": "flux-system:cloudflare-lb-sync"}
env:
- name: CLOUDFLARE_API_TOKEN
valueFrom:
+1 -1
View File
@@ -47,7 +47,7 @@ spec:
type: RuntimeDefault
containers:
- name: gatus
image: twinproduction/gatus:v5.37.0 # {"$imagepolicy": "flux-system:gatus"}
image: twinproduction/gatus:v5.38.0 # {"$imagepolicy": "flux-system:gatus"}
securityContext:
allowPrivilegeEscalation: false
capabilities:
@@ -32,7 +32,7 @@ spec:
type: RuntimeDefault
containers:
- name: sync
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
command:
- /opt/hermes/.venv/bin/python
- /opt/hermes-skills/gitea-issues/scripts/sync_webhook.py
+10 -10
View File
@@ -51,7 +51,7 @@ spec:
# connects from the pod network. No env equivalent exists, so it is
# set in config.yaml on every start (idempotent).
- name: config
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
command:
- /opt/hermes/.venv/bin/hermes
- config
@@ -75,7 +75,7 @@ spec:
mountPath: /opt/data
# WebSocket keepalive ping interval in seconds (default 20).
- name: config-ws-ping
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
command:
- /opt/hermes/.venv/bin/hermes
- config
@@ -99,7 +99,7 @@ spec:
mountPath: /opt/data
# Skills from git (ConfigMap hermes-skills) beside the PVC's skills/.
- name: skills-config
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
command:
- /opt/hermes/.venv/bin/hermes
- config
@@ -130,7 +130,7 @@ spec:
# - alert-monitor: hourly; the script wakes the agent only for
# untriaged alerts.
- name: cron-jobs
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
# Non-fatal: a broken jobs.json or a changed cron CLI after an image
# bump logs a warning instead of keeping Hermes from starting.
command:
@@ -187,7 +187,7 @@ spec:
readOnly: true
# Home Assistant MCP server; token resolved from HA_TOKEN at connect, writes need approval.
- name: config-mcp-homeassistant
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
command:
- /opt/hermes/.venv/bin/hermes
- config
@@ -214,7 +214,7 @@ spec:
# skills, the Home Assistant MCP and the tea login are re-applied on
# every start. default gets the kanban toolset to hand cards to them.
- name: experts
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
# Non-fatal like cron-jobs: a broken expert logs a warning.
command:
- sh
@@ -305,7 +305,7 @@ spec:
# Gitea issues -> kanban (skills/gitea-issues): route script, webhook route on default,
# completion shell hook plus its allowlist entry on every profile.
- name: gitea-issues
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
# Non-fatal like cron-jobs: a failure logs a warning.
command:
- sh
@@ -368,7 +368,7 @@ spec:
readOnly: true
# Multiplexed default reads platform credentials only from .env (rationale: commit message).
- name: default-env
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
# Fatal on a writer failure: without these Matrix and the API server stay down.
command:
- /opt/hermes/.venv/bin/python
@@ -428,7 +428,7 @@ spec:
# tea login hermes-bot, written from the mounted token into an
# emptyDir at the agent's ~/.config/tea.
- name: tea-login
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
command:
- sh
- -ec
@@ -457,7 +457,7 @@ spec:
readOnly: true
containers:
- name: hermes
image: gitea.renner.uno/konairius/hermes:build.1793088058 # {"$imagepolicy": "flux-system:hermes"}
image: gitea.renner.uno/konairius/hermes:2026-10-28-0704 # {"$imagepolicy": "flux-system:hermes"}
args: ["gateway", "run"]
env:
- name: HERMES_DASHBOARD
@@ -84,7 +84,7 @@ missing permission and the one step that grants it.
9. **After the owner says "merged", verify the effect, not the merge.** Check
`GET /repos/<o>/<r>/commits/<merge_sha>/statuses` for the expected
`ci/woodpecker/push/<pipeline>` context (only cron contexts = the push pipeline never
triggered), the registry tag list for a new `build.<ts>`, `flux get images policy <name>`,
triggered), the registry tag list for a new `YYYY-MM-DD-HHMM` tag, `flux get images policy <name>`,
and the Deployment image. Report each as observed; say plainly what you couldn't see
(e.g. Woodpecker UI) instead of guessing the cause.
- With several PRs in flight, query the state of every one; "merged" may mean only one
@@ -7,8 +7,8 @@ Use when a homelab change adds or modifies an image built from `ci/images/<name>
| Concern | Owner | How |
|---|---|---|
| Versions of base images and bundled tools | **Renovate** (`renovate.json`, `dockerfile` manager) | Express every version as an image reference: `FROM <img>:<tag>@<digest>` and `COPY --from=<img>:<tag>@<digest> <path> /usr/local/bin/<tool>`. Renovate opens the bump PR; merging it is the trigger. |
| Building the image | **Woodpecker**, on push to the default branch of `ci/images/<name>/**` (+ `event: manual`) | kaniko, tag `build.$(date +%s)` + `latest`. |
| Rolling a built tag out | **Flux image automation** | `ImageRepository` + `ImagePolicy` (`filterTags.pattern '^build\.(?P<ts>\d+)$'`, `extract '$ts'`, `numerical asc`) and the `# {"$imagepolicy": "flux-system:<name>"}` marker on every image line. |
| Building the image | **Woodpecker**, on push to the default branch of `ci/images/<name>/**` (+ `event: manual`) | kaniko, exactly one tag `$(date +%Y-%m-%d-%H%M)`. No `latest`, `main` or sha tags: Gitea's konairius cleanup rule counts each tag as a version and keeps only the newest. |
| Rolling a built tag out | **Flux image automation** | `ImageRepository` + `ImagePolicy` (`filterTags.pattern '^\d{4}-\d{2}-\d{2}(-\d{4})?$'`, `alphabetical asc`) and the `# {"$imagepolicy": "flux-system:<name>"}` marker on every image line. No hand-written pins. |
Never add cron polling / "decide" steps that compare upstream releases — that duplicates
Renovate. Never curl binaries with hand-maintained sha256 lines when the tool ships an
@@ -47,4 +47,4 @@ image: `COPY --from` a digest-pinned image is tracked and needs no checksums.
- Check what the base image's last `USER` is before adding one (`grep -n '^USER'` in the
upstream Dockerfile).
- Rollout order is two PRs: image + pipeline first; the manifest switch only with a real
`build.<ts>` (see SKILL.md, Multi-stage changes).
date tag (see SKILL.md, Multi-stage changes).
@@ -44,6 +44,9 @@ spec:
# reachable over net1. The mDNS resolver is a separate
# ZeroconfResolver socket set and is unaffected.
k8s.v1.cni.cncf.io/networks: shelly-manager-iot
# Changing this rolls the pod, which is the only way a change to the
# shelly-manager-iot attachment takes effect.
net.renner.uno/iot-attachment-config: "tuning-no-ra"
spec:
# eth0 (Cilium-managed pod network) is still the primary — it
# carries the ingress, mosquitto LB return path, etc. The macvlan
@@ -61,11 +64,17 @@ spec:
type: RuntimeDefault
containers:
- name: shelly-manager
image: gitea.renner.uno/konairius/shelly-manager:build.1793145992 # {"$imagepolicy": "flux-system:shelly-manager"}
image: gitea.renner.uno/konairius/shelly-manager:2026-10-29-0807 # {"$imagepolicy": "flux-system:shelly-manager"}
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
# Firmware images only (/fw/...), for the devices: Service shelly-manager-firmware.
- name: firmware
containerPort: 8082
# /metrics only: Service shelly-manager-metrics.
- name: metrics
containerPort: 9090
env:
- name: TZ
value: Europe/Berlin
@@ -83,8 +92,20 @@ spec:
valueFrom:
fieldRef:
fieldPath: status.podIP
# 8082 and 9090 are separate pipelines in the app that answer only
# /fw/... and /metrics; the UI and API stay on 8080.
- name: ASPNETCORE_URLS
value: "http://$(POD_IP):8080"
value: "http://$(POD_IP):8080;http://$(POD_IP):8082;http://$(POD_IP):9090"
# Both pod addresses (IPv4,IPv6). When set, the app binds all three
# ports to exactly these and ignores ASPNETCORE_URLS, so nothing
# listens on net1 and the firmware LoadBalancer gets an IPv6 listener.
- name: POD_IPS
valueFrom:
fieldRef:
fieldPath: status.podIPs
# The URL devices download firmware from: Service shelly-manager-firmware.
- name: Firmware__PublicBaseUrl
value: http://firmware.shelly-manager.renner.uno
- name: ShellyManager__DnsSuffix
value: iot.renner.uno
# BLE radios: ESP32 esphome bluetooth_proxy nodes (e.g. the M5 Atom
@@ -44,6 +44,12 @@ spec:
# the pod goes NotReady and its ingress returns 504. Static IPAM used to
# dodge this by simply never setting a gateway; leased addresses come with
# one, so it has to be confined instead of omitted.
#
# sbr covers IPv4 only. With Router Advertisements net1 gets global IPv6
# addresses plus on-link and default routes in the main table, and replies
# to IPv6 clients on v30iot leave via net1, bypassing Cilium's LoadBalancer
# reverse NAT. tuning turns RA processing off on net1; the link-local
# address mDNS uses stays.
config: |
{
"cniVersion": "0.3.1",
@@ -57,6 +63,13 @@ spec:
"type": "dhcp"
}
},
{
"type": "tuning",
"sysctl": {
"net.ipv6.conf.IFNAME.accept_ra": "0",
"net.ipv6.conf.IFNAME.autoconf": "0"
}
},
{
"type": "sbr"
}
@@ -29,3 +29,56 @@ spec:
port: 80
targetPort: api
protocol: TCP
---
# Firmware downloads for the Shelly fleet. Devices on the IoT VLAN reach it over
# the UDM's BGP routes (like mosquitto-iot) by the name
# firmware.shelly-manager.renner.uno, which shelly-manager hands them in the
# update URL.
# The app serves only /fw/... on this port. Ingress: shelly-manager-firmware-
# world-ingress in network-policies/home-assistant-ns.yaml.
apiVersion: v1
kind: Service
metadata:
name: shelly-manager-firmware
namespace: home-assistant
labels:
app.kubernetes.io/name: shelly-manager
lb-pool: bgp
dns.renner.uno/internal: "true"
annotations:
# Pinned like the other bgp-pool Services, which auto-assignment would give
# the pool's base address 10.2.99.0 (see trmnl/device-service.yaml).
lbipam.cilium.io/ips: "10.2.99.90,fd99::8a"
external-dns.kubernetes.io/hostname: firmware.shelly-manager.renner.uno
spec:
type: LoadBalancer
externalTrafficPolicy: Local
ipFamilyPolicy: PreferDualStack
ipFamilies:
- IPv4
- IPv6
selector:
app.kubernetes.io/name: shelly-manager
ports:
- name: http
port: 80
targetPort: firmware
protocol: TCP
---
# Scrape target for monitoring-rules/shelly-manager-servicemonitor.yaml.
apiVersion: v1
kind: Service
metadata:
name: shelly-manager-metrics
namespace: home-assistant
labels:
app.kubernetes.io/name: shelly-manager
app.kubernetes.io/component: metrics
spec:
selector:
app.kubernetes.io/name: shelly-manager
ports:
- name: metrics
port: 9090
targetPort: metrics
protocol: TCP
@@ -1,10 +1,11 @@
---
# Mosquitto broker LoadBalancer on the BGP-routed off-link pool. IoT
# devices reach the broker via the UDM's BGP-learned routes — no
# on-link L2 announce on v30iot. Companion Shelly fleet is updated via
# shelly-manager's `/api/v1/mqtt/push` so each device's stored MQTT
# server resolves to `mqtt.renner.uno`, not a bare IP — that way future
# IP changes only need DNS updates, no reflash sweep.
# on-link L2 announce on v30iot. The Shelly fleet's MQTT server is set
# by shelly-manager's root group setting `mqtt.server` (Groups → All
# Shellies → settings) and reconciled to every device, so each device's
# stored MQTT server resolves to `mqtt.renner.uno`, not a bare IP — that
# way future IP changes only need DNS updates, no reflash sweep.
apiVersion: v1
kind: Service
metadata:
@@ -223,9 +223,7 @@ spec:
type: RuntimeDefault
containers:
- 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-b1f294d@sha256:8081adfee171c36cfa83734e5e7ccc3aad3debf1c0154488de70143674cde85f
image: gitea.renner.uno/konairius/mqtt-account-controller:2026-10-28-0657 # {"$imagepolicy": "flux-system:mqtt-account-controller"}
env:
- name: BROKER_URL
value: ssl://mosquitto.home-assistant.svc.cluster.local:8884
@@ -66,7 +66,7 @@ spec:
# (machine.registries.mirrors, applied by bootstrap.sh), and containerd
# keeps the repository path when it falls through — so the path has to
# exist on both registries. Steady state still pulls from Gitea.
image: gitea.renner.uno/konairius/alpine-jq:2026-09-04
image: gitea.renner.uno/konairius/alpine-jq:2026-10-28-0711 # {"$imagepolicy": "flux-system:alpine-jq"}
command:
- sh
- -c
+1 -38
View File
@@ -17,44 +17,7 @@ spec:
spec:
containers:
- name: ical-proxy
# Pinned to a commit build rather than :latest.
#
# :latest never rolls. The tag moves when a build publishes, but
# nothing tells the Deployment about it: the pod template is
# unchanged, so Kubernetes has no reason to replace anything, and
# this image is deliberately NOT enrolled in Flux image automation
# — see kubernetes/infrastructure/image-automation/README.md, which
# lists gitea.renner.uno/konairius/ical-proxy as having "only main, latest,
# and main-<sha> commit builds; no release tags", so there is no
# ImagePolicy watching it. The pods therefore sat on a 21-day-old
# image while :latest moved underneath them, and the only way to
# pick up a new build was a manual restart.
#
# A pinned tag makes the rollout a git change: Flux applies the new
# pod template and Kubernetes rolls it, which is also what makes
# what is RUNNING recoverable from this repo rather than "whatever
# :latest meant the last time a pod happened to restart".
#
# The image comes from the Gitea package registry, built by
# Woodpecker (.woodpecker.yml in konairius/ical-proxy) on every
# push to main. It is amd64-only and pulls without credentials, so
# no imagePullSecret. main-4783abe adds two hardening changes on
# top of main-db90a59: an SSRF guard (upstreams and redirect hops
# resolving to private/loopback/link-local addresses are refused;
# public redirects are still followed, up to 5) and a cap of 20
# concurrent /proxy upstream fetches (over the cap: 429 +
# Retry-After). main-73b5d97 then raises the server WriteTimeout
# from 10s to 40s (the 30s upstream fetch timeout + 10s headroom),
# so a slow-but-legitimate upstream is no longer cut off before
# /proxy can answer. main-926ead0 caps the upstream response body
# at 2 MiB (larger feeds get 413 instead of being buffered whole,
# REN-230) and logs upstream fetch failures with the host,
# answering 502 instead of 500. The summary_match/summary_replace
# parameters this deployment needs are unchanged.
#
# Bumping this is a manual, reviewed edit. The tag format is
# main-<short-sha>, the first 7 characters of the main commit.
image: gitea.renner.uno/konairius/ical-proxy:main-926ead0
image: gitea.renner.uno/konairius/ical-proxy:2026-10-28-0651 # {"$imagepolicy": "flux-system:ical-proxy"}
ports:
- containerPort: 8080
name: http
+1 -1
View File
@@ -30,7 +30,7 @@ spec:
type: RuntimeDefault
containers:
- name: controller
image: gitea.renner.uno/gitea_admin/kuma-sync:2026-10-27-0801 # {"$imagepolicy": "flux-system:kuma-sync"}
image: gitea.renner.uno/gitea_admin/kuma-sync:2026-10-28-0712 # {"$imagepolicy": "flux-system:kuma-sync"}
env:
- name: KUMA_URL
value: https://kuma.renner.uno
@@ -63,6 +63,9 @@ resources:
# deadlocked the whole apps layer. See the file for the cycle.
- rook-servicemonitors.yaml
- home-assistant-servicemonitor.yaml
# shelly-manager firmware rollout: halts, index freshness, config.
- shelly-manager-servicemonitor.yaml
- prometheusrule-shelly-manager.yaml
- immich-servicemonitor.yaml
- loki-servicemonitor.yaml
- smartctl-exporter-servicemonitor.yaml
@@ -0,0 +1,54 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: shelly-manager
namespace: monitoring
labels:
# kube-prometheus-stack's Prometheus resource selects on this label;
# without it the rule is created but never loaded into Prometheus.
release: kube-prometheus-stack
spec:
groups:
- name: shelly-manager.rules
rules:
# Metrics from shelly-manager's /metrics (ServiceMonitor
# shelly-manager-servicemonitor.yaml). Tests: tests/shelly-manager_test.yaml.
- alert: ShellyFirmwareRolloutHalted
# 1 per device type while its rollout is halted; cleared by Resume or
# Skip version on the Firmware page.
expr: |
max by (type) (shelly_manager_firmware_halted) == 1
labels:
severity: warning
namespace: home-assistant
annotations:
summary: "Shelly firmware rollout halted for {{ $labels.type }}"
description: "A firmware update of a {{ $labels.type }} device failed, so shelly-manager stopped rolling out to that type. The Firmware page names the device and the reason; Resume retries that device, Skip version leaves the version out."
- alert: ShellyFirmwareIndexStale
# Set only by a refresh in which every index source (Gen1 and Gen2+)
# succeeded, and restored from the stored releases on start. The
# index is read every 6 h.
expr: |
time() - max(shelly_manager_firmware_index_last_success_timestamp_seconds) > 172800
for: 30m
labels:
severity: warning
namespace: home-assistant
annotations:
summary: "Shelly firmware index not read successfully for 48 h"
description: "shelly-manager has not completed a refresh of all firmware index sources for 48 hours, so new firmware is not picked up. Check its logs for index errors (TLS pin, egress to *.shelly.cloud, HTTP errors); shelly_manager_firmware_index_errors_total shows which source fails."
- alert: ShellyFirmwareConfigInvalid
# Firmware:Window or Firmware:TimeZone cannot be read; the rollout
# does not start while this is 0.
expr: |
max(shelly_manager_firmware_config_valid) == 0
for: 10m
labels:
severity: warning
namespace: home-assistant
annotations:
summary: "Shelly firmware rollout disabled by invalid config"
description: "Firmware__Window or Firmware__TimeZone in kubernetes/apps/home-assistant-iot/shelly-manager-deployment.yaml (or the image defaults) is invalid, so shelly-manager starts no automatic firmware updates."
@@ -0,0 +1,19 @@
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: shelly-manager
namespace: home-assistant
labels:
app.kubernetes.io/name: shelly-manager
# Not required for discovery — serviceMonitorSelector is empty — but kept
# for consistency with the other monitors.
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app.kubernetes.io/name: shelly-manager
app.kubernetes.io/component: metrics
endpoints:
- port: metrics
path: /metrics
interval: 60s
@@ -34,6 +34,7 @@ retired the depth rule in favour of one persistence rule (`> 5` for `20m`).
| `gitea_test.yaml` | Hand-written suite for `GiteaDataVolumeLow` in `prometheusrule-gitea.yaml` (REN-583). |
| `uptime-kuma_test.yaml` | Hand-written suite for `prometheusrule-uptime-kuma.yaml` and the kuma exclusion in `prometheusrule-blackbox.yaml`. |
| `k8s-gateway_test.yaml` | Hand-written suite for `prometheusrule-k8s-gateway.yaml` (#1290). |
| `shelly-manager_test.yaml` | Hand-written suite for `prometheusrule-shelly-manager.yaml`. |
| `mutations.txt` | Negative controls. Each must make the suite fail. |
## Adding a suite
@@ -0,0 +1,80 @@
# Suite for prometheusrule-shelly-manager.yaml.
rule_files:
- ../prometheusrule-shelly-manager.yaml
evaluation_interval: 1m
tests:
# A halted type fires at once, per type; a type at 0 stays silent.
- interval: 1m
input_series:
- series: 'shelly_manager_firmware_halted{type="rpc:Plus1PM",pod="shelly-manager-a"}'
values: '0 0 1 1 1'
- series: 'shelly_manager_firmware_halted{type="gen1:SHSW-1",pod="shelly-manager-a"}'
values: '0 0 0 0 0'
alert_rule_test:
- eval_time: 1m
alertname: ShellyFirmwareRolloutHalted
exp_alerts: []
- eval_time: 2m
alertname: ShellyFirmwareRolloutHalted
exp_alerts:
- exp_labels:
severity: warning
namespace: home-assistant
type: rpc:Plus1PM
exp_annotations:
summary: "Shelly firmware rollout halted for rpc:Plus1PM"
description: "A firmware update of a rpc:Plus1PM device failed, so shelly-manager stopped rolling out to that type. The Firmware page names the device and the reason; Resume retries that device, Skip version leaves the version out."
# Last success at t=0: stale from 48h01m (first evaluation past 172800 s),
# fires 30 m later.
- interval: 1m
input_series:
- series: 'shelly_manager_firmware_index_last_success_timestamp_seconds{pod="shelly-manager-a"}'
values: '0+0x3000'
alert_rule_test:
- eval_time: 48h
alertname: ShellyFirmwareIndexStale
exp_alerts: []
- eval_time: 48h30m
alertname: ShellyFirmwareIndexStale
exp_alerts: []
- eval_time: 48h31m
alertname: ShellyFirmwareIndexStale
exp_alerts:
- exp_labels:
severity: warning
namespace: home-assistant
exp_annotations:
summary: "Shelly firmware index not read successfully for 48 h"
description: "shelly-manager has not completed a refresh of all firmware index sources for 48 hours, so new firmware is not picked up. Check its logs for index errors (TLS pin, egress to *.shelly.cloud, HTTP errors); shelly_manager_firmware_index_errors_total shows which source fails."
# A success that keeps up with time stays silent.
- interval: 1m
input_series:
- series: 'shelly_manager_firmware_index_last_success_timestamp_seconds{pod="shelly-manager-a"}'
values: '0+60x6000'
alert_rule_test:
- eval_time: 100h
alertname: ShellyFirmwareIndexStale
exp_alerts: []
# Invalid config fires after 10 m; valid config stays silent.
- interval: 1m
input_series:
- series: 'shelly_manager_firmware_config_valid{pod="shelly-manager-a"}'
values: '1 1 0+0x20'
alert_rule_test:
- eval_time: 11m
alertname: ShellyFirmwareConfigInvalid
exp_alerts: []
- eval_time: 12m
alertname: ShellyFirmwareConfigInvalid
exp_alerts:
- exp_labels:
severity: warning
namespace: home-assistant
exp_annotations:
summary: "Shelly firmware rollout disabled by invalid config"
description: "Firmware__Window or Firmware__TimeZone in kubernetes/apps/home-assistant-iot/shelly-manager-deployment.yaml (or the image defaults) is invalid, so shelly-manager starts no automatic firmware updates."
@@ -32,6 +32,7 @@ from music_assistant_models.media_items import (
Podcast,
PodcastEpisode,
ProviderMapping,
RecommendationFolder,
SearchResults,
UniqueList,
)
@@ -62,6 +63,7 @@ UP_NEXT_PLAYLIST_ID = "up_next"
SUPPORTED_FEATURES = {
ProviderFeature.LIBRARY_PODCASTS,
ProviderFeature.LIBRARY_PLAYLISTS,
ProviderFeature.RECOMMENDATIONS,
ProviderFeature.BROWSE,
ProviderFeature.SEARCH,
ProviderFeature.LIBRARY_PODCASTS_EDIT,
@@ -282,6 +284,38 @@ class PocketCastsProvider(MusicProvider):
)
return self._get_up_next_playlist()
async def get_recommendations(self) -> list[RecommendationFolder]:
"""Get the Up Next row, without items."""
return [
RecommendationFolder(
item_id=UP_NEXT_PLAYLIST_ID,
provider=self.instance_id,
name="Up Next",
icon="mdi-playlist-play",
)
]
async def get_recommendation_items(
self, item_id: str
) -> UniqueList[MediaItemType | ItemMapping | BrowseFolder]:
"""
Get the Up Next playlist, shown as the episode at the head of the queue.
:param item_id: The item_id of the row, as returned by get_recommendations.
"""
if item_id != UP_NEXT_PLAYLIST_ID:
return UniqueList()
episodes = await self._get_special_folder_episodes(UP_NEXT_PLAYLIST_ID)
if not episodes:
return UniqueList()
first = episodes[0]
playlist = self._get_up_next_playlist()
playlist.name = first.name
playlist.owner = first.podcast.name
if first.metadata.images:
playlist.metadata.images = first.metadata.images
return UniqueList([playlist])
async def get_playlist_tracks(
self, prov_playlist_id: str, page: int = 0
) -> list[PodcastEpisode]:
+1 -1
View File
@@ -43,7 +43,7 @@ spec:
mountPath: /consume
containers:
- name: vsftpd
image: gitea.renner.uno/konairius/vsftpd:build.1792310564 # {"$imagepolicy": "flux-system:vsftpd"}
image: gitea.renner.uno/konairius/vsftpd:2026-10-28-0649 # {"$imagepolicy": "flux-system:vsftpd"}
ports:
- name: ftp-ctrl
containerPort: 21
+1 -1
View File
@@ -155,7 +155,7 @@ spec:
# exist on both registries — hence konairius/, which matches the ghcr
# namespace. Steady state still pulls from Gitea and never leaves the
# network.
image: gitea.renner.uno/konairius/alpine-openssl:2026-09-04
image: gitea.renner.uno/konairius/alpine-openssl:2026-10-28-0711 # {"$imagepolicy": "flux-system:alpine-openssl"}
command:
- sh
- -c
+1 -1
View File
@@ -32,7 +32,7 @@ spec:
type: RuntimeDefault
containers:
- name: review-bot
image: gitea.renner.uno/gitea_admin/review-bot:2026-10-27-2231 # {"$imagepolicy": "flux-system:review-bot"}
image: gitea.renner.uno/gitea_admin/review-bot:2026-10-28-0709 # {"$imagepolicy": "flux-system:review-bot"}
env:
- name: GITEA_URL
value: https://gitea.renner.uno
+1 -1
View File
@@ -184,7 +184,7 @@ spec:
# (machine.registries.mirrors, applied by bootstrap.sh), and containerd
# keeps the repository path, so it has to exist on both registries.
# Steady state still pulls from Gitea.
image: gitea.renner.uno/konairius/alpine-jq:2026-09-04
image: gitea.renner.uno/konairius/alpine-jq:2026-10-28-0711 # {"$imagepolicy": "flux-system:alpine-jq"}
command:
- sh
- -c
+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.78.0 # {"$imagepolicy": "flux-system:terminus"}
image: ghcr.io/usetrmnl/terminus:0.79.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.78.0 # {"$imagepolicy": "flux-system:terminus"}
image: ghcr.io/usetrmnl/terminus:0.79.0 # {"$imagepolicy": "flux-system:terminus"}
command: ["bundle", "exec", "sidekiq", "-r", "./config/sidekiq.rb"]
envFrom:
- configMapRef:
+1 -1
View File
@@ -53,7 +53,7 @@ spec:
type: RuntimeDefault
containers:
- name: wiki
image: gitea.renner.uno/konairius/wiki:build.1793094262 # {"$imagepolicy": "flux-system:wiki"}
image: gitea.renner.uno/konairius/wiki:2026-10-28-0700 # {"$imagepolicy": "flux-system:wiki"}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
+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.19.0 # {"$imagepolicy": "flux-system:woodpecker-agent"}
image: woodpeckerci/woodpecker-agent:v3.20.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.19.0 # {"$imagepolicy": "flux-system:woodpecker-server"}
image: woodpeckerci/woodpecker-server:v3.20.0 # {"$imagepolicy": "flux-system:woodpecker-server"}
env:
- name: WOODPECKER_HOST
value: https://ci.renner.uno
@@ -16,6 +16,11 @@ spec:
path: ./kubernetes/apps/authentik
prune: true
wait: true
# Substitutes every ${VAR} under kubernetes/apps/authentik; that path has
# no other ${...} literals. Flux image automation keeps the tag current.
postBuild:
substitute:
SAMBA_BRIDGE_LDAP_OUTPOST_IMAGE: gitea.renner.uno/konairius/authentik-samba-bridge-ldap-outpost:2026-10-28-0655 # {"$imagepolicy": "flux-system:authentik-samba-bridge-ldap-outpost"}
# decryption block dropped 2026-07-14: path is sops-free — secrets come
# from OpenBao via ESO (MIGRATION-sops-openbao.md). This was the LAST
# sops consumer; phase 4 (age-key retirement) may proceed after soak.
@@ -59,12 +59,23 @@ existing `flux-system` GitRepository whose Secret was created by
| firefox | `1.x` | `-rolling-daily` suffix extract; rejects arch-prefixed duplicates |
| mmwave-configurator | `2.x` | Plain 3-component filter (rejects `addon-`/`-armv7`/`-arm64`/`-amd64` variants) |
| cnpg-barman-plugin | `0.x` | v-prefix filter; marker lives in upstream-imported manifest (see below) |
| hermes, wiki, shelly-manager, vsftpd, ical-proxy, mqtt-account-controller, authentik-samba-bridge-{server,ldap-outpost} | `YYYY-MM-DD-HHMM` | In-house (konairius); newest tag wins |
| alpine-jq, alpine-openssl, cni-plugins | `YYYY-MM-DD-HHMM` | In-house (build-ci-images.yml, konairius/ copy); newest tag wins |
## In-house konairius images: one tag per build
Gitea's package cleanup rule for konairius counts every tag as one
version and keeps the most recent one. A build that pushes several tags
(sha, `main`, `latest`) leaves only the last-pushed tag, so every
konairius image is pushed with exactly one tag, `YYYY-MM-DD-HHMM` (UTC),
and every consumer follows it through an ImagePolicy rather than a
hand-written pin.
## Not enrolled (structurally impossible)
- `woodpeckerci/woodpecker-cli` — the lint image in
`.woodpecker/ci-manifests-test.yml`. It publishes a perfectly good
version stream (`v3.16.0-alpine`), so unlike the three below this is
version stream (`v3.16.0-alpine`), so unlike the two below this is
**not** a tag-format problem. It cannot be enrolled because
**image-automation can only patch YAML that is a Kubernetes manifest** —
a maintainer states it plainly in fluxcd/flux2#2826: *"Flux can only
@@ -87,15 +98,13 @@ existing `flux-system` GitRepository whose Secret was created by
## Not enrolled (deliberate)
These three images publish no usable version stream — there is nothing
These two images publish no usable version stream — there is nothing
for image-policy to sort against:
- `fauria/vsftpd` — only `:latest` is published.
- `openthread/otbr` — only Ubuntu codenames (`focal`, `jammy`,
`hirsute`) and `:latest`/`:test`/`:reference-device`; no semver
release stream.
- `gitea.renner.uno/konairius/ical-proxy` — only `main`, `latest`, and
`main-<sha>` commit builds; no release tags.
The security-scan agent will continue to surface vuln findings on
these as "Skipped / Flux image-automation owns this" even without
@@ -0,0 +1,23 @@
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: alpine-jq
namespace: flux-system
spec:
image: gitea.renner.uno/konairius/alpine-jq
interval: 30m
---
# build-ci-images.yml tags each build YYYY-MM-DD-HHMM (older builds YYYY-MM-DD); newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: alpine-jq
namespace: flux-system
spec:
imageRepositoryRef:
name: alpine-jq
filterTags:
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
alphabetical:
order: asc
@@ -0,0 +1,23 @@
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: alpine-openssl
namespace: flux-system
spec:
image: gitea.renner.uno/konairius/alpine-openssl
interval: 30m
---
# build-ci-images.yml tags each build YYYY-MM-DD-HHMM (older builds YYYY-MM-DD); newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: alpine-openssl
namespace: flux-system
spec:
imageRepositoryRef:
name: alpine-openssl
filterTags:
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
alphabetical:
order: asc
@@ -0,0 +1,46 @@
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: authentik-samba-bridge-server
namespace: flux-system
spec:
image: gitea.renner.uno/konairius/authentik-samba-bridge-server
interval: 30m
---
# konairius/authentik-samba-bridge CI tags each build YYYY-MM-DD-HHMM; newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: authentik-samba-bridge-server
namespace: flux-system
spec:
imageRepositoryRef:
name: authentik-samba-bridge-server
filterTags:
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
alphabetical:
order: asc
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: authentik-samba-bridge-ldap-outpost
namespace: flux-system
spec:
image: gitea.renner.uno/konairius/authentik-samba-bridge-ldap-outpost
interval: 30m
---
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: authentik-samba-bridge-ldap-outpost
namespace: flux-system
spec:
imageRepositoryRef:
name: authentik-samba-bridge-ldap-outpost
filterTags:
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
alphabetical:
order: asc
@@ -0,0 +1,23 @@
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: cni-plugins
namespace: flux-system
spec:
image: gitea.renner.uno/konairius/cni-plugins
interval: 30m
---
# build-ci-images.yml tags each build YYYY-MM-DD-HHMM (older builds YYYY-MM-DD); newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: cni-plugins
namespace: flux-system
spec:
imageRepositoryRef:
name: cni-plugins
filterTags:
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
alphabetical:
order: asc
@@ -7,7 +7,7 @@ spec:
image: gitea.renner.uno/konairius/hermes
interval: 10m
---
# .woodpecker/hermes-image.yml tags every build `build.<unix-ts>`; latest push wins.
# .woodpecker/hermes-image.yml tags each build YYYY-MM-DD-HHMM; newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
@@ -17,8 +17,7 @@ spec:
imageRepositoryRef:
name: hermes
filterTags:
pattern: '^build\.(?P<ts>\d+)$'
extract: '$ts'
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
numerical:
alphabetical:
order: asc
@@ -0,0 +1,23 @@
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: ical-proxy
namespace: flux-system
spec:
image: gitea.renner.uno/konairius/ical-proxy
interval: 30m
---
# konairius/ical-proxy CI tags each build YYYY-MM-DD-HHMM; newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: ical-proxy
namespace: flux-system
spec:
imageRepositoryRef:
name: ical-proxy
filterTags:
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
alphabetical:
order: asc
@@ -2,22 +2,28 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- update-automation.yaml
- alpine-jq.yaml
- alpine-openssl.yaml
- audiobookshelf.yaml
- authentik-gate-sync.yaml
- authentik-samba-bridge.yaml
- backup-verify.yaml
- ceph.yaml
- cloudflare-lb-sync.yaml
- cni-plugins.yaml
- cnpg-barman-plugin.yaml
- gatus.yaml
- gitea-mcp.yaml
- gotenberg.yaml
- hermes.yaml
- home-assistant.yaml
- ical-proxy.yaml
- immich.yaml
- kuma-sync.yaml
- lidarr.yaml
- matter-server.yaml
- mmwave-configurator.yaml
- mqtt-account-controller.yaml
- music-assistant.yaml
- navidrome.yaml
- netbird-ingress.yaml
@@ -0,0 +1,23 @@
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImageRepository
metadata:
name: mqtt-account-controller
namespace: flux-system
spec:
image: gitea.renner.uno/konairius/mqtt-account-controller
interval: 30m
---
# konairius/mqtt-account-controller CI tags each build YYYY-MM-DD-HHMM; newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
name: mqtt-account-controller
namespace: flux-system
spec:
imageRepositoryRef:
name: mqtt-account-controller
filterTags:
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
alphabetical:
order: asc
@@ -7,9 +7,7 @@ spec:
image: gitea.renner.uno/konairius/shelly-manager
interval: 30m
---
# CI publishes `build.<unix-ts>` alongside SHA and `main` tags so the
# ImagePolicy has a sortable tag stream to track. Numerical desc =
# latest push wins. The SHA-only tags are filtered out by the pattern.
# konairius/shelly-manager CI tags each build YYYY-MM-DD-HHMM; newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
@@ -19,8 +17,7 @@ spec:
imageRepositoryRef:
name: shelly-manager
filterTags:
pattern: '^build\.(?P<ts>\d+)$'
extract: '$ts'
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
numerical:
alphabetical:
order: asc
@@ -7,9 +7,7 @@ spec:
image: gitea.renner.uno/konairius/vsftpd
interval: 30m
---
# CI publishes `build.<unix-ts>` alongside SHA and `main` tags so the
# ImagePolicy has a sortable tag stream to track. Numerical desc =
# latest push wins. The SHA-only tags are filtered out by the pattern.
# konairius/vsftpd CI tags each build YYYY-MM-DD-HHMM; newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
@@ -19,8 +17,7 @@ spec:
imageRepositoryRef:
name: vsftpd
filterTags:
pattern: '^build\.(?P<ts>\d+)$'
extract: '$ts'
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
numerical:
alphabetical:
order: asc
@@ -7,7 +7,7 @@ spec:
image: gitea.renner.uno/konairius/wiki
interval: 10m
---
# The Wiki release pipeline tags every main build `build.<unix-ts>`; latest push wins.
# konairius/Wiki CI tags each build YYYY-MM-DD-HHMM; newest wins.
apiVersion: image.toolkit.fluxcd.io/v1
kind: ImagePolicy
metadata:
@@ -17,8 +17,7 @@ spec:
imageRepositoryRef:
name: wiki
filterTags:
pattern: '^build\.(?P<ts>\d+)$'
extract: '$ts'
pattern: '^\d{4}-\d{2}-\d{2}(-\d{4})?$'
policy:
numerical:
alphabetical:
order: asc
@@ -1,4 +1,3 @@
---
# Installs the standard containernetworking/plugins binaries (macvlan,
# static, host-local) into /opt/cni/bin on every node. Talos ships only
# what Cilium installs there (cilium-cni, bandwidth, …); macvlan is
@@ -74,7 +73,7 @@ spec:
# No imagePullSecret: the ghcr namespace is private and its credential
# is in machine.registries too, because a Secret would have to come
# from OpenBao, which needs Ceph, which needs these binaries.
image: gitea.renner.uno/konairius/cni-plugins:2026-10-05
image: gitea.renner.uno/konairius/cni-plugins:2026-10-28-0711 # {"$imagepolicy": "flux-system:cni-plugins"}
command:
- sh
- -c
@@ -212,6 +212,26 @@ spec:
- port: "8123"
protocol: TCP
---
# Shelly devices download firmware from shelly-manager through the
# shelly-manager-firmware LoadBalancer. Only :8082, where the app serves
# nothing but /fw/...; the UI on :8080 stays Traefik-only.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: shelly-manager-firmware-world-ingress
namespace: home-assistant
spec:
endpointSelector:
matchLabels:
app.kubernetes.io/name: shelly-manager
ingress:
- fromEntities:
- world
toPorts:
- ports:
- port: "8082"
protocol: TCP
---
# Secret rotation: the openbao-ns rotation CronJobs reach HA to roll its three
# long-lived access tokens. HA mints/revokes LLATs only over its WebSocket API,
# so the rotator must reach HA directly.
@@ -360,8 +380,9 @@ spec:
- toEntities:
- world
---
# shelly-manager: HTTP RPC to Shelly devices on the IoT VLAN, and mDNS
# queries for their *.local names (sent out the default interface eth0).
# shelly-manager: HTTP RPC to Shelly devices on the IoT VLAN, mDNS queries
# for their *.local names (sent out the default interface eth0), and Shelly's
# firmware index and image hosts over HTTPS.
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
@@ -372,8 +393,10 @@ spec:
matchLabels:
app.kubernetes.io/name: shelly-manager
egress:
# v30iot over eth0: IPv4 and the VLAN's ULA prefix.
- toCIDR:
- 10.2.30.0/24
- fd02:30::/64
toPorts:
- ports:
- port: "80"
@@ -384,6 +407,28 @@ spec:
- ports:
- port: "5353"
protocol: UDP
# rules.dns lets Cilium learn the toFQDNs mapping
- toEndpoints:
- matchLabels:
"k8s:io.kubernetes.pod.namespace": kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDP
- port: "53"
protocol: TCP
rules:
dns:
- matchPattern: "*"
# api.shelly.cloud (Gen1 index), updates.shelly.cloud (Gen2+ index),
# firmware./fwcdn.shelly.cloud and other *.shelly.cloud image hosts.
- toFQDNs:
- matchPattern: "*.shelly.cloud"
toPorts:
- ports:
- port: "443"
protocol: TCP
---
# esphome: OTA/API to devices on the IoT VLAN; HTTPS for GitHub, PlatformIO and PyPI during builds.
apiVersion: cilium.io/v2
@@ -71,7 +71,7 @@ spec:
type: RuntimeDefault
containers:
- name: rotate
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-10-27 # {"$imagepolicy": "flux-system:secret-rotator"}
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-10-28 # {"$imagepolicy": "flux-system:secret-rotator"}
args: ["ha-token"]
securityContext:
allowPrivilegeEscalation: false
@@ -133,7 +133,7 @@ spec:
type: RuntimeDefault
containers:
- name: rotate
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-10-27 # {"$imagepolicy": "flux-system:secret-rotator"}
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-10-28 # {"$imagepolicy": "flux-system:secret-rotator"}
args: ["ha-token"]
securityContext:
allowPrivilegeEscalation: false
@@ -195,7 +195,7 @@ spec:
type: RuntimeDefault
containers:
- name: rotate
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-10-27 # {"$imagepolicy": "flux-system:secret-rotator"}
image: gitea.renner.uno/gitea_admin/secret-rotator:2026-10-28 # {"$imagepolicy": "flux-system:secret-rotator"}
args: ["ha-token"]
securityContext:
allowPrivilegeEscalation: false
@@ -32,7 +32,7 @@ spec:
type: RuntimeDefault
containers:
- name: backup-verify
image: gitea.renner.uno/gitea_admin/ci-backup-verify:2026-10-27 # {"$imagepolicy": "flux-system:backup-verify"}
image: gitea.renner.uno/gitea_admin/ci-backup-verify:2026-10-28 # {"$imagepolicy": "flux-system:backup-verify"}
command: ["bash", "/script/backup-verify.sh"]
env:
- name: HOME
+7
View File
@@ -45,7 +45,14 @@
"ghcr.io/paperless-ngx/paperless-ngx",
"ghcr.io/usetrmnl/terminus",
"ghcr.io/usetrmnl/trmnl-ha-amd64",
"gitea.renner.uno/konairius/alpine-jq",
"gitea.renner.uno/konairius/alpine-openssl",
"gitea.renner.uno/konairius/authentik-samba-bridge-ldap-outpost",
"gitea.renner.uno/konairius/authentik-samba-bridge-server",
"gitea.renner.uno/konairius/cni-plugins",
"gitea.renner.uno/konairius/hermes",
"gitea.renner.uno/konairius/ical-proxy",
"gitea.renner.uno/konairius/mqtt-account-controller",
"gitea.renner.uno/konairius/shelly-manager",
"gitea.renner.uno/konairius/vsftpd",
"gitea.renner.uno/konairius/wiki",