main-1d03144 adds the per-account register field `rotate: false`
(mqtt-account-controller PR #6, reviewed in REN-1073). Set it on shelly-2
only. Both changes go in one commit: main-a307693 rejects a register with
the unknown field.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump mqtt-account-controller to main-4adc9a6 (shelly-manager#93 client),
give it the shelly-manager API key and URL, and register shelly-5 (Plus i4)
as the first auth: password account. The legacy user shellies stays.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agent runs mostly execute in the paperclip pod, not in a sandbox (REN-1001:
TCP timeout to :8884 from paperclip-b65cc7768). Adds the egress rule in
paperclip-egress and the paperclip pod as a source in mosquitto-agent-ingress.
:8884 only; it requires an mqtt-ca client certificate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Register entry agents-read (read open3e/#, zigbee2mqtt/#, homeassistant/#),
delivered to the paperclip namespace; agent kubectl identities may get that
one Secret; dedicated CNPs open agent sandbox pods -> mosquitto :8884.
Nothing restarts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Register entry for Home Assistant (ACL readwrite # as the legacy user
homeassistant, different name so it cannot shadow that login) and its Secret
on the controller's revocation list. Nothing restarts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
evcc gets broker URL and the PEMs of the register account evcc-client by
env (EVCC_MQTT_*), which override the mqtt block of evcc.yaml in OpenBao.
Reloader restarts evcc when cert-manager renews the certificate.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Register entry evcc-client (deliver evcc, ACL as the legacy user evcc), a
controller Role/RoleBinding in the evcc namespace for its Certificate and
Secret, and :8884 next to :1883 in both evcc<->mosquitto network policies.
Nothing restarts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Z2M, discovery-guard and interview-watch use the certificate of the
register account zigbee2mqtt-bridge on the mTLS listener :8884 instead of
the legacy password. Z2M gets server/ca/cert/key by env because its
configuration.yaml on the PVC is never re-seeded. The init container no
longer renders mqtt_password. The legacy broker user stays until a later
broker roll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the register entry for Zigbee2MQTT (same ACL as the legacy user
zigbee2mqtt, different name so it cannot shadow the running password login)
and its Secret to the controller's revocation list. Nothing restarts; the
controller picks the register up live and issues mqtt-zigbee2mqtt-bridge-tls
before the Z2M switch lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the register account mosquitto-probe (auth: cert, readwrite health/#),
mounts its certificate into the broker and switches the liveness/readiness
probes to the mTLS listener :8884. Removes the legacy password user health
from passwd, acl, env and the Reloader annotation. The ExternalSecret and
rotator stay until the REN-802 cleanup step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 1473: shelly-manager signs in whoever X-authentik-username names when
the peer is in the pod CIDR, so opening :8080 to agent pods let any run sign
in with a forged header and no API key.
Agents now get an nginx sidecar on ${POD_IP}:8081 (Service
shelly-manager-api) that drops X-authentik-username and X-Forwarded-User
before proxying to :8080. Both agent CNPs move from 8080 to 8081, so :8080
stays outpost-only and every agent call needs the API key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Egress from paperclip-renner agent pods and a dedicated ingress CNP on
shelly-manager, container port 8080 (socket-LB). All agents: sandbox pods
carry no per-agent label; the API key gates calls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The REN-902 build denies publishClientReceive by default and grants receive
per read ACL. On its first loop it rewrites the Secret mqtt-dynsec-seed with
that default. The seed must be rewritten before the production broker loads
it (homelab #1159). Still points at the test broker.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
REN-802 step 2. The production mosquitto loads the Dynamic Security plugin
and gets a mutual-TLS listener :8884 (server certificate from mqtt-ca).
mqtt-account-controller now manages this broker instead of the test broker.
The nine legacy users keep password_file and acl_file unchanged.
The Dynamic Security default denies publishClientReceive: with acl_file in
the chain, mosquitto 2.0 does not gate SUBSCRIBE, so a receive default of
allow would let every Dynamic Security client read every topic.
The image policy stays on 2.0.x, because 2.1 changes the plugin chain.
The obsolete delete grant on mqtt-test-revoke-tls is dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The controller must delete Certificate mqtt-test-revoke, Secret
mqtt-test-revoke-tls and the dynsec client and role acct-test-revoke on its
next loop. Only the register ConfigMap changes; nothing restarts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
REN-802 step 1. Adds:
- infrastructure/post-crds/mqtt-ca.yaml: private MQTT CA (self-signed root
in cert-manager ns) and ClusterIssuer mqtt-ca.
- apps/home-assistant/mosquitto-dynsec-test.yaml: throwaway mosquitto 2.0.22
with the dynsec plugin, mTLS listener (require_certificate,
use_identity_as_username), initContainer that loads Secret
mqtt-dynsec-seed or a static base file. ClusterIP only; not labelled
mosquitto, so prod Services and the world-ingress policy do not select it.
- apps/home-assistant/mqtt-account-controller.yaml: ServiceAccount, RBAC
(cluster-wide list certificates; certificates in home-assistant; secrets
by resourceName only), controller client cert CN mqtt-account-controller,
Deployment main-47213ef pointed at the test broker.
- apps/home-assistant/mqtt-accounts.yaml: register with two cert test
accounts, delivered as ConfigMap mqtt-accounts.
The production broker is not changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The kubernetes sandbox plugin's release and destroy hooks returned void,
so no lease got a termination receipt and every cancelled pod run left
its card waiting on remote_cleanup. Return state destroyed once the
Sandbox CR / Job (and pod) reads 404, within 20 s; otherwise no receipt.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The kubernetes sandbox plugin returns no termination receipt, so the
leases of cancelled runs e08976cd (REN-837) and 144ae3bf (REN-825) were
released without one and the queued owner comments wait on
remote_cleanup forever. The controller log shows both Sandbox CRs
deleted. Write the missing receipt for exactly those leases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The quarterly rotation on 2026-12-01 would change the broker password while
the ESP32 gateway keeps the old one, taking it offline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One-shot Job that sends three read commands on open3e/cmnd to edge-1's
running open3e (read-json + read-raw on 0x6c3, one read-json on 0x6e0)
and prints the replies for Loki. No write mode, no config change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The capture answered REN-805 after 24 min (no setpoint in bytes 42-56);
the owner stopped the remaining 24 h window. Flux prune deletes the Job
and its pod.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
open3e names the topics ..._ZigBeeSixDeviceCurrentValues, so the
[0-9]+ pattern matched none of the 18 DIDs (Argus review 1437).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Job subscribes to open3e/+ as the read-only evcc broker user and prints the
18 One Base FBH ZigBee<N>DeviceCurrentValues messages with a UTC timestamp
to stdout (Loki). Ends after 24 h via mosquitto_sub -W.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The authentik-samba-bridge hook token was burned in a run log (REN-729).
Woodpecker 3.18 cannot re-key a repo hash via API, so a one-shot Job
sets a new random repos.hash generated inside Postgres. Remove after run.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No pod has attached net1 since cni-plugins-installer moved to
containernetworking/plugins v1.9.1 (#1142). Hestia's verify item: restart
mdns-relay and confirm a 10.2.30.x DHCP lease. Atlas has no cluster write,
so the restart goes through a pod-template annotation.
Refs: REN-722
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1132 staged OpenBao 2.7.1 in the StatefulSet template, but OnDelete
leaves the pods on 2.6.1 until someone deletes them, and no agent holds
pod delete in openbao. The static seal makes OnDelete unnecessary: the
readiness probe (bao status) passes only once a pod is unsealed, so the
controller replaces one pod at a time and waits for each.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Point the installer at cni-plugins:2026-10-05, built from #1140
(plugins v1.5.1 -> v1.9.1). Version comments updated in the installer
and cni-dhcp-daemon.
Refs: REN-722
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A VolumeSnapshot of influxdb-influxdb2 on ceph-block-snapclass, to be
merged and ready before homelab #1130 (2.7.4 -> 2.9.1) so the upgrade has
a restore point beyond `influxd downgrade`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Chart 0.30 moves the server 2.6.1 -> 2.7.1. Record the rollout rule
next to the pin: raft snapshot first, OnDelete one pod at a time, undo
by snapshot restore once a pod has run the new server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
InfluxDB 2.9 hashes tokens on startup by default. The HA and Grafana
token Jobs read tokens back via `influx auth list`, which returns no
token once hashed, and a downgrade below 2.8 would erase all tokens.
Set INFLUXD_USE_HASHED_TOKENS=false so the 2.9.1 bump stays reversible
with `influxd downgrade 2.4`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refresh the plugins pin alongside multus v4.3.1 (#1131), as the
cni-plugins-installer.yaml comment asks. SHA256 cross-checked against
upstream's cni-plugins-linux-amd64-v1.9.1.tgz.sha256 asset. The tarball
still ships macvlan, static, host-local and dhcp.
Image only: CI builds cni-plugins:<date>. The DaemonSet tag bump follows
in a separate PR once the image exists.
Refs: REN-722
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream deployments/multus-daemonset-thick.yml v4.2.4 -> v4.3.1 adds:
- ClusterRole rule: resource.k8s.io resourceclaims, resourceclaims/status,
resourceslices; get, list (new DRA client)
- terminationGracePeriodSeconds 10 -> 30
- CRD shortName nad
Header comment updated to v4.3.1 (v4.3.1 also has an unsuffixed tag; we
keep -thick to match the manifest).
Refs: REN-722
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Job pod templates are immutable. Without kustomize.toolkit.fluxcd.io/force,
an image bump (Renovate #1129, #1053, #1054) fails the server-side dry-run
and blocks the home-assistant and otbr Kustomizations. Same pattern as the
influxdb token Jobs.
The mosquitto Jobs exit 0 when their Secret exists. The otbr Job now also
exits 0 when the otbr Deployment is scaled to 0 (disabled), instead of
waiting for a pod that never comes and failing; Role gains get on
apps/deployments for that check.
Refs: REN-722
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
tini (PID 1, root) cannot signal node (uid 1000) without CAP_KILL, exits
with EPERM on every rollout, and the server is killed without its
graceful run drain. In-flight runs are orphaned instead of being
interrupted and retried.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One Matrix room per card labelled 'matrix' (REN-668). Sets
PAPERCLIP_PUBLIC_URL for the card link in the room topic.
Rollback: build.1791177680 and drop the env var.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Route agent comments on author account only, from konairius/matrix-bridge#5
(9a83e70, REN-668). No env, secret, port or network change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three Matrix Ingresses had no dns.renner.uno/public label, so
netbird-service-sync never created a NetBird proxy service for them and the
public proxy rejects their SNI (tlsv1 alert internal error). Label them like
audiobooks/immich and add Gatus checks for the three hosts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
as_token and hs_token come from an ESO Password generator (CreatedOnce)
instead of OpenBao, so no one has to seed them by hand. Synapse mounts
only the generated Secret and no longer depends on the OpenBao path;
OpenBao homelab/apps/matrix-bridge now holds only paperclip_api_key.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the matrix-bridge Deployment, Service and ExternalSecret in the
matrix namespace, loads its appservice registration in Synapse, and opens
the bridge -> Paperclip API path (:3100) in both namespaces' policies.
Tokens and the board key come from OpenBao homelab/apps/matrix-bridge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Authentik 2026.5 checks each authorize against the provider's
grant_types, and a provider created by a blueprint starts with an empty
list. Every Matrix login through Authentik failed with invalid_request
(log: "Invalid grant_type for provider", grant_type=authorization_code).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner wants QR sign-in from the desktop app as well. Linking a phone from
a signed-in Element (Link new device) only needs the server side from the
previous commit (MSC4108 + MAS device-code grant). This flag also lets a
new browser sign in by QR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Element X signs in only with a password or through MAS. Put MAS in front
of Synapse with Authentik (passkey) as the upstream provider, and enable
the QR rendezvous (MSC4108/MSC4388).
- apps/matrix/mas.yaml: MAS 1.26.0, `mas` database in mx-pg (CNPG
Database), account.matrix.renner.uno.
- synapse.yaml: matrix_authentication_service, password_config off; a
syn2mas init container moves the existing accounts and sessions once
(marker on synapse-data).
- Drop the owner-account Job; MAS owns accounts now.
- Authentik: public PKCE client `matrix-mas`, passkey flows, only the
Matrix Users group (k.renner).
- matrix-ns: MAS egress to Traefik :8443 (auth.renner.uno) only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scoped access for the zwave-js restart (REN-628): forced-command user
atlas-zwave on edge-1. One /32, TCP 22 only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Loki runs SingleBinary with auth disabled and answers GET on
/ingester/shutdown and /flush, so an RBAC `get` on services/proxy for
loki:3100 would not be read-only. Put an nginx allow-list in front of it
(query, query_range, labels, label/<name>/values, series; GET only;
upstream URI rebuilt per location) and grant the paperclip read SA
`get` on that Service only. Owner decision: REN-622 (option A2).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The primary pclp-pg-2 was OOMKilled 6 times on 3-4 Oct at the 1Gi
limit. Each kill drops Paperclip's DB and fails in-flight agent runs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1116 left spec.volumeName out of pvc.yaml. kustomize-controller has owned
that field since the namespace move pre-created the claim (eb23d3ac), so
server-side apply tried to clear it and the API server rejected the claim
as an immutable-spec change. The gitea Kustomization is Ready=False and the
claim stays at 40Gi. Pin volumeName to the live value.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The volume filled on REN-580. volumeClaimTemplates is immutable, so own the
live claim data-gitea-0 in apps/gitea/pvc.yaml (prune disabled) and let Flux
server-side apply grow it; ceph-block expands online. Add GiteaDataVolumeLow
(<25% free for 15m) with a promtool suite and two mutation controls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stable session fingerprint for the per-run managed AI home paths, plus a
persistent CLAUDE_CONFIG_DIR/projects so the resumed session finds its
transcript. Local runs only. Fix-forward of #1111 (reverted in #1113).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This reverts #1111 (3f3305cf). With the fingerprint stable, the ACPX engine
now resumes the previous Claude session, but the Claude transcript lives in
the per-run CLAUDE_CONFIG_DIR, which the server deletes after each run. acpx
uses resumePolicy same-session-only for persistent sessions, so the turn
fails ("Persistent ACP session ... could not be resumed: Resource not
found") instead of falling back to a fresh session (run 12a2a911).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New `matrix` namespace and Flux KS: Synapse v1.162.0 on a CNPG Postgres
(mx-pg, Barman to Hetzner), media store + signing key on synapse-data
(Velero matrix-daily), Element Web locked to matrix.renner.uno, and an
idempotent Job that creates the one owner account from OpenBao
apps/matrix. Federation off, registration and guests closed, LAN-only
ingress, egress limited to the in-cluster DB and the push gateway.
Appservice slot left empty for REN-565 Part 2.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The -v2 token leaked into Argus run d2eede8a (REN-542). Renaming makes the
token controller mint a new token; Flux prunes the old Secret, which
invalidates the leaked token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent-atlas-token-v2 token leaked in an Atlas run log (REN-536).
Rename the Secret to -v3 so Flux prunes -v2 and the token controller
issues a new token; update the agent-token-read resourceNames entry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude auto memory defaults to $CLAUDE_CONFIG_DIR/projects/<p>/memory.
Paperclip makes CLAUDE_CONFIG_DIR fresh per run and deletes it, so every
memory is lost. New image patch: the ACPX engine writes
autoMemoryDirectory=<AGENT_HOME>/memory into the per-run
.claude/settings.local.json. AGENT_HOME is restored and saved around each
run, local and in k8s pods. Not part of the session fingerprint, so
resume is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The token leaked into Hestia run 13bd8a68 (REN-535). Renaming makes the
token controller mint a new token; Flux prunes the old Secret, which
invalidates the leaked token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GITEA_VERIFY_REPO still named k.renner/shelly-manager, which now
answers 307 to the new path. Point it at the new path directly.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
In-cluster app-health target via the ClusterIP Service, so a dead app
behind the live shelly-manager-gate outpost no longer reads green.
Covered by the existing BlackboxProbeFailing rule; no CNP change needed
(home-assistant default-deny already admits the monitoring namespace).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The token leaked into Argus run 373367ed (REN-523). Renaming makes the
token controller mint a new token; Flux prunes the old Secret, which
invalidates the legacy token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The three tokens leaked into run logs (REN-483, REN-484, REN-507).
Renaming makes the token controller mint new tokens; Flux prunes the
old Secrets, which invalidates the legacy tokens.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The host/remote-node rule had no toPorts, so any node or hostNetwork pod
could reach the relay's unauthenticated gRPC on :4245. Restrict it to the
kubelet gRPC probe port (chart deployment.yaml probes use port 4222).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
yaml-citations failed on external-secrets-podmonitor.yaml:35, which cited the
upstream chart's values.yaml by line. That file is not in the repo, so no
path#anchor form exists; name the keys instead. Comment-only change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Hubble is already on in every agent; the relay gives one gRPC endpoint so
`hubble observe -n paperclip-renner --verdict DROPPED` runs without exec
into kube-system. The chart renders no change to cilium-config or the agent
DaemonSet, so the reloader does not roll the CNI.
The relay has no client auth: a new CNP admits only the paperclip pod (and
nodes for probes), and paperclip-egress gains the matching :4245 rule.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Built static in the go stage with go install (module checked against
sum.golang.org); added to the default-PATH check and the README table.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- apiserver CNP selects paperclip.io/role=agent: the plugin writes the run id
into paperclip.io/agent-id, so the agent-id selector never matched (REN-428).
- agent pods reach the wiki API (egress here, ingress in ai-ns.yaml).
- FQDN allow for api.github.com, GitHub release-asset hosts, vuln.go.dev,
uptime.renner.uno.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
immich-server mounts immich-library-bulk + immich-thumbs since 4cc1b643;
immich-library is mounted by no pod but is still backed up (~250 GiB)
and restored by the restore test. The newest immich-daily backup
(immich-daily-20261001051507) holds immich-library-bulk and immich-thumbs.
Drop the PVC manifest and update comments that cited it. The PVC keeps
prune: disabled, so this merge does not delete the live volume; that is
a separate, owner-approved step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On 2026-10-01 the immich restore took the fullest OSD from 74% to 95%;
the Job died at its deadline, the Restore kept writing unwatched and
Ceph went OSD_FULL at 15:10.
- Mid-run guard: each restore poll reads the fullest OSD; at 83%
(OSD_ABORT_PCT, below nearfull) or after 4 polls with no reading,
cancel the DataDownloads, tear the restore down, stop the sweep (FAIL).
- A restore timeout cancels the same way.
- TERM/EXIT trap cancels the restore in flight; grace period 600 s.
- Time budget: no restore starts whose timeout + 900 s reserve passes
the Job deadline (SKIP).
- RBAC: get/list/patch on velero.io datadownloads only.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Restore took every PVC in the backup, and the parity probe then
snapshotted and cloned each restored PVC name in the live namespace. A
backup that holds a CNPG PVC would give two extra copies of the
database, one of them next to production.
- Restore: labelSelector cnpg.io/cluster DoesNotExist.
- Parity loop: only restored PVCs without cnpg.io/cluster.
- Capacity gate: leave CNPG DataUploads out, and add the source-side
probe clone (size-3 pool) for every PVC the run will probe.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Before each schedule's restore, project its raw size (DataUpload
totalBytes x pool replication/EC factor, spread over all OSDs) onto
current OSD usage from Prometheus. If the fullest OSD would go above
OSD_MAX_PCT (80), skip the schedule and report SKIP with the projected %.
Skips are listed in the summary and do not fail the Job; a projection
that cannot be made fails that schedule without restoring.
Allows the restore-test pod to reach Prometheus :9090 and notes the gate
in RECOVERY.md. No alert rule changes. Refs REN-441, REN-442.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The WS90 reporting configuration (zigbee2mqtt-ws90-reporting.md) only
lived on the device and in database.db; every interview or
device/configure reset it to the converter defaults (2026-09-24..28,
zero frames recurred).
New external converter extends the built-in WS90 definition with a
configure step that sets the documented values. Its version
(ws90-reporting-1) makes Z2M reconfigure the device once after load.
seed-data copies converters from a ConfigMap into
/app/data/external_converters on every start; not mounted because Z2M
writes a temp copy into that directory while loading.
Tested in the running pod against a mocked device: the definition is
picked over the built-in one, and configure ends with the ten
documented reporting values.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Case A sampled at 1h; with the 5m lookback the series were absent for most
of each hour and the pending alert reset, so the projection never fired.
Now 5m samples (1m exceeds promtool's sample limit on the 7d rate), same
0.04272 %/day and 140-day summary.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The owner decided EPHEMERAL (/var) drives run to failure (REN-420), so
NvmeDriveWearProjectedExhaustion and NvmeDriveWearCritical now skip the
drive carrying /var. The drive is resolved live by joining node-exporter's
/var mount onto smartctl-exporter's (node, device) through a recording rule;
if the join stops matching, the alerts return rather than go quiet. Spare,
critical-warning and media-error alerts still cover EPHEMERAL drives.
Adds a promtool suite and four negative controls for the exclusion.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Five ServiceAccounts agent-{atlas,iris,hestia,argus,hermes} in paperclip
with empty service-account-token Secrets. The four readers get the same
rights they hold today via the shared pod SA: `view` cluster-wide and the
existing monitoring/paperclip-prometheus-read Role. Hermes and the shared
SA get `get` on the five named token Secrets only.
CiliumNetworkPolicy paperclip-renner/paperclip-agents-kube-apiserver lets
the five agents' pods reach the kube-apiserver entity on 6443.
Spec: REN-412. Nothing restarts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gitea.renner.uno resolves to the Traefik LB VIP; Cilium DNATs it to a Traefik
pod before egress policy, so the plugin's toFQDNs allow never matches.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adapter environment tests sync managed config into the pod before any
workspace is realized, so the lease has no remoteCwd and the Claude test
fails with claude_managed_config_dir_failed. #13733 fixes only the run
path. #14769 (open upstream, head 10dc7371) makes the Kubernetes plugin
set remoteCwd=/workspace on acquire and keep a recorded one on resume.
The plugin build from REN-405 is unchanged, so dist/ carries the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Upstream's production target ships the plugin source without dist/, so the
plugin loader reports paperclip.kubernetes-sandbox-provider as "no longer
exposes a Paperclip manifest". prepare.sh now adds upstream's cloud-plugins
commands for kubernetes to the build stage, after the server build, and fails
the build if dist/manifest.js is missing. The #13733 patch is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pipeline 11717 build-paperclip exited 137 at the 8Gi memory limit during
the server build. Disable compressed caching so kaniko does not keep
compressed layer tarballs in memory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
k8s sandbox runs fail because the run path never sets the lease remoteCwd
(paperclipai/paperclip#13864). The fix, paperclipai/paperclip#13733, is not
merged upstream yet. Build our own image from the master commit the nightly
runs now plus that patch, and point Flux image automation at it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Node derives the V8 heap from the container limit: 512Mi gave a
259 MB heap, and Z2M crashed with "JavaScript heap out of memory"
(exit 134) at 2026-09-28 17:56Z and 2026-09-29 07:36Z. Request
raised to 256Mi to cover the measured 232Mi working set.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Koda (Garmin Connect IQ) calls POST /auth/login during sync. Only /rest
and /share bypassed the navidrome-gate, so the gate answered 302 with an
empty body and the watch failed with -400
INVALID_HTTP_BODY_IN_NETWORK_RESPONSE ("Native login failed -400").
An IngressRoute sends /auth and /api straight to Navidrome when the
request carries no authentik_proxy_ session cookie; requests with the
cookie (the web UI) keep going through the gate, so SSO is unchanged.
X-Authentik-Username is stripped on this route as on /rest.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved on REN-350. Only the Lidarr pod, only 8686; nothing else in
navidrome is admitted. Lidarr uses AUTH__METHOD External, so this gives
agents in the paperclip pod full Lidarr access.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review 1258: the 74 written albums are re-imported, not duplicate-skipped.
Fix the comment and report before/after mb_albumid per album.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two dry runs were checked; the spot check now matches original releases.
Music mount becomes writable for tag writes; move/copy/delete stay off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first dry run (write: no) matched 78 of 560 albums; some matched
reissues (JP/RU) and would have changed year to the reissue year.
Set original_date and preferred countries/media/original_year, and
drop the dry-run beets DB once so the next run re-matches. Still write: no.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nightly beets import over navidrome-music-bulk, in place: no move, copy,
rename or delete. First phase is write: no with fetchart/embedart off;
the job log prints matched/skipped counts and a beet write -p preview.
Renovate rule for the LinuxServer -ls<n> tag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
C.1 (CNPG ha-pg) and C.2 (Velero /config) passed; results are on REN-339.
Removing the Flux Kustomization lets prune delete namespace ha-restore-test
with the recovered cluster, the Jobs, the Velero Restore and the restored PVC.
Also drops ha-restore-test from the OpenBao ClusterSecretStore allowlist.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The operator will not roll ha-pg-restore-1 after #1063; it waits on an
in-place restart the stopped instance manager cannot answer. A new name
makes Flux prune the old Cluster (pod + ephemeral PVC) and create a fresh
one. The check Job is renamed too (Job templates are immutable).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The instance manager refuses to start PostgreSQL when the Cluster references
a plugin (externalClusters) that has no sidecar in the pod. Add a plugins
entry with isWALArchiver: false and a distinct serverName.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The seed initContainer was removed in 21a0a43f; Velero home-assistant-daily is
now the only /config restore path. home-assistant-backup-password has had no
consumer since then.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recovers ha-pg from Barman into ha-pg-restore (no WAL archiver) and restores
the home-assistant-daily Velero backup into namespace ha-restore-test. Two
Jobs log the runbook C.1/C.2 checks. The /config probe is deny-all and never
runs Home Assistant. Nothing in namespace home-assistant changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- gitea.renner.uno/**: pinDigests false. Our images are pinned by dated
tag; registry GC deletes their digests (no-digest-pins, homelab #1053).
- The weekly non-major group becomes: one PR each for gitea, authentik,
influxdb, openbao and multus/CNI (migrations or CNI, a revert does not
undo them); one for kubernetes/apps/home-assistant/** (HA-impact review,
02:00-05:00 window); one CI-only for .woodpecker/** and ci/images/**.
Everything else stays in "all non-major dependencies".
- openbao chart: rangeStrategy bump. The default rewrote the bounded range
">=0.28.4 <0.29.0" to "<0.30.0" (homelab #1054) and dropped the lower
bound; bump gives ">=0.29.6 <0.30.0".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The slim Renovate image installs tools (golang for go.sum, helm) at run
time into /opt/containerbase, which it ships as root:root 0775 for its
user ubuntu (uid 12021, gid 0). The job ran with gid 12021 and a
read-only root, so every install failed ("Install tool golang failed",
ical-proxy #12) and go.sum was never updated.
Run with gid 0 and a writable root file system. The user stays
non-root, capabilities stay dropped, no API token is mounted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision on REN-107: option B, 30 days on the existing disk.
retention 90d -> 30d, retentionSize 30GB -> 40GiB, PVC unchanged (50Gi).
At ~1 GB/day the 30GiB cap bound first and the real window was a
20.5-27.2 day saw-tooth; 30d peaks near 38 GB, under the 40GiB backstop.
PrometheusRetentionNotInForce now divides the 7d store peak by the cap in
force NOW, not the cap at the time of the peak, so it clears on the day
the fix lands instead of firing for up to 7 days on the old cap's peak.
Adds tests/prometheus-retention_test.yaml (5 scenarios) and 5
`retention-*` mutations, all caught.
Restarts Prometheus once (StatefulSet args change): merge 02:00-05:00
Europe/Berlin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`retention: 90d` and `retentionSize: 30GB` are both set on kube-prometheus-stack.
Prometheus honours whichever binds first, and here it is the size cap — so the
time setting is decoration. Measured on the live TSDB through the read-only
proxy grant, 2026-09-26:
storage.tsdb.retention.size = 30GiB -> 32,212,254,720 bytes
storage.tsdb.retention.time = 90d -> 7,776,000 s
window actually held -> 2,291,239 s (26.5 days, 29% of 90d)
Size eviction is not pending — it has been running on a ~7-day cycle for at
least as long as the TSDB keeps history. At 2026-09-19 23:00->23:30 the store
went 28.16 GB -> 21.14 GB, `prometheus_tsdb_size_retentions_total` went 0 -> 1,
and the window held collapsed 27.21 -> 20.48 days. Effective retention is a
20.5-27.2 day saw-tooth. Nothing reported any of it: none of the 182 alert
rules already loaded touch retention.
REN-107 was filed on a reading of `prometheus_tsdb_size_retentions_total == 0`,
concluding the cap was ~3 days away from binding. That counter is per-process
and resets on restart; this Prometheus has had seven distinct pod IPs in 26
days, and the reading was taken on a pod started 5 days earlier. So this rule
does not read it — it compares the window on disk against the limits the
running process reports, which are true regardless of process age.
No hardcoded 90d or 30GB: both sides come from
`prometheus_tsdb_retention_limit_seconds` / `_bytes`, so editing the HelmRelease
cannot leave this rule stale.
Two clauses. The window is <90% of configured time retention, AND the store
reached 80% of the size cap within the last 7 days. The second is a guard, not
decoration: clause 1 alone fires on a legitimately young TSDB on a fresh PVC.
`max_over_time(...[7d:1h])` spans a full saw-tooth so the alert does not resolve
and re-fire twice a week on an unchanging condition, and the expression is fully
aggregated so that lookback survives the pod restarts above.
Verified against the live TSDB, not just linted — the expression as js-yaml
parses it out of this manifest returns one series with $value 2,291,414 s
(26.52 days). Counterfactuals also run live: it goes silent for a 100GiB cap
(90d intent), for `retention: 21d`, for 30d+40GiB, and for a short window on a
store far from its cap (the fresh-PVC false positive the guard exists to stop).
Scope is the alert only. This does NOT change `retention`, `retentionSize` or
the PVC — which number is the real intent is an owner call on storage cost vs
history, and changing either restarts Prometheus and gaps every series. That
decision is on REN-107 for Zeus; the config change is a separate PR.
Blast radius: a PrometheusRule is additive and loaded by the operator's config
reloader. No workload restarts, no Prometheus restart, no scrape or ingest
change, no existing rule touched, no ingress or DNS change. Worst case is a
malformed rule, which surfaces as PrometheusRuleFailures — already alerted —
and affects only this rule's evaluation.
The alert fires immediately on merge, because the condition is true today. It
clears within one evaluation once the retention config is made honest.
Revert: revert this one commit. The rule is deleted and nothing else changes.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The blackbox-ren28-negative Probe existed only to take the REN-140 §7
before/after pair across REN-28 PR 2b (#1044). The "after" reading was
taken at merge (27b505e6, 2026-09-28 00:09-00:11Z): all four targets
probe_success 0 at probe_duration_seconds ~5 s, collateral still 1.
Delete the Probe, its kustomization entry, and the now-dangling mention
of the job in the CNPNegativeControlReachable comment. gotenberg:3000
stays covered by the permanent blackbox-negative-control job.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
npm install -g ignored Paperclip's pnpm override and pulled
claude-agent-sdk 0.3.257. Install from a package.json with the override
(0.3.280) into /opt/claude-agent-acp and link the bin into /usr/local/bin.
The build fails if the SDK version is not 0.3.280.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The k8s-go adapter test failed with 'Claude ACP server command is not
available: claude-agent-acp'. Both agent-runtime images now install
@agentclientprotocol/claude-agent-acp@0.73.0, the version Paperclip pins,
with Paperclip's patch for it applied (vendored in agent-runtime-common/).
go and gofmt are also linked into /usr/local/bin, so every tool is found
with the default PATH, not only with the image ENV. The build checks that
with env -i. Kaniko context is now ci/images so both images can COPY the
shared patch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Paperclip Kubernetes sandbox plugin (0.1.0-alpha.1) hardcodes
agents.x-k8s.io/v1alpha1; v1.0.x serves only v1beta1, so the k8s-go
probe gets 404. v0.5.6 serves v1alpha1 (webhook conversion) and stores
v1beta1, the same storage version as v1.0.4.
- GitRepository tag and controller image -> v0.5.6 (digest pinned).
- Deployment: emptyDir for the self-signed webhook cert (read-only root).
- CiliumNetworkPolicy: allow kube-apiserver ingress (conversion webhook).
- Renovate: hold agent-sandbox below 1.0.0 until the plugin moves to v1beta1.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
main-926ead0 includes ical-proxy PR #7 (REN-230): the upstream response
body is capped at 2 MiB, so an oversized feed gets 413 instead of being
buffered whole into a 256Mi pod. It also includes PR #10: upstream fetch
failures are logged with the host and answered with 502 instead of 500.
Refs: REN-294
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On the #1046 merge (pipeline 11306) the build-agent-runtime step got no
pod: the full rebuild of build-ci-images.yml had filled the 24-CPU
woodpecker-ci-quota ("exceeded quota"), so the go image was never built.
Move the step to .woodpecker/build-agent-runtime.yml. It runs only when
ci/images/agent-runtime-*/** or its own file change, and can be re-run
alone with a deployment to target build-agent-runtime. Step content is
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HelmRelease agent-sandbox is Stalled (InstallFailed): the chart's common
labels render app.kubernetes.io/version from image.tag (contains the
digest, >63 chars) and helm.sh/chart with Flux's +<rev> suffix. Remove
both labels in a kustomize postRenderer. Selector and pod labels are
unaffected.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The first push of this branch started all 13 build steps: the push trigger
had no branch filter. 13 pods at the 2-CPU default limit exceeded the
24-CPU woodpecker-ci-quota, so the PR checks and one build failed.
- push trigger: branch master only
- go and ops build in one step, one pod, at 1 CPU: a full rebuild is 23 CPU
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two pod images for the Paperclip Kubernetes sandbox (REN-284 step 2):
the stock agent-runtime-claude, pinned by digest, plus
- agent-runtime-go: Go 1.27.1, golangci-lint v2.14.0
- agent-runtime-ops: kubectl v1.36.5, flux v2.9.5, kustomize v5.8.1, helm 4.3.0
Tools are copied from their upstream images pinned by tag and digest, so
the existing Renovate dockerfile manager bumps them. Pushed to
konairius/agent-runtime-{go,ops}/agent-runtime-claude:v1 because the plugin
keeps that name and swaps only the registry prefix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- infrastructure/agent-sandbox: Flux GitRepository (tag v1.0.4) + HelmRelease
of the upstream in-repo chart; image pinned by digest, restricted
securityContext, limits. CRDs via crds: CreateReplace.
- namespaces: agent-sandbox-system and paperclip-renner, both PSS restricted.
- network-policies: CNP for agent-sandbox-system (probes in, apiserver out).
- apps/paperclip/sandbox-rbac.yaml: Role + RoleBinding in paperclip-renner and
a create-namespaces ClusterRole, bound to paperclip-prometheus-read (the SA
the paperclip pod actually runs as; there is no SA `paperclip`).
- renovate.json: group the git tag and the image pin into one PR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
default-deny-ingress in the paperless namespace admitted the whole
`database` and `monitoring` namespaces on every port, on
endpointSelector:{} — so on paperless's HTTP listener too, which trusts
X-Authentik-Username from anything that connects. Both admits are
removed and the three load-bearing flows are kept as per-workload
policies:
pprl-pg-allow-cnpg-operator database/cloudnative-pg -> pprl-pg :8000
pprl-pg-allow-prometheus monitoring/prometheus -> pprl-pg :9187
paperless-allow-blackbox monitoring/blackbox -> paperless :8000
No ICMP rule on them (REN-144 option (a)); the baseline ICMP and the
host/remote-node L3 admit are untouched.
Also, per plan rev 8 §5 and REN-148:
- CNPNegativeControlReachable, a second group in
prometheusrule-blackbox.yaml, scoped to job=blackbox-negative-control.
- Comment-only corrections of every citation of the removed
#database-admit / #monitoring-admit anchors (paperless
network-policy, probes-blackbox, gatus header, probes-ren28-negative),
plus the IPv4-fallback and scrapeTimeout notes on the
negative-control Probe (PR 2a review findings 1 and 2).
gatus ConfigMap data is byte-identical (sha256 unchanged); every changed
line in that file is a comment above `data:`.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CephPoolLostRedundancy (critical) joins three metrics on `pool_id` and filters
on ceph_pool_metadata{type="replicated"}. If an upgrade or relabel renames the
label, the value, or the join key, the rule returns nothing and goes dark
silently. CephPoolLostRedundancyBlind is the same join with the thresholds
dropped, wrapped in absent(): warning, for: 1h to ride out mgr failovers.
Its six scenarios are a promtool suite at monitoring-rules/tests/ceph_test.yaml,
with five negative controls in mutations.txt, run by the existing `promtool`
step (#1026). The CI half of the original #989 is dropped: #1026 covers it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New PrometheusRule home-assistant with four `for: 5m`, severity critical
alerts. Before this the only signal for any of them was the cluster-wide
KubePodNotReady (15m, warning).
- HomeAssistantDown: HA scrape target down or absent.
- HomeAssistantRecorderDBUnhealthy: ha-pg has no primary with Postgres up,
or fewer than spec.instances (2) instances with Postgres up.
- MosquittoDown: no Ready broker pod (bootstrap Job pods excluded).
- Zigbee2MQTTDownOrCoordinatorOffline: pod not Ready, or HA's
bridge_connection_state sensor reads 0.
Each alert keeps one label set across its failure modes, so one outage
is one alert with one `for` timer. Unit suite and five negative controls
added; scripts/ci/promtool-rules.sh passes locally with promtool 3.15.0.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner decision on REN-240 (protection rule 5):
- README.md: disruptive work only 02:00-05:00 Europe/Berlin; PRs touching
kubernetes/apps/home-assistant/ or cluster-wide disruptive changes need
an HA-impact review (Hestia; owner until she exists).
- kubernetes/RUNBOOK-home-assistant-image-rollback.md: pin the previous
HA tag in one commit (revert bump + drop the imagepolicy marker, so
image automation does not re-bump it).
Docs only. No manifest changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Argus and Atlas need to verify that a cluster secret exists (for example
before reviewing or merging a PR that consumes it through an
ExternalSecret) without being able to read its value. The policy grants
read/list on homelab/metadata/* only: paths, versions and timestamps, no
data, no writes. Whole tree, per owner decision.
Identity is a userpass user (house precedent: claude-bot) whose password
is one Paperclip agent secret bound to both agents. Applied once by hand
with an admin token, like the other policies in this directory.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump the pinned ical-proxy image from main-4783abe to main-73b5d97.
The only change is konairius/ical-proxy #9 (REN-232): the server
WriteTimeout goes from 10s to 40s (30s upstream fetch timeout + 10s
headroom), so a slow-but-legitimate upstream is no longer cut off
before /proxy can write its response.
Refs: REN-239, REN-232
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump the pinned ical-proxy image from main-db90a59 to main-4783abe.
The new build adds two hardening changes from konairius/ical-proxy:
- #6: refuse upstreams and redirect hops that resolve to private,
loopback, link-local or CGNAT addresses. Public redirects are still
followed (up to 5).
- #8 (REN-231): cap concurrent /proxy upstream fetches at 20; requests
over the cap get 429 + Retry-After instead of piling up goroutines.
Refs: REN-238, REN-231
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ical-proxy moved from GitHub to Gitea and its image is now built by
Woodpecker and pushed to gitea.renner.uno/konairius/ical-proxy. The
ghcr.io image gets no new builds.
main-db90a59 has the same application code and Dockerfile as the
running main-99a912b; the commits in between touch only CI and docs.
The package pulls anonymously, so no imagePullSecret is added.
Revert: git revert this commit; Flux rolls the pods back to
ghcr.io/konairius/ical-proxy:main-99a912b.
Refs: REN-201
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1035: image-automation/ has 33 ImageRepositories, the
disable list had 32. Without this, Renovate and Flux would both bump
ghcr.io/matter-js/python-matter-server.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- apps/renovate: CronJob (daily 04:00, routine PRs Monday only via the
shared preset's schedule), runner config, renovate-bot token ExternalSecret.
- clusters/homelab/renovate.yaml: dedicated Flux Kustomization with the
ExternalSecret health check from uptime-monitor.yaml.
- infrastructure: `renovate` namespace, its CiliumNetworkPolicy (DNS,
Traefik, world:443; no ingress, no API server), and the namespace in the
openbao ClusterSecretStore allowlist.
- renovate.json: extends local>k.renner/renovate-config. Managers flux,
kubernetes, woodpecker, dockerfile. Skips the images Flux image automation
already bumps, and Flux's own gotk-components. Digest pins and
Cilium/Rook-Ceph minors get their own PRs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#1026 (REN-161) landed a promtool suite asserting CNPGWalArchiveBacklogSurge
and `for: 15m`, and mutations targeting `for: 8m`/`for: 15m`. This PR retires
the surge rule and moves the backlog rule to `> 5` for `20m`, so master CI
would break on merge.
- Generator: drop every Surge assertion; firing cases move to t=25m.
- New case 9: the worst night in Prometheus' 27-night retention (2026-09-11,
peak 141) as raw 30s samples, 12m pending at a 1m evaluation step.
- mutations.txt: surge-for-2m/-5m removed; backlog-for-12m added (caught only
by case 9, the margin control); backlog-for-30m retargeted at `for: 20m`.
- README and the woodpecker step comment updated to match.
Verified: scripts/ci/promtool-rules.sh with promtool 3.15.0 (CI's pin) —
18 manifests parsed, 2 suites green, 10 mutations caught;
scripts/ci/test-promtool-rules.sh passes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Retune under Zeus's ruling on REN-152: the instrument is persistence, not
depth. This was meant to land on #1010 before it merged; #1010 merged
first, so this is the same change applied to master. Still
observability-only text — no expr semantics change, nothing restarts, no
storage and no traffic path is touched.
- CNPGWalArchiveBacklog `for`: 15m -> 20m.
- CNPGWalArchiveBacklogSurge (>64 for 8m) removed.
Why the depth rule goes. The failure that costs something is an archiver
that has stopped, and a stopped archiver sits wherever it stopped — it can
park at 8 segments and never approach 64, let alone 150. A threshold tuned
above the nightly envelope is blind to exactly the case the rule exists
for, and has to be raised again every time the recorder grows. It also had
no room left: measured across Prometheus' full 27-night retention rather
than the 7 nights it was built on, >64 for 8m fires once, on 2026-09-11.
Why 20m and not 15m. Same 27-night replay, 30s resolution, all 12 CNPG
instances. Only ha-pg-1 ever exceeds 5; every other instance peaks at 2.
Longest contiguous excursion above 5 is 13.0 minutes, on 2026-09-11 at a
peak of 141. Firing evaluations: 0 at 20m and also 0 at 15m, but 15m
leaves 2 minutes of margin where 20m leaves 7, and the nightly peak moved
116 -> 141 inside the measured month. The 5 minutes of added latency buy
nothing back: a real stall lasts hours, and at 32Gi the volume holds about
four nights of stalled WAL.
The description now says outright that the 02:14Z (04:14 local) burst is
normal and self-clearing, so a page means the backlog did not clear. The
drain-rate and headroom pointers (wal.maxParallel, spec.storage.size,
REN-135) move up from the deleted rule rather than being lost.
Verified with promtool 3.5.0 at evaluation_interval 30s against the rule
file as committed: the real 2026-09-11 series is silent; a stall at 8
segments fires; a backlog filling to 100 and not draining fires; a backlog
crossing a failover still fires on time, which is what `max by (namespace,
pod)` is for. Negative controls bracket the value — at `for: 12m` the real
burst fires, at `for: 25m` the stall cases stop firing.
Revert: `git revert` this commit. That restores exactly what is on master
today, >5 for 15m plus the Surge rule.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The version range >=72.0.0 let Flux upgrade the chart unattended
(91.5.2 -> 91.7.0 at 19:43 UTC on 2026-09-26, no git commit). That
restart moved Prometheus onto k8s-1 while its Ceph RBD mounts were
broken, causing a ~48 min alerting outage (REN-198).
Pinned to the version already deployed, so reconcile is a no-op.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
promtool-rules.sh (REN-161, merged after this branch was opened) discovers
suites only as tests/*_test.yaml. At its old path the ESO suite would have
been skipped without a word. Moved, rule_files pointed at ../ for the
stripped mirror, the "inert until REN-96" header dropped, and five negative
controls added to mutations.txt: for 5m, sum-not-max, by-namespace,
no == 1, any condition. All five are caught.
promtool 3.15.0 (the CI pin): 18 manifests parsed, 2 suites green,
11 mutations caught. Rule file unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment-only. The open question on this rule was whether the Ready condition
gauge flaps in ordinary operation, which could not be answered until #994 had
been collecting for a while. It has now run 464 consecutive scrapes since
00:18Z: all 52 ExternalSecrets reconciled (959 syncs), the gauge never reached
1 in any scrape, the error counter did not move, and the scrape target never
went down. Recorded in the rule file with the queries that produced it.
Also drops a reference to prometheusrule-ceph-test.yaml, which is on the
unmerged REN-96 branch and not in this repo, so it read as an in-repo
precedent a reviewer could not find.
Non-comment lines are byte-identical to 92dbc0de in both files, so the
promtool 2.53.2 check-rules and test-rules runs recorded on the PR still hold.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Comment-only. The counts the rule's reasoning rests on are now measured on the
running Prometheus rather than derived from the repo: 105 series = 52
ExternalSecrets x 2 Ready polarities + 1 Deleted tombstone, across 20
namespaces, and the ExternalSecretSyncFailing expression returns empty today.
The last point is the one a reviewer wants: the rule arrives silent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment-only, again: `diff` of all non-comment lines against bbaf0c6a is empty.
Answers the grouping objection raised in review. Aggregating by
`exported_namespace` leaves these alerts without a `namespace` label, so they
share one Alertmanager group — but `group_interval: 5m` on the root route
notifies a group whose alert set has CHANGED, which is not `repeat_interval:
12h` re-sending an unchanged one. A second failing ExternalSecret therefore
mails within 5m rather than being swallowed. Records the receiver this rule
routes to and why it misses all four child routes.
Also records that the three inhibit rules cannot suppress it: all key on
`equal: ['namespace', 'alertname']` and nothing shares this alertname.
OpenBaoQuorumLost in particular does not inhibit it, and being critical at 2m
arrives first anyway.
Refs REN-103
Comment-only. No rule, expression, `for:` window, label or annotation changes —
`diff` of all non-comment lines against the previous revision is empty, so
promtool's SUCCESS and the committed unit test still hold as run.
Argus's post-merge measurement on #994 raised the ESO sync-error counter as the
better primary signal, citing the 2026-09-20 `ai/wiki-embed-basic-auth`
incident as a case a Ready gauge would miss. Reading that incident back, it
argues the other way: the key needed a manual force-sync, so it never
self-healed and Ready=False was continuous for hours, which `for: 15m` catches
with a wide margin. It was a stuck condition, not a flap.
The measured counter data points the same way: `externalsecret_sync_calls_error`
has one series cluster-wide, `ai/wiki-anthropic` at 8 errors over 47 calls, on
an ExternalSecret that is Ready=True throughout — 8 counter events, zero sync
failures.
Also states plainly what `for: 15m` does not cover: a sync that fails and
recovers on its own inside the window, which is a deliberate choice because the
Secret ends up fresh and there is nothing for a human to do.
Refs REN-103
Merge B of REN-103. #994 made the series exist; this is the rule built on
them. Split on purpose — the `for:` window had to be chosen against real
data, and Prometheus held 0 series for this metric until #994 landed.
Two corrections to what REN-100 specified, both found only once there was
data or source to read:
* `exported_namespace`, not `namespace`. The metric carries its own
`namespace` label and the PodMonitor attaches a target label of the same
name, so Prometheus renames the metric's to `exported_namespace` and every
live series reads `namespace="external-secrets"`. REN-100's expression
still fires, and reports every failing ExternalSecret in the cluster as
living in `external-secrets`. Found by Argus against the live data.
* `max`, not `sum`. Both give 1 today on a single controller replica, but a
Deployment rollout is briefly scraped as two pods, where `sum(...) == 1`
evaluates to 2 and silently stops matching.
The condition gauge is the primary signal and `externalsecret_sync_calls_error`
is deliberately unused, which reverses the guess recorded on #994. The counter
increments on strictly more paths than the condition does — a failed Get of the
ExternalSecret, a failed Get or label Patch of the target Secret, an
unresolvable SecretStore — none of which call markAsFailed(). It is the noisier
signal, not the quieter one. The gauge does not flap: ESO sets it to 1 only in
markAsFailed() and clears it on the next successful reconcile, writing both
polarities every pass, so there is no unset state in between.
`for: 15m`, because a failed sync degrades freshness and not availability —
consumers keep the last Secret ESO wrote. Long enough to ride out a rolling
restart of the 3-replica OpenBao StatefulSet, and the real outage has its own
critical alert at 2m, so nothing is hidden by waiting.
Also adds a dead-man's-switch on `absent(externalsecret_status_condition)`.
Deleting the PodMonitor or reverting the `monitoring` ingress grant would
otherwise disarm the alert silently, which is the failure mode REN-103 exists
to end; TargetDown covers a target that is failing, not one that is absent.
Verified with promtool 2.53.2: `check rules` passes, and the committed
`-test.yaml` asserts the 15m window, the rendered namespace in the annotation,
the two-pod rollout case, that the `Deleted` tombstone never alerts, and that
the dead-man's-switch fires when the series vanish. Not yet evaluated against
the live Prometheus — this runner has no cluster access.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review #1114: 18Gi gave ~3.5 days of archiver outage, below the ~4.0 days
the shared 32Gi gives today. 21Gi gives ~4.1. The storage comment now says
WAL headroom moved to walStorage and 32Gi stays because PVCs cannot shrink.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Argus's review of PR #1032: the name exists in neither pkg/ nor the metrics
docs for 1.20.2, and the agent skips unknown --metrics names silently. Zeus
dropped it from scope. metricsService, the Service delete and the new
selector stay.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Owner-approved option B on REN-176. Adds a separate ceph-block WAL volume
per instance and sets primaryUpdateMethod: switchover so the pod roll this
triggers promotes the rolled replica instead of restarting the primary in
place.
Permanent: the CNPG 1.30.1 webhook rejects removing walStorage, so a git
revert of this commit fails admission. Merge only in an owner-named window.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- prometheus.metrics: +cilium_policy_verdict_total (opt-in; per-direction
policy verdict attribution on top of cilium_drop_count_total).
- prometheus.metricsService: true, so the chart renders the headless
`cilium-agent` metrics Service.
- Drop the hand-declared `cilium-agent-metrics` Service from
cilium-servicemonitor.yaml and point the ServiceMonitor at the chart's
Service, so the endpoints are scraped once, not twice.
This is a values change: helm-controller upgrades the CNI release, the
cilium-config `metrics` key changes, and the reloader annotation rolls the
agents node by node. Merge only inside the window Zeus set on REN-166.
Revert: git revert this commit; Flux restores the old Service and selector
and rolls the agents back the same way.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both pilot EndpointMonitors were created with an empty alert_contacts
list, so they notified nobody. Set uptimeRobotConfig.alertContacts to the
two contacts every hand-made monitor in the account already uses
(7880965 e-mail, 8026823 type 13, threshold 0, recurrence 0), in the
order the API returns them so IMC's string comparison stays stable.
Per the owner's ruling on REN-138 no test alert is fired; delivery is
owner-verified. Revert this commit to return to empty contacts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
backupPVC in node-agent-config is keyed by source storage class.
ceph-filesystem-bulk had no entry, so its data-mover took the full
CephFS subvolume clone path and sat Pending on "unbound immediate
PersistentVolumeClaims" (4 min audiobookshelf, 22 min immich on
2026-09-26, paging KubePodNotReady). Growth with data would hit the
data-mover prepare timeout and fail the backup.
- node-agent-config: ceph-filesystem-bulk -> readOnly: true with
storageClass ceph-filesystem-bulk-snapshot-readonly
- new StorageClass ceph-filesystem-bulk-snapshot-readonly:
backingSnapshot "true" (shallow RO volume, no clone), reclaimPolicy
Delete
- nodeAgent.podLabels revision label so node-agent restarts and loads
the config (node-agent only, not the server)
Refs REN-177.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Agents outside the pod (Claude Code on the workstation, via
`paperclipai agent local-cli` / agent API keys) only make outbound REST
calls to /api/* with Authorization: Bearer. The Authentik gate on
paperclip.renner.uno rejects those (intercept_header_auth), so they get a
separate machine door, following the wiki-index pattern:
- paperclip-api.renner.uno (no dns.renner.uno/public label: LAN-only)
routes /api to the paperclip Service, bypassing the gate. Paperclip's
bearer agent/board keys are the authentication.
- /api/auth on that host is refused (Traefik ipAllowList matching no
address -> 403), so Better Auth password sign-in/sign-up stays behind
the gate; upstream serves all password login under /api/auth.
- The host is added to PAPERCLIP_ALLOWED_HOSTNAMES (private-hostname
guard would 403 it) and the CNP admits Traefik on 3100.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments that pointed at other YAML as `file.yaml:N` went stale on any
edit above the cited line; three drift fixes had been merged by hand
(REN-181). The cited block now carries `# anchor: <name>` and the
comment cites `path.yaml#<name>`.
- Sweep: all 32 line-number citations in YAML comments converted.
Several were already stale at head and now point at the right block
(see PR). Vendored files (Flux gotk-components, IMC CRD) get no
anchor; those two citations name the object/schema path instead.
- Guard: scripts/ci/yaml-citations.sh in ci-manifests-test.yml; fails
on a line-number citation or a path#name whose file/anchor is gone.
Self-test in ci-scripts-test.yml.
- CONTRIBUTING.md: the convention in three lines.
Comment-only under kubernetes/: every file parses to identical objects
before and after, so Flux applies nothing and no ConfigMap data changes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nothing in this repo checked an alert expression. ci-manifests-test.yml proved
every PrometheusRule assembled into valid YAML and stopped there — no check that
an expression parses, that a label join matches anything, or that a `for` window
behaves the way its comment claims.
On REN-129 the CNPG surge rule was written `for: 2m` and reviewed at that value.
Against Iris's seven measured nights it fires on six of them: two owner emails a
night, 04:16 and 04:22 local, for a WAL backlog that clears itself in under ten
minutes. The arithmetic that caught it was hand-written from scratch twice, in
two runs, and thrown away both times because there was nowhere in the repo to
commit it. It is now a negative control.
Adds:
* scripts/ci/promtool-rules.sh — discovers every PrometheusRule (17 today),
strips `spec:` into a temporary mirrored tree, runs `promtool check rules` on
each, then `promtool test rules` over the committed suites. Discovery floors
on both, as in the sibling steps.
* a `promtool` step in ci-manifests-test.yml, `depends_on: []` like the other
four, on prom/prometheus:v3.15.0. The 3.x pin is load-bearing: the cluster
runs kube-prometheus-stack >= 72.0.0, and alert `for` evaluation is
version-sensitive, so a 2.x promtool would check a different evaluator than
production. The script asserts the major version it is handed.
* kubernetes/apps/monitoring-rules/tests/ — the REN-129 suite (14 cases over
the seven measured nights, an adversarial envelope built from the extremes of
the measured fill/drain ranges, two stall shapes, a threshold boundary, a
failover, a multi-instance case and the epoch-zero guard), its generator, and
six negative controls in mutations.txt.
* a mutation phase. Each control breaks one value the rules depend on and the
suites must fail; a control whose sed matches nothing is a failure, not a
skip.
* scripts/ci/test-promtool-rules.sh, run from ci-scripts-test.yml — 17 cases
asserting the script refuses to report success when it has checked nothing.
The mutation phase is not belt-and-braces, it is the point. Two failure modes
bit while writing this and neither changed promtool's exit code: a `rule_files`
entry that did not resolve, where promtool WARNs, loads zero rules, and passes
every `exp_alerts: []` assertion against an empty rule set; and traces generated
against the wrong clock window, where all seven nights were flat zero. Both were
green. A suite that passes proves the rules match the assertions, not that the
assertions reach the rules.
Production: nothing applied to the cluster. No manifest changed, so the
monitoring-rules kustomization builds identically — its `resources:` list is
explicit, and the new tests/ directory is not in it and has no kustomization.yaml
of its own, so Flux does not see it. Worst case is a red step on PRs touching
kubernetes/**; enable_status_check is false on master, so it cannot block a merge
on its own.
Verified with real promtool 3.15.0 against the real tree: 17 manifests parsed
(56 rules), the suite green, all 6 mutations caught. Run again under BusyBox
1.35.0 sh/awk/sed/find/xargs, the toolchain prom/prometheus actually ships.
Guard tests 17/17 under both bash and BusyBox sh. shellcheck 0.11.0 clean at
every severity. woodpecker-cli 3.18.0 lint clean on all 17 pipeline files.
Not run in Woodpecker itself — this PR is its first real pipeline execution.
Revert: revert this commit. It adds files and one step; nothing else reads them.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ceph's mon_osd_nearfull_ratio is 0.85 and nothing reported the approach to
it, so the first signal would have been Ceph flagging nearfull — already the
incident. With failureDomain host, size 3 and three OSDs there is no
rebalance to hide behind: all three cross together.
Two warning rules, both observability only:
- CephOSDNearfullApproaching: fullest OSD sustained >= 75% for 30m. 0.75 is
0.85 minus the largest measured bulk burst (+10.24 pp in 25 min, 09-26
05:18-05:43Z), so it marks the point where one more such burst reaches
nearfull. 30m is the shortest wait that outlives a self-reclaiming burst
(that burst held >= 75% for 19.0 min) and short enough to fire before 0.85
at the fastest observed sustained climb (+11.44 pp/h).
- CephReplicapoolHeadroomLow: replicapool max_avail < 160 GiB for 30m.
160 = 89 GiB held above the 0.85 line + 38 GiB ha-pg has not yet consumed
of its approved 32Gi + 35 GiB observed replicapool churn.
Replayed against the 14d of real series to 2026-09-26 08:00Z: both fire zero
times. Worst 30m-sustained OSD floor 71.632%; lowest 30m-sustained
replicapool ceiling 208.97 GiB. Both expressions evaluated against live
Prometheus at the 09-26 05:40Z peak do select (3 OSDs at 81.87%,
replicapool at 117.41 GiB), so the 30m `for` is what keeps that excursion
quiet, not a join that never matches.
Thresholds are unmerged pending the owner's yes on REN-163.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Criterion 3 passed: the removal test (#1018, merged 101c11f4) deleted monitor
804092841 and the controller logged `Monitor Removed` at 03:57:58Z. This commit
is the deliberate revert of that test, which Zeus sequenced to run only after
the cap reading below. The restored object is byte-identical at the object level
to the version that provably created 804092841.
It also corrects a wrong claim this file asserted. The header stated that a
monitor-count cap was EXCLUDED as the cause of the `www.renner.uno` 403, on the
reasoning that a cap would arrive as HTTP 200 + `stat: fail`. The removal test
refuted that:
03:26:27-03:56:27 account 10/10, seven consecutive `Status Code: 403`
03:57:58 804092841 deleted, account 9
04:01:27 `Monitor Added: www.renner.uno-uptime-monitor`, id
804092931, account back to 10
One freed slot is the only variable. UptimeRobot returns HTTP 403 when the
account monitor limit is reached, so "account is full" is the first hypothesis
for an IMC 403, not a discarded one. Capacity is now settled on fact for
REN-158; REN-8's 18-monitor page against a cap of 10 stays blocked on it.
Expect this restored create to be refused with 403 until the tier is raised —
`www` took the freed slot. Left in git per the capacity ruling so the retry loop
creates it unaided once capacity exists. `audiobooks.renner.uno` stays covered
by its 60s Gatus check throughout, so there is no coverage gap.
Also recorded: the real delete log string is `Removing monitor … from provider`
/ `Monitor Removed`, not the `Deleting Monitor` this file predicted; and the
status page is in all-monitors mode, proven in both directions (9->10 on create,
10->9 on prune) with no API key, which settles REN-9's monitor-list mode.
Verified: js-yaml loadAll gives 2 EndpointMonitors in `uptime-monitor`; the only
functional changed line is the restored object, every other changed line is a
comment; restored spec compared equal to `f20fe8bf^`. Not applied — Flux applies
from master and I cannot merge my own work.
Revert: revert this commit. Flux prunes the object and, because its monitor does
not exist, nothing in UptimeRobot changes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
#992's header comment says RecordDuration "IS called, by all six controllers",
established by grepping the tagged upstream source trees. Now that the scrape is
merged the claim is measurable, and five of six emit the family: 170 series from
kustomize (33), source (43), image-reflector (68), helm (25) and image-automation
(1), and zero from notification-controller.
Five is permanent, not today-only. notification-controller's three reconcilers
are registered but never walked -- controller_runtime_reconcile_total for
controller=alert/provider/receiver reads 0 over 56 h of uptime with 2 Alerts and
1 Provider live in the apiserver -- so the family is absent from its /metrics
entirely and creating a Flux Alert or Provider does not produce the sixth series.
Comment-only. Every one of the 17 changed lines is a pure comment line and the
comment-stripped file is byte-identical to master (same sha256), so the object
Flux applies is unchanged and no pod rolls.
Refs REN-127, REN-102, REN-97
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
probes-blackbox.yaml:209 cited apps/gatus/configmap.yaml:176-185 as the
place gatus documents the paperless blind spot. That was true at cb9e73bd,
where the sentence was written. A later +29-line insert into configmap.yaml
pushed the block down; at master those lines are now Web Service 9
(music.renner.uno), which carries no prose at all.
The same 10-line Paperless blind-spot comment now runs 205-214.
Comment-only. No spec, no pod template, no new resource.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Reviewed and merged by argus-bot (REN-145). Approved at head 088db00e.
Independently verified: the eight GVKs against the single served CRD version in Flux v2.9.5; every CRD field path the config reads; object census = 149 and the reconcile intervals behind the 2h/1h thresholds; the rendered chart keeps --resources and omits --custom-resource-state-only; the whole rendered ClusterRole uses only list+watch with no wildcard; the real kube-state-metrics v2.20.0 binary accepts the rendered config while a malformed control is rejected at startup; the kustomize CI gate green on the simulated merge result, which CI itself did not test.
Own promtool fixtures: 9 alert tests plus a 13.5-day recording-rule simulation, all passing. Warm-up guard empty at 1201 samples and populated at 1297; the pruning case yields 0 under the shipped comparator against 1.1376 under the withdrawn ratio; tag counting survives a kube-state-metrics pod roll.
The missing staleness alert is an accepted, measured reduction; follow-ups tracked in REN-143.
Revert: git revert -m 1 <this merge commit>. Both Kustomizations are prune: true, so nothing is orphaned.
Reviewed by argus-bot (four-eyes: authored by atlas-bot, merged by argus-bot).
Verified against the PR head 17c1dece:
- helmrelease.yaml diff is comments only; parsed YAML of HEAD^ and HEAD is byte-identical (1683 bytes each). No values change, so no helm upgrade and no cilium agent restart.
- ServiceMonitor selector {managed-by: homelab, name: cilium-agent} matches only the new Service; endpoint port `metrics` exists on it; targetLabels k8s-app=cilium present.
- Service selector k8s-app: cilium, headless, distinct name from the chart's cilium-agent Service - no double-scrape or ownership collision if a chart flag is flipped later.
- Revert is one step: the monitoring-rules Flux Kustomization has prune: true, so git revert removes the Service.
- ci-manifests-test and ci-scripts-test green; advisory review-bot: no blocking issues.
REN-156
Approved by argus-bot at head c1b9b259 after re-review (head moved from dd6720f2).
Ceph capacity gate cleared at 03:49Z: replicapool max_avail 168.8 GiB usable, per-OSD 76.1% -> 78.8% worst case against nearfull 0.85, HEALTH_OK.
Four eyes: authored by atlas-bot, merged by argus-bot.
PVCs do not shrink - manifest reverts with git revert <merge-sha> -m 1, volumes do not.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Approved at 7aff5250 (review 1086). CNPGWalArchiveBacklog for: 30m -> 15m; new CNPGWalArchiveBacklogSurge > 64 for 8m; both exprs wrapped in max by (namespace, pod) so a failover does not restart the for timer.
Verified with promtool 3.5.0: 12-case test rules suite over Iris's seven measured nights plus an adversarial envelope case, all passing, with five negative controls each failing. Revert tested: git revert restores the file byte-for-byte.
Closes REN-129.
Deletes the `audiobooks.renner.uno` EndpointMonitor so that the other half of
REN-7 can be proven: an EndpointMonitor creates an UptimeRobot monitor, and
deleting it removes that monitor again. "A monitor that cannot be removed is a
leak." The create side is already shown — monitor 804092841, created
2026-09-26 03:26:27, controller log `Monitor Added`.
What to observe after this merges, keyless and from either side:
* `https://uptime.renner.uno/api/getMonitorList/UgMXmP1iRV` goes from
`psp.totalMonitors: 10` to `9`, and id 804092841 is absent.
* controller log: `Deleting Monitor: audiobooks.renner.uno-uptime-monitor`.
Precondition, checked before writing this: v2.2.13 carries no finalizer, so a
delete while the controller is down is never reconciled and orphans the monitor
permanently. Pod ingressmonitorcontroller-5d577cc58-pgqwg was Ready=True with
0 restarts. Whoever merges this must re-check that the pod is Ready first.
Also records, in the file, why the second pilot object still has no monitor:
UptimeRobot answers its create with HTTP 403 and IMC retries every 300s
forever. 403 is the non-200 branch of `Add()`, so it is not the provider's
validation path — that returns 200 with `stat: fail` — which rules out the
duplicate-URL, monitor-cap and bad-parameter guesses. Tracked on REN-155; the
object is left in place as the reproducer.
Blast radius: removes one EndpointMonitor from `uptime-monitor` and one
UptimeRobot HTTP check every 300s. No Ingress, workload, ingress controller,
DNS record or RBAC rule is touched, and `www.renner.uno` is unchanged.
Revert: revert this commit. Flux re-applies the object and the controller
re-creates the monitor under the same friendly name — a new id, since the name
and not the id is what binds object to monitor. That round trip is also the
idempotent-reconcile check and is worth doing deliberately.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The sizing comment blamed the 10-minute archiving stall on Velero repository
maintenance. REN-136 disproves that over seven nights:
- sustained archiver drain is ~14 segments/min, flat whether Kopia is
running or not, so Kopia does not slow the drain;
- Kopia's nearest batch drifts across 02:03-02:24Z while the burst stays
pinned to 02:13Z, and a cause that drifts 20 minutes cannot produce an
effect fixed to the minute.
The real driver is a nightly producer burst: 56-119 segments queued at
02:13-02:15 UTC on all seven nights, against a steady-state queue of 0.
Comment now states that, and adds REN-136's stalled-night framing (6.36 GiB
free survives one stalled night, ~18.6 GiB carries about four).
Comment text only. `spec.storage.size: 32Gi` is unchanged and still the sole
semantic change on this branch — verified by parsing origin/master and HEAD
with js-yaml: with spec.storage.size masked the two parse to identical
objects.
Raised by Argus in review of #1014 as a non-blocking finding.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Reviewed at head 3b96b3ab, test-merged clean into master dd2256ea (tree af52adab).
Verified: one file +73/-0; 0 YAML parse failures repo-wide; jobName blackbox-negative-control unique; all four ports re-derived from the paperless Service/Cluster objects; tcp_connect module declared; paperless-ns.yaml still admits monitoring namespace-wide so the controls read 1 today.
Pages nobody: all 16 PrometheusRules enumerated, the only non-job-pinned probe_success rule (HetznerBackupEndpointDown) is instance-pinned to the Hetzner URL; the chart's up-based TargetDown cannot fire because blackbox answers 200 with probe_success 0 and the 5s module timeout stays inside the 10s scrapeTimeout; all 5 blackbox-overview.json panels are pinned to job=blackbox-http; no Grafana-managed alert references probe_success.
Monitoring-only, no traffic-path change. Revert with: git revert -m 1 <this-sha> on a branch, through a PR.
REN-28 §6.4 gate NOT yet taken - reviewer has no Prometheus credential either. PR 2b must not open until the probe_success == 1 reading exists.
The cilium chart renders templates/cilium-agent/service.yaml only when
prometheus.enabled AND (prometheus.serviceMonitor.enabled OR
prometheus.metricsService). infrastructure/cilium/helmrelease.yaml sets the
first and neither of the second, so no cilium-agent Service existed. The
standalone ServiceMonitor in apps/monitoring-rules selected a Service that was
never there: scrape pool serviceMonitor/kube-system/cilium-agent/0 sat at 0
active / 46 dropped targets and cilium_* had zero series.
Consequence: CiliumBPFMapPressureHigh and CiliumBPFMapFull read
cilium_bpf_map_pressure, so both evaluated an empty vector and could never
fire — the exact failure the helmrelease comment says the metrics were turned
on to prevent. cilium_policy_verdict_total and cilium_drop_count_total were
equally absent, which is why REN-140 needed a purpose-built prober to observe
a network-policy deny.
Declares a sibling metrics Service next to the ServiceMonitor instead of
setting prometheus.metricsService in the HelmRelease. The flag is the
chart-native fix and renders exactly this one object, but it is a values
change: helm-controller would upgrade the CNI release and three-way-merge
against live objects, and the configmap.reloader annotation turns any
resulting cilium-config change into a rolling restart of every agent. A
Service applied by the apps Kustomization cannot do that.
Naming follows authentik-outpost-servicemonitors.yaml: distinct name plus
app.kubernetes.io/managed-by: homelab, with the ServiceMonitor selector pinned
to it, so turning either chart flag on later neither collides on ownership nor
double-scrapes the same endpoints.
The helmrelease.yaml change is comment only — verified by rendering the chart
against the values block before and after: byte-identical, so Flux performs no
helm upgrade.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewed and merged by argus-bot (REN-149). Author atlas-bot; four-eyes split intact.
Verified live: no alerting rule can match job="blackbox-ren28-negative" (24 probe_/up/scrape_ rules enumerated; tcp_connect module timeout 5s < scrapeTimeout 10s keeps up==1); pod IPs 10.244.4.166/10.244.2.220 current; probeSelector strict so the release label is load-bearing and is set; monitoring has no egress enforcement.
Temporary. Delete probes-ren28-negative.yaml and its kustomization line once the REN-28 "after" reading is recorded.
Revert: git revert -m 1 <this sha>
REN-28 item 3a. Narrows the navidrome baseline CNP's whole-monitoring-namespace, every-port ingress admit to the blackbox-exporter pod on :4533 and :8686, and repoints two comments elsewhere in the tree that cited the replaced rule.
Reviewed by argus-bot at head 1c1a637c in a private detached worktree. ci-manifests-test and ci-scripts-test green at that head. Blocking review 1065 (a false ICMP justification) resolved by deletion in 1c1a637c.
Revert: git revert -m 1 <this sha>, through a PR.
ha-pg-1 queues WAL for archiving every night: flat 0 until 02:12Z, peak
02:14-02:15Z, drained by 02:22Z, peaks of 56-119 segments on the seven
nights measured 2026-09-19 to 2026-09-26. Point-in-time recovery for
home-assistant is degraded for the ~10 minutes each burst lasts. Nothing
fires, because CNPGWalArchiveBacklog's `for` was 30m.
The cause is a write burst, not IO contention. Drain was a flat 13.0-14.9
segments/min on all seven nights, including the night whose Kopia
repository-maintenance batch started only after the burst had drained;
that batch drifts across 02:03-02:24Z while the peak stays pinned to
02:15Z. Fill is what varies, 18.7-39.7 segments/min. 02:12Z is 04:12
local, when Home Assistant's recorder purge runs. An earlier revision of
this commit named Velero/Kopia as the cause; the 7-night data retracts
that, and the annotations no longer send anyone to the velero namespace.
Three changes, all alert-rule text. Nothing restarts, no storage and no
traffic path is touched.
- CNPGWalArchiveBacklog `for`: 30m -> 15m. This does not fire on the
nightly burst at either value, and should not: every burst in the week
drained itself, and the longest contiguous excursion above 5 was 10
minutes. 15m is for the archiver that has actually stopped, which is
the failure that fills the PVC and stops Postgres.
- New CNPGWalArchiveBacklogSurge, >64 segments (1 GiB) for 8m, warning.
Covers the shape the 15m rule cannot see: a working archiver outrun by
production. 8m because the measured envelope spends at most 5
consecutive minutes above 64 on any night, so 8m clears the worst
observed night by 3 minutes while still catching 1 GiB that is not
draining -- from 119, a healthy archiver is back under 64 inside 5
minutes.
- Both exprs wrapped in `max by (namespace, pod)`. `role` flips on
failover, a changed label is a new series, and a new series restarts
the `for` timer. On the raw series a backlog that persists through a
failover at minute 11 is silent at 16m and 20m and only pages by 25m.
Verified with promtool 3.6.0 against the measured traces, not applied to
the cluster: `check rules` passes, and unit tests cover the ordinary
night, the worst night, the quiet night, a stalled archiver, and a role
flip mid-backlog. Negative controls confirm the tests have teeth -- at
`for: 2m` the surge rule fires at 02:17Z with $value 99 on the ordinary
night, and without `max by` the role-flip case fails.
Revert: `git revert <this commit>` restores both rules exactly. No state
outside Prometheus's rule evaluation is affected either way.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds one Probe, `blackbox-negative-control-paperless`, with its own jobName
`blackbox-negative-control` and four tcp_connect targets in the `paperless`
namespace. The targets exist to be REFUSED: each must read probe_success == 1
at this commit and 0 once REN-28 PR 2b narrows the whole-namespace `monitoring`
admit in paperless-ns.yaml. That flip is the negative test, and it then stays
on as a standing regression alarm.
This is monitoring-only. No traffic-path change, no restart, no new grant, no
policy edit. blackbox-exporter already runs in `monitoring`, whose baseline CNP
is ingress-only, and default-deny.yaml is scoped `namespace: apps`, so the
paperless ingress policy is the only variable that can move these controls.
Deliberately silent: both blackbox alerts are scoped job="blackbox-http", so a
distinct jobName is outside them and four permanently-failing targets page
nobody after 2b. The inverted alert CNPNegativeControlReachable ships in PR 2b,
not here — between 2a and 2b these controls are supposed to read 1.
Appended at end of file so existing line-number citations (including
paperless-ns.yaml's reference to probes-blackbox.yaml:250) do not shift.
Refs REN-28 §5.6, §6.4. Revert: drop this one Probe document.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The `fromEntities: cluster` + `icmps` rule added earlier in this branch was
justified by a false premise. Its comment claimed the new `toPorts` was "the
first layer 4 ingress rule in this namespace" — at f177685d the ingress already
ended in an `icmps:` rule, which is itself a layer 4 rule. So the allow was
neither required by this change nor new with it.
It also did not match its own header ("ICMP from the host and the other nodes"
vs `fromEntities: cluster`), and for pods it was redundant: IPv4 type 3 and
IPv6 types 1 and 2 are already in the namespace-wide ICMP rule below it.
ha-pg-allow-cnpg-operator does not transfer as precedent: it is pod-scoped and
carries no `host`/`remote-node` allow, so `cluster` is the only thing admitting
the host there. Four peer baselines with exactly this shape — monitoring,
home-assistant, ai, external-secrets — carry no entity-scoped ICMP allow, and
monitoring is where blackbox-exporter and Prometheus themselves run.
The branch is now exactly the approved REN-28 §4.1 scope: the monitoring
namespace admit is narrowed to the blackbox-exporter pod on 4533/8686, and
nothing else in navidrome-ns.yaml changes.
Revert: `git revert` this commit restores the allow; reverting the whole PR
restores the namespace-wide monitoring admit.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Two of the four REN-28 §7 negative tests need a prober inside `monitoring`
that is not the workload under test. `pods/exec` in `monitoring` was declined
as a wider grant than the change it verifies, so this uses the prober the
cluster already runs: blackbox-exporter, driven by a Probe object, read back
through the existing read-only REN-36 Prometheus grant.
Adds one Probe, jobName `blackbox-ren28-negative`, module tcp_connect, four
targets that are reachable from `monitoring` TODAY and that the REN-28
paperless PR must close:
10.244.4.166:9187 / 10.244.2.220:9187 pprl-pg metrics port -> §7 test 4
pprl-pg-r.paperless.svc:5432 same, stable name
gotenberg.paperless.svc:3000 another paperless-ns workload -> §7 test 2
This is the "reachable before" half. It can only be taken while the current
policy is still in force, which is why it goes in ahead of the paperless PR.
No alert can match it: every rule in prometheusrule-blackbox.yaml is scoped to
job="blackbox-http", and TargetDown cannot fire because a Probe's scrape target
is blackbox-exporter itself, which answers 200 with probe_success 0 on a failed
probe.
No workload is touched, no restart, no policy change, no secret. Revert is
deleting the file and its kustomization line.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One semantic line: spec.storage.size 20Gi -> 32Gi. The sizing comment above it
is rewritten in the same commit because it justified 20Gi on table growth
("leaves room for autovacuum bloat and index churn"), which is no longer the
reason for the number.
The reason is WAL headroom during an archiving stall. 20Gi left 6.63 GiB free;
when WAL archiving stalled for 10 minutes under Velero repository maintenance
(REN-129) the un-archived backlog hit 1.84 GiB, 28% of that free space. Postgres
cannot recycle a WAL segment until the archiver ships it, so a longer stall
walks the volume toward full and a full PGDATA stops the recorder. 32Gi takes
free space to ~18.6 GiB and the same backlog to 10% of headroom.
Owner approved option A on REN-135 (apply now, do not gate on the capacity
read). Options B (archiver maxParallel test) and C (throttle Velero) were
declined, so ha-pg-objectstore.yaml and Velero are untouched.
Verified by parsing the file before and after: with spec.storage.size masked,
the two documents are byte-identical as parsed objects. instances: 2,
storageClass: ceph-block, all 8 postgresql.parameters and resources are
unchanged.
NOT verified here: `ceph df` for the replicapool pool. This runtime has no
kubectl and no Ceph read path — see the PR body.
Revert: `git revert <merge-sha> -m 1` restores the manifest, but the PVC
expansion itself is NOT reversible; PVCs do not shrink.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Per-object Flux readiness cannot come from the controllers: gotk_reconcile_
condition and gotk_suspend_status are declared by fluxcd/pkg/runtime and
populated by nothing at the pinned tags, so the REN-97 scrape yields 2,273
gotk_* series and none of them are about readiness. This turns on
kube-state-metrics custom-resource-state over eight Flux kinds instead, and
builds the standing not-Ready alerts on it.
The not-Ready half is complete. The Ready-but-frozen ImagePolicy half ships
as recording rules only: no threshold is derivable from the history that
exists today, and the measurements are in the rule file rather than left
implicit.
- kube-state-metrics customResourceState + rbac.extraRules (list, watch only)
- prometheusrule-flux.yaml: FluxImageAutomationNotReady (2h),
FluxResourceNotReady (1h), FluxImagePolicyMetricsMissing (absent, 30m),
and four recording rules for the staleness signal
- image-automation README: the "What watches this" section said both cases
were uncovered
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewed and merged by argus-bot (four-eyes: authored by atlas-bot).
Review 1063 on REN-137. Claims 1-3 re-derived from IMC v2.2.13 source and the
vendored CRD; host choice checked against the live status-page feed (9 baseline
monitors, neither target present). Revert: revert this merge commit; prune=true
plus enableMonitorDeletion=true returns the account to the 9-monitor baseline.
Recorded finding (non-blocking): UpTimeMonitorService.Add() returns nothing and
logs every failure, so a rejected newMonitor call is invisible in Kubernetes and
in Flux. A negative status-page result must not be read as "explicit-list mode"
for REN-9 without a controller-log read.
Implements the owner's decision on REN-52 (2026-09-25, option B), tracked by REN-111: one ClusterRoleBinding, built-in `view` ClusterRole, subject the paperclip/paperclip-prometheus-read ServiceAccount from REN-36.
Reviewed at head 230756c3 in a private worktree. Verified: exactly one object, no Secrets, no write verb, no new ServiceAccount; subject resolves against prometheus-read-rbac.yaml:48-52; name paperclip-cluster-view is unique in the repo. Flux CRD coverage confirmed in git — flux-view-flux-system carries aggregate-to-view: "true" (gotk-components.yaml:240) over all six toolkit groups, so the REN-52 pre-approved extra ClusterRole is correctly not added.
Revertibility confirmed rather than assumed: clusters/homelab/paperclip.yaml:14 is prune: true over ./kubernetes/apps/paperclip and the object has no prune: disabled annotation.
Revert: `git revert <this-merge-sha> -m 1`. Flux deletes the binding within the Kustomization's 10m interval and the token falls back to its REN-36 scope (get services/proxy on two named Services). No workload is touched either way.
Not verified here, owed on REN-111: the post-merge SelfSubjectAccessReview from inside the pod, proving the grant took effect and that view-aggregation actually covers the Flux CRDs in the live cluster.
Reviewed and merged by argus-bot (REN-134). CI green on 97b7f79b: ci-manifests-test, ci-scripts-test, pr-review.
One semantic line: OpenBaoUnavailable for: 1m -> 5m. Verified against the repo that 1m was the tightest probe window in the cluster (next tightest 5m), that probe and rule evaluation both run at 30s, and that severity=critical routes to the email receiver rather than a pager. Revert: git revert 97b7f79b.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
navidrome and lidarr both take identity from X-Authentik-Username
unconditionally (navidrome/deployment.yaml), so any pod that can open a TCP
connection to their listeners can claim any username. The baseline CNP
admitted the WHOLE monitoring namespace on EVERY port, which put every
current and future monitoring workload inside that boundary. Only two flows
actually cross it, and both are blackbox probes.
Exposure delta, monitoring -> navidrome namespace:
before: every pod in `monitoring`, all 4 workloads in `navidrome`, every port
after: blackbox-exporter only, :4533 (navidrome) and :8686 (lidarr)
Both selectors and both ports are measured on the live cluster, not inferred:
* `app.kubernetes.io/name: prometheus-blackbox-exporter` resolves to pod
blackbox-exporter-prometheus-blackbox-exporter-57844f6f8d-6gqz8 in
`monitoring` (Prometheus service-discovery pod labels, read through the
REN-36 read-only proxy grant). Chart prometheus-blackbox-exporter-11.19.1.
* `probe_success` is 1 for both
http://navidrome.navidrome.svc.cluster.local:80/ping and
http://lidarr.navidrome.svc.cluster.local:80/ping. Those Services select
`app.kubernetes.io/name: navidrome` / `lidarr` and would have no endpoints
if the pods did not carry the labels, so the probe path is proven end to
end, prober pod to app pod.
* Service :80 -> containerPort 4533 (deployment.yaml) and 8686
(lidarr.yaml). Cilium matches the POD port after service translation, so
`port: "80"` would read clean in review and break both probes.
ICMP rule added, and it is NOT cosmetic. The toPorts above is the first
layer 4 INGRESS rule in this namespace -- navidrome-ns.yaml is the only CNP
selecting these pods, and its ingress was L3-only until now. Cilium drops
ICMP to an endpoint selected by any layer 4 policy unless the traffic is
CT-related, and the existing ICMP allow matches pods only
(io.kubernetes.pod.namespace Exists), which `host` and `remote-node` do not
carry. The kubelet sources the HTTP probes from the host netns, so its PMTUD
path is host-sourced: without the new rule, Destination Unreachable / Packet
Too Big toward these pods are dropped silently and the symptom is a stalled
transfer, not a policy denial. Same rule, same three types, same reason as
ha-pg-allow-cnpg-operator in home-assistant-ns.yaml. This is an ADDITION to
the approved plan text, flagged here because it is mine, not the plan's.
Two citations elsewhere pointed at the rule being replaced and would have
become false -- the defect REN-28 is about, in reverse. Both now cite what a
rule admits rather than a line number, so they cannot go stale again:
* navidrome/deployment.yaml -- the union list of what really reaches the pod
* monitoring-rules/probes-blackbox.yaml -- the justification for why the
REN-21 probes needed no policy change
NOT touched: `host`/`remote-node` (deleting it CrashLoops both apps -- the
kubelet probes come from there), the in-namespace allow, the Traefik and both
gate-outpost allows, the existing ICMP rules, every egress rule, and
ND_EXTAUTH_TRUSTEDSOURCES (item 2 of REN-28, declined and documented in-tree).
No workload restart: a CNP change is picked up by Cilium in seconds.
Revert: `git revert -m 1 <merge-sha>` on master, through a PR like any other
change. Pure CNP + comment change -- no data migration, no restart, no
rollout; Flux reconciles and Cilium re-applies in seconds. Emergency path if
Flux is impaired:
kubectl apply -f <(git show <merge-sha>^:kubernetes/infrastructure/network-policies/navidrome-ns.yaml)
Detection that a revert is needed, inside one 30s scrape interval:
`probe_success == 0` on either navidrome target (alerting already exists in
prometheusrule-blackbox.yaml), or either pod leaving Ready.
REN-28 item 3a, approved by the owner. Paperless (item 3b) is a separate PR
and does not open until >=24h of green probes here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first two of the survey's 14 page rows, and the first objects in this
repo that cause an UptimeRobot write. Both hosts are ungated, both serve a
real application response rather than a gate, and neither has a monitor in
the account today — so the pilot neither duplicates an existing monitor nor
puts anything on the public page that the owner has not already approved.
Uses `spec.url`, not `spec.urlFrom.ingressRef` as REN-7 was written: an
EndpointMonitor can only resolve an ingressRef in its OWN namespace
(pkg/kube/util/url.go:71), `IngressURLSource` has no namespace field, and
every app Ingress lives elsewhere. Reaching them would mean RBAC and a
widened WATCH_NAMESPACE in each production app namespace. Full reasoning in
the file header.
Deliberately omits `uptimeRobotConfig.statusPages`, so that whether these two
monitors appear on uptime.renner.uno on their own answers REN-9's open
question about the page's monitor-list mode.
Also records two source-verified properties that change how this must be
operated, both in comments: a Ready Kustomization cannot prove the monitor
was created (EndpointMonitorStatus is an empty struct, so kstatus returns
Current unconditionally), and v2.2.13 uses no finalizer, so deleting an
EndpointMonitor while the controller is down orphans its monitor.
Verified: kustomize build over all 68 kustomizations, 68 ok / 0 fail; both
rendered EndpointMonitors validated with kubeconform -strict against the
schema extracted from the vendored CRD, with a negative control confirming
the check rejects a bad enum. Lint/render only — NOT applied; Flux applies
from master and these objects do not exist in the cluster yet.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
OpenBaoUnavailable went pending at 01:07 UTC on a single 5s HTTPS probe
timeout while OpenBao was healthy, one evaluation short of paging a
critical. At 1m it was the tightest probe-based window in the cluster by
5x, and isolated single-sample probe blips are ambient on the
blackbox-http fleet at roughly one per two hours (three on three
unrelated targets in six hours).
5m matches WoodpeckerDown in the same group. Nothing real is lost: the
rule's own description says existing Secrets keep working and the damage
surfaces at the next ExternalSecret refresh, and OpenBaoQuorumLost
(for: 2m) still covers the fast-moving failure.
expr and severity are unchanged; this only moves the for: window and
replaces the now-wrong "two consecutive failed probes" comment.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment-only. The narrowing in 16c0c0a7 is unchanged; this records what the
two expressions do with the shape neither of them mentions — an ExternalSecret
with no `Ready` condition.
Review of REN-106 asked for the third state to be made explicit, on the
strength of a metrics census showing `ai/wiki-index-basic-auth` with only a
`Deleted=False` condition. That series is real; the object behind it is not.
ESO v2.11.0 builds the `Deleted` condition on a throwaway object in the
`IsNotFound` branch of Reconcile, purely to retire the object's metric series,
and never persists it — so the series outlives the object until the ESO pod
restarts. That ExternalSecret was retired from git in 419cbed3, its target
Secret is gone from `ai`, and its sync counter is frozen while its six live
siblings advance. On a live object ESO writes conditions of type `Ready` only.
What that leaves is three shapes, all decided: no conditions yet (in-progress,
bounded by `timeout: 5m`), `Ready=True` (current), `Ready=False` (failed for
the two non-retrying reasons, in-progress otherwise). Flux evaluates
inProgress -> failed -> current and falls back to in-progress, so an explicit
`inProgress` expression would be a no-op on all three; the reasoning is written
down instead of encoded twice.
Also noted: `Ready=True` covers reasons `SecretDeleted` and `SecretMissing`,
where ESO reports healthy with no Secret behind it. Unreachable under this
object's default Retain/Owner policies, and a trap for whoever changes them.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewed as argus-bot (review 1056) at head 2821629e. Comment-only, reproduced independently in a private worktree: `git diff -U0` against merge-base 64ee49ab has no added or removed line that is not a whole-line `#` comment, and with comment lines stripped both files are md5-identical to master (crd f753baa9c9684002ad01d309e98755ba, rbac 092ddad9c003209d4bd27721c04048b1). Non-comment bytes identical means the parsed YAML is identical, so the applied objects cannot change: no apply, no RBAC change, no restart of the ingressmonitorcontroller pod.
Provenance re-derived from upstream, not taken from the PR body: 2deeba18 is the v2.2.13 tag commit (2026-05-06), aaffd8a8 is a 2-parent master merge commit (2026-09-25), the two chart CRDs are byte-identical, and the vendored body matches upstream at the tag (md5 f753baa9c9684002ad01d309e98755ba over `apiVersion:` to EOF) — so the new header is a check a human can actually run. All seven cmd/main.go + ingress-wrapper.go + url.go line citations in rbac.yaml verified at the tag.
Render check is CI's ci-manifests-test (green at 2821629e, with ci-scripts-test and pr-review; combined state success) — no kustomize in my sandbox either, so I am not claiming a local render.
Revert: `git revert <this-merge-sha> -m 1`. Nothing in the cluster to unwind.
Closes the two follow-ups recorded on the #995 merge (REN-6).
Reviewed and merged by argus-bot (REN-124). Comments only: js-yaml loadAll base vs head parsed-identical for all three files; gatus data["config.yaml"] byte- and hash-identical at 6572 bytes, so the gatus Deployment does not roll. All 7 repointed ranges resolved against the target files at 317ddbe9. Revert: git revert 317ddbe9.
Both found by Argus in the #995 review and handed to REN-6 as follow-ups.
Comments only — every added and removed line starts with `#`, and both files
are md5-identical to master once comment lines are stripped.
rbac.yaml misstated the failure mode of the omitted pods/services grant. It
claimed a denial "costs a log line, not a reconcile". The function's own error
branches do log and continue, but an RBAC denial never reaches them: the
reconciler holds `mgr.GetClient()` (cmd/main.go:168), so the first Service Get
starts an informer and blocks in cache sync on a `context.TODO()` that nothing
cancels. A forbidden list/watch means the Get never returns and the single
reconcile worker (default `--max-concurrent-reconciles=1`, cmd/main.go:84)
stops entirely, while `/readyz` — `healthz.Ping` — keeps the pod Ready. The
omission is still right (the ingressRef path cannot reach this namespace), but
the grant has to precede an Ingress landing here, not follow the breakage.
crd-endpointmonitors.yaml cited aaffd8a866aa09a77030979d938e766895bde5d4 as the
copy point. That is a master merge commit from 2026-09-25, four months after
the v2.2.13 tag the image is pinned to. Now cites the tag commit
2deeba188fbd2860d0b3a7c3590a954a1a709924 plus the md5 of the vendored body, so
the file can be checked against the version actually deployed. The content was
never wrong: that chart CRD is byte-identical between the tag and the commit
the old header named.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PR #997 inserted 9 comment lines into `paperless-ns.yaml` and 21 into
`navidrome/deployment.yaml`. The citations that #997 itself wrote were
renumbered; the citations elsewhere in the repo pointing *into* those two
files were not. REN-116 (merged as a8c48b54) then added one more line to
`paperless-ns.yaml`, shifting everything below it by one again.
Seven pointers repointed, all verified against this commit's tree:
probes-blackbox.yaml:156 navidrome/deployment.yaml 61-74 -> 80-93
probes-blackbox.yaml:218 paperless-ns.yaml 24-26 -> 34-36
probes-blackbox.yaml:221 paperless/network-policy 8-12 -> 22-26
gatus/configmap.yaml:47 paperless-ns.yaml 24-26 -> 34-36
paperless/network-policy.yaml:15 paperless-ns.yaml 30-32 -> 31-33
paperless/network-policy.yaml:16 paperless-ns.yaml 33-35 -> 34-36
paperless/network-policy.yaml:17 paperless-ns.yaml 36-38 -> 37-39
REN-119 specified 34-36 as `33-35` and listed only the first four rows.
Both were correct at `bebf9aad` but are stale after REN-116's one-line
insert, which is above every allow rule in the file. The last three rows
are the same defect from the same commit; leaving them would put the
file back in review for an identical fix.
`paperless-ns.yaml:9-11` (paperless namespace) is above the insert and is
unchanged — verified, not assumed. `navidrome/lidarr.yaml:114-127`,
`lidarr.yaml:29` and `probes-blackbox.yaml:250` were re-checked and are
still correct.
Comments only, and nothing is applied to the cluster. Verified per file:
`js-yaml` loadAll before and after yields identical structures (3 docs,
1 doc, 2 docs), and stripping comment/blank lines from both versions
gives identical text. `gatus/configmap.yaml:47` is in the header above
`data:` (line 62), outside the `config.yaml: |` block scalar, so
ConfigMap `data["config.yaml"]` stays byte-identical at 6572 bytes and
`configmap.reloader.stakater.com/reload: gatus-config` does not roll the
gatus Deployment. All seven edits substitute a range in place; line
counts are unchanged (312/283/97 before and after), so nothing cascades.
Revert: `git revert <this commit>`. Comments only, so a revert restores
the stale numbers and changes no cluster state.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The proxy-grant paragraph named `prometheus-read-rbac.yaml` and
`helmrelease-prometheus.yaml` by bare filename. Both are now repo-relative
paths, matching how the rest of this page cites manifests, so a 403 at 03:00
does not start with a grep.
Docs-only. No manifest changed. `docs/` is outside every Flux Kustomization
`spec.path` (all of them are under `./kubernetes/`), so Flux renders nothing
from this commit.
Follow-up to Argus's non-blocking note 2 on REN-113/REN-47.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Reviewed-by: argus-bot <svc-argus@renner.uno>
Comments-only. Citation re-verified at head a8c48b54: line 250 of
kubernetes/apps/monitoring-rules/probes-blackbox.yaml is the quoted probe URL,
inside the blackbox-http Probe in namespace monitoring. Both versions of
paperless-ns.yaml parse to identical JSON across all three CiliumNetworkPolicy
documents, so re-applying is a no-op against the running policy.
Revert: git revert -m 1 <this commit>
Reviewed and merged by Argus (REN-105), review 1038 at c40c337e.
Delete-path bound re-derived from v2.2.13: GetByName exact FriendlyName equality against the default template {{.Name}}-{{.Namespace}}; GetAllByName and uptimerobot GetAll have no reachable callers. Ships zero EndpointMonitors, so the first reconcile makes no UptimeRobot call and the 9 existing monitors are untouched.
CI green at c40c337e (ci-manifests-test, ci-scripts-test, pr-review). Nothing applied to a cluster pre-merge; verification was lint/render only.
Open follow-ups for Atlas on REN-6, both comment-only: rbac.yaml misstates the failure mode for the omitted pods/services grant (a denial blocks the cached client in cache sync and wedges the single reconcile worker while the pod stays Ready, rather than costing a log line), and the CRD vendor header cites a post-tag master commit instead of the v2.2.13 tag.
Revert: git revert -m 1 of this merge commit.
`paperless-ns.yaml` cited the blackbox probe target as
`monitoring-rules/probes-blackbox.yaml:250`. From
`kubernetes/infrastructure/network-policies/`, that fragment reads as a
sibling `infrastructure/monitoring-rules/` directory, which does not
exist — `monitoring-rules/` lives only under `kubernetes/apps/`.
Use the full repo-root path, matching the style already used six lines
above for `kubernetes/apps/paperless/network-policy.yaml`. Line 250 and
the probe target itself were correct and are re-verified at this commit:
line 250 is
`- http://paperless.paperless.svc.cluster.local:80/accounts/login/`.
Comments only. Parsing the file before and after with js-yaml yields
identical structures for all three CiliumNetworkPolicy documents, and
stripping comment lines from both versions gives an identical diff. No
`spec` change, no new resource, nothing applied to the cluster.
Follow-up from the REN-110 review of PR #997 (merged as 0995e8d5).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous wording claimed home-assistant-ns.yaml was a second
port-restricted `monitoring` grant and that 17 others were namespace-wide.
Both numbers were wrong. Counted per ingress list item over
kubernetes/infrastructure/network-policies at master:
20 `monitoring` grants across 19 files
(influxdb-ns.yaml carries two — one whole-namespace, one Grafana-pod)
exactly one of them — this rule — carries a `toPorts`
home-assistant-ns.yaml looks port-scoped only because the `toPorts` that
follows its `monitoring` rule is indented under the next list item, the
`evcc` rule. Its `monitoring` grant is namespace-wide.
Comment-only. The parsed CiliumNetworkPolicy is byte-for-byte identical in
structure to master's.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Docs-only. One file, docs/superpowers/specs/2026-09-25-paperclip-design.md, +20/-2.
Closes the four documentation items on the REN-47 review bar (1/14, 5, 7, 15) for the
option-B apiserver-proxy grant, and drops the stale Prometheus-only resourceName text
that REN-95 left behind.
BLAST RADIUS: none. Every Flux Kustomization spec.path in this repo is under
./kubernetes/; docs/ is rendered by nothing. Flux applies nothing from this merge and
no workload restarts. No owner-facing surface changes.
REVERT: git revert %(SHA)s -m 1
One commit, one file, no cluster state to unwind. Nothing to re-apply, no token to
rotate, no pod to restart.
Reviewed by argus-bot (review 1031) against manifests, not against the PR body:
Role is one rule, verbs [get], resources [services/proxy], both resourceNames match
prometheus-read-rbac.yaml on master; chart float >=72.0.0 confirmed in
helmrelease-prometheus.yaml. Authored by atlas-bot, merged by argus-bot.
Implements the owner's decision recorded on REN-52 (2026-09-25, option B),
which was never shipped: one ClusterRoleBinding, roleRef the built-in `view`
ClusterRole, subject the ServiceAccount REN-36 created. No new
ServiceAccount, no new ClusterRole, no write verb, no Secrets.
Kept in its own file rather than appended to prometheus-read-rbac.yaml so it
reverts alone, and because that file's header argues for staying narrow —
which this grant is not. Both of that file's now-false claims are corrected
in place: the token is no longer worth only what its Role grants, and the
`paperclip-prometheus-read` name no longer describes it (REN-52 ruled the
name stays, since `serviceAccountName` is in the pod template and renaming it
would restart the Deployment and evict running agents).
No extra Flux CRD ClusterRole: `flux-view-flux-system` already carries
`rbac.authorization.k8s.io/aggregate-to-view: "true"` with get/list/watch on
`*` across all six Flux API groups, so `view` picks them up by aggregation.
REN-52 pre-approved adding one only if that were absent.
Additive RBAC on an existing ServiceAccount: no Pod restart, no remount, no
traffic path changed. RBAC is evaluated per request, so it takes effect on
the already-mounted token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stakater IngressMonitorController in its own namespace, with zero
EndpointMonitor resources, so it creates nothing on arrival.
- Vendored EndpointMonitor CRD from chart 2.2.13, image pinned to v2.2.13
and deliberately kept out of Flux image automation.
- Namespaced Role only (no ClusterRole): WATCH_NAMESPACE scopes the
manager's cache to uptime-monitor. Verbs derived from what v2.2.13
actually calls; every omission is justified inline in rbac.yaml.
- The ExternalSecret now renders IMC's config.yaml, because IMC reads its
whole configuration from one Secret key rather than an env var. Config
stays in git; only the key is injected.
- Per-namespace CiliumNetworkPolicy, copied from the netbird-service-sync
neighbour: kubelet probes in, DNS + kube-apiserver + world:443 out.
- The Flux Kustomization keeps the healthCheckExprs gate and the
`wait: true` correction that #993 already merged, byte for byte. What
this commit adds to that file is only what is about REN-6: the
EndpointMonitor-vs-annotation mechanism in the header, and why
Deployment health is not a proxy for a current key now that the
controller shares the Kustomization with the ExternalSecret.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Argus's review bar for the option-B proxy grant (REN-47) asks for three notes on
the hand-over page. None of them was ever written down, and the page had also
gone stale: it still named only the Prometheus resourceName after REN-95 added
Alertmanager.
Added, next to `automountServiceAccountToken: true` where the grant is already
described:
- Both granted resourceNames, not just Prometheus (bar item 1/14).
- The Role scopes the HTTP *method*, not the endpoint — `get` refuses
`POST /-/reload` in the authorizer, but every GET endpoint of both services is
reachable, including `/api/v1/status/config` (bar item 5). Verified live this
run: the six `credentials` fields in that config render as `<secret>`.
- Callers must force GET; Prometheus accepts POST on `/api/v1/query` and that
form 403s under this Role (bar item 7).
- The drift defence: a 403 means check the Service name against `resourceNames`
first, because the chart floats at `>=72.0.0` and a rename breaks this path
silently (bar item 15).
BLAST RADIUS: none. One markdown file under docs/, not rendered by any
Kustomization, so Flux applies nothing and no workload restarts.
REVERT: revert this commit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds a PodMonitor for the six Flux controllers in flux-system. Scrape only —
the not-Ready alert rules that were in the first revision of this branch are
removed, because they cannot work at the versions we run.
Why the rules are gone rather than merely deferred: `gotk_reconcile_condition`
and `gotk_suspend_status` are still declared by fluxcd/pkg/runtime v0.110.3
(runtime/metrics/recorder.go), but at the tags pinned in gotk-components.yaml
(source/kustomize v1.9.5, helm v1.6.4, notification v1.9.4, image-reflector and
image-automation v1.2.5) no controller calls RecordReadiness, RecordCondition
or RecordSuspend — zero call sites across all six tagged source trees, only
RecordDuration. An unpopulated GaugeVec exports no series, so those rules would
have evaluated against nothing for ever and the `absent()` dead-man's-switch
would have mailed admin@renner.uno every 12h from 30m after merge. Flux moved
per-object readiness to kube-state-metrics custom-resource-state
(`gotk_resource_info{ready=...}`), which is REN-102 — a prerequisite for the
alert, not a follow-up to it. Caught in review by argus-bot on #992.
What the scrape still buys on its own: `gotk_reconcile_duration_seconds` per
kind/name/namespace, controller-runtime's reconcile/error/workqueue series,
`up{job="monitoring/flux-controllers"}` under the chart's default TargetDown
rule, and the target REN-102 needs in place anyway.
PodMonitor and not ServiceMonitor is forced: four of the six controllers have
no Service, and the two that do publish the artifact/webhook port, not :8080.
No network policy change is needed and that was checked, not assumed —
gotk-components.yaml ships `allow-scraping` admitting every namespace to :8080
and flux-system has no CiliumNetworkPolicy. Nothing in the Flux install is
touched; Prometheus adds a read-only target.
Verified offline only: `kustomize build kubernetes/apps/monitoring-rules` clean
and the PodMonitor renders; all six Deployments carry both selector labels and
the `http-prom` container port. Not applied to the cluster — no kubeconfig in
this account's grants.
Refs REN-97, REN-102.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The healthCheckExprs block merged in #993 treated any `Ready=False` on the
ExternalSecret as Failed. The `flux-failures` Alert forwards every Kustomization
error event to Alertmanager and Flux health checks have no `for:` window, so a
single unlucky poll mailed the owner about something that was about to fix
itself.
REN-106 asked for `reason == 'SecretSyncedError'`, matching wordpress.yaml.
Checked against the ESO the cluster runs (v2.11.0) that is the wrong reason:
`SecretSyncedError` is the reason ESO sets when it returns an error and keeps
retrying — an OpenBao blip, a token renewal and a not-yet-populated key all
produce it. `SecretOwnedByOther` and `SecretImmutable` are the two reasons ESO
returns nil on, i.e. where it has stopped retrying.
So `failed` now names those two, and `SecretSyncedError` falls through to
in-progress and fails the Kustomization at the existing `timeout: 5m`, which
becomes the missing `for:` window. Detection of a genuinely stuck sync moves
from immediate to ~5m; a transient blip no longer reaches the owner at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment-only. No manifest field changes, so `kustomize build` output is
byte-identical before and after.
Two gaps in what REN-103's merged scrape says about itself, both raised in
review of PR #994/#996:
* The metrics endpoint is plain HTTP with no authentication. That is chart
2.11.0's default (`metrics.listen.secure.enabled` and
`metrics.listen.auth.enabled` both false, values.yaml:365-369) and this
repo overrides neither, so what actually bounds the exposure is the
CiliumNetworkPolicy, not the endpoint. A reader adding `tlsConfig` to the
PodMonitor would have broken the scrape; say so in the file.
* The network policy comment implied the grant reaches the controller. It
does not: the CNP's `endpointSelector` is {}, so monitoring→:8080 is
admitted for all three ESO deployments, which all serve metrics on that
port. Only the controller is scraped, because the PodMonitor's selector is
exact on the unsuffixed name. Reachable-but-unvisited is the accurate
description and the one a future reader needs.
Also corrected two counts in the same comment. The port-scoping is not unique
in this directory — home-assistant-ns.yaml does it too, so 17 of the other 18
`monitoring` grants are namespace-wide, not all 18. And there is no single
"health port": the controller's is 8082, the webhook's and cert-controller's
are 8081 (values.yaml:501-504, 564-570, 882-885).
Revert: `git revert` this commit. It restores the previous comment text and
touches no applied field.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comments only. No `spec` change, no pod template change, no rendered
manifest change.
Three comments told the reader the ingress side of paperless and navidrome
is locked to the Authentik gate outposts. It is not. `default-deny-ingress`
for both namespaces uses `endpointSelector: {}`, so it selects the app pods
too, and Cilium unions the allow rules of every CNP selecting an endpoint.
The effective ingress set is therefore the outpost PLUS whole namespaces:
`database` and `monitoring` for paperless, and `monitoring` plus the
lidarr-gate outpost for navidrome. Because both apps trust
X-Authentik-Username unconditionally, any pod in those namespaces can claim
any username.
- kubernetes/apps/paperless/network-policy.yaml: replace "Lock the ingress
side to the gate outpost only" with what is actually enforced, citing the
rules in paperless-ns.yaml.
- kubernetes/infrastructure/network-policies/paperless-ns.yaml: note that
the database and monitoring allows below do broaden the trust boundary in
exactly the way the traefik note above warns about, and why they are kept
(CNPG, and the blackbox probe of the paperless login page).
- kubernetes/apps/navidrome/deployment.yaml: ND_EXTAUTH_TRUSTEDSOURCES is
the whole pod CIDR and is not a narrowing; record why it stays that way
(the outpost pod IP is not stable) and list what the CNP really admits.
Verification: `kubectl kustomize` (v1.31.0 / kustomize v5.4.2) over all 68
kustomization roots in the repo renders byte-identical before and after;
every added and removed line in the diff is a comment line.
Revert: `git revert <this commit>`; restores master 3d6c9940 exactly.
Refs REN-108, REN-28.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Make the lidarr and tika ImagePolicy sort keys a total order, and undo the
automated ls42 -> ls11 downgrade that the defective lidarr key caused.
Reviewed and merged by argus-bot; authored by atlas-bot (four-eyes).
Pod roll of deploy/lidarr in ns navidrome signed off by Zeus on REN-91.
Revert: policy only `git revert e50ccaec`; pin only `git revert 430475a7`;
everything `git revert 430475a7 e50ccaec` (restores master ff4ddaa6 exactly).
Refs REN-91.
Nothing in this cluster reported a failed ExternalSecret sync:
`externalsecret_status_condition` held 0 series, ESO's metrics port was never
scraped, and monitoring-rules had no ESO alert. A key that silently stops
syncing was invisible to Prometheus and to Alertmanager.
This is the scrape only. The alert rule is deliberately left out: its `for:`
window has to be tuned against real data, and that data does not exist until
this has been live for a while. Shipping a guessed window to the owner's
channel is the failure mode REN-100 warned about.
A PodMonitor rather than the chart's `serviceMonitor.enabled`, for two reasons
checked against chart 2.11.0:
* There is no metrics Service to select. `metrics.service.enabled` defaults
to false and the HelmRelease overrides nothing, so templates/service.yaml
renders nothing for the controller. A hand-written ServiceMonitor would
match no Service.
* `serviceMonitor.enabled: true` would work but triggers a Helm upgrade of
ESO, re-rendering its Deployment and re-applying its CRDs
(`crds: CreateReplace`). ESO is what all 54 ExternalSecret manifests in
this repo depend on. A PodMonitor gets the same series and touches the
release not at all.
Unlike the Flux PodMonitor, this needs a network policy change: the cluster is
default-deny and external-secrets-ns.yaml admitted only intra-namespace
traffic, host/remote-node and ICMP. Without the `monitoring` grant the scrape
is dropped and the metric stays absent, so the PodMonitor alone would have been
decoration. The grant is port-restricted to 8080, tighter than the equivalent
rule in velero-ns.yaml, and is additive — it removes nothing, so ESO's existing
reachability to OpenBao and the apiserver is unchanged.
Revert: `git revert <sha>`. Removes the PodMonitor and the ingress grant
together, returning to the current no-ESO-metrics state. No ESO object, no
ExternalSecret and no Secret is touched in either direction.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment in this file claimed `wait: true` was how a key that failed to
sync became visible. It is not. Flux ships no status reader for
`ExternalSecret`, so kstatus falls back to its generic rule, which inspects
only `status.observedGeneration` and conditions of type `Reconciling` /
`Stalled`. ESO sets none of those — a failed sync is `Ready=False,
reason=SecretSyncedError` — so kstatus returns Current and the Kustomization
goes Ready with no Secret behind it.
Corroborated in-repo rather than from docs: `llama-server` is also `wait: true`
and carries `ai/wiki-embed-basic-auth`, which sat in ESO retry backoff through
~10 failures on 2026-09-20 and needed a manual `force-sync` while its
Kustomization stayed Ready.
Adds a `healthCheckExprs` entry for `external-secrets.io/v1 ExternalSecret`,
the same mechanism already used here for CNPG `Cluster`, MariaDB and the Ceph
CRs, and corrects the comment.
Both expressions are guarded with `has(status.conditions)`, copying
wordpress.yaml. Unguarded, CEL errors rather than evaluating while `status` is
still empty — the state a freshly applied ExternalSecret is in, which is
precisely the case this check exists for.
Blast radius is this Kustomization alone: nothing `dependsOn` uptime-monitor,
and its only object today is the ExternalSecret itself. No existing workload,
ingress controller, DNS record or other Kustomization is touched.
Revert: `git revert <sha>`, or drop the `healthCheckExprs` block. Reverting
restores the previous behaviour (Ready regardless of sync state) and cannot
affect any other Kustomization.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Appends kube-prometheus-stack-alertmanager:9093 to the resourceNames of
Role paperclip-prometheus-read in namespace monitoring. Additive, get-only,
no pod template change.
Reviewed and merged by argus-bot; authored by atlas-bot (four-eyes).
Negative baseline reproduced live via SelfSubjectAccessReview before merge.
Makes the UptimeRobot API key available in the new `uptime-monitor` namespace via ESO. No controller, no monitors, no workload — the key only. First step of REN-2, closes REN-5.
Reviewed by argus-bot against a fresh clone at 25cabaed, not against the PR body. Verified: remoteRef key is mount-relative (store sets provider.vault.path: homelab; `grep -rn "key: homelab/"` = 0 hits); eso-read.hcl already grants homelab/data/*; allowlist is 20 entries, sorted, one occurrence; uptime-monitor is absent from apps/kustomization.yaml and clusters/homelab has no kustomization.yaml, so no double-application; both dependsOn targets exist in flux-system; no duplicate namespace name across the 31 declared; no key material in the diff.
Blast radius is one leaf Kustomization that nothing dependsOn -- deliberately NOT added to infrastructure/post-crds, where an unpopulated OpenBao path would stall the 19 Kustomizations that dependsOn infrastructure-crds.
Merge commit on purpose so the documented revert stays valid: git revert 25cabaed. prune: true then removes the Kustomization, ExternalSecret, Secret and namespace.
Non-blocking finding handed to REN-6: the comment claiming `wait: true` makes a failed ExternalSecret sync visible is wrong -- kstatus reports an ESO ExternalSecret Current regardless of Ready=False, which is why the `ai` ExternalSecret under the wait:true llama-server KS could sit in backoff through ~10 failures on 2026-09-20. Fix is a healthCheckExprs block, as already used for CNPG and Ceph CRs here.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Reverts the image change in b858b4aa. ls11 is 3.1.0.4875 as built on
2025-11-19; ls42 is the same upstream build rebuilt on 2026-09-23. The
31 rebuilds in between are LinuxServer security rebuilds of the base
image, so the cluster is currently running ten months of known-missing
base-image patches.
Kept as its own commit so it can be reverted independently of the policy
change, and done here rather than left to the controller: with the fixed
policy merged, image-automation would compute ls42 and push the same two
lines to master itself within ~30 min. Doing it in the PR makes the pod
roll a reviewed change that lands at a known time instead of an
unattended bot commit -- which is the failure mode this card is about.
Blast radius: one rolling restart of deploy/lidarr in ns navidrome, same
shape as the 60s outage b858b4aa caused. Lidarr is a read-only
missing-album view; nothing else consumes it. No other workload moves.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
lidarr extracted only <build> from <maj>.<min>.<patch>.<build>-ls<n>, so
all 32 published ls rebuilds of build 4875 shared one sort key and the
winner was an arbitrary tie-break. It fired: b858b4aa auto-committed
ls42 -> ls11 to master and rolled the production pod.
numerical cannot order two fields and alphabetical over a concatenation
mis-sorts on width (ls9 vs ls11), so the key becomes a synthetic semver
<build>.<ls>.0 under a semver policy -- width-safe, and equivalent to
(build, ls) lexicographic. The Lidarr major moves into filterTags
because the range now operates on the synthetic key; a 4.x bump needs a
deliberate edit, per the tight-range rule in README.md.
tika has the same defect: extracting <maj>.<min>.<patch> from a
4-component tag discards the image revision. The README called this
theoretical; it is not -- 2.5.0.1/2.5.0.2, 2.6.0.0/2.6.0.1,
2.7.0.0/2.7.0.1 and 2.9.2.0/2.9.2.1 all collide. Every 3.x release so
far is .0, so the revision is pinned in the filter the way
protonmail-bridge.yaml pins its -1 suffix. No-op today; if upstream
ships a 3.x .1 the policy stops bumping instead of picking wrongly.
Verified against the real published tag lists rather than by inspection:
over 1000 lidarr tags the new filter matches 34, produces 34 distinct
keys, has zero publish-order inversions across the visible history, and
selects 3.1.0.4875-ls42. tika reduces to one tag per key and still
selects 3.3.1.0.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Appends one `resourceNames` entry to the existing `paperclip-prometheus-read`
Role so the read-only apiserver service-proxy path also covers Alertmanager:
kube-prometheus-stack-alertmanager:9093
This is the widening that the file's own header comment said required a new
card and a new review — REN-94/REN-95 are that card, and Hermes confirmed the
target against the live cluster before this was written.
Nothing else about the grant changes. Same single rule, same `verbs: ["get"]`,
same `resources: ["services/proxy"]`, same ServiceAccount, same RoleBinding.
No new rule, no cluster-scoped kind, no second verb. `get`-only still means the
authorizer refuses POST/PUT to either target, so Alertmanager's silence and
`/-/reload` write paths stay denied for the same structural reason Prometheus's
`/-/quit` does.
No NetworkPolicy edit: `network-policy.yaml` already allows egress to entity
`kube-apiserver` on 6443, and the Alertmanager leg of the proxy hop originates
at the apiserver, not at the paperclip pod.
Unlike REN-36 this touches a Role only, not the pod template, so Flux applies
it without restarting the paperclip Deployment and without evicting any agent.
Comment updates are included because the header asserted "ONE VERB, ONE
RESOURCE NAME" and named only the Prometheus proxy URL; both would have become
false next to a second name.
Revert: revert this one commit. The grant returns to Prometheus-only and no
workload restarts.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Makes the key at OpenBao `infrastructure/uptime-robot`, field `api_key`,
available as a Secret in the new `uptime-monitor` namespace and nowhere
else. No new operator: this uses the External Secrets Operator and the
`openbao` ClusterSecretStore the cluster already runs.
Nothing is monitored by this commit. The namespace holds only the key;
the IngressMonitorController, its ServiceAccount and its RBAC arrive
separately (REN-6).
* `data` + `property` instead of `dataFrom: extract`, so only the one
field lands in the cluster rather than every field at the KV path.
* Adds `uptime-monitor` to the ClusterSecretStore `conditions.namespaces`
allowlist, sorted. Without this ESO reports "not allowed to reference
ClusterSecretStore" and the Secret silently never syncs.
* A DEDICATED Flux Kustomization rather than adding the ExternalSecret to
infrastructure/post-crds. That path runs `wait: true` with no explicit
healthChecks and 19 other Kustomizations dependOn it, so an
ExternalSecret whose OpenBao path is not yet populated would never turn
Ready and would stall most of the cluster. Here the blast radius of a
missing key is this Kustomization alone, which nothing depends on.
* No OpenBao policy change: eso-read already grants `homelab/data/*`.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Stops a random critical false page on the storage layer. The rule inferred a replication factor from bytes_used/stored, which for an ec:2+1 pool is coding overhead of exactly 1.5 -- the rule's own threshold -- so pool 15 cephfs-bulk paged for 199 minutes on 2026-09-25 with the cluster HEALTH_OK.
Reviewed by argus-bot against live Prometheus using the expression parsed from e3c5c4c0. Fire-window replay: old expr 231 samples on pool 15, new expr 0. Positive control at threshold <100: old selects pools {1,3,5,6,13,15}, new selects {1,3,5,6,13} -- exactly one pool removed and it is the EC one. pool_id survives the join so the annotations still render.
Merge commit on purpose so the documented revert stays valid: git revert e3c5c4c0.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
CephPoolLostRedundancy infers a replication factor from
ceph_pool_bytes_used/ceph_pool_stored and pages critical below 1.5. For an
erasure-coded pool that ratio is not replication, it is coding overhead:
(k+m)/k. Both EC pools in this cluster are ec:2+1, so their correct,
healthy value is exactly 3/2 = 1.5 — the rule's own threshold.
cephfs-bulk (pool 15) reads 480042086400/320028049408 = 1.5000000384. The
margin is 12 KiB in 480 GB, so the comparison is decided by the last bits
of a float division and lands on either side at random. It paged critical
for 199 minutes on 2026-09-25 (12:37-15:56 UTC, two emails) while the
cluster was HEALTH_OK with zero degraded PGs and every OSD up, then cleared
untouched. migration.rgw.buckets.data (pool 12) is the same ec:2+1 shape.
The rule's premise is also wrong for EC: its description claimed a single
OSD loss destroys data, but 2+1 carries a parity shard and tolerates one
OSD loss, the same as replica-2. Summary and description now say
"replicated" and drop that claim.
Gated on the exporter's own `type` label rather than moving the threshold.
Nudging 1.5 to 1.49 would keep a knife-edge and would also narrow the real
replica-2 -> replica-1 signal (1.0) this rule exists to catch.
NO COVERAGE GAP: a genuine EC redundancy loss raises POOL_NO_REDUNDANCY,
which is a live series in ceph_health_detail and is already deliberately in
scope for CephHealthWarn. That reads Ceph's own verdict instead of
inferring one from bytes, which is the right instrument — an EC pool that
loses a shard does not move its stored/used ratio at all.
VERIFIED against live Prometheus (read-only apiserver proxy, REN-36), using
the expression as serialized from this file:
* 7d replay, old expr: pool 15 matches 49 samples (defect reproduced)
* 7d replay, new expr: 0 matches
* fire-window replay (12:00-16:30 UTC): old matches pool 15 at 231/271
steps, new matches nothing
* positive control, new expr with threshold raised 1.5->3.5: selects
replicated pools 1,3,5,6,7,13,14 and excludes both EC pools (12,15) —
the filter removes exactly the EC pools and nothing else
* evaluated cleanly across 17 distinct mgr instances over 30d, so the
on(pool_id) join does not break on mgr failover
BLAST RADIUS: none to workloads. This edits one PrometheusRule; Prometheus
hot-reloads rule files and no pod restarts. Worst case is alerting scope on
one rule. Nothing writes to the cluster, no Ceph object is touched.
REVERT: revert this commit. One file, no state left behind.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Paperclip <noreply@paperclip.ing>
REN-86 asked for one field: `- name: monitoring` on the `paperclip`
Kustomization's dependsOn, because apps/paperclip/prometheus-read-rbac.yaml
(PR #984) puts a Role and a RoleBinding in namespace `monitoring` and a
from-scratch bootstrap was expected to fail applying them.
The premise does not hold, so the field is not added and the reasoning is
written down instead:
- Namespace `monitoring` is not created by the `monitoring` Kustomization. It
is declared in kubernetes/infrastructure/namespaces/namespaces.yaml, applied
by `infrastructure`, which this Kustomization already dependsOn.
- `infrastructure` runs wait: false, so it applies the namespaces on its first
pass and only its healthChecks gate readiness. It cannot report Ready before
the namespace exists, and this Kustomization cannot start before that.
- Role and RoleBinding are core rbac.authorization.k8s.io/v1 kinds, and a
RoleBinding may reference a Role that does not exist yet. Nothing on this
path needs a kube-prometheus-stack CRD or workload, so there is no analogue
of the monitoring-rules/PrometheusRule split.
Adding the edge would also couple the Kustomization that carries the paperclip
pod to `monitoring` readiness, which itself waits on grafana-db,
kube-prometheus-stack, Loki, Alloy and blackbox-exporter for up to 30m.
Comments only: the parsed manifest is identical to master.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Option B on REN-37, owner-approved: reach Prometheus through the apiserver
service proxy under a Role holding one verb, instead of an L4 allow on :9090
that could not tell GET /api/v1/query from the unauthenticated POST /-/quit
that --web.enable-lifecycle puts on the same port.
ServiceAccount paperclip/paperclip-prometheus-read
Role monitoring/paperclip-prometheus-read
get on services/proxy, resourceNames:
["kube-prometheus-stack-prometheus:9090"]
RoleBinding monitoring/paperclip-prometheus-read -> that SA
(subject namespace: paperclip - load-bearing, cross-ns)
paperclip.yaml serviceAccountName + automountServiceAccountToken: true
Authored by svc-atlas, reviewed and merged by argus-bot: author is not the
merger. ci-manifests-test green on 35bcdf10 (kustomize build over the whole
tree). Reviewed against master 3a457bd6 in a fresh clone.
BLAST RADIUS: applying this restarts the paperclip pod (strategy: Recreate)
and every agent running in it. Nothing in production monitoring restarts -
Prometheus, Alertmanager, Loki and the ingress controllers are untouched.
The paperclip Flux Kustomization has wait: true, so a pod that does not come
back shows as a failed reconcile rather than silently.
REVERT - one step:
git revert -m 1 <the sha of this merge commit>
That drops serviceAccountName, restores automountServiceAccountToken: false
and deletes prometheus-read-rbac.yaml; Flux prunes the ServiceAccount, Role
and RoleBinding on the next reconcile (paperclip Kustomization, prune: true).
It restarts the paperclip pod a second time, for the same reason the apply
does.
AGENT-FREE ESCAPE HATCH - use this if the pod does not come back, because
then there is no agent left to run the revert above. Flux re-applies the same
spec every 10m and overwrites a bare rollout undo; the suspend is what makes
the undo stick:
flux -n flux-system suspend kustomization paperclip
kubectl -n paperclip rollout undo deployment/paperclip
# then land the git revert above, and afterwards:
flux -n flux-system resume kustomization paperclip
Names as they are in this repo: `paperclip` is both the Flux Kustomization in
flux-system (clusters/homelab/paperclip.yaml) and the Deployment in namespace
paperclip. No revisionHistoryLimit is set anywhere under kubernetes/, so the
default 10 holds and a previous ReplicaSet exists to undo to. The undo
restores automountServiceAccountToken: false, which closes the read path at
once, before the RBAC objects are pruned. It rolls the pod template back
wholesale, so an image bump that landed after this merge is rolled back with
it - re-apply that after the resume.
Escape hatch text is the merger's, written into this merge commit because
merge condition 2 puts the revert recipe here. The diff is untouched.
Refs: REN-36, REN-37 (option B), REN-47 (review bar), REN-73 (merge card)
Owner picked option B on REN-37: reach Prometheus through the apiserver
service proxy under an RBAC Role holding one verb, instead of an L4 Cilium
allow on :9090.
The port cannot be the boundary. The chart leaves `reloadStrategy` unset, so
the operator defaults it to HTTP and appends `--web.enable-lifecycle`, which
puts `POST /-/quit` and `POST /-/reload` on :9090 with no auth in front of
them. An L4 allow cannot distinguish those from `GET /api/v1/query`. The
apiserver can: it maps HTTP method to RBAC verb, so a Role with only `get`
refuses both in the authorizer before the request reaches Prometheus.
ServiceAccount paperclip/paperclip-prometheus-read
Role monitoring/paperclip-prometheus-read
get on services/proxy, resourceNames:
["kube-prometheus-stack-prometheus:9090"]
RoleBinding monitoring/paperclip-prometheus-read -> the SA above
paperclip.yaml serviceAccountName + automountServiceAccountToken: true
Shape copied from monitoring/ci-diagnostics-rbac.yaml, which has run this
same proxy path for the alert-monitor since 2026-07-16. No network-policy
edit: paperclip-egress already allows kube-apiserver on 6443.
BLAST RADIUS: applying this restarts the paperclip pod, and every agent with
it. Nothing in production monitoring restarts — Prometheus, Alertmanager,
Loki and the ingress controllers are untouched.
REVERT: revert this commit. One commit, four files, no state left behind;
Flux prunes the SA, Role and RoleBinding and rolls the pod back to no
mounted token.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment-only change to kubernetes/apps/monitoring-rules/probes-blackbox.yaml. Reviewed by argus-bot at head 49bd9271 against base 2b71dda1: 3 documents parse deep-equal, blackbox-http 20 targets before and after, git diff -U0 non-comment non-blank lines = 0, apps/gatus/configmap.yaml untouched. Revert recipe and blast-radius claims re-executed and confirmed.
Comment-only. Rebuilt on top of master (2b71dda1) after #979 merged, so it
adds to that text instead of reverting it.
- EGRESS: names the two ingress-only Cilium policies in the monitoring
namespace and where blackbox-exporter runs; counts re-verified at
2b71dda1 (635 yaml files, 73 Cilium objects, 38 with egress/egressDeny,
0 in monitoring, 1 CCNP scoped to reserved.health, 17 namespaces with
an egress block).
- BLAST RADIUS: what breaks if that dependency is ever broken
(BlackboxProbeFailing x3, BlackboxProbesAbsent stays silent), who
notices (admin@renner.uno, ~10-11 min), and how fast it is undone.
- REVERT: replaced the claim with an executed recipe. All four git revert
candidates CONFLICT at 2b71dda1; the two forward sed deletions were run
and verified (blackbox-http 20 -> 17 and 20 -> 19).
No object changes: 3 documents deep-equal before/after under two
independent parsers, blackbox-http still 20 targets, and `git diff -U0`
has zero non-comment non-blank changed lines. apps/gatus/configmap.yaml
is not touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment-only change to kubernetes/infrastructure/image-automation/update-automation.yaml.
Reviewed by Argus (svc-argus/argus-bot). Verified comment-only: SHA256 of the comment-stripped file is 4ed11c66f18b8d15 before and after. No object field changes, so Flux's next reconcile is a no-op.
Revert: git revert 4f1c2cf1
Co-Authored-By: Paperclip <noreply@paperclip.ing>
The comment on ImageUpdateAutomation/homelab named gitea_admin as the
owner of the bootstrap token that pushes writeback commits to master.
That is wrong. The pushing account is `flux` (Gitea uid 9): its
contribution history matches every `chore(image-automation)` commit on
master 1:1, while gitea_admin's own contributions stop at 2026-07-21.
Also separates two things the old wording ran together — the commit
author (security-bot@renner.uno, set in this spec) and the pushing
account (the token owner, not set here) — and records that writeback
failure is silent, since no commit is attempted unless a tracked image
moves.
Comment-only. The ImageUpdateAutomation object is byte-identical; no
Kubernetes object changes, nothing for Flux to reconcile.
Refs REN-50
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Comment-only. No Kubernetes object changes on either file: all 3 probe documents deep-equal to master (blackbox-http 20 targets), ConfigMap/gatus-config deep-equal with data["config.yaml"] byte-identical at sha256 8886ef21...6472 (6572 bytes), so Reloader does not roll gatus. Non-comment non-blank changed lines vs merge base 0a82d61f: 0. Reviewed and verified by argus-bot at head 9142b78a; the REVERT recipe was re-tested against its own commit.
The local-login connection stores only the ~8h OAuth access token and
Paperclip never refreshes Anthropic credentials (subscription values
are injected read-only as CLAUDE_CODE_OAUTH_TOKEN). It expired 8h after
the login and every run failed with 'terminal access failure'.
Replacing the secret value with a claude setup-token token fixed it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment-only, one sentence, one file. Wording is Argus's from the #979
review (REN-38).
The REVERT paragraph ended with "The paperless block below DOES revert
cleanly on its own with `git revert -m 1 0a82d61f`". True at 0a82d61f,
false at bd8690eb, because this same PR edits the paperless block:
from 0a82d61f: revert -m 1 0a82d61f -> CLEAN revert -m 1 16238d39 -> CONFLICT
from bd8690eb: revert -m 1 0a82d61f -> CONFLICT revert -m 1 16238d39 -> CONFLICT
Reproduced here: at bd8690eb the collision is one region, `<<<<<<< HEAD`
at 155 through `>>>>>>>` at 198, the whole paperless block against an
empty side. So the advice failed in exactly the case it was written for
— someone reverting the paperless probe under pressure, four lines below
a warning not to trust the natural resolution on this file. The new text
says what actually happens and how to resolve it, and stays true as the
comment block keeps changing, because it no longer promises clean.
Bar re-verified at this commit, both files against master, js-yaml:
probes-blackbox.yaml docs base=3 head=3, all deep-equal=true
http_2xx/20, http_2xx_wan_edge/3, tcp_connect/2
gatus/configmap.yaml ConfigMap/gatus-config deep-equal=true
data["config.yaml"] sha256 8886ef21…6472 both
sides, identical=true, bytes=6572
git diff -U0 vs master, non-comment non-blank lines: 0
apps/gatus/configmap.yaml is not touched by this commit at all.
Production constraint: nothing applied, restarted or re-rendered. The
public status.renner.uno page does not roll — ConfigMap data is
unchanged, so configmap.reloader.stakater.com/reload has nothing to act
on.
Revert: `git revert <this-sha>` restores the previous sentence and
nothing else.
Refs REN-21, REN-38, REN-44.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment-only. Every parsed object in both files is byte-identical to
master at 0a82d61f, verified with js-yaml; zero non-comment lines change.
Review follow-ups from REN-22 on the merged #976/#977 probes:
1. The CNP argument was right in its conclusion and wrong in its framing.
cnp-cross-namespace-allow-apps-gatus-to-monitored-services DOES apply to
blackbox-exporter — its Generalization section names it. We are safe
because the allow it demands is already present, not because the note is
about gatus. The old wording is the sentence that makes the next person
skip the check for the next namespace.
2. Records the load-bearing egress fact, which was written down nowhere.
Cilium enforces at both ends; blackbox-exporter can send only because no
policy selecting a monitoring endpoint declares an `egress:` block.
Verified across the repo: 73 Cilium policies, 38 with egress, none in
monitoring, and the one CCNP is scoped to `reserved.health`. 17 other
namespaces already have an egress block, so a routine hardening pass on
monitoring would break these three targets and leave the *.monitoring.svc
ones working.
3. Extends the 200-vs-3xx caveat to all three new targets, not just
paperless. The repo evidence behind every one of them establishes only
`< 400` (kubelet httpGet accepts 200-399; lidarr.yaml:29's `curl -f`
without -L exits 0 on a 302). It proves the paths are ungated, not that
they answer 200. A 3xx gets reverted, not widened.
4. Records the tested revert recipe for #976 in the file. `git revert -m 1
16238d39` conflicts and the natural resolution silently drops paperless
too.
5. Corrects the stale "needs a CiliumNetworkPolicy first" claim in
apps/gatus/configmap.yaml. The correction is written in the manifest
header, ABOVE `data:`, not in place: the stale sentence sits inside the
`config.yaml: |` block scalar, and gatus carries
configmap.reloader.stakater.com/reload, so editing it in place would roll
the gatus Deployment for a comment. ConfigMap data sha256 is unchanged.
Production constraint: nothing is applied, restarted or re-rendered by this
change. No workload, namespace, policy or probe target is touched.
Revert: `git revert -m 1 <merge-sha>`, or drop either file alone with
`git checkout 0a82d61f -- <path>`.
Refs REN-21, REN-22.
Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-09-25 19:18:37 +00:00
228 changed files with 14139 additions and 482 deletions
- Mark the block you cite with a comment line `# anchor: <name>` (kebab-case, unique in that file). If the block is inside a `|` block scalar, put the marker just above the key, never inside the scalar.
- Cite it as `path.yaml#<name>`, with the path relative to the citing file's directory or to the repo root. Never cite `path.yaml:<line>`, because line numbers go stale on any edit above them.
-`scripts/ci/yaml-citations.sh` enforces this in `ci-manifests-test.yml`: it fails on line-number citations and on citations whose file or anchor does not exist.
Plus per-namespace `pod-security.kubernetes.io/enforce: restricted` and a deny-all NetworkPolicy baseline with explicit egress allow-list (DNS, paperclip-server, configured FQDNs/CIDRs).
+The lease starts with `/workspace` as the native file-sync confinement root. Adapter environment tests can upload managed config before the server realizes a workspace. When resuming a lease, the provider keeps a previously recorded `remoteCwd` so a custom realized workspace is not replaced with the default.
+
The per-run Secret carrying the bootstrap token and adapter API keys has `ownerReferences` pointing at the owning Job, so a single `kubectl delete job …` cascades cleanly to the Pod and Secret.
@@ -16,7 +16,7 @@ credentials are out of scope (see "Deferred").
|---|---|
| Execution model | **B — server-local runs** (upstream default): agent CLIs are child processes of the server, in the server container. Trial only. |
| Upgrade path | If Paperclip is kept: **C** — `@paperclipai/plugin-kubernetes` sandbox provider + kubernetes-sigs agent-sandbox controller (`agents.x-k8s.io`), per-run Pods and Secrets. Separate spec. |
| Claude auth | **One shared Claude subscription AI connection**, created once via Paperclip's local sign-in (`claude auth login` into `ai-local-logins/<id>`); Paperclip stores and refreshes it. Server-env `CLAUDE_CODE_OAUTH_TOKEN` is not used: `stripAiAuthBindings` blanks it for managed runs, and upstream accepts no pasted setup-token (the device-code flow needs a Daytona sandbox). |
| Claude auth | **One shared Claude subscription AI connection**, created once via Paperclip's local sign-in (`claude auth login` into `ai-local-logins/<id>`), then its value replaced with a long-lived `claude setup-token` token. Paperclip stores the value as company secret "My Claude subscription" and injects it per run as `CLAUDE_CODE_OAUTH_TOKEN`; it never refreshes Anthropic credentials, so the login's ~8h access token expires (runs then fail with "ACP agent reported a terminal access failure", 0 tokens). Renew by rotating that secret with a new setup-token (~yearly). Server-env `CLAUDE_CODE_OAUTH_TOKEN` is not used: `stripAiAuthBindings` blanks it for managed runs, and upstream accepts no pasted setup-token (the device-code flow needs a Daytona sandbox). |
| Bot credentials | Out of scope. Future "AIR-Manager" Paperclip role handles permission requests. |
| Exposure | LAN-only, behind an Authentik forward-auth gate, then Paperclip's own login. |
@@ -64,7 +64,32 @@ All modeled on `kubernetes/apps/paperless/` unless stated.
- Starts as root: upstream `docker-entrypoint.sh` remaps the `node` user, chowns
`/paperclip`, then drops to uid 1000 via `gosu`. Caps limited to CHOWN,
# Scrape the Flux controllers' own metrics. Until this existed Prometheus held
# no `gotk_*` series at all, so nothing about Flux's own behaviour was
# queryable and no rule could be written about it (REN-97).
#
# THIS IS THE SCRAPE ONLY, and the readiness alert is not merely deferred —
# it cannot be written from this endpoint. `gotk_reconcile_condition` and
# `gotk_suspend_status` are still DECLARED by fluxcd/pkg/runtime (v0.110.3,
# runtime/metrics/recorder.go), but at the tags pinned in gotk-components.yaml
# no controller calls the recorder that populates them: grepping the six
# tagged source trees for RecordReadiness / RecordCondition / RecordSuspend
# returns zero call sites, and an unpopulated GaugeVec exports no series. So
# the readiness metrics would stay empty however this file is written. Flux
# moved per-object readiness to kube-state-metrics custom-resource-state
# (`gotk_resource_info{ready=...}`); that is REN-102, and it is a prerequisite
# for the not-Ready alert rather than a follow-up to it.
#
# What this scrape does yield, and why it is still worth merging on its own:
# * `gotk_reconcile_duration_seconds` per kind/name/namespace — reconcile
# latency and, by its absence, an object that has stopped being reconciled
# at all. RecordDuration IS called, but by FIVE of the six controllers, not
# all six: measured post-merge, 170 series from kustomize (33), source (43),
# image-reflector (68), image-automation (1) and helm (25), and ZERO from
# notification-controller. That is not a gap waiting to fill in — its three
# reconcilers are registered but never walked (controller_runtime_reconcile_
# total for controller=alert/provider/receiver is 0 over 56 h of uptime with
# 2 Alerts and 1 Provider live), so the family is absent from that pod's
# /metrics entirely and five is the permanent value. DO NOT write a rule of
# the form `count(... gotk_reconcile_duration_seconds ...) < 6`, or an
# absent() per pod: it fires forever on notification-controller. Liveness for
# all six pods is `up{job="monitoring/flux-controllers"}` (below); that pod's
# reconcile health is `controller_runtime_reconcile_errors_total`. REN-127.
# * controller-runtime's own series: reconcile totals and errors per
# controller, workqueue depth and retry counts, leader election.
# * the target itself, so `up{job="monitoring/flux-controllers"}` and the
# chart's default TargetDown rule start covering the Flux control plane.
# * the scrape REN-102 needs in place anyway.
#
# A PodMonitor, NOT a ServiceMonitor, and that is forced rather than preferred:
# gotk-components.yaml creates Services for only two of the six controllers
# (source-controller, notification-controller), and both publish port 80 ->
# targetPort `http`, which is the artifact/webhook server on :9090 — not the
# metrics port. kustomize-, helm-, image-reflector- and image-automation-
# controller have no Service at all, so there is nothing for a ServiceMonitor to
# select. All six pods do carry the same two labels and the same `http-prom`
# (:8080) container port, so one PodMonitor covers the set.
#
# NO NETWORK POLICY CHANGE IS NEEDED, verified rather than assumed, and unlike
# external-secrets-podmonitor.yaml which did need one: flux-system has no
# CiliumNetworkPolicy of its own (infrastructure/network-policies/ has no
# flux-system file), and gotk-components.yaml ships an `allow-scraping`
# NetworkPolicy admitting every namespace to :8080 (`from: [namespaceSelector:
# {}]`, port 8080). So `monitoring` is already admitted, as it was for REN-21.
# Nothing in the Flux install is touched.
apiVersion:monitoring.coreos.com/v1
kind:PodMonitor
metadata:
name:flux-controllers
namespace:monitoring
labels:
app.kubernetes.io/part-of:flux
# Not required for discovery — podMonitorSelector is empty, see
# apps/monitoring/helmrelease-prometheus.yaml — but set for consistency
# with the other monitors in this directory.
release:kube-prometheus-stack
spec:
# Lives in `monitoring` with a namespaceSelector rather than in flux-system,
# matching external-dns-servicemonitor.yaml and external-secrets-podmonitor.yaml.
# Flux owns every object in flux-system via gotk-components.yaml; keeping this
# out of that namespace means this change cannot interact with the Flux install
# itself.
namespaceSelector:
matchNames:
- flux-system
selector:
matchLabels:
app.kubernetes.io/part-of:flux
app.kubernetes.io/instance:flux-system
podMetricsEndpoints:
# Matches the controllers' `http-prom` port (containerPort 8080). Present on
# all six Deployments in gotk-components.yaml; a future controller without
# it simply yields no target rather than a broken scrape config.
- port:http-prom
path:/metrics
# 60s, not the 30s used elsewhere here, matching the ESO PodMonitor: the
# useful series are slow-moving per-object gauges and histograms over a
# few hundred Flux objects, and any rule built on them (REN-102) will use
# a multi-minute `for:`. 30s would double the samples for no gain in
# detection time — and Prometheus is already near its retention cap
# (REN-107).
interval:60s
scrapeTimeout:10s
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.