From 62f1a62b2b382512bdafb4e153069cc505ed3b2f Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 28 Aug 2026 12:08:46 +0200 Subject: [PATCH 1/2] fix(gatus): run as non-root and satisfy PodSecurity restricted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- kubernetes/apps/gatus/deployment.yaml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/kubernetes/apps/gatus/deployment.yaml b/kubernetes/apps/gatus/deployment.yaml index a971f47a..a4ef1f62 100644 --- a/kubernetes/apps/gatus/deployment.yaml +++ b/kubernetes/apps/gatus/deployment.yaml @@ -28,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 -- 2.54.0 From ad0102d2a9b8e82c768ec7d58d5db5e183864c3d Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Fri, 28 Aug 2026 12:09:26 +0200 Subject: [PATCH 2/2] feat(monitoring): probe ical-proxy and Loki, add Paperless to the status page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- kubernetes/apps/gatus/configmap.yaml | 24 +++++++++++++++++++ .../monitoring-rules/probes-blackbox.yaml | 10 ++++++++ 2 files changed, 34 insertions(+) diff --git a/kubernetes/apps/gatus/configmap.yaml b/kubernetes/apps/gatus/configmap.yaml index bd671b76..266856c9 100644 --- a/kubernetes/apps/gatus/configmap.yaml +++ b/kubernetes/apps/gatus/configmap.yaml @@ -29,6 +29,7 @@ metadata: # 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: @@ -172,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. diff --git a/kubernetes/apps/monitoring-rules/probes-blackbox.yaml b/kubernetes/apps/monitoring-rules/probes-blackbox.yaml index 950fabb2..90bc23fa 100644 --- a/kubernetes/apps/monitoring-rules/probes-blackbox.yaml +++ b/kubernetes/apps/monitoring-rules/probes-blackbox.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 -- 2.54.0