fix(gatus): harden the deployment, probe ical-proxy and Loki, add Paperless #702

Merged
k.renner merged 2 commits from gatus-hardening into master 2026-08-28 10:45:50 +00:00
Member

Alle drei offenen Punkte, in zwei trennbaren Commits.

1. Gatus als Nicht-Root

Das Upstream-Image ist FROM scratch ohne USER — verifiziert am Dockerfile, nicht geraten. Der Pod lief also als root und verletzte restricted an allen vier Stellen.

fsGroup ist der Teil, der das überlebensfähig macht statt zum Crash: der einzige Schreibpfad ist der sqlite-Store auf dem RWO-Volume (storage.type: sqlite, path: /data/gatus.db). fsGroup chownt das bestehende Volume beim Mount und lässt das Verzeichnis gruppenschreibbar — was sqlite braucht, um -wal und -journal neben der Datenbank anzulegen.

Die uid ist numerisch, weil ein scratch-Image keine /etc/passwd hat, gegen die das kubelet einen Namen auflösen könnte.

readOnlyRootFilesystem bewusst nicht gesetzt: restricted verlangt es nicht, und auf einem scratch-Image ist nichts zu gewinnen, was das Risiko rechtfertigt, zur Laufzeit herauszufinden, dass die Binary doch irgendwo hinschreiben will.

Der Dry-Run ist danach warnungsfrei.

2. Drei ungeprüfte Dienste — drei verschiedene Antworten

Dienst Befund Lösung
ical-proxy / gibt 404 by design (bedient nur Kalenderpfade); /health gibt 200 mit {"status":"healthy"} externe Probe auf /health — echte App-Gesundheit
Loki jeder Pfad durch die Ingress gibt 401; extern ist nur „Ingress lebt" messbar In-Cluster-Probe auf /ready, im selben Namespace wie der Prober
Paperless jeder Pfad inkl. /api/ gibt 302 vom Gate Statusseite, nur Gate-Ebene — und der Eintrag sagt das

Bei ical-proxy wäre die naive Reaktion auf das 404 ein Blackbox-Modul gewesen, das 404 akzeptiert. Der Dienst hat aber einen echten Health-Endpunkt; ein Modul hätte den Fehler zementiert.

Loki /ready habe ich vor dem Commit über den apiserver-Proxy geprüft — antwortet ready, unauthentifiziert.

Paperless bleibt bewusst blind gegenüber der App selbst. Das zu schließen hieße, die ClusterIP aus dem Cluster zu proben; das überquert eine Namespace-Grenze und braucht erst eine CiliumNetworkPolicy. Die bestehenden Monitoring-Probes kommen nur deshalb ohne aus, weil ihre Ziele im selben Namespace wie der Prober liegen. Eigene Änderung, nicht hier hineingezogen.

Paperless bekommt einen neuen Slot statt des zurückgezogenen Web Service 7, den die Legende ihm früher fälschlich zuschrieb — ein öffentliches Label wiederzuverwenden würde still seine Bedeutung ändern.

Nach dem Merge

Der interessante Teil ist Commit 1: ob gatus als uid 65532 überhaupt hochkommt und die bestehende gatus.db noch schreiben kann. Ich prüfe Pod-Status, Logs und ob die Statusseite weiter Ergebnisse liefert — ein Running allein beweist nichts, wenn sqlite still nicht schreiben kann.

Alle drei offenen Punkte, in zwei trennbaren Commits. ## 1. Gatus als Nicht-Root Das Upstream-Image ist `FROM scratch` **ohne `USER`** — verifiziert am Dockerfile, nicht geraten. Der Pod lief also als root und verletzte `restricted` an allen vier Stellen. `fsGroup` ist der Teil, der das überlebensfähig macht statt zum Crash: der einzige Schreibpfad ist der sqlite-Store auf dem RWO-Volume (`storage.type: sqlite`, `path: /data/gatus.db`). `fsGroup` chownt das bestehende Volume beim Mount **und** lässt das Verzeichnis gruppenschreibbar — was sqlite braucht, um `-wal` und `-journal` neben der Datenbank anzulegen. Die uid ist numerisch, weil ein scratch-Image keine `/etc/passwd` hat, gegen die das kubelet einen Namen auflösen könnte. `readOnlyRootFilesystem` bewusst **nicht** gesetzt: `restricted` verlangt es nicht, und auf einem scratch-Image ist nichts zu gewinnen, was das Risiko rechtfertigt, zur Laufzeit herauszufinden, dass die Binary doch irgendwo hinschreiben will. Der Dry-Run ist danach warnungsfrei. ## 2. Drei ungeprüfte Dienste — drei verschiedene Antworten | Dienst | Befund | Lösung | |---|---|---| | **ical-proxy** | `/` gibt **404 by design** (bedient nur Kalenderpfade); `/health` gibt **200** mit `{"status":"healthy"}` | externe Probe auf `/health` — echte App-Gesundheit | | **Loki** | **jeder** Pfad durch die Ingress gibt 401; extern ist nur „Ingress lebt" messbar | In-Cluster-Probe auf `/ready`, im selben Namespace wie der Prober | | **Paperless** | **jeder** Pfad inkl. `/api/` gibt 302 vom Gate | Statusseite, **nur Gate-Ebene** — und der Eintrag sagt das | Bei ical-proxy wäre die naive Reaktion auf das 404 ein Blackbox-Modul gewesen, das 404 akzeptiert. Der Dienst hat aber einen echten Health-Endpunkt; ein Modul hätte den Fehler zementiert. Loki `/ready` habe ich vor dem Commit über den apiserver-Proxy geprüft — antwortet `ready`, unauthentifiziert. **Paperless bleibt bewusst blind gegenüber der App selbst.** Das zu schließen hieße, die ClusterIP aus dem Cluster zu proben; das überquert eine Namespace-Grenze und braucht erst eine CiliumNetworkPolicy. Die bestehenden Monitoring-Probes kommen nur deshalb ohne aus, weil ihre Ziele im selben Namespace wie der Prober liegen. Eigene Änderung, nicht hier hineingezogen. Paperless bekommt einen **neuen Slot** statt des zurückgezogenen `Web Service 7`, den die Legende ihm früher fälschlich zuschrieb — ein öffentliches Label wiederzuverwenden würde still seine Bedeutung ändern. ## Nach dem Merge Der interessante Teil ist Commit 1: ob gatus als uid 65532 überhaupt hochkommt und die bestehende `gatus.db` noch schreiben kann. Ich prüfe Pod-Status, Logs und ob die Statusseite weiter Ergebnisse liefert — ein `Running` allein beweist nichts, wenn sqlite still nicht schreiben kann.
claude-bot added 2 commits 2026-08-28 10:09:56 +00:00
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.
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
ad0102d2a9
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.
review-bot left a comment
Member

🤖 Automated review — model CLI default (complexity: complex), commit ad0102d2a9b8e82c768ec7d58d5db5e183864c3d.

Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.


Summary

This PR (1) adds a restricted-compliant securityContext to the gatus Deployment (non-root uid, fsGroup, seccomp, dropped capabilities) and (2) adds three new health probes: an external /health check for ical-proxy, an in-cluster /ready check for Loki, and a reachability-only public status-page entry for Paperless. I verified the concrete claims made in the PR body against the checked-out repo (ical-proxy's /health endpoint, Loki's namespace/ingress-auth setup, the monitoring namespace's intra-namespace-allow CiliumNetworkPolicy, Paperless's Authentik-outpost gating, and the 65532 uid convention already used elsewhere in the repo) and found them all accurate. No blocking issues.

🟡 Suggestions

  • kubernetes/apps/gatus/deployment.yaml:40-46 — readOnlyRootFilesystem is deliberately omitted, which the PR body explains (nothing to gain on a scratch image, risk of an unexpected write path). That's a reasonable call, but it does mean the hardening isn't quite "all four restricted fields plus root-fs" — worth a one-line comment in the deployment itself (not just the PR description) so a future reader doesn't file it as an oversight. Currently the in-repo comment explains uid/fsGroup choices but not this omission.
  • kubernetes/apps/monitoring-rules/probes-blackbox.yaml:18 — all static targets in the blackbox-http Probe share one module: http_2xx. That's pre-existing and consistent with how the file already handles heterogeneous targets (gated hostnames, TCP checks in a separate Probe), so not a defect, just noting the new Loki/ical-proxy entries inherit that same coupling — if either ever needs a different valid-status-code range or timeout, it will need its own Probe/module rather than a per-target override.

Tests

No automated tests apply to this class of change (Kubernetes manifests / Flux GitOps). Verification is necessarily post-merge (pod comes up as non-root, sqlite can still write, the new probes go green), which the PR body's "Nach dem Merge" section explicitly commits to. That's the right verification strategy for this repo; nothing to add.

Recommendation

Looks reasonable — no blocking issues; comments above are optional.

🤖 **Automated review** — model `CLI default` (complexity: complex), commit `ad0102d2a9b8e82c768ec7d58d5db5e183864c3d`. > Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review. --- ## Summary This PR (1) adds a `restricted`-compliant `securityContext` to the gatus Deployment (non-root uid, `fsGroup`, seccomp, dropped capabilities) and (2) adds three new health probes: an external `/health` check for ical-proxy, an in-cluster `/ready` check for Loki, and a reachability-only public status-page entry for Paperless. I verified the concrete claims made in the PR body against the checked-out repo (ical-proxy's `/health` endpoint, Loki's namespace/ingress-auth setup, the `monitoring` namespace's intra-namespace-allow CiliumNetworkPolicy, Paperless's Authentik-outpost gating, and the `65532` uid convention already used elsewhere in the repo) and found them all accurate. No blocking issues. ## 🟡 Suggestions - `kubernetes/apps/gatus/deployment.yaml:40-46` — `readOnlyRootFilesystem` is deliberately omitted, which the PR body explains (nothing to gain on a scratch image, risk of an unexpected write path). That's a reasonable call, but it does mean the hardening isn't quite "all four `restricted` fields plus root-fs" — worth a one-line comment in the deployment itself (not just the PR description) so a future reader doesn't file it as an oversight. Currently the in-repo comment explains uid/fsGroup choices but not this omission. - `kubernetes/apps/monitoring-rules/probes-blackbox.yaml:18` — all static targets in the `blackbox-http` Probe share one `module: http_2xx`. That's pre-existing and consistent with how the file already handles heterogeneous targets (gated hostnames, TCP checks in a separate Probe), so not a defect, just noting the new Loki/ical-proxy entries inherit that same coupling — if either ever needs a different valid-status-code range or timeout, it will need its own Probe/module rather than a per-target override. ## Tests No automated tests apply to this class of change (Kubernetes manifests / Flux GitOps). Verification is necessarily post-merge (pod comes up as non-root, sqlite can still write, the new probes go green), which the PR body's "Nach dem Merge" section explicitly commits to. That's the right verification strategy for this repo; nothing to add. ## Recommendation Looks reasonable — no blocking issues; comments above are optional. <!-- pr-review:sha=ad0102d2a9b8e82c768ec7d58d5db5e183864c3d -->
k.renner merged commit 09c10e3fa6 into master 2026-08-28 10:45:50 +00:00
Sign in to join this conversation.