Compare commits

...
Author SHA1 Message Date
claude-botandClaude Opus 5 2abd946c05 fix(rook-ceph): rotate CephX daemon keys to close CVE-2025-30156
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
Ceph v19.2.6 ships the fix for CVE-2025-30156, a CephX authentication
bypass: AES-128-CBC service tickets carry no HMAC and use a hard-coded
IV, so a holder of any low-privilege key can bit-flip a ticket into
cluster-admin in linear time. Upgrading the binaries does not resolve
it — the keys themselves must be rotated to the aes256k cipher.

The cluster is mid-procedure. auth_allowed_ciphers (aes, aes256k),
auth_preferred_cipher (aes256k) and auth_service_cipher (aes256k) are
already set, and the rotating service keys were wiped, but no actual
key rotation has happened: every entity in status.cephx is still
keyGeneration 1 with keyType aes, created under 19.2.3.

Rook automates daemon-key rotation but it is opt-in and defaults to
Disabled, so spec.security.cephx was left empty and nothing rotated.
Rook v1.20.6 and Ceph v19.2.6 are both past the version floor the
feature requires.

Rotating daemon keys clears AUTH_INSECURE_SERVICE_KEY_TYPE, the only
one of the six new checks at ERR severity. That is the operationally
urgent part: Rook skips CephFS and RGW reconciliation entirely while
health is HEALTH_ERR — which is what deadlocked the mds/rgw rollout in
issue #527 — and the check is presently held down only by a mon-side
mute with a finite TTL. When that mute lapses the cluster returns to
HEALTH_ERR and the reconcile freeze comes back.

CSI keys are deliberately left on aes. aes256k over the CSI mount path
requires a host kernel of 7.0 or newer and these nodes run 6.18, so
`aes` must stay in auth_allowed_ciphers for CSI to authenticate at all.
AUTH_INSECURE_CLIENT_KEY_TYPE, AUTH_INSECURE_KEYS_ALLOWED and
AUTH_INSECURE_KEYS_CREATABLE therefore stay raised at WARN until the
nodes run a kernel that supports it.

Rotation restarts most Ceph daemons. With size 2 pools across 3 OSDs
this should be applied deliberately rather than alongside other churn,
and Ceph should reach HEALTH_OK before anything else touches the
cluster.

Refs #527, #528

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-21 22:05:10 +02:00
claude-bot cf6ff839be fix(monitoring): CephHealthWarn has been firing continuously and hiding everything
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
CephHealthWarn alerted on `ceph_health_status == 1`. It has been FIRING since
at least 2026-08-21T00:26Z and cannot clear: four AUTH_INSECURE_* checks are
permanently active from CVE-2025-30156 (#527) and stay that way until Rook
>=1.20.5 lands. A permanently-firing alert reports nothing -- every Ceph
warning since then has been invisible behind it.

This is a pre-existing gap, not a consequence of the cephfs-ci work, though
that work would have added a second permanent cause (POOL_NO_REDUNDANCY, since
the CI pools are size 1 on purpose).

ceph_health_detail exposes each check as its own series, so the alert now fires
on any check that is NOT known-accepted, and names it. Verified against live
data: 5 checks active, 0 unexpected, so it is quiet now and fires the moment
something genuinely new appears.

Excluding POOL_NO_REDUNDANCY wholesale would give something up: that check is
cluster-wide and does not say WHICH pool, so accepting it for cephfs-ci would
equally hide a production pool dropping to one replica. CephPoolLostRedundancy
restores that. There is no pool-size metric, but bytes_used/stored IS the
replication factor -- verified live as replicapool 2, cephfs-data0 2,
cephfs-metadata 3, cephfs-ci-metadata 1.

Both alerts were tested against live data before committing, in BOTH
directions. The positive control matters as much as the negative one: an early
draft used a >1e9 floor which silently excluded the CI pools entirely, so the
expression could not have matched anything. It looked correct precisely because
it returned no results. The control caught it; the floor is now 1e6.

The floor is not what suppresses the near-empty rgw pools. Their ratios are
9-36 because per-object overhead dominates, so they never match `< 1.5`.

6h on CephHealthWarn is unchanged -- Ceph warns for self-clearing conditions
and paging on those trains people to ignore the alert.
2026-08-21 09:53:37 +02:00
k.renner a11493ee22 Merge pull request 'perf(woodpecker): put CI workspaces on the unreplicated CephFS' (#543) from repoint-ci-workspaces into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#543
2026-08-21 07:32:11 +00:00
claude-bot 7666d220c5 perf(woodpecker): put CI workspaces on the unreplicated CephFS
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
Repoints WOODPECKER_BACKEND_K8S_STORAGE_CLASS at ceph-filesystem-ci, the
dedicated filesystem added in #542 whose pools are both size 1.

Measured 2026-08-21, polling both CephFS pools every 15s:

  CI active   1.5 min (6 intervals):  data0 143.2 MB   metadata 17.2 MB
  CI idle    16.0 min (63 intervals): data0   0.0 MB   metadata 11.0 MB
  CI share of writes: data0 100%, metadata 61%

data0 takes zero bytes across 16 minutes without a pipeline, so its entire
write volume is CI. The 15s cadence is what made this visible: a pipeline
creates, uses and deletes its PVC inside ~2 minutes, so a 5-minute sample
reported no CI pods during bursts CI was causing.

Per pipeline this moves the drive cost from 71.6x2 + 8.2x3 = ~168 MB to
~80 MB, since neither pool is replicated on the new filesystem.

RWX is unchanged and still required: pipeline steps share the workspace PVC
and the k8s backend can schedule them on different nodes, so an RWO volume
fails to mount on the second node. Only the filesystem underneath changes.

Also corrects the justification written into #542 for leaving
ceph-filesystem-ephemeral alone. That commit claimed restore-test.sh handles
"REAL data which must not land on unreplicated pools". That is wrong: its
probe PVCs are as throwaway as a CI workspace -- the authoritative copy is the
backup on Hetzner, and losing a probe just means re-running the test.

The actual reason it cannot move is technical: those probes are snapshot
CLONES, and `ceph fs subvolume snapshot clone` operates within a single
filesystem, so a snapshot taken in `cephfs` cannot be cloned into `cephfs-ci`.
Landing them unreplicated would require a second size-1 data pool inside
`cephfs`; restore-test runs weekly against CI's ~96 pipelines/day, so that is
not worth the complexity.

cephfs-ci verified healthy before this change: PHASE Ready, MDS Running, both
pools reporting size 1.
2026-08-21 09:31:01 +02:00
k.renner baa5e62bbe Merge pull request 'feat(rook-ceph): add an unreplicated CephFS for CI workspaces' (#542) from ci-throwaway-cephfs into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#542
2026-08-21 07:27:47 +00:00
claude-bot 503d2a4876 feat(rook-ceph): add an unreplicated CephFS for CI workspaces
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
Woodpecker's Kubernetes backend creates a PVC per pipeline, fills it with a
checkout plus build output, and deletes it when the pipeline ends. Those
workspaces currently live on the shared `cephfs`, so throwaway data is
replicated as if it mattered: 2x in cephfs-data0 and 3x in cephfs-metadata.

Measured 2026-08-21 by polling both pools every 15s. The interval matters --
at 5 minutes a whole pipeline is invisible, because its PVC is created, used
and deleted inside ~2 minutes. Sampling that slowly reported zero CI pods
during bursts CI was causing, which sent this investigation down three wrong
paths before the cadence was fixed.

  one pipeline, a single 15s interval:  data0 +71.6 MB   metadata +8.2 MB
  every interval with no CI pod:        data0   ~0        metadata   ~0

So CI accounts for essentially all CephFS write volume; between pipelines the
metadata pool is quiet at ~4 MB/day. On the shared filesystem one pipeline
costs 71.6x2 + 8.2x3 = ~168 MB of drive writes. On this one it costs ~80 MB.

size 1 is correct here even though it was rejected for `cephfs`. There,
metadataPool stays at 3 because losing it destroys the entire filesystem
rather than the objects on one OSD, and 550 GiB of real data depends on it.
Here every byte is a fresh git clone: total loss means in-flight pipelines
fail and get re-run. Replicating throwaway data was the cost, so it is what
gets removed.

`ceph-filesystem-ephemeral` is deliberately NOT repointed. It has a second
consumer -- velero/restore-test.sh restores REAL data through it for
verification, which must not land on unreplicated pools. A new class also
sidesteps StorageClass.parameters being immutable.

Nothing uses ceph-filesystem-ci yet, so this commit is inert: it provisions
the filesystem and its MDS and changes no existing workload. Repointing the
Woodpecker agent is a separate change, once this is confirmed healthy.

Known consequence: size 1 raises POOL_NO_REDUNDANCY, so cluster health will
warn until that is addressed. Silencing it via mon_warn_on_pool_no_redundancy
is cluster-wide and would also hide a production pool dropping to 1, so it
wants a targeted alert instead -- deliberately left out of this commit rather
than switching the warning off blind.

The MDS is 512Mi/1Gi with no standby, not the 4Gi active+standby that `cephfs`
runs. That figure is driven by immich's cached inode count; a CI workspace is
a few thousand files that vanish with the pipeline, and a second daemon to
protect throwaway work is not worth its memory.
2026-08-21 08:55:33 +02:00
security-bot 48407aa202 chore(image-automation): bump redis:8.10.1-alpine
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-08-20 17:37:50 +00:00
security-bot ce6e8ba2cb chore(image-automation): bump redis:7.4.11-alpine
ci/woodpecker/cron/alert-monitor Pipeline was successful
Updates managed by image-automation-controller.
2026-08-20 17:19:40 +00:00
k.renner 96387108ff Merge pull request 'perf(cnpg): roll archive_timeout out to the remaining six clusters' (#541) from rollout-archive-timeout into master
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#541
2026-08-20 13:39:00 +00:00
7 changed files with 203 additions and 15 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ spec:
# Pinned to 7.4-alpine to match the LXC 225 compose exactly.
# Any redis major-version bump is a separate migration.
- name: redis
image: redis:7.4.10-alpine # {"$imagepolicy": "flux-system:redis-immich"}
image: redis:7.4.11-alpine # {"$imagepolicy": "flux-system:redis-immich"}
args:
- redis-server
- --save
@@ -12,18 +12,65 @@ spec:
- name: ceph.rules
rules:
- alert: CephHealthWarn
# ceph_health_status: 0 = HEALTH_OK, 1 = HEALTH_WARN, 2 = HEALTH_ERR.
# 6h is deliberately long. Ceph warns for self-clearing conditions —
# a slow BlueStore op during the Sunday restore sweep, a mon briefly
# below its disk threshold — and paging on those trains people to
# ignore the alert. What matters is a warning that does not clear.
expr: ceph_health_status == 1
# Was `ceph_health_status == 1`, which had been FIRING CONTINUOUSLY
# since at least 2026-08-21T00:26Z and was therefore telling us
# nothing: four AUTH_INSECURE_* checks are permanently active
# (CVE-2025-30156, issue #527) and cannot clear until Rook >=1.20.5.
# A permanently-firing alert masks every subsequent Ceph warning,
# which is the opposite of what this rule is for.
#
# ceph_health_detail exposes each check individually, so alert on any
# check that is NOT a known-accepted one. Verified against live data:
# 5 checks active, 0 unexpected — so this is quiet now and fires the
# moment a genuinely new condition appears.
#
# Accepted, each for a documented reason:
# AUTH_INSECURE_* CVE-2025-30156, tracked in #527
# POOL_NO_REDUNDANCY cephfs-ci is size 1 ON PURPOSE (CI
# workspaces are throwaway). Non-CI pools
# losing redundancy is caught by
# CephPoolLostRedundancy below, since this
# check does not say WHICH pool.
#
# 6h is kept: Ceph warns for self-clearing conditions — a slow
# BlueStore op during the Sunday restore sweep, a mon briefly below
# its disk threshold — and paging on those trains people to ignore
# the alert. What matters is a warning that does not clear.
expr: ceph_health_detail{name!~"AUTH_INSECURE_.+|POOL_NO_REDUNDANCY"} == 1
for: 6h
labels:
severity: warning
annotations:
summary: "Ceph has been HEALTH_WARN for 6h"
description: "The Ceph cluster has held HEALTH_WARN for six hours, so it is not a self-clearing blip. Get the detail with `kubectl -n rook-ceph exec deploy/rook-ceph-tools -- ceph health detail`."
summary: "Ceph health check {{ $labels.name }} active for 6h"
description: "Ceph health check {{ $labels.name }} has been active for six hours, so it is not a self-clearing blip. Known-accepted checks (AUTH_INSECURE_*, POOL_NO_REDUNDANCY) are excluded from this alert, so this is a new condition. Detail: `kubectl -n rook-ceph exec deploy/rook-ceph-tools -- ceph health detail`."
- alert: CephPoolLostRedundancy
# Restores what excluding POOL_NO_REDUNDANCY from CephHealthWarn
# gives up. That check fires cluster-wide without naming a pool, so
# accepting it for cephfs-ci would also hide a PRODUCTION pool
# dropping to one replica.
#
# There is no pool-size metric, but bytes_used/stored IS the
# replication factor. Verified live: replicapool 2, cephfs-data0 2,
# cephfs-metadata 3, cephfs-ci-metadata 1.
#
# The >1e6 floor excludes trivially small pools. It is not needed to
# suppress the near-empty rgw pools — their ratios are 9-36 because
# per-object overhead dominates, so they never match `< 1.5` anyway.
#
# Both directions were tested against live data before committing:
# including cephfs-ci the expression matches (pool 13), excluding it
# the expression is empty. An alert that cannot fire is worse than no
# alert, so the positive control matters as much as the negative.
expr: |
(ceph_pool_bytes_used / ceph_pool_stored < 1.5 and ceph_pool_stored > 1e6)
and on(pool_id) ceph_pool_metadata{name!~"cephfs-ci-.+"}
for: 30m
labels:
severity: critical
annotations:
summary: "Ceph pool {{ $labels.pool_id }} is running unreplicated"
description: "Pool {{ $labels.pool_id }} has a replication factor below 1.5, meaning a single OSD loss destroys data. Only the cephfs-ci pools are meant to be unreplicated. Check with `kubectl -n rook-ceph exec deploy/rook-ceph-tools -- ceph osd pool ls detail`."
- alert: CephHealthError
# HEALTH_ERR means data availability or redundancy is at risk.
+1 -1
View File
@@ -25,7 +25,7 @@ spec:
type: RuntimeDefault
containers:
- name: redis
image: redis:8.10.0-alpine # {"$imagepolicy": "flux-system:redis-paperless"}
image: redis:8.10.1-alpine # {"$imagepolicy": "flux-system:redis-paperless"}
args:
- redis-server
- --save
+32 -5
View File
@@ -50,12 +50,39 @@ spec:
# clone + subsequent steps share the workspace PVC and the k8s backend
# can schedule them on different nodes -> an RWO volume then fails to
# mount on the second node. CephFS (RWX) lets the workspace follow
# the pipeline across nodes. The -ephemeral variant (reclaim Delete,
# rook-ceph-cluster/storageclass.yaml) is REQUIRED here: workspaces
# are throwaway, and the Retain class leaked one Released PV per
# finished pipeline (issue #262 follow-up).
# the pipeline across nodes. Reclaim MUST be Delete: workspaces are
# throwaway, and a Retain class leaked one Released PV per finished
# pipeline (issue #262 follow-up).
#
# ceph-filesystem-ci, not ceph-filesystem-ephemeral: a dedicated
# CephFS whose pools are BOTH size 1 (infrastructure/
# rook-ceph-cluster/cephfs-ci.yaml). The shared filesystem
# replicates data0 2x and metadata 3x, which is the correct trade
# for real data and pure waste for a git clone that is deleted
# minutes later.
#
# Measured 2026-08-21, polling both pools every 15s (a 5-minute
# interval misses an entire pipeline -- PVC created, used and
# deleted inside ~2 minutes):
# CI active 1.5min: data0 143.2 MB, metadata 17.2 MB
# CI idle 16.0min: data0 0.0 MB, metadata 11.0 MB
# data0 write volume is 100% CI. Per pipeline this moves the drive
# cost from 71.6x2 + 8.2x3 = ~168 MB to ~80 MB.
#
# ceph-filesystem-ephemeral is NOT repointed, and the reason is a
# technical constraint rather than a safety one. Its other consumer
# is velero/restore-test.sh, whose probe PVCs are snapshot CLONES:
# `ceph fs subvolume snapshot clone` operates within one filesystem,
# so a snapshot taken in `cephfs` cannot be cloned into `cephfs-ci`.
#
# Those probes are just as throwaway as a CI workspace -- the
# authoritative copy is the backup on Hetzner, and a lost probe just
# means re-running the test -- so they are equally undeserving of
# replication. Landing them unreplicated would mean a second size-1
# data pool inside `cephfs`, and restore-test runs WEEKLY against
# CI's ~96 pipelines/day, so it is not worth the complexity.
- name: WOODPECKER_BACKEND_K8S_STORAGE_CLASS
value: ceph-filesystem-ephemeral
value: ceph-filesystem-ci
- name: WOODPECKER_BACKEND_K8S_STORAGE_RWX
value: "true"
# MUST be a JSON object literal — the backend json.Unmarshal's this
@@ -17,6 +17,33 @@ spec:
modules:
- name: pg_autoscaler
enabled: true
# CVE-2025-30156: CephX AES-128-CBC service tickets carry no HMAC and use a
# hard-coded IV, so a holder of any low-privilege key can bit-flip a ticket
# into cluster-admin. The fix is rotating keys to the aes256k cipher —
# upgrading the binaries alone does not close it. Rotation is opt-in and
# defaults to Disabled, so it must be requested here.
#
# Daemon keys only. CSI keys stay on aes: aes256k over the CSI mount path
# needs a host kernel of 7.0 or newer and these nodes run 6.18. `aes`
# therefore has to remain in auth_allowed_ciphers for CSI to authenticate,
# which leaves AUTH_INSECURE_CLIENT_KEY_TYPE, AUTH_INSECURE_KEYS_ALLOWED and
# AUTH_INSECURE_KEYS_CREATABLE raised. Revisit once the nodes run a kernel
# that supports it.
#
# Rotating the daemon keys clears AUTH_INSECURE_SERVICE_KEY_TYPE, the only
# one of the six checks at ERR severity. That matters beyond the CVE itself:
# Rook skips CephFS and RGW reconciliation entirely while health is
# HEALTH_ERR, and that check is currently held down only by a mon-side mute
# with a finite TTL.
#
# keyType is deliberately unset — Rook picks the best type for daemon keys.
# To rotate again later, increment keyGeneration; Rook restarts the daemons
# to pick up the new keys.
security:
cephx:
daemon:
keyRotationPolicy: KeyGeneration
keyGeneration: 2
# Bump CephFS subvolume cloner pool from 4 → 16. Velero CSI Snapshot
# Data Movement provisions a backupPVC per source PVC by cloning the
# VolumeSnapshot, and a 4-thread default stalls migrate-immich's
@@ -0,0 +1,86 @@
---
# A SEPARATE CephFS whose only purpose is Woodpecker CI workspaces, with both
# pools at size 1.
#
# WHY THIS EXISTS
# Woodpecker's Kubernetes backend creates a PVC per pipeline, fills it with a
# checkout plus build output, and deletes it when the pipeline ends. Measured
# 2026-08-21 by polling both pools at 15s (a 5-minute interval misses a whole
# pipeline — its PVC is created, used and deleted inside ~2 minutes):
#
# one pipeline, a single 15s interval: data0 +71.6 MB metadata +8.2 MB
# every interval without a CI pod: data0 ~0 metadata ~0
#
# CI drove essentially all of it. On the shared filesystem that costs
# 71.6x2 + 8.2x3 = ~168 MB of drive writes per pipeline. Here it costs ~80 MB.
#
# WHY size 1 IS CORRECT HERE, HAVING BEEN REJECTED FOR THE MAIN FILESYSTEM
# `cephfs` keeps metadataPool at size 3 because losing it destroys the whole
# filesystem, not the objects on one OSD, and 550 GiB in cephfs-data0 depends
# on it. None of that applies to this filesystem. Every byte in it is a fresh
# git clone; the worst case of total loss is that in-flight pipelines fail and
# get re-run. Replicating throwaway data is the thing being paid for, so it is
# the thing removed.
#
# Losing an OSD takes these PGs offline and kills running pipelines. That is
# not a regression: CI already breaks during Ceph maintenance.
apiVersion: ceph.rook.io/v1
kind: CephFilesystem
metadata:
name: cephfs-ci
namespace: rook-ceph
spec:
metadataPool:
replicated:
size: 1
# Rook refuses size 1 without this — it is asserting that an unreplicated
# pool is deliberate rather than a typo. It is.
requireSafeReplicaSize: false
dataPools:
- name: data0
replicated:
size: 1
requireSafeReplicaSize: false
metadataServer:
activeCount: 1
# No standby-replay, unlike `cephfs`. A standby exists there so a failed
# MDS does not hang every CephFS mount cluster-wide; here the only casualty
# is a running pipeline, and a second daemon to protect throwaway work is
# not worth its memory.
activeStandby: false
resources:
# Deliberately NOT the 4Gi that `cephfs` needs. That figure is driven by
# immich's millions of cached inodes; a CI workspace is a few thousand
# short-lived files that vanish with the pipeline.
requests:
cpu: 100m
memory: 512Mi
limits:
memory: 1Gi
---
# CI-only class. `ceph-filesystem-ephemeral` is deliberately left pointing at
# the main filesystem: velero/restore-test.sh uses it to restore REAL data for
# verification, which must not land on unreplicated pools.
#
# A new class rather than repointing the old one — StorageClass.parameters are
# immutable, so `fsName` cannot be edited in place.
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: ceph-filesystem-ci
provisioner: rook-ceph.cephfs.csi.ceph.com
parameters:
clusterID: rook-ceph
fsName: cephfs-ci
pool: cephfs-ci-data0
csi.storage.k8s.io/provisioner-secret-name: rook-csi-cephfs-provisioner
csi.storage.k8s.io/provisioner-secret-namespace: rook-ceph
csi.storage.k8s.io/controller-expand-secret-name: rook-csi-cephfs-provisioner
csi.storage.k8s.io/controller-expand-secret-namespace: rook-ceph
csi.storage.k8s.io/node-stage-secret-name: rook-csi-cephfs-node
csi.storage.k8s.io/node-stage-secret-namespace: rook-ceph
# Delete, for the same reason ceph-filesystem-ephemeral uses it: with Retain,
# every finished pipeline leaks a Released PV pinning a subvolume (191 of them
# by 2026-07-16, issue #262 follow-up).
reclaimPolicy: Delete
allowVolumeExpansion: true
@@ -4,5 +4,6 @@ resources:
- cephcluster.yaml
- toolbox.yaml
- storageclass.yaml
- cephfs-ci.yaml
- volumesnapshotclass.yaml
- objectstore.yaml