Compare commits

...
Author SHA1 Message Date
claude-bot 469cdbc2ee feat: give the remaining unprobed workloads a health check
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Continues the paperless change. Under the split where gatus watches from
outside and Kubernetes watches the pods, a container with no probe at all is
the hole: it stays Ready while dead, and nothing downstream can tell.

Not every one of them got a probe, and the ones that did not are the more
interesting half.

Added:

  whisper, openwakeword       tcpSocket on the wyoming port. These speak a raw
  speech-to-phrase            TCP protocol with no HTTP surface and no health
                              verb, so accepting a connection is all that can
                              be asserted from outside the process. Weak, but
                              the previous state was nothing at all, and a dead
                              server kept being routed speech.

                              speech-to-phrase gets a startupProbe rather than
                              a long liveness delay: it runs --retrain-on-start
                              and retraining takes as long as it takes. The
                              gate allows 10 minutes and only then hands over,
                              so a slow retrain is not mistaken for a hang
                              while a real hang is still caught in a minute.

  ftp-scanner                 tcpSocket on the FTP control port. This is the
                              failure that hides best: if vsftpd dies, the
                              scanner cannot deposit anything and paperless
                              just sees an empty consume directory, which looks
                              exactly like nobody having scanned.

  code-server                 httpGet /healthz. Answers 200 whenever the server
                              is up; its body reports session heartbeat, which
                              is not health — an idle editor reports "expired"
                              and is fine.

  external-secrets            livenessProbe/readinessProbe enabled through the
                              chart's own values, which is also what starts its
                              health server: without them the controller
                              exposes nothing but a metrics port. A wedged
                              reconciler stayed Ready while every ExternalSecret
                              silently stopped refreshing.

Deliberately left alone:

  protonmail-bridge           Already carries a rationale: the bridge can be
                              offline while the user re-authenticates, and
                              paperless retries mail fetches itself. A liveness
                              probe would restart it mid-re-auth. That is a
                              considered decision, not an omission.

  bridge-tls-proxy            socat binds 127.0.0.1:11143. Probes are dialled
                              by the kubelet against the pod IP, so there is
                              nothing for it to reach.

  mariadb-operator            Its chart exposes no probe values and no health
                              bind address. Forcing them through a postRenderer
                              would break on the next chart bump, so it stays
                              unprobed and this records why.

  ak-outpost-* (11)           Pod specs are owned by the goauthentik.io
                              operator; nothing here can add to them.

Validated with Flux's field manager against the live cluster.
2026-08-28 13:24:05 +02:00
claude-bot 3f6f1edcb3 fix(paperless): probe a real Django view instead of a TCP connect
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Under the split where gatus watches from outside and Kubernetes watches the
pods, paperless was the weakest link on the Kubernetes side: both probes were
tcpSocket, which only proves gunicorn accepted a connection. A worker deadlock
or a broken Django config would keep the pod Ready.

The existing comment explained why: HTTP probes against /api/ flood
`paperless.auth`, because ENABLE_HTTP_REMOTE_USER makes the remote-user
middleware log every unauthenticated API request. That was a real constraint
and the TCP fallback was a deliberate trade, not an oversight.

It is also narrower than the comment assumed. The middleware only covers the
API surface, and /accounts/login/ is a plain Django view. Measured against the
running pod:

    4 requests to /accounts/login/  ->  0 new paperless.auth lines
    4 requests to /api/            ->  4 new paperless.auth lines

So the noise is avoidable without giving up an HTTP probe. /accounts/login/
renders a template, which means URL routing, settings and the template engine
all have to be working. It does not prove Postgres is reachable — nothing cheap
on this surface does, and the comment now says so rather than implying more.

ALLOWED_HOSTS was the other way this could have failed: the kubelet sends the
pod IP as Host, and a restrictive value would answer 400 and crashloop the pod.
PAPERLESS_ALLOWED_HOSTS is "*", so it is safe today, and the comment records
what narrowing it would require.

Timings are unchanged — the 120s liveness delay is there for paperless's slow
start and nothing here makes it start faster.

Validated with Flux's own field manager. Applying as any other manager reports
the deployment invalid with two handler types, because tcpSocket stays owned by
kustomize-controller and is not cleared by a foreign apply; that is an artifact
of the check, not of the manifest.
2026-08-28 12:59:19 +02:00
claude-bot ad0102d2a9 feat(monitoring): probe ical-proxy and Loki, add Paperless to the status page
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Three services that nothing watched. Each needed a different answer, and the
difference is what the comments record.

ical-proxy serves an unauthenticated /health returning 200 with
{"status":"healthy"}. That is real app health, not reachability. Its root path
answers 404 by design — it only serves calendar paths — which is why a naive
probe of / would have looked broken and a naive fix would have been a module
that accepts 404.

Loki has no usable external vantage at all: every path through its ingress
answers 401, so an outside probe could only ever assert that the ingress is up.
In-cluster /ready answers 200 unauthenticated and lives in the monitoring
namespace, so it rides the same intra-namespace allowance the existing
Prometheus and Alertmanager health probes depend on. Verified through the
apiserver proxy before committing.

Paperless goes on the status page rather than into blackbox, and only at gate
level. Every path including /api/ answers 302 to unauthenticated callers, so
there is nothing to probe that proves the app instead of the Authentik outpost.
The entry says so. Closing that blind spot means probing the ClusterIP from
in-cluster, which crosses a namespace boundary and needs a CiliumNetworkPolicy
— the monitoring probes avoid one only because their targets share a namespace
with the prober. That is a separate change.

Paperless takes a new slot rather than the retired Web Service 7, which the
legend used to misattribute to it. Reusing a public label would silently
reassign what it means.
2026-08-28 12:09:26 +02:00
claude-bot 62f1a62b2b fix(gatus): run as non-root and satisfy PodSecurity restricted
The upstream image is FROM scratch with no USER directive, so the pod ran as
root and tripped restricted on all four counts: allowPrivilegeEscalation,
capabilities, runAsNonRoot and seccompProfile.

fsGroup is what makes this survivable rather than a crash. The only write path
is the sqlite store on the RWO volume — storage.type sqlite, path
/data/gatus.db in the ConfigMap — and fsGroup both chowns the existing volume
on mount and leaves the directory group-writable, which sqlite needs to create
its -wal and -journal files beside the database.

The uid is numeric because a scratch image has no /etc/passwd for the kubelet
to resolve a name against.

readOnlyRootFilesystem is deliberately not set: restricted does not require it,
and on a scratch image there is nothing to gain that would justify the risk of
finding out at runtime that the binary wants a scratch path.
2026-08-28 12:08:46 +02:00
claude-bot 5d21426a6b fix(gatus): drop the file.renner.uno check left behind by #696
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
#696 deleted the Ingress and ExternalName Service for the NAS file UI because
they pointed at nas-1, which is off. It did not delete the gatus check for the
same hostname, so the public status page has been showing Web Service 7 as
failing ever since — verified against the live API, success: false.

That was my omission in #696: I removed the route and left the thing watching
it.

The label is retired rather than renumbered, matching Infrastructure 1 and 2.

The maintainer legend claimed Web Service 7 was Paperless. It never was — the
slot held the NAS file UI, and nothing in this config checks Paperless at all.
I added the missing Web Service 10 entry to this same map in #699 without
verifying the rest of it, which is how a wrong line survived a pass that was
supposed to be about exactly this map. Every remaining entry is now checked
against its URL.

Paperless is genuinely unmonitored here and is left that way deliberately:
adding coverage is a separate decision from correcting a map that lies.

This is also the ConfigMap change that exercises the Reloader annotation from
#700. That annotation landed after the ConfigMap it watches had already
changed, so no event ever fired and the pod kept serving the pre-#699 endpoint
list, still probing pbs2. This edit should reload it and clear both.
2026-08-28 11:54:27 +02:00
claude-bot 1d8ba05123 fix(gatus): reload the pod when its config changes
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Dropping the PBS 2 check in the previous commit reconciled cleanly and changed
nothing. The ConfigMap in the cluster no longer lists it; the running instance
still reports Infrastructure 1, because Gatus reads its config once at startup
and the pod is 26 hours old.

Nothing about that state looks wrong from the outside. Flux reports the
Kustomization applied, the ConfigMap matches git, and no resource is failing —
the only symptom is that the endpoint list served by the app disagrees with the
one in the repo. Adding a check would have been just as invisible as removing
one.

Reloader has been installed cluster-wide for 40 days and eleven other workloads
already carry its annotation, across the apps, home-assistant, woodpecker,
wordpress and authentik namespaces. Gatus was simply never given one. The
annotation form for a ConfigMap is confirmed against upstream rather than
recalled: configmap.reloader.stakater.com/reload takes the ConfigMap name.

The pod restarts on reload and the deployment uses Recreate for its RWO volume,
so the status page has a short gap whenever the config changes. That is the
correct trade against serving checks that silently no longer match the repo.

Not addressed: the deployment trips PodSecurity restricted on four counts
(allowPrivilegeEscalation, capabilities, runAsNonRoot, seccompProfile). That
predates this and is its own change.
2026-08-28 11:44:19 +02:00
k.renner f935776f2b Merge pull request 'chore(gatus): drop the PBS 2 check, which has no clients left' (#699) from drop-pbs2-gatus into master
ci/woodpecker/cron/issue-fix Pipeline was successful
Reviewed-on: k.renner/homelab#699
2026-08-28 09:33:48 +00:00
claude-bot 75e8a81c56 chore(gatus): drop the PBS 2 check, which has no clients left
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
pbs2.renner.uno was probed every 60 seconds as "Infrastructure 1". Both of its
declared clients are gone: the PVE cluster, retired to bare-metal Talos, and
nas-1, powered off and removed from the repo. A liveness check on a backup
server that nothing backs up to reports on nothing — and hide-errors: true
meant its status carried no detail on the public page either.

The opaque label keeps its number and is marked retired, the way Infrastructure
2 already records the Proxmox VE slot. Those labels are public; renumbering
would silently reassign what every other number means.

Also fills a gap in the maintainer legend: Web Service 10 (TRMNL) was serving
but undocumented, so the map stopped matching the endpoint list at 9.

Checked: the embedded config still parses, 12 endpoints remain, and the only
surviving pbs2 mention is the removal note.
2026-08-28 11:29:35 +02:00
k.renner 5c412f0486 Merge pull request 'feat(monitoring): alert on any blackbox target without a rule of its own' (#698) from generic-blackbox-alert into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#698
2026-08-28 09:16:18 +00:00
claude-bot e15067a784 feat(monitoring): alert on any blackbox target without a rule of its own
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
Nine of the thirteen blackbox-http targets were probed, graphed and completely
unalerted: gitea, immich, auth, home, prometheus, alerts and dash, plus the two
internal /-/healthy probes that exist specifically to catch a dead app behind a
live Authentik gate. That is the shape that let file.renner.uno serve 502 for
months.

BlackboxProbeFailing covers everything except the instances that already have a
dedicated rule. Measured against the running Prometheus before committing: the
selector returns exactly those nine and excludes the six with bespoke handling,
and the full expression as YAML delivers it evaluates to zero results today.

The exclusion list and the dedicated rules are one invariant, stated in the
file: an instance belongs in the list only while a rule elsewhere covers it,
and a new dedicated rule has to add its instance. Wrong in one direction the
alert fires twice, wrong in the other it never fires. Severity is warning
because a catch-all cannot know what any given target means — anything
deserving critical earns its own rule, which is also what puts it in the list.
That is what stops the list from being arbitrary.

BlackboxProbesAbsent is the check on the checks. Every endpoint alert reads
probe_success; if the job stops producing series none of them can fire, and
that silence is indistinguishable from health.

Also corrects the comment in prometheusrule-hetzner-backup.yaml claiming CNPG
pod metrics are not scraped and a granular archiving alert is a follow-up. Both
have existed for a while: cnpg-podmonitors.yaml scrapes the instances and
CNPGWalArchiveBacklog reads that collector.
2026-08-28 11:10:45 +02:00
k.renner 541f3fcfe9 Merge pull request 'feat(monitoring): probe and alert on Woodpecker and OpenBao' (#697) from probe-ci-openbao into master
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#697
2026-08-28 08:59:10 +00:00
k.renner 847664eaa3 Merge pull request 'chore: remove the nas-1 remnants and make incomplete scans visible' (#696) from cleanup-nas-1-remnants into master
Reviewed-on: k.renner/homelab#696
2026-08-28 08:47:36 +00:00
k.renner 2ef9811b13 Merge pull request 'chore(ci): bump ci-claude image to 2.1.250' (#693) from ci-image/claude-2.1.250 into master
Reviewed-on: k.renner/homelab#693
2026-08-28 08:46:58 +00:00
claude-bot 290befa208 chore(ci): bump ci-claude image to 2.1.250
ci/woodpecker/pr/ci-manifests-test Pipeline was successful
ci/woodpecker/pr/ci-scripts-test Pipeline was successful
ci/woodpecker/pr/pr-review Pipeline was successful
2026-08-28 08:33:12 +00:00
k.renner fbf41d27cd Merge pull request 'fix(ci): dispatch the remaining pipelines by deploy target' (#692) from woodpecker-deployment-dispatch-rest into master
ci/woodpecker/push/ci-claude-refresh Pipeline was successful
ci/woodpecker/push/build-ci-images Pipeline was successful
ci/woodpecker/push/deploy-infrastructure Pipeline was successful
ci/woodpecker/push/deploy-netbird-client Pipeline was successful
ci/woodpecker/push/deploy-claude-workstation Pipeline failed
ci/woodpecker/push/deploy-netbird-server Pipeline was successful
ci/woodpecker/push/deploy-netbird-proxy Pipeline was successful
ci/woodpecker/cron/issue-fix Pipeline was successful
ci/woodpecker/cron/alert-monitor Pipeline was successful
Reviewed-on: k.renner/homelab#692
2026-08-28 08:32:20 +00:00
20 changed files with 287 additions and 49 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ steps:
bash scripts/ci/make-diag-kubeconfig.sh
- name: alert-triage-agent
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.247
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.250
environment:
CLAUDE_CODE_OAUTH_TOKEN:
from_secret: claude_code_oauth_token
+1 -1
View File
@@ -17,6 +17,6 @@ when:
steps:
- name: model-selection-tests
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.247
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.250
commands:
- sh scripts/ci/test-model-selection.sh
+1 -1
View File
@@ -26,7 +26,7 @@ steps:
# fetch-models.sh response mapping. No cluster, no secrets, no network — the
# ci-claude image just provides sh + jq. Keeps the flag-injection regression
# test from rotting (it exercises the exact injection shapes).
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.247
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.250
commands:
- sh scripts/ci/test-model-selection.sh
+1 -1
View File
@@ -62,7 +62,7 @@ steps:
- bash scripts/ci/make-diag-kubeconfig.sh
- name: issue-fix-agent
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.247
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.250
environment:
CLAUDE_CODE_OAUTH_TOKEN:
from_secret: claude_code_oauth_token
+1 -1
View File
@@ -65,7 +65,7 @@ when:
steps:
- name: pr-review-agent
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.247
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.250
environment:
CLAUDE_CODE_OAUTH_TOKEN:
from_secret: claude_code_oauth_token
+1 -1
View File
@@ -70,7 +70,7 @@ steps:
# this step is skipped — otherwise we'd fork-bomb PRs.
when:
- branch: master
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.247
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.250
environment:
CLAUDE_CODE_OAUTH_TOKEN:
from_secret: claude_code_oauth_token
+1 -1
View File
@@ -30,7 +30,7 @@ when:
steps:
- name: update-audit-agent
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.247
image: gitea.renner.uno/gitea_admin/ci-claude:2.1.250
environment:
CLAUDE_CODE_OAUTH_TOKEN:
from_secret: claude_code_oauth_token
+37 -28
View File
@@ -18,15 +18,18 @@ metadata:
#
# Opaque-label map (maintainer reference only — NOT served publicly):
# services group infrastructure group
# Web Service 1 = Authentik Infrastructure 1 = PBS 2 (pbs2.renner.uno)
# Web Service 1 = Authentik Infrastructure 1 = retired (was PBS 2)
# Web Service 2 = Gitea Infrastructure 2 = retired (was Proxmox VE)
# Web Service 3 = Immich Infrastructure 3 = UDM DNS (10.2.40.1)
# Web Service 4 = Woodpecker CI Infrastructure 4 = Mosquitto MQTT broker
# Web Service 5 = Grafana
# Web Service 6 = Home Assistant
# Web Service 7 = Paperless
# Web Service 7 = retired (was the NAS file UI; never Paperless, which
# this map claimed and which nothing here checks)
# Web Service 8 = Audiobookshelf
# Web Service 9 = Music Assistant
# Web Service 10 = TRMNL
# Web Service 11 = Paperless (gate-level only, see its entry)
data:
config.yaml: |
storage:
@@ -121,18 +124,10 @@ data:
conditions:
- "[STATUS] == any(200, 301, 302)"
- name: Web Service 7
group: services
url: https://file.renner.uno
interval: 60s
ui:
hide-hostname: true
hide-url: true
hide-port: true
hide-conditions: true
hide-errors: true
conditions:
- "[STATUS] == any(200, 301, 302)"
# Removed 2026-08-28: the NAS file UI (file.renner.uno). Its Ingress and
# ExternalName Service were deleted in #696 because they pointed at
# nas-1, which is powered off; this check outlived them and had been
# reporting failure on the public page since.
- name: Web Service 8
group: services
@@ -178,6 +173,29 @@ data:
conditions:
- "[STATUS] == any(200, 301, 302)"
# Paperless sits entirely behind the Authentik gate — every path,
# including /api/, answers 302 to unauthenticated callers, so there is no
# endpoint that proves the app rather than the gate. This check is
# therefore reachability only: DNS, TLS, Traefik and the outpost. It
# stays green if paperless itself is dead behind a live gate.
#
# Closing that blind spot needs an in-cluster probe of the paperless
# ClusterIP, which crosses a namespace boundary and so needs a
# CiliumNetworkPolicy first — the monitoring probes get away without one
# only because their targets share a namespace with the prober.
- name: Web Service 11
group: services
url: https://paperless.renner.uno
interval: 60s
ui:
hide-hostname: true
hide-url: true
hide-port: true
hide-conditions: true
hide-errors: true
conditions:
- "[STATUS] == any(200, 301, 302)"
# Removed 2026-05-21: Jellyfin (media.renner.uno), Wiki (wiki.renner.uno),
# InfluxDB (influx.renner.uno) — hostnames NXDOMAIN, no Ingress; services
# no longer deployed. Re-add when/if they come back.
@@ -191,20 +209,11 @@ data:
# NoData). Monitor the offsite PBS from the offsite side or via a
# check that doesn't depend on this cluster's DNS path.
- name: Infrastructure 1
group: infrastructure
url: https://pbs2.renner.uno:8007
interval: 60s
client:
insecure-skip-verify: true
ui:
hide-hostname: true
hide-url: true
hide-port: true
hide-conditions: true
hide-errors: true
conditions:
- "[STATUS] == any(200, 301, 302)"
# Removed 2026-08-28: PBS 2 (pbs2.renner.uno:8007) — it has no clients
# left. Its two were the PVE cluster, retired to bare-metal Talos, and
# nas-1, powered off and dropped from the repo. Checking the liveness of
# a backup server that nothing backs up to reports on nothing. Re-add
# alongside the first host that is declared to back up to it.
- name: Infrastructure 3
group: infrastructure
+27
View File
@@ -5,6 +5,12 @@ metadata:
namespace: apps
labels:
app: gatus
annotations:
# Gatus reads its config once at startup and never re-reads the file, so
# without this a ConfigMap change reconciles cleanly and then does nothing:
# git and the running checks drift apart silently, with no failed resource
# to notice. Adding a check would not take effect either.
configmap.reloader.stakater.com/reload: gatus-config
spec:
replicas: 1
# RWO PVC (`gatus-data`, ceph-block) — RollingUpdate's default 25% surge
@@ -22,9 +28,30 @@ spec:
labels:
app: gatus
spec:
# The upstream image is FROM scratch with no USER, so without this the
# pod runs as root and trips PodSecurity restricted on four counts.
#
# The uid is numeric because a scratch image carries no /etc/passwd for
# the kubelet to resolve a name against. fsGroup is what makes the switch
# survivable: the only write path is the sqlite store on the RWO volume
# (/data/gatus.db, see the storage stanza in the ConfigMap), and fsGroup
# both chowns the existing volume and leaves the directory group-writable
# so sqlite can create its -wal and -journal siblings next to the db.
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
containers:
- name: gatus
image: twinproduction/gatus:v5.36.0 # {"$imagepolicy": "flux-system:gatus"}
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
ports:
- containerPort: 8080
name: http
@@ -210,6 +210,28 @@ spec:
- name: vscode
containerPort: 8080
protocol: TCP
# /healthz answers 200 whenever the HTTP server is up; its body
# reports session heartbeat state, which is not health — an idle
# editor reports "expired" and is perfectly fine. So this asserts
# that code-server is serving, not that anyone is using it.
#
# Liveness matters here more than elsewhere in this pod: code-server
# shares the Home Assistant config volume, and a wedged editor is
# otherwise invisible until someone tries to open it.
livenessProbe:
httpGet:
path: /healthz
port: vscode
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /healthz
port: vscode
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: config
mountPath: /config
@@ -4,6 +4,7 @@ kind: Kustomization
# come from the kube-prometheus-stack chart. Flux applies this Kustomization
# only after `monitoring` is Ready (chart installed → CRDs present).
resources:
- prometheusrule-blackbox.yaml
- prometheusrule-ceph.yaml
- prometheusrule-ci-openbao.yaml
- prometheusrule-cilium.yaml
@@ -66,6 +66,11 @@ spec:
# parameter, 200 means initialized, unsealed and serving, on whichever
# node answers; 503 sealed and 501 uninitialized both stay red.
- https://openbao.renner.uno/v1/sys/health?standbyok=true
# ical-proxy serves an unauthenticated /health that answers 200 with
# {"status":"healthy"}. Unlike the gated hostnames this is real app
# health, not reachability — the root path returns 404 by design, since
# the service only answers on calendar paths.
- https://ical-proxy.renner.uno/health
# APP-HEALTH probes for the gated monitoring UIs, from the correct
# in-cluster vantage (monitoring-vantage-point-rule): the gate is
# only on the ingress, so the ClusterIP /-/healthy endpoints are
@@ -78,6 +83,11 @@ spec:
# external reachability probe above; it is not the health vantage.)
- http://kube-prometheus-stack-alertmanager.monitoring.svc.cluster.local:9093/-/healthy
- http://kube-prometheus-stack-prometheus.monitoring.svc.cluster.local:9090/-/healthy
# Loki has no external vantage at all: its ingress requires auth on
# every path, so an outside probe can only ever report 401. In-cluster
# /ready answers 200 unauthenticated and is in this namespace, so the
# same intra-namespace allowance the two above rely on covers it.
- http://loki.monitoring.svc.cluster.local:3100/ready
# WAN-path probes of the public edge BY PROXY IP. The name-based
# home.renner.uno probe above resolves in-cluster (CoreDNS/k8s_gateway)
# and never leaves the LAN — the vantage-point blind spot that let the
@@ -0,0 +1,59 @@
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: blackbox
namespace: monitoring
labels:
# kube-prometheus-stack's Prometheus resource selects on this label;
# without it the rule is created but never loaded into Prometheus.
release: kube-prometheus-stack
spec:
groups:
- name: blackbox.rules
rules:
# Catch-all for every blackbox-http target that has no rule of its own.
# Before this existed, a target could be probed, graphed and completely
# unalerted — which is how file.renner.uno served 502 for months.
#
# THE EXCLUSION LIST AND THE DEDICATED RULES ARE ONE INVARIANT: an
# instance belongs here only while a rule elsewhere covers it, and
# adding a dedicated rule means adding its instance here. Get it wrong
# in one direction and the alert fires twice; wrong in the other and it
# never fires at all. Currently excluded:
#
# fsn1.your-objectstorage.com HetznerBackupEndpointDown (critical)
# netbird.renner.uno prometheusrule-netbird-availability
# proxy-1/2:9000/health same file, relay redundancy
# ci.renner.uno WoodpeckerDown
# openbao .../sys/health OpenBaoUnavailable
#
# Warning, not critical, on purpose: this rule cannot know what a given
# target means. A target whose loss deserves a critical page gets its
# own rule — and lands in the list above. That is what keeps the
# exclusion list from being arbitrary.
- alert: BlackboxProbeFailing
expr: probe_success{job="blackbox-http", instance!~"https://fsn1\\.your-objectstorage\\.com|https://netbird\\.renner\\.uno|http://proxy-[12]\\.renner\\.uno:9000/health|https://ci\\.renner\\.uno|https://openbao\\.renner\\.uno/v1/sys/health.*"} == 0
for: 10m
annotations:
summary: "Blackbox probe of {{ $labels.instance }} has been failing for 10 minutes"
description: >-
The endpoint is not answering as expected. For the Authentik-gated
hostnames this is a reachability check covering DNS, TLS, Traefik
and the gate outpost — it says nothing about the app behind the
gate, which the internal /-/healthy probes cover separately.
labels:
severity: warning
# The check that the checks are running. Every rule above reads
# probe_success; if the series stop being produced at all, none of them
# can fire and the silence looks exactly like health.
- alert: BlackboxProbesAbsent
expr: absent(probe_success{job="blackbox-http"})
for: 15m
annotations:
summary: "No blackbox-http probe results are reaching Prometheus"
description: >-
Either the blackbox exporter is down or the Probe objects stopped
being discovered. Every endpoint alert depends on this job, so
they are all blind until it returns.
labels:
severity: critical
@@ -17,11 +17,12 @@ spec:
# drains on recovery) and Velero backups fail. Replaces the old
# GarageDown alert now that backups live on Hetzner, not nas-1.
#
# CNPG has no metric-based archiving alert yet — its pod metrics
# are not scraped (enablePodMonitor=false). Enabling CNPG
# PodMonitors for a granular "archiving falling behind" alert is
# a follow-up. Velero's own VeleroBackupValidationFailure /
# VeleroBackupStale rules already cover backup-level failures.
# The granular counterpart is CNPGWalArchiveBacklog in
# prometheusrule-cnpg-backup.yaml, reading the instance collector
# scraped by cnpg-podmonitors.yaml: this alert says the target is
# unreachable, that one says archiving is falling behind whatever the
# cause. Velero's VeleroBackupValidationFailure / VeleroBackupStale
# cover backup-level failures.
expr: probe_success{instance="https://fsn1.your-objectstorage.com"} == 0
for: 10m
labels:
@@ -24,6 +24,22 @@ spec:
- name: wyoming
containerPort: 10400
protocol: TCP
# Wyoming is raw TCP with no HTTP surface and no health verb, so
# accepting a connection is all that can be asserted from outside the
# process. Weaker than an HTTP probe, but the previous state was none
# at all: a dead server stayed Ready and kept being routed to.
livenessProbe:
tcpSocket:
port: wyoming
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
tcpSocket:
port: wyoming
initialDelaySeconds: 15
periodSeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 100m
@@ -70,6 +70,27 @@ spec:
- name: consume
mountPath: /home/scanner
subPath: scanner
# FTP has no health verb, so the control port accepting a connection
# is what can be asserted. It is enough for the failure that matters:
# if vsftpd dies the scanner cannot deposit anything and nothing else
# in the chain notices, because paperless only ever sees an empty
# consume directory — which is indistinguishable from nobody having
# scanned.
#
# Only the control port is probed. The passive range is negotiated
# per transfer and is not listening at rest.
livenessProbe:
tcpSocket:
port: ftp-ctrl
initialDelaySeconds: 15
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
tcpSocket:
port: ftp-ctrl
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 20m
+21 -9
View File
@@ -155,21 +155,33 @@ spec:
- name: bridge-cert
mountPath: /shared
readOnly: true
# TCP probes — HTTP probes against /api/ flooded the
# `paperless.auth` logger every 10 s with "No authentication
# provided from private IP …". With ENABLE_HTTP_REMOTE_USER
# the middleware logs every unauth API request, and there's
# no public health endpoint to probe. TCP confirms gunicorn
# is bound; that's the same liveness signal we'd get from
# /api/ minus the noise.
# /accounts/login/ rather than /api/ or a TCP connect.
#
# /api/ is unusable as a probe target: with ENABLE_HTTP_REMOTE_USER
# the remote-user middleware logs "No authentication provided from
# private IP …" for every unauthenticated API request, so probing it
# floods `paperless.auth`. Measured: four requests to /api/ produce
# four log lines, four to /accounts/login/ produce none — the
# middleware only covers the API surface.
#
# A TCP connect avoids the noise but only proves gunicorn accepted a
# socket. /accounts/login/ renders a real Django view, so URL
# routing, settings and templating all have to work. It does not
# prove Postgres is healthy; nothing cheap here does.
#
# Safe against ALLOWED_HOSTS: the kubelet sends the pod IP as Host,
# and PAPERLESS_ALLOWED_HOSTS is "*". Narrowing that value later
# means giving these probes an explicit Host header.
livenessProbe:
tcpSocket:
httpGet:
path: /accounts/login/
port: http
initialDelaySeconds: 120
periodSeconds: 30
timeoutSeconds: 10
readinessProbe:
tcpSocket:
httpGet:
path: /accounts/login/
port: http
initialDelaySeconds: 30
periodSeconds: 10
@@ -34,6 +34,32 @@ spec:
- name: wyoming
containerPort: 10300
protocol: TCP
# Wyoming is raw TCP with no HTTP surface and no health verb, so
# accepting a connection is all that can be asserted from outside the
# process — weaker than an HTTP probe, but the previous state was
# none at all.
#
# A startupProbe rather than a long liveness delay: this container
# runs with --retrain-on-start, and retraining against the Home
# Assistant entity list takes as long as it takes. The startup gate
# allows up to 10 minutes (60 x 10s) and only then hands over to
# liveness, so a slow retrain cannot be mistaken for a hang, and a
# genuine hang after startup is still caught within a minute.
startupProbe:
tcpSocket:
port: wyoming
periodSeconds: 10
failureThreshold: 60
livenessProbe:
tcpSocket:
port: wyoming
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
tcpSocket:
port: wyoming
periodSeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: models
mountPath: /models
+21
View File
@@ -29,6 +29,27 @@ spec:
- name: wyoming
containerPort: 10300
protocol: TCP
# Wyoming is a raw TCP protocol with no HTTP surface and no health
# verb, so accepting a connection is the only thing that can be
# asserted from outside the process. That is weaker than an HTTP
# probe but strictly better than the previous state of none at all,
# where a dead server stayed Ready and Home Assistant kept routing
# speech to it.
#
# The liveness delay is generous because the model is loaded at
# startup from the data volume.
livenessProbe:
tcpSocket:
port: wyoming
initialDelaySeconds: 90
periodSeconds: 30
timeoutSeconds: 5
readinessProbe:
tcpSocket:
port: wyoming
initialDelaySeconds: 20
periodSeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: data
mountPath: /data
@@ -33,3 +33,16 @@ spec:
crds: CreateReplace
remediation:
retries: 3
values:
# The chart ships these disabled, so the controller ran with no probe of
# any kind: a wedged reconciler stayed Ready and every ExternalSecret would
# simply stop refreshing, which surfaces only when a consumer needs a
# credential that has since rotated.
#
# Enabling them also starts the health server — the chart derives
# --health-probe-bind-address from these values, and without them the
# controller exposes nothing but its metrics port.
livenessProbe:
enabled: true
readinessProbe:
enabled: true