Skip to content

OPNsense exporter

Purpose

kubernetes/apps/observability/opnsense-exporter scrapes the firewall two ways: the opnsense-exporter API-based exporter for gateway, service and IPsec state, and a plain node-exporter ScrapeConfig for kernel-level CPU/memory/ZFS/disk/netdev the API cannot see. The node-exporter path was added after a WAN-wedge outage had to be reconstructed entirely from cluster-side probes, because the box exposed no OS-level metrics at the time.

Why the WAN-traffic-frozen alert's for stays short

OPNsenseWanTrafficFrozen fires on a rate(...) == 0 over a 5-minute window with for: 1m, not a longer for on top of that window. The 5-minute rate() window is itself the debounce: the ServiceMonitor scrapes every 60s, and rate() needs at least two samples in range, so a 1-minute window would usually see only one and could never sustain long enough to fire at all. Stacking another 5 minutes of for on top of that window would double the effective detection time to about 10 minutes for no benefit, since the rate window has already absorbed the debounce.

The alert exists because of a specific outage signature: both WAN interface counters froze to exactly zero for 15 minutes while the link stayed up and nothing else logged a fault. dpinger's own monitor traffic rides the same interface, so a healthy WAN can never legitimately read zero bytes in both directions for 5 minutes; any observed instance of that is the interface itself, not absence of traffic to send.

Why the backup WAN tier alert is gated on state, not a time window

OPNsenseBackupWanTierDown fires on sustained 100% loss on a backup WAN tier, gated on the exporter's own opnsense_gateways_status != 6 ("offline forced", OPNsense's state for a gateway an admin has explicitly forced down) rather than on any time-based window. A tier being wired up or deliberately retired stays silent by forcing it down in OPNsense; anything not force-down alerts on sustained loss with no expiry.

The earlier version of this gate used min_over_time(...[24h]) < 100 to both arm on first success and disarm after 24 hours, using the same rolling window for both jobs. Once a real outage ran past 24 hours, the window filled entirely with 100%-loss samples and the alert silently resolved itself while the failure was still active, indistinguishable from a deliberate decommission. That self-silenced during the one case the alert exists to catch, which is why the gate moved to an explicit administrative state instead of an implicit time-based one.

IPsec tunnel monitoring

OPNsenseIPsecTunnelDown exists because both ends of the site-to-site tunnel pin their identity to a literal WAN IP address, so an upstream address change leaves the tunnel down until both sides are updated together, a manual, two-sided fix rather than something that resolves on its own. Before this alert, phase1_status had been scraped all along with no rule on it, so the tunnel could sit down indefinitely with every dashboard reading green.

The absent() half of the expression is load-bearing, not decoration. If the connection is removed, renamed, or simply stops being returned by the API, its series vanishes entirely, and a bare == 0 comparison can never fire on an empty vector. That is the same blind spot that once left an unrelated exporter's outage unmonitored for eight days; this alert was written to not repeat it, and was verified live against a description that does not exist to confirm it actually fires.

OPNsenseIPsecChildSAMissing catches the case one layer down: phase 1 (IKE) can be up while phase 2 (the actual traffic path) has no child security association, which the tunnel-down alert above cannot see. There is no phase2_status metric, and the byte counters are not a usable substitute, since a healthy, idle tunnel legitimately reads zero bytes transferred. Existence of the child SA is therefore the only usable signal. The 15-minute for clears a normal phase 2 rekey window (a lifetime of roughly 2300 seconds, rekeying around 1500 seconds in) without flapping on it.