Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96f11cb0bf | ||
|
|
7f71e6655f | ||
|
|
15ec3ce2f0 | ||
|
|
617e5c5650 | ||
|
|
2a7c6de1b2 | ||
|
|
45b0249d45 | ||
|
|
2080260212 |
@@ -1,8 +1,20 @@
|
||||
# Trivy scan of the IaC tree and every image referenced from ansible/, with an
|
||||
# agent pass that opens a PR for CRITICAL/HIGH findings.
|
||||
#
|
||||
# Triggers:
|
||||
# - cron : the named `daily-security-scan` cron, matched by NAME.
|
||||
# Crons cannot carry variables — `woodpecker-cli repo cron add`
|
||||
# has no --var — so a variable gate can never match a cron.
|
||||
# - deployment : woodpecker-cli pipeline deploy k.renner/homelab <n> security-scan
|
||||
# The target rides in pipeline metadata as
|
||||
# CI_PIPELINE_DEPLOY_TARGET, which the workflow-root filter
|
||||
# does see, unlike manual-run variables on Woodpecker 3.18
|
||||
# (#582).
|
||||
when:
|
||||
- event: cron
|
||||
evaluate: 'PIPELINE == "security-scan"'
|
||||
- event: manual
|
||||
evaluate: 'PIPELINE == "security-scan"'
|
||||
cron: daily-security-scan
|
||||
- event: deployment
|
||||
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "security-scan"'
|
||||
|
||||
steps:
|
||||
- name: iac-scan
|
||||
|
||||
@@ -1,50 +1,10 @@
|
||||
---
|
||||
# The goauthentik.io operator (inside authentik-worker) auto-creates a
|
||||
# ServiceMonitor per outpost, but it does not stamp the
|
||||
# `release: kube-prometheus-stack` label that this cluster's Prometheus
|
||||
# requires on serviceMonitorSelector. Those operator-owned SMs are
|
||||
# silently ignored. Declare standalone SMs here with the correct label
|
||||
# so the outposts actually get scraped.
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: authentik-outpost-ldap
|
||||
namespace: authentik
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- authentik
|
||||
selector:
|
||||
matchLabels:
|
||||
goauthentik.io/outpost-name: ldap-outpost
|
||||
goauthentik.io/outpost-type: ldap
|
||||
endpoints:
|
||||
- port: http-metrics
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
---
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: ServiceMonitor
|
||||
metadata:
|
||||
name: authentik-outpost-paperless-gate
|
||||
namespace: authentik
|
||||
labels:
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
namespaceSelector:
|
||||
matchNames:
|
||||
- authentik
|
||||
selector:
|
||||
matchLabels:
|
||||
goauthentik.io/outpost-name: paperless-gate
|
||||
goauthentik.io/outpost-type: proxy
|
||||
endpoints:
|
||||
- port: http-metrics
|
||||
path: /metrics
|
||||
interval: 30s
|
||||
---
|
||||
# Only radius needs a hand-written ServiceMonitor. The goauthentik.io operator
|
||||
# creates one per outpost and Prometheus discovers those directly, because
|
||||
# serviceMonitorSelector here is empty (see monitoring/helmrelease-prometheus.yaml).
|
||||
# Standalone copies for ldap and paperless-gate used to live here as well; they
|
||||
# duplicated the operator's and were removed.
|
||||
#
|
||||
# Radius requires extra plumbing. The radius binary listens on :9300 just
|
||||
# like ldap/proxy (hardcoded in authentik/lib/default.yml), but authentik's
|
||||
# Python operator (`providers/radius/controllers/kubernetes.py`) omits the
|
||||
@@ -53,6 +13,11 @@ spec:
|
||||
# it, radius does not. Result: the operator-managed `ak-outpost-local-radius`
|
||||
# Service exposes only UDP 1812. Add a sibling Service here that selects the
|
||||
# same pod labels and exposes 9300/TCP, then a ServiceMonitor for it.
|
||||
#
|
||||
# The operator's own `ak-outpost-local-radius` ServiceMonitor cannot cover
|
||||
# this: it selects on `app.kubernetes.io/managed-by: goauthentik.io` together
|
||||
# with `goauthentik.io/service-type: metrics`, and no Service carries both —
|
||||
# the operator's has the first, the one below has the second.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
@@ -86,6 +51,8 @@ spec:
|
||||
matchNames:
|
||||
- authentik
|
||||
selector:
|
||||
# Pinned to the Service above. Without managed-by this would also match the
|
||||
# operator's Service and scrape the same endpoint twice.
|
||||
matchLabels:
|
||||
app.kubernetes.io/managed-by: homelab
|
||||
goauthentik.io/outpost-name: local-radius
|
||||
|
||||
@@ -5,9 +5,8 @@ metadata:
|
||||
namespace: monitoring
|
||||
labels:
|
||||
app.kubernetes.io/name: alloy
|
||||
# kube-prometheus-stack's Prometheus resource selects ServiceMonitors
|
||||
# by this label; without it the resource exists but is never
|
||||
# discovered.
|
||||
# Not required for discovery — serviceMonitorSelector is empty — but kept
|
||||
# for consistency with the other monitors.
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# Scrape the CNPG Postgres instances (port 9187, built-in exporter).
|
||||
# CNPG's own enablePodMonitor would generate PodMonitors without the
|
||||
# `release: kube-prometheus-stack` label, which this Prometheus requires
|
||||
# (podMonitorSelector), so we define them here instead.
|
||||
# Defined here rather than via CNPG's own enablePodMonitor so the scrape
|
||||
# config lives with the other monitors. The `release` label below is not
|
||||
# required for discovery — podMonitorSelector is empty — but kept for
|
||||
# consistency.
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: PodMonitor
|
||||
metadata:
|
||||
|
||||
@@ -5,9 +5,8 @@ metadata:
|
||||
namespace: home-assistant
|
||||
labels:
|
||||
app.kubernetes.io/name: home-assistant
|
||||
# kube-prometheus-stack's Prometheus resource selects ServiceMonitors
|
||||
# by this label; without it the resource exists but is never
|
||||
# discovered. Same trap the garage PrometheusRule documents.
|
||||
# Not required for discovery — serviceMonitorSelector is empty — but kept
|
||||
# for consistency with the other monitors.
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
selector:
|
||||
|
||||
@@ -5,9 +5,8 @@ metadata:
|
||||
namespace: monitoring
|
||||
labels:
|
||||
app.kubernetes.io/name: loki
|
||||
# kube-prometheus-stack's Prometheus resource selects ServiceMonitors
|
||||
# by this label; without it the resource exists but is never
|
||||
# discovered.
|
||||
# Not required for discovery — serviceMonitorSelector is empty — but kept
|
||||
# for consistency with the other monitors.
|
||||
release: kube-prometheus-stack
|
||||
spec:
|
||||
# Matches the `loki` (single-binary) and `loki-canary` services, but
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# HTTP probes — fan out to blackbox-exporter. Probe targets mirror the
|
||||
# legacy Ansible role's blackbox_http_targets list.
|
||||
#
|
||||
# The `release` label on every Probe here IS load-bearing: probeSelector still
|
||||
# matches on it, unlike serviceMonitorSelector and podMonitorSelector, which
|
||||
# are empty. Removing it silently stops the probe from being discovered.
|
||||
apiVersion: monitoring.coreos.com/v1
|
||||
kind: Probe
|
||||
metadata:
|
||||
|
||||
@@ -11,25 +11,22 @@ spec:
|
||||
groups:
|
||||
- name: cnpg-backup.rules
|
||||
rules:
|
||||
# WHY THIS FILE EXISTS
|
||||
# Seven Postgres clusters back up to Hetzner and NOTHING alerted if that
|
||||
# stopped. The only related rule was HetznerBackupEndpointDown, which
|
||||
# probes the S3 endpoint — it proves the bucket is reachable, not that
|
||||
# anything was written to it. Same failure shape as the WordPress backup
|
||||
# that failed nightly for three days against a bucket that was declared
|
||||
# in tofu but never applied: the status surface looked fine because
|
||||
# nothing checked the thing that mattered.
|
||||
# The Postgres clusters back up to Hetzner. HetznerBackupEndpointDown
|
||||
# probes that S3 endpoint, which proves the bucket is reachable and
|
||||
# nothing more — these two rules check that something is actually being
|
||||
# written to it.
|
||||
|
||||
- alert: CNPGWalArchiveBacklog
|
||||
# `ready` counts WAL segments sitting in pg_wal/archive_status waiting
|
||||
# to be archived. Steady state is 0 across every cluster (verified
|
||||
# 2026-08-19). A sustained backlog means the archiver is failing:
|
||||
# point-in-time recovery is silently broken from that moment, and
|
||||
# pg_wal keeps growing until it fills the PVC and takes Postgres down.
|
||||
# to be archived. Steady state is 0. A sustained backlog means the
|
||||
# archiver is failing: point-in-time recovery is silently broken from
|
||||
# that moment, and pg_wal keeps growing until it fills the PVC and
|
||||
# takes Postgres down.
|
||||
#
|
||||
# This is the load-bearing alert of the two. It depends only on the
|
||||
# instance-level collector, so unlike the backup-age metrics below it
|
||||
# is unaffected by the missing Backup CRs (#522).
|
||||
# This is the load-bearing alert of the two. It reads the
|
||||
# instance-level collector directly, so unlike the backup-age metric
|
||||
# discussed below it does not depend on Cluster status being
|
||||
# populated.
|
||||
#
|
||||
# Threshold: >5 segments sustained for 30m. Brief non-zero values are
|
||||
# normal under write bursts — a segment is 16 MiB, so 5 is roughly
|
||||
@@ -63,16 +60,17 @@ spec:
|
||||
#
|
||||
# The natural expression is
|
||||
# (time() - cnpg_collector_last_available_backup_timestamp) > 48*3600
|
||||
# and it CANNOT be used today: that metric reads exactly 0 on every
|
||||
# instance in the cluster (verified 2026-08-19 across gf-pg, ak-pg-1/2,
|
||||
# pprl-pg-2/3, im-pg, wp-pg, ha-pg). CNPG has no record of any
|
||||
# successful backup anywhere, because the Backup CRs it derives that
|
||||
# from are being deleted within a minute of completing — see #522.
|
||||
# and it cannot be used, because that metric reads exactly 0 on every
|
||||
# instance. The collector derives it from `Cluster.status
|
||||
# .lastSuccessfulBackup`, and that field is absent on every Cluster
|
||||
# here: all of them back up with `method: plugin` (barman-cloud), and
|
||||
# the plugin path does not write it back.
|
||||
#
|
||||
# Backups themselves DO work; that was verified independently by
|
||||
# listing the bucket with barman-cloud-backup-list and restoring a WAL
|
||||
# segment back out of S3. The gap is in the record, not the data.
|
||||
# The backups themselves run and are retained — completed Backup CRs
|
||||
# going back weeks exist for every scheduled cluster. The gap is in the
|
||||
# status field the metric reads, not in the data.
|
||||
#
|
||||
# Adding the rule now would fire permanently for all seven clusters and
|
||||
# be silenced within a day, which is worse than not having it. Add it as
|
||||
# part of resolving #522, once the metric reflects reality.
|
||||
# Adding the rule against that metric would fire permanently for every
|
||||
# cluster, which is worse than not having it. Either the plugin starts
|
||||
# populating the field, or the alert has to be built on the Backup CRs
|
||||
# directly, which Prometheus does not see today.
|
||||
|
||||
@@ -12,21 +12,54 @@ spec:
|
||||
- name: disk-wear.rules
|
||||
rules:
|
||||
# Wear percentages are self-reported: 100 means the rated endurance is
|
||||
# spent, not that the drive stops. Planning thresholds, not predictions.
|
||||
- alert: NvmeDriveWearHigh
|
||||
# Long `for` because wear is monotonic and cannot flap.
|
||||
expr: smartctl_device_percentage_used >= 80
|
||||
for: 1h
|
||||
# spent, not that the drive stops.
|
||||
#
|
||||
# Wear alone is not a signal. It only ever rises, so a plain threshold
|
||||
# starts firing the day a drive crosses it and never stops — a status
|
||||
# dressed as an alert (#673, #675, #682, #686 were all one drive set
|
||||
# crossing 80%). What matters is wear that is high AND still climbing,
|
||||
# so the alert below is a projection: at the write rate of the last
|
||||
# week, when does this drive reach 95%?
|
||||
#
|
||||
# The rate is self-calibrating. Each drive reports both its lifetime
|
||||
# bytes written and its own wear percentage, so bytes-per-percent comes
|
||||
# from the drive itself and no rated-endurance table has to be kept in
|
||||
# sync with the hardware. percentage_used is a whole-number percent and
|
||||
# advances only every few weeks here, which is far too coarse to
|
||||
# differentiate; bytes_written moves on every scrape.
|
||||
- alert: NvmeDriveWearProjectedExhaustion
|
||||
# The >= 80 gate is numerical, not editorial: bytes-per-percent is
|
||||
# derived by dividing by an integer percentage, so on a lightly worn
|
||||
# drive a half-percent rounding error swings the projection by tens
|
||||
# of percent. Above 80 the same rounding is under one percent.
|
||||
# Idle drives divide by a zero rate and yield +Inf, which is not < the
|
||||
# threshold, so they stay silent on their own.
|
||||
expr: >-
|
||||
(
|
||||
(95 - smartctl_device_percentage_used)
|
||||
/
|
||||
(
|
||||
rate(smartctl_device_bytes_written[7d]) * 86400
|
||||
* smartctl_device_percentage_used
|
||||
/ smartctl_device_bytes_written
|
||||
)
|
||||
) < 180
|
||||
and smartctl_device_percentage_used >= 80
|
||||
# Long, because a projection off a 7d rate should not react to a day.
|
||||
for: 6h
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
summary: "NVMe {{ $labels.device }} on {{ $labels.instance }} is at {{ $value }}% rated wear"
|
||||
summary: "NVMe {{ $labels.device }} on {{ $labels.instance }} projects to 95% wear in {{ $value | printf \"%.0f\" }} days"
|
||||
description: >-
|
||||
{{ $value }}% of rated write endurance used. Nothing is failing
|
||||
yet. Urgency depends on what the drive carries — the Disk Health
|
||||
dashboard shows the role. Losing an OSD costs a rebalance;
|
||||
EPHEMERAL is /var and rebuilds, apart from the node's etcd member
|
||||
data, which has to be removed and re-added.
|
||||
The drive is past 80% of its rated endurance and the write rate
|
||||
of the last week would spend the rest within six months. This is
|
||||
a lead-time warning, not a fault: decide whether to replace the
|
||||
drive or move what is writing to it. The Disk Health dashboard
|
||||
shows which drives carry which role.
|
||||
# Kept as a flat threshold on purpose: past rated endurance the drive
|
||||
# runs outside what the manufacturer stands behind regardless of how
|
||||
# fast it got there, and the alert clears by replacing it.
|
||||
- alert: NvmeDriveWearCritical
|
||||
expr: smartctl_device_percentage_used >= 95
|
||||
for: 1h
|
||||
|
||||
@@ -24,8 +24,10 @@ spec:
|
||||
enabled: true
|
||||
defaults:
|
||||
labels:
|
||||
# Required for kube-prometheus-stack's Prometheus to pick up
|
||||
# this ServiceMonitor (its selector matches release).
|
||||
# Not required for discovery — serviceMonitorSelector is empty — but
|
||||
# kept for consistency with the other monitors. The Probes in
|
||||
# monitoring-rules/probes-blackbox.yaml do need it; probeSelector
|
||||
# still matches on release.
|
||||
release: kube-prometheus-stack
|
||||
interval: 30s
|
||||
# Helm DOES deep-merge this map with the chart's default (that is why
|
||||
|
||||
@@ -56,26 +56,21 @@ spec:
|
||||
#
|
||||
# By default kube-prometheus-stack sets these selectors to
|
||||
# `matchLabels: {release: kube-prometheus-stack}`. ServiceMonitors
|
||||
# created at RUNTIME by third-party operators carry that operator's own
|
||||
# labels instead and can never be made to match — no static manifest or
|
||||
# Kustomize patch can relabel an object the operator creates itself.
|
||||
# created at RUNTIME by third-party operators — Rook, goauthentik.io —
|
||||
# carry that operator's own labels instead and can never be made to
|
||||
# match: no static manifest or Kustomize patch can relabel an object
|
||||
# the operator creates itself. Left strict, those targets are dropped
|
||||
# silently, with no failed resource and no gap visible anywhere except
|
||||
# in the missing series.
|
||||
#
|
||||
# This was silently dropping 12 of 30 ServiceMonitors (2026-07-27):
|
||||
# rook-ceph/rook-ceph-mgr, rook-ceph/rook-ceph-exporter (team: rook)
|
||||
# authentik/ak-outpost-* x10 (goauthentik.io/...)
|
||||
# Consequence: ZERO `ceph_*` series existed in Prometheus for all 79
|
||||
# days of the cluster's life — the entire storage layer had no
|
||||
# telemetry, no history, and any Ceph alerting rule would have had
|
||||
# nothing to fire on. The Rook side is the reason this was found; the
|
||||
# authentik outpost targets come along with it.
|
||||
# Because these two are empty, the `release` label on this repo's own
|
||||
# ServiceMonitors and PodMonitors is decorative. It is still set for
|
||||
# consistency, and the comments there say so.
|
||||
#
|
||||
# This is the ServiceMonitor twin of the trap already documented for
|
||||
# rules in monitoring-rules/prometheusrule-velero.yaml ("without this
|
||||
# label the rule is created but never loaded").
|
||||
#
|
||||
# ruleSelector is deliberately NOT loosened: this repo's PrometheusRules
|
||||
# all carry `release: kube-prometheus-stack` on purpose, and keeping
|
||||
# that selector strict stops a stray third-party rule from firing here.
|
||||
# ruleSelector, probeSelector and scrapeConfigSelector are deliberately
|
||||
# NOT loosened: nothing outside this repo creates those, so keeping the
|
||||
# selectors strict stops a stray third-party rule from firing here. For
|
||||
# PrometheusRules and Probes the label IS required.
|
||||
serviceMonitorSelectorNilUsesHelmValues: false
|
||||
podMonitorSelectorNilUsesHelmValues: false
|
||||
# Rewrites the in-cluster URL embedded in alert generatorURLs and
|
||||
|
||||
Reference in New Issue
Block a user