Commit Graph
4354 Commits
Author SHA1 Message Date
security-bot 9bd4c23af9 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784872746
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-07-24 06:26:12 +00:00
k.renner 6b905c6b37 Merge pull request 'chore(ci): bump ci-claude image to 2.1.218' (#382) from ci-image/claude-2.1.218 into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#382
2026-07-24 04:40:07 +00:00
security-bot 5a96a46ea4 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784866857
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-07-24 04:26:12 +00:00
claude-bot d4f91779a1 chore(ci): bump ci-claude image to 2.1.218
ci/woodpecker/pr/pr-review Pipeline was successful
2026-07-24 04:25:34 +00:00
k.renner 0810d4464d Merge pull request 'chore(ci): make ci-claude reproducible and keep it on latest (#351)' (#378) from claude-bot/ci-claude-reproducible into master
ci/woodpecker/push/ci-claude-refresh Pipeline was successful
ci/woodpecker/push/build-ci-images Pipeline was successful
Reviewed-on: k.renner/homelab#378
2026-07-24 04:24:50 +00:00
security-bot 46a63eaee7 chore(image-automation): bump netbirdio/netbird:v0.75.0-rootless
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-07-23 17:26:53 +00:00
claude-botandClaude Opus 4.8 f414f6e7bb fix(ci): only build/push ci-claude from master, never a PR branch
ci/woodpecker/pr/pr-review Pipeline was successful
The push trigger matched any branch, so pushing this feature branch ran the
refresh pipeline from the branch: it built and pushed ci-claude:2.1.218 and
opened a bump PR (#379) prematurely. Restrict the push event to branch: master
so only master ever builds/pushes images or opens bump PRs; cron and manual
are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:19:17 +02:00
claude-botandClaude Opus 4.8 16eecf07e8 chore(ci): provision ci-claude bump token; use claude-bot identity
ci/woodpecker/push/ci-claude-refresh Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was canceled
The bump credential is a claude-bot PAT scoped write:repository only (a
dedicated Gitea user needs admin to create, which claude-bot lacks; the
scoped-down PAT is the same least-privilege at the token level). Token minted
and stored as the ci_claude_bump_bot_token Woodpecker secret (events:
push/cron/manual). Commit author set to claude-bot accordingly. Only the cron
registration remains, to be done after this merges to master.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 17:12:47 +02:00
claude-botandClaude Opus 4.8 a675bf6cf0 chore(ci): make ci-claude reproducible and keep it on latest (#351)
ci/woodpecker/pr/pr-review Pipeline was successful
The ci-claude image installed a fixed, pinned Claude Code version and every
consumer pipeline pinned a stale dated image tag. Two problems:

- The npm install layer changed on every build (install-time mtimes + the
  npm cache), so it never deduped in the Gitea registry — the single largest
  orphan pattern behind #336 (~260 MB orphaned per no-op rebuild).
- A frozen version silently rots: old Claude Code releases eventually stop
  working against the API.

Containerfile: default to @latest (npm auto-selects the musl native binary on
this alpine base) and make the install layer byte-stable — clean the npm cache
and normalize install-time mtimes on the installed tree and the directories the
RUN touches. Two independent --no-cache builds now produce a byte-identical npm
layer, so a rebuild of the same version dedups instead of orphaning a copy.

New ci-claude-refresh.yml owns the image lifecycle: weekly (and on-demand) it
resolves the latest published version, and only when it differs from what the
consumers pin does it build ci-claude:<version> (+:latest) and open a PR
rewiring the 5 consumer pipelines. Version tags make "did it change?" a
race-free string compare and keep the cron from opening no-op PRs. Nothing
auto-merges. build-ci-images.yml no longer builds ci-claude so the two never
fight over tags.

Requires owner setup before it does anything (documented in the pipeline
header): a dedicated ci-claude-bump-bot identity (write:repository only) as the
ci_claude_bump_bot_token secret, and a cron named "ci-claude-refresh".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:58:36 +02:00
k.renner 2c5e54865a Merge pull request 'Stable predictable interface names (net.ifnames=1) — fixes the v30iot NIC shuffle' (#375) from claude-bot/talos-predictable-naming into master
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#375
2026-07-23 14:29:32 +00:00
security-bot f65f52f160 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784816363
Updates managed by image-automation-controller.
2026-07-23 14:26:48 +00:00
claude-botandClaude Fable 5 3939fe51f6 fix(ci): ash-portable schematic substring; actually finish the eth1 sweep
ci/woodpecker/pr/pr-review Pipeline was successful
Review blocker: my previous filename fix used bash substring syntax
${TALOS_SCHEMATIC:0:12}, but the apply step runs under busybox ash
(Alpine /bin/sh) which errors 'Bad substitution' — the repo already uses
cut for this exact reason. Replaced with $(echo "$TALOS_SCHEMATIC" |
cut -c1-12). The HCL substr() in talos_image.tf is unaffected (not shell).

Also finishes the comment sweep the prior commit claimed but didn't: 4
more stale 'eth1' host-NIC references in the matter/otbr/music-assistant
NAD headers are now ens19. Grep confirms zero eth1 left in the v30iot
NAD/deployment files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
2026-07-23 16:21:37 +02:00
k.renner 40a20cb3bd Merge pull request 'fix(ci): strip REVIEW_TOKEN from the review agents' environment' (#377) from claude-bot/pr-review-strip-review-token into master
Reviewed-on: k.renner/homelab#377
2026-07-23 14:18:51 +00:00
claude-botandClaude Opus 4.8 a4836879a9 fix(ci): strip REVIEW_TOKEN from the review agents' environment
ci/woodpecker/pr/pr-review Pipeline was successful
The step exports REVIEW_TOKEN (write:repository PAT) for its own curl/gitea_post
calls, but that also placed it in the environment of the claude triage+review
agents, which process attacker-controlled PR content — a prompt-injected diff
could read /proc/self/environ and exfiltrate it into the public report. The
agents never call Gitea (the shell does, via $AUTH), so run them under
'env -u REVIEW_TOKEN'. Defence in depth alongside the settings.json /proc deny.
Caught by the review bot; matches the fix applied to the other fleet repos.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 16:12:27 +02:00
claude-botandClaude Fable 5 b762cc7941 docs(nad): finish the eth1->ens19 comment sweep in the NAD headers
ci/woodpecker/pr/pr-review Pipeline was successful
Review follow-up (optional): the shelly/matter/otbr NAD header comments
still described the old eth1 probe-order naming, contradicting this PR's
own root-cause writeup. Updated to the stable ens19 predictable name and
dropped the stale k8s-edge/probe-order narration.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
2026-07-23 16:12:23 +02:00
claude-botandClaude Fable 5 7eb2e02b1d fix(ci): key Talos image cache on schematic; reconcile stale comments
ci/woodpecker/pr/pr-review Pipeline was successful
Review blocker: the Proxmox image-exists check derived TALOS_FILENAME
from version only, so bumping the schematic left the cached filename
unchanged — a future VM (re)provision would find the stale old-schematic
image and skip the download, silently reintroducing net.ifnames=0. Embed
the schematic hash in the filename (pipeline + opentofu/talos_image.tf,
kept in sync) so a schematic bump forces a fresh download.

Also reconciles stale comments the rename left behind: the MA reconcile
docstring (hostNetwork-era eth1 explanation), cluster.yaml kubelet nodeIP
(eth1->ens19), and the matter NAD's k8s-edge claim (edge isn't migrated
and isn't a current node; the pod is pinned off it anyway).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
2026-07-23 15:59:18 +02:00
k.renner 285b52eb55 Merge pull request 'feat(secret-rotator): pin image, reconcile path, unsuspend the two rotators' (#373) from claude-bot/rotator-pin-unsuspend into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#373
2026-07-23 13:54:10 +00:00
claude-botandClaude Fable 5 4ee087ec5b fix(talos): stable predictable interface names (net.ifnames=1) for v30iot
ci/woodpecker/pr/pr-review Pipeline was successful
Root cause of the IoT NIC name shuffle: the nocloud platform driver
unconditionally injects net.ifnames=0, disabling systemd predictable
naming, so eth0/1/2 followed driver-probe order and renamed across
reboots (a k8s-3 reboot put the v30iot NIC on eth2 and mgmt on eth1,
silently mis-targeting the v6-disable sysctl onto mgmt and attaching
macvlan children to the Server VLAN). LinkAliasConfig cannot fix this
(it only sets a cosmetic IFLA_IFALIAS, invisible to sysctl/CNI).

Fix: re-enable predictable naming via a new Image Factory schematic
(extraKernelArgs: -net.ifnames=0, net.ifnames=1). Because the cluster
boots sd-boot/UKI, the cmdline is baked into the image — machine.install
extraKernelArgs is silently ignored — so this MUST be delivered as a
schematic + talosctl upgrade, not a config patch. The busPath-derived
names are topology-stable and identical across the three identical VMs:
  0000:06:13.0 (v30iot) -> ens19
  0000:06:12.0 (mgmt)   -> ens18
  0000:02:00.0 (ceph)   -> enp2s0np1

Re-keys everything that referenced the volatile eth1 onto the stable
ens19: the four v30iot macvlan NADs (matter/otbr/shelly/music-assistant)
and the per-node v6-disable sysctl. Bumps the schematic id in the deploy
pipeline. Applied live via rolling talosctl upgrade (one node at a time,
full Ceph recovery between each); verified ens19 identical on all three,
v30iot NIC addressless v4+v6, macvlan children retain their own v6.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
2026-07-23 15:47:42 +02:00
claude-botandClaude Opus 4.8 0b4ccd66f8 fix(secret-rotator): restore spec: key in claude-oauth CronJob
ci/woodpecker/pr/pr-review Pipeline was successful
The earlier edit that removed the stale suspend-comment block also dropped the
'spec:' line, leaving suspend/schedule/jobTemplate nested under metadata: — an
invalid CronJob that would be rejected by the API server (kustomize build does
not validate against the k8s schema, so it passed). Re-add spec: and sync the
top-of-file doc comment to the reconciled infrastructure/claude#setup-token
path. Caught by the review bot on this PR; verified with kubectl --dry-run=server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:25:50 +02:00
k.renner 211f1314ba Merge pull request 'Expose Zigbee2MQTT frontend via Authentik gate (zigbee2mqtt.home.renner.uno)' (#374) from claude-bot/zigbee2mqtt-ingress into master
ci/woodpecker/manual/alert-monitor Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#374
2026-07-23 13:08:45 +00:00
claude-botandClaude Opus 4.8 de402ba531 Expose Zigbee2MQTT frontend via Authentik gate
ci/woodpecker/pr/pr-review Pipeline was successful
Gate zigbee2mqtt.home.renner.uno behind the same passkey-step-up Authentik
proxy-outpost pattern used for esphome/shelly-manager/vscode. The Z2M frontend
has no auth of its own (anyone on :8080 controls the whole Zigbee network), so
Traefik only ever routes the hostname to a dedicated outpost that validates the
session before forwarding.

New:
  - apps/home-assistant/zigbee2mqtt-gate-service.yaml — ExternalName mirror to
    ak-outpost-zigbee2mqtt-gate in the authentik ns (:9000).
  - apps/home-assistant/zigbee2mqtt-ingress.yaml — zigbee2mqtt.home.renner.uno →
    the outpost; cert-manager ingress-shim issues zigbee2mqtt-tls.
  - apps/authentik/zigbee2mqtt-gate-egress.yaml — CNP letting the outpost egress
    to the zigbee2mqtt pod on :8080.

Edited:
  - apps/authentik/blueprints-configmap.yaml — 5-entry blueprint block (group /
    proxyprovider / application / policybinding / outpost) named zigbee2mqtt-gate;
    internal_host carries the explicit :8080 (Service listens on 8080, unlike
    esphome's :80). Authentik spawns the ak-outpost-zigbee2mqtt-gate Deployment
    +Service from the outpost entry.
  - infrastructure/network-policies/home-assistant-ns.yaml — fromEndpoints rule
    admitting the zigbee2mqtt-gate outpost to :8080 (shared 8080 with vscode/
    shelly-manager; the fromEndpoints selector + the authentik-side egress scope
    it to the zigbee2mqtt pod).
  - the two kustomizations registering the new files.

DNS (k8s-gateway watches Ingress) and TLS (cert-manager) are automatic. Access
limited to the "Zigbee2MQTT Managers" group (k.renner). Verified: kustomize
builds, dry-run OK, blueprint YAML parses with the new block.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:03:37 +02:00
claude-botandClaude Opus 4.8 a9e1dfc85a feat(secret-rotator): activate review-bot + claude-oauth rotators
ci/woodpecker/pr/pr-review Pipeline was successful
Pin both CronJobs to the built secret-rotator image that carries woodpecker-secret
mode (sha256:828bf486…), flip suspend:false, and reconcile the claude-oauth
BAO path to homelab/infrastructure/claude#setup-token (where the OAuth value is
actually seeded) — with the matching policy update (applied out-of-band).

Bootstraps + policies/roles are already applied and the org secrets are live, so
the rotators now maintain them on schedule: review-bot mints+syncs review_bot_token
quarterly; claude-oauth syncs claude_code_oauth_token daily.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 15:00:35 +02:00
k.renner e2045eeb0b Merge pull request 'Z2M: adopt coordinator via explicit network params (final cutover fix)' (#371) from claude-bot/zha-to-zigbee2mqtt into master
ci/woodpecker/cron/e2e Pipeline is running
ci/woodpecker/cron/security-scan Pipeline is running
ci/woodpecker/manual/alert-monitor Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#371
2026-07-23 11:53:14 +00:00
claude-botandClaude Opus 4.8 796d9688da Z2M: adopt coordinator via explicit network params (not backup restore)
The coordinator_backup.json approach did NOT make herdsman adopt the existing
network. herdsman's zstack manager compares the CONFIG network params against
the adapter's NV and refuses on mismatch; a backup only prevents accidental
re-commissioning, it does not supply the network to an already-commissioned
adapter. So Z2M sat on defaults (PAN 0x1a62 / channel 11) vs the real network
(PAN 0x8b2b / channel 25) and correctly refused — no re-pair, no damage.

Set the real network params explicitly (values are the adapter's own, read from
herdsman's own mismatch log, cross-checked against the OpenBao backup which
matches exactly: key fe0b..2d, pan 8b2b, ext fc7a735d08c15476, ch 25):

  advanced:
    channel: 25
    pan_id: 0x8B2B
    ext_pan_id: [0xfc,0x7a,0x73,0x5d,0x08,0xc1,0x54,0x76]   # natural order
    network_key: "!secret network_key"

Byte order pinned against zigbee-herdsman v10.6.1 source (determineStrategy):
ext_pan_id is reversed only when WRITING to NV, but compared/logged
non-reversed, and the network key is never reversed — so both go in natural
order matching the displayed adapter hex.

network_key stays secret: the init container extracts it from the OpenBao
coordinator backup (jq + sed -> natural-order int array) into
/app/data/secret.yaml, referenced by the config as a quoted "!secret" ref.
channel/pan_id/ext_pan_id are non-secret and live in the ConfigMap.

Verified: kustomize builds, deployment dry-run OK, extraction produces the
correct 16-byte array, config schema types valid. The final config<->adapter
match is confirmed at startup, where the coordinator_backup.json guarantees a
safe refusal (never a re-commission) if anything is still off.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:46:04 +02:00
k.renner 44c0abd484 Merge pull request 'Remove k8s node host addressing on the IoT VLAN (security + hairpin fix)' (#369) from claude-bot/iot-nic-unaddress into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#369
2026-07-23 11:42:09 +00:00
security-bot a3a3b9e6b7 chore(image-automation): bump gitea.renner.uno/k.renner/shelly-manager:build.1784804902
ci/woodpecker/cron/issue-fix Pipeline was successful
Updates managed by image-automation-controller.
2026-07-23 11:32:53 +00:00
claude-botandClaude Fable 5 5a9693bdd8 fix(music-assistant): macvlan child on v30iot instead of hostNetwork
ci/woodpecker/pr/pr-review Pipeline was successful
Unblocks the node-unaddressing change: MA ran hostNetwork and its
reconcile initContainer fails closed if no 10.2.30.0/24 address exists
on a host interface — removing node host addressing would crash-loop it
on every node it can schedule to (no edge toleration).

MA now attaches net1 via macvlan on v30iot (DHCP-leased), exactly like
matter-server/otbr/shelly-manager: its own IoT address, direct L2
adjacency for Snapcast/SlimProto/Sendspin audio sync (macvlan bridge
mode puts frames straight on the wire — same property hostNetwork gave,
without the node holding an IoT address). The reconcile script is
unchanged in logic: it scans the pod's interfaces for the 10.2.30.0/24
address and now finds net1 (Multus attaches it with its lease before any
container runs) instead of the host eth1. bind_ip stays 0.0.0.0 -> binds
eth0 + net1; publish_ip = net1's leased IP. NAD omits matter's Thread
accept_ra tuning (MA has no Thread); keeps sbr (leased default gateway
would otherwise blackhole cluster-internal replies).

Folded into this PR so the change is atomic: on merge Flux migrates MA
to its own IoT lease before the manual node-unaddressing rollout removes
the host addresses.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
2026-07-23 13:31:50 +02:00
k.renner 8ebd870459 Merge pull request 'Z2M: quote the !secret ref (fixes 'unknown scalar tag', Z2M won't start)' (#370) from claude-bot/zha-to-zigbee2mqtt into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#370
2026-07-23 11:28:21 +00:00
claude-botandClaude Opus 4.8 8a7557eb3c Z2M: quote the !secret ref (fixes "unknown scalar tag")
Root cause of Z2M refusing to start, verified against Z2M's own validator: the
MQTT password was written as a bare YAML tag `password: !secret mqtt_password`,
and Z2M 2.x does NOT register a `!secret` YAML tag — js-yaml throws
"unknown scalar tag !<!secret>", which surfaces as the generic
"configuration is invalid (use yaml-validator)" message.

Z2M's secret mechanism is parseValueRef (settings.ts read()): it matches the
regex `!<file> <key>` against the STRING VALUE of mqtt.user/password/server,
advanced.network_key, frontend.auth_token. So the reference must be a QUOTED
string, not a tag:

    password: "!secret mqtt_password"

Reverted the interim env-override attempt: ZIGBEE2MQTT_CONFIG_* is applied only
in write()/onboarding, not on a plain read of an existing config, so it did NOT
set mqtt.password (confirmed: mqtt.password was empty). The quoted-!secret +
init-rendered /app/data/secret.yaml path is the one Z2M actually resolves on
read.

Verified end-to-end on the REAL rendered ConfigMap via `settings.validate()` in
the running image: VALIDATE_ERRORS: [], mqtt.password resolves from secret.yaml,
31 devices parsed.

Post-merge: the PVC still holds the pre-fix seeded configuration.yaml (init only
seeds when absent), so it must be cleared once so it re-seeds from this fixed
ConfigMap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:25:35 +02:00
k.renner 6b578aa35b Merge pull request 'feat(secret-rotator): sync bot tokens into Woodpecker org secrets' (#363) from claude-bot/rotator-woodpecker-secrets into master
ci/woodpecker/push/build-ci-images Pipeline was successful
Reviewed-on: k.renner/homelab#363
2026-07-23 11:23:59 +00:00
claude-botandClaude Fable 5 02c6128dc6 docs(talos,shelly): flag the eth1 name dependency; fix stale eth2 comment
ci/woodpecker/pr/pr-review Pipeline was successful
Review follow-ups (non-blocking):
- The disable_ipv6 sysctl keys on the literal 'eth1' while the interface
  is selected by busPath; a rename would no-op the sysctl SILENTLY and
  leave the host v6-addressed on IoT. Document the dependency and that
  the rollout must verify eth1 is addressless per node.
- shelly-manager-deployment comment said 'master: eth2' — the NAD uses
  eth1. Last lingering eth2-means-v30iot reference, corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
2026-07-23 13:08:40 +02:00
k.renner be30af53d0 Merge pull request 'Z2M cutover fix: remove permit_join (invalid in 2.x), replicas 1' (#368) from claude-bot/zha-to-zigbee2mqtt into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#368
2026-07-23 11:04:00 +00:00
claude-botandClaude Opus 4.8 0f41efee35 chore(secret-rotator): address pr-review — suspend rotators, doc caveats
ci/woodpecker/pr/pr-review Pipeline was successful
Review on PR #363:
- Suspend both CronJobs at merge (review-bot quarterly, claude-oauth daily):
  the pinned image is still :latest and predates woodpecker-secret mode, so an
  un-suspended merge would fail on schedule with 'unknown mode'. The image-pin
  follow-up flips them on. (suggestion 2)
- GITEA_VERIFY_REPO: note shelly-manager verified private 2026-07-23 (unauth
  404 + Woodpecker SCM PRIVATE=yes) and the degradation-if-public risk, so the
  pull-only gate's assumption is documented. (suggestion 1)
- make-before-break header: caveat that the 'sync-fail keeps working' property
  holds for one cycle; a persistent sync failure must be fixed before the next
  quarterly run reaps the still-referenced token. (suggestion 3)

Left as-is per the review: the woodpecker.rs defensive else-arms (mirror
bao.rs) and no ureq-mocking test (repo convention).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:03:44 +02:00
claude-botandClaude Fable 5 b077de8556 fix(talos): remove node host addressing on the v30iot NIC
ci/woodpecker/pr/pr-review Pipeline was successful
Security + routing. The k8s nodes held a DHCP-leased v4 address and an
RA-derived v6 address on the IoT VLAN — a network that must be treated
as compromised. That gave every IoT device an L3-reachable path to the
node (kubelet, SSH-equivalent apid, any host service) and, as a side
effect, installed a connected 10.2.30.0/24 route so node replies to
IoT-subnet clients bypassed the UDM entirely — breaking every UDM-DNATed
flow (the hairpin NAT: un-translated SYN-ACK -> client kernel RST,
verified live 2026-07-23).

The NIC (busPath 0000:06:13.0) stays up as the macvlan PARENT for the
v30iot pod attachments (matter-server, otbr, shelly-manager), whose
children do their own DHCP in their own netns — host addressing was
never needed for that. dhcp: false drops the v4 address+route;
sysctl net.ipv6.conf.eth1.disable_ipv6=1 drops v6 including link-local,
so the host has no addressable presence on the L2 at all. The old
comment's claimed purpose (Cilium L2 announce for an IoT LB pool) does
not exist — no L2AnnouncementPolicy, BGP-only pool.

RESIDUAL (not addressed here): the NIC remains a macvlan parent, so the
node kernel still processes frames from the IoT L2. Full node isolation
from IoT would require relocating how those three pods reach IoT devices
— a separate, larger change.

Same suppression pattern the repo already uses (edge-host 10-vlan40,
HA macvlan /32) rather than policy routing (Talos 1.12 has no
RoutingRuleConfig; that is 1.13+).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S5CLfizEtzHS51kxJFQgc
2026-07-23 13:03:14 +02:00
claude-botandClaude Opus 4.8 37f8758150 Z2M cutover: remove permit_join (invalid in 2.x), set replicas 1
Live cutover fix. Two changes:

1. Remove the top-level `permit_join: false` from the Z2M config. It was
   REMOVED in Zigbee2MQTT 2.0 — a config containing it fails schema validation
   and Z2M refuses to start (serves the failure page). Joining is runtime-only
   now (default off; toggled via frontend or the bridge/request/permit_join
   topic), which is what we want anyway. Verified the other 2.x-sensitive keys
   (homeassistant.enabled, frontend.enabled, mqtt.*) against the current docs —
   they were already correct.

2. Flip replicas 0 -> 1. The cutover is committed: ZHA is disabled, the
   coordinator-backup Secret is populated in OpenBao, and Z2M is now the live
   Zigbee stack. Flux owns this field, so it must be 1 in git (a manual
   `kubectl scale` gets reverted on the next reconcile). Rollback = set 0 and
   re-enable ZHA.

Note: the running PVC still holds the pre-fix seeded configuration.yaml; the
init container only seeds when absent, so after merge that stale file must be
cleared once (delete it + restart the pod) so it re-seeds from this fixed
ConfigMap. Tracked as the immediate post-merge step.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 13:02:07 +02:00
claude-botandClaude Opus 4.8 16013a4ca0 feat(secret-rotator): sync bot tokens into Woodpecker org secrets
ci/woodpecker/push/build-ci-images Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Add a woodpecker-secret mode to secret-rotator that upserts an OpenBao-held
value into a Woodpecker ORG-level CI secret (create if absent, else patch
value+events), and wire two rotators that use it — moving review_bot_token and
claude_code_oauth_token off hand-loading into the rotation toolchain and onto
org-level secrets every repo inherits.

secret-rotator:
- woodpecker.rs: org-id lookup, existence probe (404=absent), create/patch an
  org secret via a dedicated woodpecker-bot bearer token. Never logs the value.
- main.rs: woodpecker-secret mode reads a value from BAO_PATH#BAO_FIELD (errors
  if missing/empty, so a blank never overwrites a working secret) and the
  woodpecker-bot token from the bootstrap, then upserts the org secret with the
  configured events (must include pull_request for the PR-review pipeline).

rotators (kubernetes/infrastructure/openbao/rotation/):
- review-bot.yaml: one CronJob, init step mints review-bot's PAT (gitea-token
  mode, pull-verified against a private repo it collaborates on; push not
  verified — it's a read collaborator that posts reviews via the API), main step
  syncs it to org secret review_bot_token (events cron,manual,pull_request).
- claude-oauth.yaml: sync-only (the OAuth token is interactive setup-token, not
  mintable) — daily push of an owner-seeded OpenBao value to org secret
  claude_code_oauth_token.
- policies/rotator-{review-bot,claude-oauth}.hcl: least-privilege OpenBao
  policies + documented hand-seeded bootstraps (review-bot password,
  woodpecker-bot token, seeded OAuth/token fields).

netpol: rotator-{review-bot,claude-oauth}-egress (openbao-ns) +
gitea-allow-review-bot-rotator + a new woodpecker-allow-rotators, so the
rotators reach gitea:3000 / woodpecker:8000 through apps default-deny.

Deploy prerequisites (can't be done from git): pin the image @sha256 once a
build including woodpecker-secret mode lands; apply the two OpenBao policies/
roles; seed the bootstraps. After the org secrets exist, drop homelab's
repo-level claude_code_oauth_token / review_bot_token copies.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:52:49 +02:00
k.renner 35ba7bc63c Merge pull request 'fix(ci): pr-review.yml parse error — literal ${...} in a comment broke the live pipeline' (#367) from claude-bot/fix-pr-review-var-comment into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#367
2026-07-23 10:52:30 +00:00
claude-botandClaude Opus 4.8 2573547753 fix(ci): remove literal ${...} from pr-review.yml comment
ci/woodpecker/pr/pr-review Pipeline was successful
Woodpecker runs its ${...} variable substitution over the ENTIRE pipeline YAML
including comments, so the literal dollar-brace in the triggering comment
(added when documenting the dropped manual path) failed to parse — 'unable to
parse variable name' — erroring every pull_request pipeline before any step
ran. Reword the comment to avoid the glyph; the live homelab pipeline was down
since #362 merged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:50:29 +02:00
k.renner 8b9468caa3 Merge pull request 'feat(ci): trigger pr-review on pull_request events instead of hourly cron' (#362) from claude-bot/pr-review-on-pr-events into master
Reviewed-on: k.renner/homelab#362
2026-07-23 10:45:29 +00:00
k.renner d72ca03fe8 Merge pull request 'Z2M migration: restore via coordinator_backup.json (fix ext_pan_id byte-order trap)' (#366) from claude-bot/zha-to-zigbee2mqtt into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#366
2026-07-23 10:33:45 +00:00
claude-botandClaude Opus 4.8 7e40d443de fix(ci): sandbox the pr-review agents' Read tool against token exfil
The triage + review agents run with REVIEW_TOKEN and CLAUDE_CODE_OAUTH_TOKEN in
their environment, process attacker-controlled PR title/body/diff, and their
output is posted verbatim as a public PR comment. Without a Read sandbox a
prompt-injected diff could read /proc/self/environ (the tokens) or .git/config
(Woodpecker's clone credential) and get it published. Every other agent
pipeline already denies this via --settings; pr-review had none.

Add ci/agents/pr-review/settings.json (mirrors issue-fix/update-audit, gitea
#197) denying Read on /proc,/root,/home,/etc,.git, and pass --settings
$PROMPTS/settings.json to both claude invocations (resolves alongside the
prompts, so it works for the repos that fetch prompts from the homelab clone).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:33:03 +02:00
claude-botandClaude Opus 4.8 8ca0b0995c Switch Z2M network migration to coordinator_backup.json restore
Correct the cutover mechanism before it ever runs. The prior approach hand-set
advanced.pan_id/ext_pan_id/network_key in the Z2M config from the ZHA values.
That is the risky path: the extended PAN ID byte order is REVERSED between ZHA
(zigpy) and Z2M (zigbee-herdsman), so loading the ZHA value as-is makes herdsman
see a mismatch and FORM A NEW network — dropping every device (the explicit
non-goal). Mixing a manual ext_pan_id with a backup also conflicts (Z2M ignores
the reversed value).

Instead restore an Open Coordinator Backup, the shared zigpy<->herdsman
interchange format, which pins byte orders in-spec and handles the reversal on
import:
  - New zigbee2mqtt-coordinator-backup ExternalSecret (OpenBao) holds the full
    backup JSON (contains the network key -> OpenBao only, never git), replacing
    zigbee2mqtt-network-externalsecret.yaml.
  - Init container seeds /app/data/coordinator_backup.json on first run only
    (guarded on database.db too, so Z2M's own state is never overwritten), and
    now renders only the mqtt password into secret.yaml.
  - Config drops advanced.pan_id/ext_pan_id/network_key/channel entirely; the
    network comes from the restored backup.

Runbook updated: Step 1 exports the Open Coordinator Backup from zigbee.db via
zigpy (non-disruptive — no second radio session); Step 2 stores it in OpenBao;
added an explicit device re-interview step (devices stay joined but Z2M's
database starts empty), and a PVC-clear note in rollback.

Verified live: coordinator has pan 0x8b2b, ext_pan_id fc7a735d08c15476,
channel 25; zigpy emits a valid open-coordinator-backup for it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:30:51 +02:00
claude-botandClaude Opus 4.8 2a65066672 fix(ci): review the workspace checkout, not a hand-cloned copy
On a pull_request event Woodpecker's built-in clone already checks the repo out
at the PR head in the workspace, so the agents read that directly for context.
Restores full-context review (the prior commit over-corrected to diff-only)
while keeping the manual, token-bearing clone gone. Prompts point at the
working-directory checkout instead of ./work/target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 12:03:35 +02:00
k.renner 8d840b0c55 Merge pull request 'Zigbee2MQTT deployment for no-re-pair ZHA migration' (#364) from claude-bot/zha-to-zigbee2mqtt into master
ci/woodpecker/manual/pr-review Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#364
2026-07-23 10:02:17 +00:00
claude-botandClaude Opus 4.8 8aa3bbf761 refactor(ci): review from the diff, don't clone the target repo
The clone existed only to hand review_bot_token to git, which persisted it in
work/target/.git/config — a file the review agent (Read/Glob/Grep, output
posted publicly) could read. Nothing needed the checkout that the API-fetched
diff + metadata don't already provide, so drop the clone entirely: no token on
disk, no work/target, no cleanup. The PR + diff are still read over the API
with ephemeral curl Authorization headers (never written to disk).

Prompts updated to match: the agents review the diff alone (no ./work/target),
and are told to flag, not guess, when a judgement needs code they can't see.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:59:59 +02:00
claude-botandClaude Opus 4.8 c1c636e299 Pre-seed Z2M friendly names + HA-side entity reconciliation audit
Pull the live ZHA device registry (31 devices + coordinator) and pre-seed each
device's friendly_name in the Z2M config, keyed by IEEE. Devices then come up as
"EG Küche Tischlampe Oben" instead of "0x001788010856bb17" the moment Z2M
re-interviews them on the adopted network — which is what keeps the HA-side
entity reconciliation small and mechanical. Two devices that were both bare
"Deckenlampe" in ZHA are disambiguated by area (Z2M requires unique friendly
names): EG Galerie (Hue LTC001) and DG Lager (LUMI N1 switch).

Add zigbee2mqtt-entity-reconciliation.md: an audit of exactly which ZHA entities
the git-tracked config references, so the cutover knows what must be re-pointed.
Key result — the HA-side change is FAR smaller than "rewire everything":

  - 216 ZHA entities exist, but only 16 distinct entity_ids are referenced in
    the repo (200 are unreferenced diagnostics/config entities).
  - 14 of the 16 are physical-device entities, all mapping cleanly into the
    31-device inventory (no orphans).
  - The remaining 2 are the `light.texas_instruments_coordinator_*` entities —
    now identified as ZHA *groups* on the coordinator (not physical devices),
    accounting for 14 references. These don't come from Z2M device discovery and
    must be re-created as Z2M groups and re-pointed by hand.

Scope note captured in the doc: only git-tracked YAML/JSON was audited; live
.storage Lovelace dashboards and UI-created automations/helpers must be
reconciled separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:57:14 +02:00
claude-botandClaude Opus 4.8 01f79b4315 fix(ci): don't persist review_bot_token in the PR checkout config
Replace the credential-in-URL clone (which git wrote into
work/target/.git/config, readable by the review agent whose output is posted
publicly) with a tokenless clone that supplies auth via an HTTP header through
GIT_CONFIG_* env. The persisted remote is tokenless, so no repo file holds the
PAT — fixing the leak at the source instead of deleting .git after the fact.

Supersedes the previous rm -rf .git stopgap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:54:49 +02:00
claude-botandClaude Opus 4.8 2073cedbcf fix(ci): strip review_bot_token from the PR checkout before the agent runs
The clone embedded REVIEW_TOKEN in the URL, which git persists in
work/target/.git/config. The review agent runs next cd'd into the workspace
with Read/Glob/Grep and its report is posted verbatim as a public PR comment,
so the write:repository PAT was readable and could be exfiltrated via prompt
injection from the reviewed PR (caught by the review bot on PR #180). The
agent needs only the working tree, so drop .git entirely after checkout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:49:41 +02:00
claude-botandClaude Opus 4.8 becd041b21 Add Zigbee2MQTT deployment for no-re-pair ZHA migration
Lay down Zigbee2MQTT (Z2M) in the home-assistant namespace as the eventual
replacement for the ZHA integration, wired to drive the SAME coordinator ZHA
uses today (SMLIGHT SLZB-06 / TI CC2652, tcp://slzb-zigbee.renner.uno:6638).

Devices are joined to the network on the coordinator chip, so if Z2M comes up
with the existing network parameters (pan_id, ext_pan_id, network_key, channel)
zigbee-herdsman ADOPTS that network instead of forming a new one — every device
stays joined, no re-pairing.

Ships INERT and safe to merge:
  - Deployment is replicas: 0. Reconciling this PR starts nothing.
  - The ZHA network params come from the `zigbee2mqtt-network` ExternalSecret
    (OpenBao), populated only at cutover. Until then the ExternalSecret is
    NotReady and the pod cannot start, so it can never accidentally form a new
    network (which would drop every device).
The cutover is a manual, documented, reversible procedure — see
zigbee2mqtt-migration-runbook.md.

Contents:
  - zigbee2mqtt-{pvc,config,deployment,service}.yaml — the app. Config template
    is seeded to the PVC once; secrets (mqtt password + ZHA network params) are
    injected via /app/data/secret.yaml (rendered each start by an init
    container) and referenced with Z2M `!secret`, keeping the network key out
    of git and out of the ConfigMap.
  - zigbee2mqtt-network-externalsecret.yaml — OpenBao-backed network params,
    populated at cutover from a ZHA network backup.
  - New `zigbee2mqtt` Mosquitto broker user: bootstrap Job
    (mosquitto-bootstrap.yaml) + passwd/ACL/env (mosquitto-deployment.yaml),
    scoped to zigbee2mqtt/# plus write homeassistant/# for HA MQTT discovery.
  - zigbee2mqtt-migration-runbook.md — the cutover + rollback procedure.

No new egress CiliumNetworkPolicy is needed: the namespace-wide default-deny
policy already grants same-namespace (mosquitto), DNS, and world (the IoT-VLAN
coordinator) to every pod.

Follow-up (separate homeassistant-repo PR): re-point automations/dashboards
from ZHA entity ids to the new Z2M MQTT-discovery entities.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 11:10:05 +02:00
claude-botandClaude Opus 4.8 e96056dabd fix(ci): drop the no-op manual re-review path from pr-review
Woodpecker exposes --var values to YAML ${...} substitution and when-filters
only, not as a step's shell environment, so the documented
'--var PIPELINE=pr-review --var PR=<n>' re-review read an unset shell var and
silently no-opped with a green build (caught by the review bot on PR #49).

Trigger on pull_request only; re-review by pushing a commit (new head sha
clears the dedup marker). Resolve the PR from CI_COMMIT_PULL_REQUEST alone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 10:56:25 +02:00