Commit Graph
4354 Commits
Author SHA1 Message Date
k.renner 45ca838f22 Merge pull request 'docs(apps): trim the home-assistant manifest comments' (#687) from trim-comments-apps into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#687
2026-08-28 05:13:57 +00:00
claude-bot c90406cca7 docs(apps): trim otbr, trmnl, zigbee2mqtt — and correct a copy-pasted falsehood
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Comments only across otbr, trmnl/renderer, zigbee2mqtt-deployment and the
smartctl ServiceMonitor. All four still parse; monitoring-rules still renders.

Each keeps the instruction and loses the investigation. otbr stays disabled with
the condition for re-enabling it — verified partition merge, one OMR prefix, the
Nest routers visible — instead of the paragraph reconstructing how the split
partition was diagnosed. Its socat bridge keeps both dead ends, because they are
what stops someone simplifying it: forkpty needs a socat the image does not ship,
and a sidecar cannot supply the PTY because devpts is per-container. zigbee2mqtt
keeps "never run alongside ZHA, and never start without the restored coordinator
backup or you form a new network and drop every device", and loses the cutover
diary around it.

The ServiceMonitor correction is the useful part. Four monitors carry the claim
that kube-prometheus-stack's Prometheus "selects ServiceMonitors by this label;
without it the resource exists but is never discovered". The live resource has
serviceMonitorSelector {} and podMonitorSelector {} — empty, so every monitor is
picked up regardless of labels. Only ruleSelector matches on
release=kube-prometheus-stack, which is why the same sentence on the disk-wear
PrometheusRule is correct and this one is not.

Corrected here for the monitor I added yesterday, which is where I introduced it
by copying a neighbour. The other three carry the same wrong sentence and are
left for a pass that can check each rather than sed across them.
2026-08-28 06:59:04 +02:00
claude-bot 14d62f3aa1 docs(home-assistant): trim the mdns-relay and mosquitto comments
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
mdns-relay 122 -> 64 comment lines, mosquitto 131 -> 118. Comments only; the
diff filtered for non-comment lines is empty and apps/home-assistant still
renders 83 objects.

The mdns-relay header was mine, written yesterday, and was the clearest case
of the problem: a seventy-line essay with sections. What survives is what stops
someone undoing it — that the relay must stay unicast because Cilium runs in
tunnel mode without enable-multicast and drops re-emitted multicast, so an
avahi-style reflector cannot work; that python-zeroconf accepts unicast at all,
which is the non-obvious fact the design rests on; that it restores
announcements but not HA-initiated browsing; and that so-bindtodevice=net1 is
what keeps any sibling pod from injecting records into HA's zeroconf cache.

Cut: the problem statement, the Errno 113 symptom, the exposure analysis
weighing what the IoT VLAN could already forge, and a paragraph explaining that
the relay is not what makes HA's listener reachable in-cluster.

mosquitto keeps every warning and loses the reasoning around them. The one
worth naming is the retained-state note: a roll preserves connectivity but not
the retained store, and Zigbee2MQTT does not recover on its own. That is an
operational consequence someone needs before touching the rollout strategy, so
it stays — the paragraph explaining why maxSurge:1 is safe does not.
2026-08-28 06:55:34 +02:00
k.renner 85b8f0eba9 Merge pull request 'docs(infra): trim the cilium and traefik helm release comments' (#685) from trim-comments-helm into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#685
2026-08-28 04:53:37 +00:00
claude-bot 6ffbc468d7 docs(infra): trim the remaining oversized comments
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
crowdsec, netbird-ingress, woodpecker-ns, san-network-attachment, cilium-bgp
and the openbao rotation kustomization. Comments only; the diff filtered for
non-comment lines is empty and infrastructure/ still builds.

Every instruction survives, because that is the half worth having: do not
switch crowdsec back to password auth, or a wiped LAPI crashloops the agents
and wedges the Kustomization; do not move netbird-ingress off the rootless
image, or socket-LB stops seeing its traffic and the VIPs blackhole; do not
pin the rotators by digest, or a quarterly job fails to pull a manifest that
was garbage-collected months earlier and looks like it simply did not run.

What went is the incident each was learned from, and one duplicate: the
san-network-attachment header re-derived the provider: host argument that
cephcluster.yaml already makes, so it now points there instead.

Two more stale claims found on the way through:

  - woodpecker-ns.yaml described the namespace as holding wp-pg alone with
    'server and agent arriving in stage 2'. Both have been running there for
    some time.
  - cilium-bgp.yaml said each node sources its BGP session from eth0. The
    management interface is mgmt0; eth0 was the VM-era name.

infrastructure/ is now 2553 comment lines.
2026-08-28 06:50:54 +02:00
claude-bot 8c7fd25e03 docs(infra): trim the cilium and traefik helm release comments
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
cilium 86 -> 63 comment lines, traefik 71 -> 59. Comments only; the diff
filtered for non-comment lines is empty and both files still parse.

Cilium keeps everything that is a trap rather than a story: that bpf-policy-map-max
and vlan-bpf-bypass each exist in three files with nothing enforcing agreement,
that both take an agent restart, that a full policy map surfaces as a pod stuck
in sandbox creation, and why only VLAN 30 belongs in the bypass list. What went
is how each was diagnosed — the Hubble traces, the manual agent deletions, the
device-by-device walk through `cilium-dbg status`.

Traefik keeps the version ceiling and the reason it exists, and keeps the
CrowdSec bouncer note as an instruction: do not re-attach until client-IP
visibility is fixed and a LAN whitelist exists. It loses the retelling of the
outage that produced that instruction.

The traefik header was outright wrong and is the reason this file was worth
opening. It described Traefik as running "alongside ingress-nginx during the
migration", called ingress-nginx "the default IngressClass" and Traefik
"opt-in via ingressClassName: traefik". There is one IngressClass in the
cluster and it is traefik; ingress-nginx has no deployment and no class. A
reader following that comment would have added an ingressClassName they do not
need and looked for a controller that is not there.
2026-08-28 06:47:39 +02:00
k.renner 7f11867407 Merge pull request 'docs(netpol): deduplicate the home-assistant policy comments' (#684) from trim-comments-netpol into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#684
2026-08-28 04:45:23 +00:00
claude-bot c3bad3bece docs(netpol): deduplicate the home-assistant policy comments
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
152 comment lines to 125 — a much lighter cut than the previous batches, on
purpose. Most of this file is per-rule security rationale, and that is exactly
the category worth keeping: someone deleting the :8124 rule needs to know it is
the trust anchor for X-Forwarded-For, and someone deleting a gate rule needs to
know it is what stands between an --auth=none editor and the LAN.

What was cut is repetition. Four near-identical paragraphs explained the
Authentik gate pattern once per app, three of them re-deriving that
endpointSelector {} makes a :8080 grant namespace-wide. That warning is now
stated once, at the first :8080 rule, and it is stronger for being in one
place: three apps share that port, so changing any of those rules affects all
three.

The world-ingress and ICMP blocks keep their rules and lose their incident
reports — do not add a blanket fromEntities: world to an endpointSelector {}
policy; ICMP needs an explicit allow once any L4 rule exists and Cilium takes
one type per entry.

Two stale references went with it. Egress-to-world was documented as reaching
"garage S3" and "legacy InfluxDB on VM 108": Garage was retired to nas-1 and
there is no Proxmox left to host VM 108. Both replaced by what the rule
actually grants rather than a list of destinations that has already changed.

Comments only; the diff filtered for non-comment lines is empty, and the file
still passes a server-side dry run.
2026-08-28 06:43:03 +02:00
k.renner 68672d3fb9 Merge pull request 'docs(infra): cut the narration out of cephcluster and the velero schedules' (#683) from trim-comments-infra into master
ci/woodpecker/cron/issue-fix Pipeline was successful
Reviewed-on: k.renner/homelab#683
2026-08-28 04:41:06 +00:00
claude-bot 8ae1cc7187 docs(infra): cut the narration out of cephcluster and the velero schedules
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Comments only — filtering the diff for non-comment lines returns empty, and
both files still parse and build.

cephcluster.yaml goes from 224 comment lines to 76. Kept: that `ceph config`
retains values which merely disappear from this file, so deleting a line does
not restore a default and a live provider change needs `ceph config rm` first;
that osdMaxUpdatesInParallel and upgradeOSDRequiresHealthyPGs are the only
things keeping a rolling OSD change off all three replicas at once; that
`addressRanges` drags the mons onto host networking and ties them to a DHCP
lease; that devices are named by-id because kernel names move between boots;
that the bluestore deferral settings must not be raised again.

Cut from it: the paxos write-amplification measurement down to the byte, the
history of why mon_data_avail_warn was once 20, and a table of every drive
serial in the fleet sitting three lines above the by-id paths that already
name them.

velero-schedules.yaml loses a per-PVC inventory of the home-assistant namespace
— eleven names, which had already been wrong once by two — in favour of saying
the schedule sweeps the namespace.

Two stale claims went with it. Both files said PVC contents are uploaded to
Garage; the BackupStorageLocation reads bucket renner-uno-k8s-velero at
fsn1.your-objectstorage.com, and Garage was retired to nas-1. And the ha-pg
exclusion still explained itself by quoting the comment it had replaced.
2026-08-28 06:39:44 +02:00
k.renner 633a141bac Merge pull request 'docs(talos): cut the narration out of the machine config patches' (#680) from trim-comments-talos into master
Reviewed-on: k.renner/homelab#680
2026-08-28 04:34:43 +00:00
claude-bot ed86592978 docs(talos): cut the narration out of the machine config patches
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
242 comment lines out, 91 in, across the four hand-written Talos patches. The
diff touches nothing but comments — filtering it for non-comment lines returns
empty.

Kept, because each changes what someone editing that line would do: select
disks by serial or the installer takes the OSD drive; EPHEMERAL volume config
applies only on first provision and is silently ignored afterwards; the
LinkAliasConfig attempt that left a node with no address on any interface;
kube-apiserver not following a node re-IP; the fallback-network links must stay
v6-disabled or the cluster loses IPv6 egress; the installer image is Image
Factory and not the stock one, and its tag must match the version being upgraded
to.

Cut: how each of those was discovered. The four-equal-priority-routes
investigation, the 30 hours of NetBird probes, the image-GC measurement on
k8s-2 down to the percentage point, the per-extension rationale for the
schematic. All of it is in the commit history and the wiki.

Cut as stale rather than merely long, which is the point of doing this at all:

  - k8s-1.yaml opened by saying k8s-2 and k8s-3 are still Proxmox VMs. All
    three have been bare metal since neptune's conversion; DMI reports
    Minisforum on every one.
  - two files justified a disk selector with "Talos and Proxmox do not agree
    on that order". There is no Proxmox.
  - three files carried per-drive wear figures (87%, 81%, 83%) and one an
    exact media-error count. Those are measurements, they move, and a config
    file never gets re-read when they do.
  - the mgmt0 note explained VLAN 40 by way of which address the k8s-1 VM
    used to reach over that port.

One thing removed here has no other home: whether k8s-1's 18 media errors are
historical or ongoing was written up in this file and nowhere else, and #549 is
closed. Tracking it separately rather than losing it.
2026-08-28 06:27:57 +02:00
k.renner a8c2f79f84 Merge pull request 'fix: [alert] NvmeDriveWearHigh (monitoring) (#675)' (#679) from alert-fix/issue-675 into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#679
2026-08-28 04:21:02 +00:00
k.renner 9f10ea793d Merge pull request 'docs: cut the essays out of the manifests I wrote today' (#678) from trim-manifest-comments into master
Reviewed-on: k.renner/homelab#678
2026-08-28 04:20:33 +00:00
issue-fix-bot bdffe1804b fix: [alert] NvmeDriveWearHigh (monitoring) (#675)
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
2026-08-27 19:45:14 +00:00
claude-bot da1482e33f docs: cut the essays out of the manifests I wrote today
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Config files were carrying investigation write-ups: two ruled-out hypotheses in
the smartctl-exporter securityContext, a twenty-line canonical note on the
Hetzner credential, the history of an alert threshold. That is derivation, not
configuration, and it belongs where it does not rot — the commit message for how
we got here, the wiki for what holds.

The distinction this applies: a comment stays if it changes what someone editing
that line would do — a constraint of the tool, a trap, a pointer to an issue. It
goes if it narrates how a conclusion was reached, restates a measurement, or
justifies a value after the fact. The test is whether the surrounding config
could change and leave the comment quietly wrong.

160 lines out, 30 in. Nothing behavioural changed; both manifests still pass a
server-side dry run.

Alert descriptions were tightened rather than removed. Those are not comments —
they are what someone reads when paged, and an essay is worse there than
anywhere.

This covers only what I added today. The repo has roughly 8600 comment lines
with 88 files over 25, and going further needs the criterion above agreed
first — much of that volume is load-bearing warning rather than narration.
2026-08-27 21:37:20 +02:00
k.renner c6b1aa109b Merge pull request 'refactor(secrets): drop the unused barman-cloud-creds' (#677) from drop-unused-barman-creds into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#677
2026-08-27 19:20:01 +00:00
claude-bot 2f3d952656 refactor(secrets): drop the unused barman-cloud-creds
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Three ExternalSecrets synced a Secret that nothing reads. Established rather
than assumed, from three directions: every live ObjectStore names
hetzner-s3-creds, no CNPG Cluster uses the legacy inline barmanObjectStore
(all seven run the barman-cloud plugin against an ObjectStore CR), and no pod
in the cluster mounts barman-cloud-creds.

They are almost certainly left from before the plugin migration, when the
inline configuration read a Secret of that name.

Worth stating plainly because it is not merely untidy: seed/{authentik,immich,
paperless}/barman-creds hold a Hetzner S3 credential that is NOT the one in
use. Fingerprint c2c5cf3e against 87954b64 for the live hetzner-s3 path, with
the three copies identical to each other. A valid key with no consumer is
exposure without benefit, so if it is a distinct S3 key it wants revoking at
Hetzner, which only a human can do — deleting our copies does not.

Removing the manifests does NOT remove the objects. All three carry
kustomize.toolkit.fluxcd.io/prune: disabled from the seed->app-KS handoffs, so
Flux skips them during garbage collection by design. The ExternalSecrets, their
Secrets and the three OpenBao paths have to be deleted explicitly after this
merges; git alone leaves them live and orphaned, which is worse than leaving
them declared.

The OpenBao paths deliberately stay until then. Deleting them first would leave
three ExternalSecrets failing to sync against a key that no longer exists, for
no gain.
2026-08-27 21:18:08 +02:00
k.renner 0a87f91116 Merge pull request 'refactor(secrets): one OpenBao path for the one Hetzner S3 credential' (#676) from hetzner-creds-shared-path into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#676
2026-08-27 19:04:23 +00:00
claude-bot c59f0050b3 refactor(secrets): one OpenBao path for the one Hetzner S3 credential
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
#397 asked for the opposite: give woodpecker its own path so a rotation of
paperless's credentials would stop silently changing woodpecker's. The premise
does not hold. There is one Hetzner S3 key, and rotating it at Hetzner changes
it for every consumer no matter how many paths hold a copy. A fifth copy would
have bought nothing and made the separation look real.

Seven ExternalSecrets were reading it from four paths — seed/paperless (four
namespaces), seed/authentik, seed/immich, seed/trmnl. They now all read
infrastructure/hetzner-s3, which sits beside the existing
infrastructure/velero-hetzner.

That the four paths are byte-identical was verified rather than assumed, and
without reading them: SHA-256 over the three fields of each synced Secret
returns the same fingerprint in all seven namespaces. The same check says
infrastructure/velero-hetzner carries the same access key id, in a different
shape — a single `cloud` field holding an AWS credentials file.

Velero is deliberately not folded in. Moving it means templating that INI from
these fields, which is worth doing and not worth bundling with a path swap that
touches every database backup at once.

The reasoning lives once, in the paperless manifest, with the others pointing at
it. Repeating it six times is how six copies drift.

Kept from the note this replaces, because it was right and cost someone an
afternoon: the 403 recorded in #397 was a wrong-path error, not missing
permissions. The mount is `homelab`, so the path is homelab/seed/... —
`bao kv list homelab/seed` works with the identity available here.

homelab/infrastructure/hetzner-s3 now exists, copied from the paperless path
inside OpenBao so the value never left it, and fingerprints identical to its
source. Writes turned out to be possible all along: they fail through
openbao.renner.uno with a response the client cannot parse, and succeed against
the Service. That is worth knowing before the next person concludes, as I first
did, that the identity is read-only.

Closes #397
2026-08-27 21:02:28 +02:00
k.renner 48c46610a6 Merge pull request 'docs(monitoring): stop treating a worn EPHEMERAL drive as a hazard' (#674) from ephemeral-framing into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#674
2026-08-27 18:38:33 +00:00
claude-botandClaude Opus 5 53887dd88f docs(monitoring): stop treating a worn EPHEMERAL drive as a hazard
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
I described the worn drives carrying EPHEMERAL as the dangerous placement,
repeatedly, in the alert text and in the dashboard's caveats. That reads the
volume backwards. EPHEMERAL is Talos's /var and its contents are meant to be
disposable — container images, kubelet state, pod scratch, spegel and Rook's
local state all rebuild. Putting the drives with the least endurance left there
is a sensible destination, not a risk someone should be warned about.

The alert said "a node simply stops when it goes", which is simply wrong. A node
losing /var rebuilds it.

What survives the correction is narrower and checked rather than recalled:
/var/lib/etcd is on that volume too, so losing EPHEMERAL on a control-plane node
costs that member's data. Quorum holds at two of three and the member is removed
and re-added. That is a rebuild, not data loss, and the alert now says so at
that weight.

The fill-level panel keeps its warning but for the right reason: a full /var
stops a node being useful long before anything is lost, and nothing rebalances
it the way Ceph rebalances a full OSD. That is a capacity argument, not an
endurance one.

None of this changes the monitoring — measuring every drive is still right, and
the wear figures are unchanged. It changes what a reader is told the numbers
mean, which is the part that was misleading.

Refs #549

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 20:35:11 +02:00
k.renner c9a10ab015 Merge pull request 'feat(monitoring): put the SMART values on the dashboard' (#672) from disk-dashboard-smart into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#672
2026-08-27 13:27:36 +00:00
claude-botandClaude Opus 5 24b2a0178d feat(monitoring): put the SMART values on the dashboard
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The exporter reports all nine drives now, so the dashboard shows what it
measures: a worst-wear stat in the status row, wear per drive over time, and a
table of the failure-now signals — available spare, media errors, the critical
warning bitfield, power-on time and written bytes.

Every expression was run against the live TSDB and the label names read off real
series rather than a README. That mattered: the exporter labels devices `nvme0`
and carries model and serial on a separate `smartctl_device` info metric, so
each query joins on (node, device) to pull the model in. A legend on the device
name alone would compare different hardware across machines, which is the same
trap the block-layer panels already avoid.

Wear and damage are presented as different things because they are. The wear
percentage is a budget the drive spends and says nothing about tomorrow; spare
exhaustion, media errors and the critical warning mean something is wrong now.
The caveats panel says so, and no longer claims endurance is missing — it was
missing when that text was written and is not any more.

Also fixes the OSD stat, which still rendered two identical numbers side by
side. The commit that fixed it did not make it into #668 before that PR merged,
so the panel went out in the state that prompted the complaint. It now shows
OSDs down alone; up-but-out is a state rather than a fault and Ceph warns about
it separately.

Refs #549

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 15:26:09 +02:00
k.renner b8061ea05d Merge pull request 'fix(monitoring): privileged is the mechanism, not the shortcut' (#671) from smartctl-privileged into master
Reviewed-on: k.renner/homelab#671
2026-08-27 13:19:38 +00:00
claude-botandClaude Opus 5 d0bea0dc15 fix(monitoring): privileged is the mechanism, not the shortcut
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Third attempt, and the first two were wrong for the same reason: I kept looking
for the gate in the capability and syscall layers, and it was never there.

Attempt one gave the container CAP_SYS_ADMIN and CAP_SYS_RAWIO with everything
else dropped. It found all three drives per node and could open none of them.
Attempt two added seccompProfile: Unconfined, reasoning that EPERM is the
default action for a syscall an unlisted profile blocks and that the kubelet
runs seccompDefault: true. /proc/1/status confirmed both capabilities effective
and a filter loaded, so the theory was at least testable — and removing the
filter changed nothing.

What settled it was a smaller test than either: `head -c 512 /dev/nvme0n1`
inside the container also fails with EPERM. Reading a block device involves no
ioctl, no capability and nothing a seccomp profile filters. The denial is the
device cgroup, which containerd applies to every non-privileged container
regardless of whether /dev is mounted. Mounting /dev makes the nodes visible;
it does not make them usable.

Kubernetes has no per-device allowlist in a Pod spec — no equivalent of
`docker --device`. So the honest position is that `privileged: true` here is
not a lazy shortcut standing in for a narrower setting; it is the only
mechanism the API offers, short of writing a device plugin. The manifest now
says that, and records what was ruled out and how, so the next reader does not
spend the same two rounds in the capability layer.

The /dev mount loses its readOnly flag with it. smartmontools opens NVMe
devices read-write on some paths, and once the container is privileged that
flag suggests a boundary that is not there.

A device plugin advertising the drives as a schedulable resource is the
genuinely narrower answer and is noted as such. It is worth building when more
than one workload needs raw device access; for a single read-only metrics
exporter it is a component to write, deploy and maintain in exchange for the
same access.

Refs #549

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 15:14:32 +02:00
k.renner 8ccf5ac8e5 Merge pull request 'fix(monitoring): unconfine seccomp so smartctl can open the devices' (#670) from smartctl-seccomp into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#670
2026-08-27 13:05:08 +00:00
claude-bot 08b2fe7ef6 fix(monitoring): unconfine seccomp so smartctl can open the devices
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The exporter deployed cleanly, ran 3/3 with no restarts, answered its liveness
probe, and produced no drive metrics at all. Only smartctl_devices and
smartctl_exporter_build_info reached Prometheus — a green DaemonSet measuring
nothing, which is the failure mode the dashboard's own caveats panel warns
about.

The logs said why: it found all three drives per node and could open none of
them, with 'Smartctl open device: /dev/nvme0 failed: Operation not permitted'.

EPERM rather than EACCES is the tell — it is the default seccomp action for a
syscall a profile does not list. Rather than guess between that and the
capabilities, I read /proc/1/status in the running container: CapEff was
0x220000, so SYS_ADMIN and SYS_RAWIO were both effective, while Seccomp was 2
with one filter loaded. The capabilities were never the problem. The kubelet
runs with seccompDefault: true, so every pod that does not say otherwise gets
RuntimeDefault.

SELinux was ruled out the same way: /sys/fs/selinux/enforce reads 0.

This stays narrower than privileged: true, which would additionally grant every
capability, all host devices and write access to /sys. The tighter option is a
custom seccomp profile allowing exactly the ioctl smartctl issues; that is worth
building if this set of exceptions ever grows, and is not worth it for one pod.

Refs #549
2026-08-27 15:03:34 +02:00
k.renner 04df5da041 Merge pull request 'feat(monitoring): expose NVMe endurance, with the privilege exception it needs' (#669) from smartctl-exporter into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#669
2026-08-27 12:56:00 +00:00
claude-botandClaude Opus 5 e8b217e8a1 feat(monitoring): expose NVMe endurance, with the privilege exception it needs
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Drive wear has been invisible in every signal this cluster has. `ceph -s` reads
active+clean at any wear level, no Ceph health check fires until a device
actually fails, and the block-layer metrics show throughput and latency but
never endurance. That blind spot is how a set of OSD drives reached 81-87%
before anyone looked (#549, #434).

This closes it for every drive, and it costs a deliberate exception to the
no-privileged-containers rule. The owner made that call explicitly after being
shown the alternative.

The alternative was real and is worth recording. Ceph's devicehealth module
already archives complete smartctl output daily — percentage_used,
power_on_hours, media_errors, critical_warning, available_spare — collected
without any elevated privileges, because the OSD pods hold the device access
already. Exporting that needs no exception at all. It also covers OSDs and
nothing else: three of nine drives here, and the six it misses include the ones
retired from Ceph for wear that now carry EPHEMERAL. Monitoring the healthy
drives while leaving the worn ones dark was not worth a new service.

The requirement comes from the kernel, not from the tool. NVMe health is read
through an admin ioctl on the character device, which is gated on CAP_SYS_ADMIN;
sysfs publishes the controller temperature and nothing more. Swapping
smartctl_exporter for another exporter would not change that, so the header says
so — the thing to re-check one day is whether the kernel grew a sysfs interface,
not whether some other tool asks for less.

The exception is kept to the smallest form that works: no `privileged: true`, no
host namespaces, no hostPID. Root plus CAP_SYS_ADMIN and CAP_SYS_RAWIO, all
other capabilities dropped, a read-only root filesystem, and /dev as the only
host mount — required because Kubernetes cannot hand individual character
devices to a pod without a device plugin. The admission policy that forbids
privileged pods is bound to the woodpecker-ci namespace only, so nothing here
circumvents it; monitoring already enforces pod-security `privileged`.

Two details that would otherwise have made the metrics useless:

The ServiceMonitor relabels the pod's node onto every series. Without it each
sample carries only the DaemonSet pod's IP, which says nothing about which
machine the drive is in and changes on every reschedule — an unattributable wear
figure is most of the value gone.

No --smartctl.device list. The exporter scans and rescans on its own, and a
written-down device list would quietly stop covering a drive the next time one
is replaced. Device names are not stable across these machines in the first
place.

The alert rules deliberately separate wear from damage: a wear percentage is a
budget being spent and warns at 80, while spare exhaustion, a critical-warning
flag or new media errors mean the drive is failing now. Media errors alert on
the increase rather than the total, so a drive that recorded errors long ago
does not alert forever while one that starts failing today does.

The completeness check compares the number of reporting drives against
node-exporter's own NVMe inventory rather than a written-down fleet size, so
adding or removing a drive moves both sides together. A rule that alerts on
nothing looks exactly like a healthy cluster, which is the failure mode worth
guarding.

Dashboard panels follow separately, once the metrics are live and the label
names can be read off the real series rather than guessed from a README.

Closes #549

Co-Authored-By: Claude Bot <claude-bot@renner.uno>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 14:42:18 +02:00
k.renner 8c03a6e310 Merge pull request 'feat(monitoring): put the disk health verdict on the disk health dashboard' (#668) from disk-dashboard-status into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#668
2026-08-27 12:25:39 +00:00
claude-bot 1213c771ca fix(monitoring): state mechanisms in the panel text, not the current hardware
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The descriptions I wrote listed the drives fitted today: which model holds which
role, that nvme0 is the system disk on two named nodes and something else on the
third, that a count below three means a drive is gone. All true this afternoon,
none of it true after the next swap — and prose does not get re-read when
hardware changes. That is the same failure this session already found in the
velero schedule comment, in the ci-claude consumer list, and in an instruction
pointing at a deleted file. I built a fresh instance of it while writing about
the others.

The text now describes mechanisms that outlive any particular set of drives, and
points at the inventory table for anything specific. Roles are assigned by serial
in Talos and by by-id path in Rook; a broad `disk.transport == 'nvme'` selector
matches every drive in the box; a drive retired from Ceph is not necessarily out
of service. None of that changes when the hardware does.

The OSD-count panel had the same problem in a worse form, since it was a
threshold rather than a sentence: green at exactly 3. It now reads OSDs down and
out as a difference against ceph_osd_metadata, so growing or shrinking the
cluster does not turn the panel red.

Fixes a claim that was simply false: the inventory description said the OSD
column came from ceph_disk_occupation, and the query never joined it. There was
no such column. It exists now, as a PromQL outer join — the matched half via
group_left, the unmatched half via `unless`, unioned — so drives with no OSD
still appear. Verified against the live TSDB: nine rows, three carrying a
ceph_daemon.

A Grafana `merge` transformation would have been the shorter route and would not
have worked. Merge only combines rows whose shared fields all agree, and the two
queries disagree on `instance` and `job` — node-exporter reports an IP and port
where ceph-mgr reports a node name — so it would have produced twelve rows
instead of nine, silently. Doing the join in PromQL also made it testable before
commit rather than after deploy.

Temperature thresholds are now labelled as generic NVMe limits rather than any
drive's rating, since they are not derived from a datasheet.

Refs #549
2026-08-27 14:23:36 +02:00
claude-botandClaude Opus 5 afbb6c3a95 feat(monitoring): put the disk health verdict on the disk health dashboard
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The first version graphed throughput, latency and temperature but never answered
"is anything wrong". Establishing that still meant querying Prometheus and Ceph
by hand — which is exactly the work a dashboard exists to remove, and which the
next person would have had to repeat.

A status row now leads: Ceph health, OSDs up and in, Ceph capacity used, and the
fullest EPHEMERAL volume across the three nodes. Each expression was run against
the live TSDB before being written in.

EPHEMERAL fill level gets its own graph too. It was the one number checked by
hand that had no home here, and it is the failure that has no safety net: Ceph
rebalances away from a full OSD, nothing rebalances away from a full /var — the
node simply stops.

The caveats panel gains three things found while answering the question:

Ceph does hold wear for the OSDs, in its devicehealth module and not in
Prometheus. The panel names the command rather than the current value, because a
number written into a dashboard is stale the moment it is written. It also
records that the retired Crucial drives were purged from Ceph's device registry,
so their history is not recoverable either — verified by asking for it and
getting ENOENT.

Talos boots with nvme_core.io_timeout=4294967295, so NVMe commands never time
out. A dying drive stalls instead of returning errors, and the latency panel
goes flat rather than spiking. That inverts what a reader would otherwise
conclude from a quiet graph.

Talos kernel logs do not reach Loki — the only log jobs are Kubernetes pods and
a systemd-journal from elsewhere. I found this by searching Loki for NVMe errors,
getting nothing, and checking whether the collector existed before believing the
zero; it did not. talosctl dmesg is the only source, it is a ring buffer, and
this morning's reboots emptied it.

Refs #549

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 14:18:11 +02:00
k.renner 32dc191b18 Merge pull request 'feat(monitoring): add a disk health dashboard' (#667) from disk-dashboard into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#667
2026-08-27 12:06:02 +00:00
claude-botandClaude Opus 5 d04b09310f feat(monitoring): add a disk health dashboard
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Throughput, IOPS, mean operation latency, utilisation, queue depth and
controller temperature for all nine NVMe drives, plus Ceph's own OSD latency for
comparison against the block layer.

Every panel joins node_disk_* to node_nvme_info and node_uname_info rather than
plotting the device name. That is not decoration: nvme0 is the Kingston system
disk on k8s-1 and k8s-3 and the Crucial on k8s-2, so a legend keyed on the
device compares different hardware across nodes and does so silently. The
inventory table resolves the OSD column through ceph_disk_occupation for the
same reason — it follows Rook instead of restating a device list that goes stale
the next time a drive is swapped.

Every expression was run against Prometheus before being written into the JSON,
which is also how the caveats in the panel descriptions were found. Latency is
accumulated-time over completed-operations, so it is a mean with no tail and it
vanishes while a device is idle: absence of traffic, not absence of a problem.
Utilisation near 100% on NVMe does not mean saturation the way it does on a
spindle, which is why queue depth shares that panel.

Temperature is the only SMART-adjacent value present, because it is the only one
the kernel publishes through sysfs. Wear, written-bytes totals, power-on hours
and media errors need an NVMe admin ioctl and no exporter here produces them
(#549). The dashboard says so in a panel rather than letting a green screen imply
a healthy drive.

That gap is worse than #549 describes, and the note records why. The Crucial P3
drives were retired from Ceph because they were worn, and now carry the
EPHEMERAL volume — so the drives with the least endurance left are the ones
holding container state, and they are the ones nothing watches. The measured
write latency already separates them from the Samsung OSDs by more than an order
of magnitude.

Also fixes the add-a-dashboard instructions above the generator, whose third
step told the reader to tick a row off ../DASHBOARD-MIGRATION.md. That file does
not exist anywhere in the repo, so the step could not be followed.

Refs #549

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 14:02:11 +02:00
k.renner dd050d811d Merge pull request 'chore(ci): bump ci-claude image to 2.1.247' (#666) from ci-image/claude-2.1.247 into master
ci/woodpecker/push/ci-claude-refresh Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/cron/e2e Pipeline was successful
Reviewed-on: k.renner/homelab#666
2026-08-27 11:51:32 +00:00
claude-bot c4bb35173d fix(ci): derive the ci-claude consumer list instead of maintaining it
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
review-bot found that this bump reached five of seven consumers: e2e.yml and
ci-scripts-test.yml stayed on the old version. Verified — both still pinned the
previous tag on the bump branch while the other five moved.

The cause is worth more than the symptom. ci-claude-refresh.yml carried the
consumer list twice, hardcoded, under the instruction "keep this list in sync
with the consumers". It was not in sync, and nothing could ever notice: the two
missing files were invisible to the bump that would have raised them and to the
guard that checks the pins agree. They were not drifting slowly, they were
unreachable — no future run would have touched them either.

So both copies are now derived by grepping .woodpecker for a version-pinned
reference. A list that must be maintained by hand is the defect; deriving it
removes the failure mode rather than correcting one instance of it.

The pattern deliberately matches a digit after the tag, which is what keeps this
file out of its own consumer set — its steps run ci-claude:latest, which never
needs rewriting. That makes the exclusion load-bearing, so it is spelled out in
a comment: a literal version anywhere in this file, prose included, would add it
to the set and let the sed rewrite it. The tag-scheme note at the top now says
<claude-version> for exactly that reason.

An empty result is a hard stop. Without it the following grep would be invoked
with no file arguments, read stdin, and hang rather than fail — a pipeline that
never finishes is worse than one that exits non-zero.

The generated PR body no longer claims a fixed "5 pipelines"; it counts.

Both files are also bumped here rather than left for a follow-up, and the order
matters: adding them to the consumer set without bumping them would make the
next refresh see two disagreeing tags and abort with "refusing to guess". That
guard is well built — it simply never saw these two files.

Refs #666
2026-08-27 13:19:41 +02:00
claude-bot a9df3ea9ce chore(ci): bump ci-claude image to 2.1.247
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
2026-08-27 11:12:42 +00:00
k.renner 2549535961 Merge pull request 'ci(kaniko): move the remaining eight builds to the maintained fork' (#665) from kaniko-fork-rest into master
ci/woodpecker/push/ci-claude-refresh Pipeline was successful
ci/woodpecker/push/build-ci-images Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#665
2026-08-27 11:11:22 +00:00
claude-botandClaude Opus 5 60f231f839 ci(kaniko): move the remaining eight builds to the maintained fork
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
ci/woodpecker/push/build-ci-images Pipeline was successful
Pipeline 6861 answered the question the staged first step was there to ask.
build-alpine-jq ran on the fork's v1.25.18, unpacked the rootfs, executed the
RUN, and pushed both destinations to the Gitea registry at digest a430c9c6 —
while the seven untouched kaniko steps stayed green in the same run. The
executor behaves the same and the registry auth path is unaffected, so the rest
can follow.

This moves the other seven steps here plus the one in ci-claude-refresh.yml.
Nothing but the image: lines change; no flags, no commands, no credentials.
That leaves zero gcr.io/kaniko-project references in the pipelines.

The per-step note from the first commit is gone. It described a migration state
that no longer exists, and a comment that survives its own situation is how the
velero schedule ended up claiming there was no CNPG cluster in a namespace that
had one. The durable half moved to the file header, where it explains why the
kaniko image is not Google's and why the pin is past the version their repo
ended on.

Also corrects the admission policy's comment, which cited
gcr.io/kaniko-project/executor as the image its exemption was verified against.
Worth being precise about what did not change: the policy matches on the buildx
plugin image only and never on kaniko, so the swap needed no policy edit — which
is exactly why the stale image reference in the prose was easy to miss.

Closes #552

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 13:03:18 +02:00
k.renner b03d0652c2 Merge pull request 'ci(kaniko): move alpine-jq to the maintained fork' (#664) from kaniko-replacement into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
ci/woodpecker/push/build-ci-images Pipeline was successful
Reviewed-on: k.renner/homelab#664
2026-08-27 10:58:49 +00:00
claude-botandClaude Opus 5 4e040c5a2d ci(kaniko): move alpine-jq to the maintained fork
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/push/build-ci-images Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
#552 concluded that kaniko is terminal and that the nine build steps need
migrating to buildah or BuildKit. The premise is wrong. Google archived
GoogleContainerTools/kaniko on 2025-06-03, but the original authors forked it to
chainguard-dev/kaniko and kept shipping: releases run to v1.25.18, well past the
v1.24.0 the Google repo ended on. Upstream moved, it did not die.

So this is an image swap, not a migration. Same /kaniko/executor, same flags,
nothing else in the step changes. The fork publishes no binaries of its own, so
the image comes from gitlab-ci-utils, which builds it from that fork for amd64
and arm64 with versioned tags — the generic :debug tag exists too and is
deliberately not used, since every other image reference here is pinned.

Verified by pulling the image and running it rather than by reading its README:
/kaniko/executor is at the same path and reports v1.25.18, /bin/sh is a symlink
into busybox, and printf, base64, tr, mkdir and date are all present — the
utilities the auth and tag lines depend on. Anonymous pull works, so no registry
credential is needed for it.

Only this one step moves. A change to this file rebuilds every step regardless,
so a single run puts the new executor on alpine-jq beside seven unchanged kaniko
builds and shows both in the same log. alpine-jq is the smallest context here,
one FROM and one RUN, and the remaining seven plus ci-claude-refresh.yml follow
once this is green.

Two counts in #552 are also wrong and worth correcting there: the pin was
v1.23.2-debug everywhere, not v1.24.0-debug — #551 bumped alpine, not kaniko —
and there are eight steps in this file plus one in ci-claude-refresh.yml, so
nine invocations rather than ten.

Refs #552

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:54:24 +02:00
k.renner 900bc73635 Merge pull request 'docs: propose separating the four traffic classes onto their own VLANs' (#663) from proposal-traffic-classes into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#663
2026-08-27 10:26:58 +00:00
k.renner a65109fee0 Merge pull request 'docs(runbook): drop the claim that a cold rebuild needs console access' (#662) from runbook-remote-rebuild-possible into master
Reviewed-on: k.renner/homelab#662
2026-08-27 10:26:30 +00:00
k.renner 477b79cdc6 Merge pull request 'fix: [alert] VeleroBackupFailed (velero) (#657)' (#659) from alert-fix/issue-657 into master
Reviewed-on: k.renner/homelab#659
2026-08-27 10:26:03 +00:00
claude-botandClaude Opus 5 55ea98c035 docs: answer the direct-routing question from source instead of a test
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The first draft ended on an open question and proposed a two-VM sandbox to
answer it. That was unnecessary: the question is answerable from the Cilium
source and the agent flag reference, and the answer is negative.

installDirectRoute takes its next-hop as a parameter, and nodeUpdate supplies
GetNodeIP(false), which prefers NodeInternalIP. NodeInternalIP comes from the
Kubernetes Node object; --ipv4-node looks like an override but initFromK8s()
runs after initFromConfig() and overwrites it. --direct-routing-device selects
a device, not an address. The same value feeds the VXLAN endpoint in tunnel
mode.

So cluster-internal traffic is welded to the kubelet InternalIP, and the routing
mode does not matter — tunnel sources the VXLAN from it, native uses it as the
next-hop. That deletes the one disruptive, non-rolling step the first draft
contained. Native routing buys nothing for this problem.

Since cluster-internal cannot be moved, the separation has to come from moving
management services off the node IP, and those do not share a mechanism, so each
is checked separately: etcd moves via cluster.etcd.advertisedSubnets, the
apiserver endpoint is already a VIP whose Layer2VIPConfig carries a link field,
the Talos API turned out to answer on every node address, and kubelet cannot
move because it is the node IP.

The result is four VLANs, but the boundary does not fall where the class names
suggest — kubelet stays with cluster-internal. Stated plainly rather than filed
under management and hoped over.

Recommends leaving the node IP where it is and relabelling VLAN 40 as the
cluster VLAN, rather than moving every node's identity to a new VLAN. Same end
state, and the address the cluster is administered through never changes.

Also records that the Ceph pattern is the only mechanism that fully detaches a
workload from the node IP, and that it is too expensive to generalise to
east-west traffic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 11:49:19 +02:00
claude-botandClaude Opus 5 6a50ab1daa docs: propose separating the four traffic classes onto their own VLANs
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Issue #576 framed this as "give node management its own NIC, and Ceph public
its own VLAN". That is two of four classes, and one of them turned out to be
solved already — Ceph client I/O riding the pod network is correct, since that
traffic is cluster-internal.

The classes are Ceph replication (done, VLAN 49), cluster-internal,
cluster-external and management. Three of the four share mgmt0, which was never
a decision: until the bare-metal conversion there was one NIC.

Each class gets a VLAN; which interface a VLAN lands on stays a separate and
changeable decision. That ordering means the separation survives recabling and
does not need one port per class.

Three of the four are controllable with documented levers. Management is the
InternalIP and does not move — everything else moves away from it, which is
also the direction that never changes the address the cluster is administered
through. Ingress is CiliumBGPNodeConfigOverride's per-peer localAddress, on a
BGP session that has been running for months. Egress follows the output device
under eBPF masquerading.

Cluster-internal is the one that does not resolve. In tunnel mode VXLAN sources
from the node IP with no documented alternative. Native routing is the way out
and its prerequisite is already met — BGP has been advertising the pod CIDRs to
the UDM all along — but whether nodePort.directRoutingDevice moves the next-hop
address or only the interface is undocumented across the three Cilium pages
checked.

So the proposal ends on a question rather than a plan, and the next step is a
two-interface Talos sandbox, not a cable. If the answer is unfavourable the
design changes, not the wiring.

Records what was measured rather than assumed: the BGP session state, that
Cilium's direct-routing device follows the InternalIP (adding a second address
elsewhere left it on mgmt0), that the override CRD exposes localAddress, and
where Ceph's two address families actually sit.

Also states what the split does not buy. Management shares a 10 Gb link and is
not contended; nothing observed is slow because of this.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 11:34:41 +02:00
claude-botandClaude Opus 5 f51eb8bf35 docs(runbook): drop the claim that a cold rebuild needs console access
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
The rewrite earlier today stated, at the top and again in known gaps, that the
rebuild cannot be driven remotely: management is a tagged VLAN, a maintenance
mode node cannot configure VLANs, so it comes up unreachable and every machine
needs a keyboard.

That is wrong. A maintenance-mode node DHCPs untagged and is reachable at that
address — it is not on management, but it answers, and that is precisely the
address `apply-config --insecure` targets in Step 2. All three bare-metal
conversions were driven that way.

The claim came from #576 and was carried into this document without being
checked against what had actually happened days earlier in this repo. It is the
worst place to put an unverified constraint: a disaster-recovery runbook is read
when there is no capacity to discover that a stated prerequisite is fiction, and
"you need physical access to three machines" is exactly the kind of statement
that changes whether someone attempts a recovery at all.

What survives is narrower and true: MS-01 has no IPMI and nothing here does
PXE, so booting from USB is physical. Everything after the boot is remote.

Also removes the follow-on error from the first correction, which claimed the
switch port change on 2026-08-27 had fixed a reachability problem. There was no
reachability problem to fix. That change is real and useful — a maintenance mode
node now lands in management rather than the fallback network — but it did not
turn an impossible rebuild into a possible one.

#576 still carries the original claim and is flagged in known gaps as wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 11:25:50 +02:00
k.renner d1e072146f Merge pull request 'docs(runbook): rewrite the cold rebuild for bare metal' (#661) from runbook-cold-rebuild-baremetal into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#661
2026-08-27 08:11:24 +00:00
claude-botandClaude Opus 5 4678f73e9f docs(runbook): rewrite the cold rebuild for bare metal
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Step 1 was "tofu: provision the VMs" and there are no VMs. The OpenTofu stack
has no node definitions left — only the uninstantiated proxmox_vm module beside
DNS, object storage and UniFi — so `tofu apply` provisions nothing and Step 2
then has no cluster to form. The document was half-updated: it knew k8s-1 was
bare metal but still treated k8s-2 and k8s-3 as Proxmox VMs, which stopped
being true at the neptune conversion.

That matters more than an ordinary stale doc, because it is the document
someone reaches for when everything is gone — exactly when there is no capacity
to discover that step one is fiction.

Step 1 splits into two unrelated halves: a tofu apply for platform resources
that do still exist, and physically booting three machines from install media.

Step 2 stops calling `bootstrap.sh talos`, which cannot generate a config
(#581), and carries the procedure that actually works — the one
RUNBOOK-baremetal-conversion.md uses. Both traps it hides are now stated: the
whole cilium-cni.yaml cannot be passed as a patch, and the generated
HostnameConfig collides with the per-node hostname. So is the two-stage apply:
a config referencing an interface that does not exist yet hangs the install at
STAGE: Installing with nothing timing out.

Scoped that claim rather than repeating "bootstrap.sh is dead" wholesale — the
flux and wait-apps phases are still used in Steps 3, 5 and 6 and work. Only
config generation is broken, and saying otherwise would have left the runbook
contradicting itself three sections later.

Adds the constraint that decides whether this is even attemptable remotely, at
the top and again in known gaps: management is a tagged VLAN, a maintenance
mode node cannot configure VLANs, and MS-01 has no IPMI. Every node needs a
keyboard and a monitor until #576 puts VLAN 40 native on the I226-V port. That
was true before today and undocumented.

Also records what changed under the runbook recently: the schematic now carries
i915, Ceph's cluster network is a Multus attachment rather than host
networking, and OpenBao runs three replicas that each need unsealing
individually — a sealed server is neither a voter nor a standby, so doing one
leaves the cluster short of quorum.

Disk verification before install is now explicit. Talos enumerates NVMe in an
order that is not stable across boots, the per-node configs select by serial for
that reason, and the failure mode is the installer taking the Ceph OSD drive.

Closes #660.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 10:09:50 +02:00