Skip to content

Commit 027439d

Browse files
committed
Add OPNsense by REST API
A second OPNsense template next to OPNsense by HTTP-JSON, derived from it and considerably wider. It carries its own name and its own uuids, so both can be linked to the same host and importing one does not touch the other. Measured against a live OPNsense 26.7 rather than taken from documentation, including the privilege table in the README, which is derived from the patterns the firewall publishes under auth/priv/search. 127 items, 15 discovery rules with 71 item prototypes, 58 triggers, 41 macros and a dashboard of nine pages. Over the template it grew out of it adds processor utilisation split into user, system and interrupt, the packet filter in depth with the state and source tracking tables, pf counters, table entries and the loaded ruleset, kernel network memory, netisr queues, protocol error rates, clock synchronisation, service state, swap and temperature discovery, inbound errors and link state per interface, the Unbound resolver, certificate validity and DHCP lease counters. Five defects of the original are fixed rather than carried over: * diagnostics/firewall/pfStates and diagnostics/system/systemResources in camelCase, where the privilege patterns are exact and ACL.php matches them case sensitively, so a monitoring key gets HTTP 403 * IPsec phase 2 asking with a GET although searchPhase2Action reads its connection from getPost('id'), so it could only return an empty set * the literal 9999 substituted for round trip time, loss and deviation on a gateway without a monitor address, which landed in history and made the gateway graphs unreadable * the CARP discovery turning unsupported on a firewall without CARP, where an empty list is the correct answer * fourteen raw master items keeping a day of raw payloads that nothing reads, about 20 MiB a day per host The export declares format 7.0, so it imports into Zabbix 7.0 and every later release. Every widget type it uses exists in 7.0.
1 parent 640b659 commit 027439d

2 files changed

Lines changed: 7864 additions & 0 deletions

File tree

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
# OPNsense by REST API
2+
3+
Zabbix template that monitors an OPNsense firewall over its own REST API. No SNMP, no Zabbix
4+
agent on the firewall, one API key with read privileges.
5+
6+
Import into **Zabbix 7.0 or later**. The export declares format 7.0, which every later
7+
release accepts.
8+
9+
Everything in here was measured against a live OPNsense 26.7 with a restricted monitoring
10+
key, not read off the documentation. Where the API turned out to behave differently than
11+
expected, that is written down in [Notes](#notes) rather than smoothed over.
12+
13+
## Contents
14+
15+
- [What it monitors](#what-it-monitors)
16+
- [Setup](#setup)
17+
- [Privileges](#privileges)
18+
- [Macros](#macros)
19+
- [Discovery](#discovery)
20+
- [Triggers](#triggers)
21+
- [Dashboard](#dashboard)
22+
- [Notes](#notes)
23+
- [License](#license)
24+
25+
## What it monitors
26+
27+
127 items, 15 discovery rules with 71 item prototypes, 58 triggers, 41 macros and a dashboard
28+
with 9 pages.
29+
30+
| Area | Source endpoint |
31+
|------|-----------------|
32+
| Processor utilisation, split into user, system and interrupt | `diagnostics/activity/get_activity` |
33+
| Core count, load average over 1, 5 and 15 minutes, load per core | `diagnostics/cpu_usage/get_c_p_u_type`, `diagnostics/system/system_time` |
34+
| Memory, swap per device, filesystems | `diagnostics/system/system_resources`, `system_swap`, `system_disk` |
35+
| Temperature per sensor | `diagnostics/system/system_temperature` |
36+
| Kernel network memory, mbuf clusters, denied requests | `diagnostics/interface/get_memory_statistics` |
37+
| netisr queue drops, total and per protocol | `diagnostics/interface/get_netisr_statistics` |
38+
| IP and TCP protocol error rates | `diagnostics/interface/get_protocol_statistics` |
39+
| Interface traffic, errors, link state, per interface blocks | `diagnostics/traffic/_interface`, `diagnostics/firewall/pf_statistics/interfaces` |
40+
| pf state table and source tracking, with their limits | `diagnostics/firewall/pf_states`, `pf_statistics/info`, `pf_statistics/memory` |
41+
| pf counters, SYN floods, malformed packets | `diagnostics/firewall/pf_statistics/info` |
42+
| Ruleset size, fingerprint, evaluation rate, unmatched rules | `diagnostics/firewall/pf_statistics/rules` |
43+
| pf table entries against the configured ceiling | `firewall/alias/get_table_size` |
44+
| Blocked share of the firewall log | `diagnostics/firewall/stats?group_by=action` |
45+
| Gateways, address, status, round trip time, loss, deviation | `routes/gateway/status` |
46+
| CARP status per address, demotion factor, maintenance mode | `diagnostics/interface/get_vip_status` |
47+
| IPsec phase 1 and phase 2, per connection | `ipsec/sessions/searchPhase1`, `search_phase2` |
48+
| WireGuard instances and peers | `wireguard/service/show` |
49+
| OpenVPN instances and clients | `openvpn/service/search_sessions` |
50+
| Services, discovered and checked | `core/service/search` |
51+
| Clock synchronisation, offset, stratum, reachable peers | `ntpd/service/status` |
52+
| Firmware state, pending updates, business license, version | `core/firmware/status`, `core/firmware/info` |
53+
| Configuration change timestamp, uptime | `diagnostics/system/system_time` |
54+
| UPS, battery, load, runtime, voltage | `nut/diagnostics/upsstatus` |
55+
| Resolver: queries, cache hits and misses, prefetches, rate limited queries, recursion time, request queue | `unbound/diagnostics/stats` |
56+
| Certificate validity, discovered per certificate | `trust/cert/search` |
57+
| The status panel the firewall shows about itself | `core/system/status` |
58+
| DHCP leases, active and total, v4 and v6 | `kea/leases4/search`, `kea/leases6/search` |
59+
60+
## Setup
61+
62+
### 1. Create the API key
63+
64+
In the OPNsense GUI under **System, Access, Users**, create a user without a login shell, for
65+
example `zabbix`, give it the privileges from the next section, then add an API key. The
66+
download contains the key and the secret.
67+
68+
### 2. Import the template
69+
70+
**Data collection, Templates, Import**, then select `template_opnsense_by_rest_api.yaml`.
71+
72+
### 3. Create the host
73+
74+
Create a host with an agent interface carrying the firewall address, link the template, and
75+
set the three connection macros:
76+
77+
| Macro | Value |
78+
|-------|-------|
79+
| `{$OPNS.KEY}` | the API key |
80+
| `{$OPNS.SECRET}` | the API secret |
81+
| `{$OPNS.PORT}` | the HTTPS port of the web interface, default 443 |
82+
83+
The items address the firewall through `{HOST.IP}`, so the interface address is what counts,
84+
not the DNS name.
85+
86+
## Privileges
87+
88+
Derived from the privilege patterns the firewall itself publishes under `auth/priv/search`,
89+
not from trying endpoints with a key and reading the HTTP code. Trying tells you what your
90+
key happens to allow; the patterns tell you what any key needs.
91+
92+
Seventeen endpoints are covered by exactly one privilege each. Three more, the gateway
93+
status, the WireGuard status and the pf table size, can be covered by either of two, and the
94+
table names the better choice with the reason. Twenty privileges in total, ten of which only
95+
read.
96+
97+
| Privilege in the GUI | Internal ID | Needed for | Read only |
98+
|----------------------|-------------|------------|-----------|
99+
| Lobby: Dashboard | `page-system-login-logout` | system resources, time, disk, swap, temperature, CPU type, pf states | yes, the pattern names single endpoints |
100+
| Diagnostics: Netstat | `page-diagnostics-netstat` | mbuf pool, netisr queues, protocol errors | yes |
101+
| Diagnostics: Firewall statistics | `page-diagnostics-pf-info` | pf counters, memory, ruleset, per interface blocks | yes |
102+
| Diagnostics: System Activity | `page-diagnostics-system-activity` | processor utilisation | yes |
103+
| Diagnostics: Logs: Firewall: Summary View | `page-diagnostics-logs-firewall-summary` | blocked share of the firewall log | yes |
104+
| Firewall: Aliases | `page-firewall-aliases` | pf table entry usage | yes, the pattern is limited to `export`, `search*`, `list*` and `get*`. Do **not** use Firewall: Alias: Edit instead, which covers `api/firewall/alias/*` and therefore writing |
105+
| Reporting: Traffic | `page-status-trafficgraph` | interface traffic and counters | yes |
106+
| Interfaces: Virtual IPs: Status | `page-status-carp` | CARP status per address | yes |
107+
| Status: NTP | `page-status-ntp` | clock synchronisation | yes, the pattern is the single status endpoint |
108+
| Status: IPsec | `page-status-ipsec` | IPsec phase 1 and phase 2 | yes, `api/ipsec/sessions/*` is a read only controller |
109+
| System: Status | `page-system-status` | the status panel the firewall shows about itself | almost, it also permits dismissing a message |
110+
| System: Gateways | `page-system-gateways` | gateway status | **no**, its pattern also covers `api/routing/settings/*`. The alternative, System: Static Routes, covers `api/routes/*` and is no better |
111+
| Status: OpenVPN | `page-status-openvpn` | OpenVPN instances and clients | **no**, `api/openvpn/service/*` includes service control |
112+
| VPN: WireGuard: Status | `page-wireguard-diagnostics` | WireGuard instances and peers | **no**, `api/wireguard/service/*` includes service control. Still the narrower of the two: VPN: WireGuard: Configuration also covers servers, clients and general settings |
113+
| Status: Services | `page-status-services` | service run state | **no**, `api/core/service/*` permits starting and stopping services |
114+
| System: Firmware | `page-system-firmware-manualupdate` | firmware state, pending updates, business license, version | **no**, `api/core/firmware/*` also covers reboot, poweroff, install and remove |
115+
| Services: Unbound | `page-services-unbound` | resolver statistics | **no**, `api/unbound/*` covers the whole resolver configuration |
116+
| System: Certificate Manager | `page-system-certmanager` | certificate validity | **no**, `api/trust/cert/*` covers adding and removing certificates |
117+
| Services: DHCP: Kea(v4) and Kea(v6) | `page-dhcp-kea-v4`, `page-dhcp-kea-v6` | DHCP lease counters | **no**, the patterns cover the Kea configuration and service control |
118+
119+
Leaving one out costs exactly the items it feeds, which then stay unsupported, and nothing
120+
else. That is the way to trade coverage against privilege:
121+
122+
- without **Status: Services** the service discovery stays empty
123+
- without **System: Firmware** the five firmware items, the license item and the version item
124+
stay unsupported. The version is also available from
125+
`diagnostics/system/system_information`, which Lobby: Dashboard covers
126+
- without **Services: Unbound**, **System: Certificate Manager** or the two **Kea** ones the
127+
resolver, certificate and lease items stay unsupported. These four areas are the ones to
128+
drop first if the account has to stay close to read only
129+
130+
`page-nut` is needed only if a UPS is monitored. It comes from the NUT plugin, covers
131+
`api/nut/*`, and without it the UPS master returns HTTP 403 even after enabling the item.
132+
133+
## Macros
134+
135+
### Connection
136+
137+
| Macro | Default | Purpose |
138+
|-------|---------|---------|
139+
| `{$OPNS.KEY}` | empty | API key |
140+
| `{$OPNS.SECRET}` | empty | API secret |
141+
| `{$OPNS.PORT}` | `443` | HTTPS port of the web interface |
142+
143+
### Thresholds
144+
145+
| Macro | Default | Purpose |
146+
|-------|---------|---------|
147+
| `{$OPNS.CPU.UTIL.WARN}` | `85` | Processor utilisation counting as high, averaged over ten minutes |
148+
| `{$OPNS.CPU.LOAD.MAX}` | `2` | Load average counting as high |
149+
| `{$OPNS.LOAD.AVG5.WARN}` | `4` | Absolute five minute load average counting as high |
150+
| `{$OPNS.LOAD.PERCORE.WARN}` | `1` | Five minute load per core, 1 means the cores are exactly saturated |
151+
| `{$OPNS.MEMORY.UTIL.MAX}` | `90` | Memory utilisation counting as high |
152+
| `{$OPNS.MBUF.UTIL.WARN}` | `80` | Share of the mbuf cluster limit counting as filling up |
153+
| `{$OPNS.SWAP.UTIL.WARN}` | `5` | Swap in use counting as a problem. A firewall should not swap at all, so this is deliberately low. Per device through macro context |
154+
| `{$OPNS.TEMP.CRIT}` | `80` | Sensor temperature in degrees Celsius. Per sensor through context, for example `{$OPNS.TEMP.CRIT:"dev.cpu.0.temperature"}` |
155+
| `{$OPNS.FS.PUSED.MAX.WARN}` | `90` | Filesystem utilisation, warning |
156+
| `{$OPNS.FS.PUSED.MAX.CRIT}` | `95` | Filesystem utilisation, critical |
157+
| `{$OPNS.STATE.TABLE.UTIL.MAX}` | `90` | pf state table fill level counting as high |
158+
| `{$OPNS.PF.SRCNODES.UTIL.CRIT}` | `90` | Source tracking table fill level counting as critical |
159+
| `{$OPNS.PF.TABLES.UTIL.WARN}` | `80` | Share of the pf table entry budget counting as filling up. Relevant with block lists or GeoIP aliases |
160+
| `{$OPNS.GW.MIN.PACKET.LOSS}` | `10` | Gateway packet loss counting as a problem |
161+
| `{$OPNS.GW.HIGH.PACKET.LOSS}` | `50` | Gateway packet loss counting as severe |
162+
| `{$OPNS.NTP.OFFSET.WARN}` | `100` | Clock offset in milliseconds. Generous for a LAN time source and still far below what breaks Kerberos or IPsec |
163+
| `{$OPNS.LICENSE.EXPIRY.WARN}` | `30` | Days before the business license expires |
164+
| `{$OPNS.NUT.BAT.LOW}` | `30` | UPS battery charge counting as low |
165+
| `{$OPNS.NUT.BAT.RUNTIME}` | `600` | Remaining UPS runtime in seconds |
166+
| `{$OPNS.NUT.HIGH.LOAD}` | `80` | UPS load counting as high |
167+
| `{$OPNS.DNS.RECURSION.WARN}` | `1` | Average recursion time in seconds counting as slow |
168+
| `{$OPNS.DNS.HITRATIO.MIN}` | `60` | Cache hit ratio in percent below which the resolver is reported as working harder than it should |
169+
| `{$OPNS.CERT.EXPIRE.DAYS}` | `21d` | Lead time before a certificate expires |
170+
171+
### Discovery filters and switches
172+
173+
| Macro | Default | Purpose |
174+
|-------|---------|---------|
175+
| `{$OPNS.IF.CONTROL}` | `1` | Set to 0, per interface through context, to silence the interface down trigger |
176+
| `{$OPNS.IF.NAME.NOT_MATCHES}` | `^(pflog\|pfsync\|enc\|lo)\d*$` | Interfaces excluded from discovery |
177+
| `{$OPNS.SERVICE.ID.NOT_MATCHES}` | `^$` | Services excluded from discovery, excludes nothing by default |
178+
| `{$OPNS.OPENVPN.ID.NOT_MATCHES}` | `^$` | OpenVPN instances excluded from discovery |
179+
| `{$OPNS.OPENVPN.CONTROL}` | `1` | Set to 0, per instance through context, to silence the OpenVPN down trigger |
180+
| `{$OPNS.WG.INSTANCE.MATCHES}` / `.NOT_MATCHES` | `.+` / `^$` | WireGuard instance discovery filter |
181+
| `{$OPNS.WG.PEER.MATCHES}` / `.NOT_MATCHES` | `.+` / `^$` | WireGuard peer discovery filter |
182+
| `{$OPNS.FS.FSNAME.MATCHES}` / `.NOT_MATCHES` | `.+` / `^(/dev\|/sys\|/run\|/proc\|.+/shm$)` | Filesystem discovery filter by mount point |
183+
| `{$OPNS.FS.FSTYPE.MATCHES}` / `.NOT_MATCHES` | filesystem list / `^\s$` | Filesystem discovery filter by type |
184+
| `{$OPNS.CERT.NAME.NOT_MATCHES}` | `^$` | Certificates excluded from discovery, by description |
185+
| `{$OPNS.CERT.IN_USE.MATCHES}` | `1` | Which certificates are discovered, matched against the in_use flag. Set to `.*` to watch every certificate in the store |
186+
187+
## Discovery
188+
189+
| Rule | Discovers |
190+
|------|-----------|
191+
| Interface Stats Discovery | traffic, errors and per interface pf blocks |
192+
| Network interfaces | link state and inbound errors |
193+
| Gateway Discovery | address, status, round trip time, loss, deviation |
194+
| Interface CARP Discovery | CARP status per virtual address |
195+
| Disk Discovery | filesystems |
196+
| Swap devices | swap per device |
197+
| Temperature sensors | one item per sensor |
198+
| netisr queues | queue drops per protocol |
199+
| Services | run state per service |
200+
| FW Action Discovery | firewall log actions |
201+
| IPsec Phase1 Discovery | tunnels, with phase 2 per connection |
202+
| WireGuard Instance Discovery | instances |
203+
| WireGuard Peer Discovery | peers, traffic and handshake age |
204+
| OpenVPN instances and clients | instances and connected clients |
205+
| Certificates | validity per certificate in the trust store |
206+
207+
## Triggers
208+
209+
58 in total: 2 Disaster, 13 High, 13 Average, 21 Warning, 9 Info.
210+
211+
Trigger names carry no product prefix, following the Zabbix guidelines and the community
212+
template: Zabbix puts the host name in front of the trigger anyway.
213+
214+
Disaster is reserved for a gateway that is down and for a low UPS battery. High covers the
215+
cases where the firewall is losing packets or a tunnel is gone: denied kernel network memory,
216+
pf dropping packets for lack of memory, the state limit being hit, a filling source tracking
217+
table, a hot sensor, a CARP status change, a dead WireGuard instance or peer, an unconnected
218+
IPsec tunnel, an unreachable API and the UPS running on battery.
219+
220+
Warning is the working range: utilisation and load, clock offset, swap in use, malformed
221+
packets, pf table entries filling up, protocol level errors. Info reports facts rather than
222+
faults: configuration changes, ruleset and rule count changes, a version change, available
223+
firmware updates, a reboot, and a gateway whose monitoring is switched off.
224+
225+
## Dashboard
226+
227+
Nine pages, 96 widgets. The grid is the full 72 columns wide.
228+
229+
| Page | Shows |
230+
|------|-------|
231+
| Overview | memory, state table, processor and load per core as gauges, six tiles, firewall actions over time, CARP status, filesystems as a honeycomb |
232+
| Packet filter | state table, source tracking and pf table utilisation, twelve tiles, state churn and searches, rule matches against evaluations, drops and limit hits, malformed packets |
233+
| Interfaces | link state per interface, traffic, blocked and passed bytes side by side, errors, queue drops and collisions |
234+
| Gateways | status honeycomb, round trip time and packet loss side by side, deviation below |
235+
| VPN | WireGuard peers and instances, peer traffic, IPsec phase 1 tunnels and phase 2 traffic |
236+
| System | processor, memory and load per core, uptime, version, cores, configuration change timestamp, both load averages, utilisation split, temperature and swap honeycombs |
237+
| Kernel and protocols | mbuf utilisation, clusters in use, denied requests, netisr drops per protocol, IP and TCP error rates |
238+
| Services, clock and power | every service as a honeycomb, clock synchronisation, offset, stratum, reachable peers, CARP demotion and maintenance, UPS battery, status, load and runtime |
239+
| Resolver, certificates and leases | DNS cache hit ratio, query rate, recursion time and queue, the system status message, query and recursion graphs, DHCP leases, and certificate expiry as a honeycomb |
240+
241+
## Notes
242+
243+
**snake_case in API paths is not always optional.** OPNsense routes `systemResources` and
244+
`system_resources` to the same controller action, so an administrator key never notices a
245+
difference. Some privilege patterns are exact, and `ACL.php` matches them with `preg_match`
246+
without the `i` modifier, so a monitoring key gets HTTP 403 on the camelCase spelling. That
247+
is the case for `diagnostics/system/system_resources` and `diagnostics/firewall/pf_states`.
248+
Other areas carry a wildcard pattern and take either spelling, which is why
249+
`ipsec/sessions/searchPhase1` works and is left as it is. Test privileges with the restricted
250+
key, never with an admin key.
251+
252+
**Gateway monitoring switched off.** A gateway without a monitor address reports a tilde for
253+
round trip time, loss and deviation, and a status of `none` that the API translates to
254+
`Online`. The three items discard the reading in that case and stay empty instead of
255+
substituting a number. While monitoring is off, an outage of that gateway raises nothing,
256+
because the down trigger works on packet loss. That is what the Info trigger is for.
257+
258+
**IPsec phase 2** needs one request per connection. `searchPhase2Action` reads its connection
259+
from `getPost('id')` and returns an empty set for a plain GET, so phase 2 is an HTTP agent
260+
prototype inside the phase 1 rule that posts `id={#IPSECNAME}`. Where the API leaves
261+
`phase1desc` empty, the tunnel name stands in, because the discovery uses that field as the
262+
entity identity and identical keys would collapse into one.
263+
264+
**Physical interface counters** come from `diagnostics/traffic/_interface`, not from
265+
`netstat`, whose counters reset on every read.
266+
267+
**JavaScript is kept to a minimum.** Parsing happens once on the master item, dependent items
268+
read the result with plain JSONPath. A step on a prototype would run per value and per
269+
discovered instance.
270+
271+
**DHCP leases carry no trigger.** A network that addresses everything statically runs at zero
272+
leases forever, and a trigger on that would be wrong on exactly the installations that are
273+
configured most carefully. The meaningful threshold is the size of the pool, which is a local
274+
decision, so the number is there to be graphed and alerted on where it matters.
275+
276+
**The system status item** reports the code the firewall shows in its own status panel. A
277+
firewall with nothing pending answers 2 and "No pending messages". The other codes were not
278+
observed here, so the trigger fires on anything other than 2 rather than claiming to know
279+
them, and the message is carried next to it as text.
280+
281+
**Filesystem usage is a percentage.** The item reads `used_pct` and is declared with `%`. In
282+
the template this grew out of it carried `B`, which made Zabbix format 34 percent as
283+
"34.28 B" in Latest data and on the dashboard.
284+
285+
**Raw master items store nothing.** Every master carries `history: 0`. A dependent item is
286+
fed from the value as it arrives, not from the stored history, so the chain works without it,
287+
and nothing else reads a raw payload. Measured on one small firewall with this template
288+
linked, keeping a day of them costs about 20 MiB per host per day. What is given up is
289+
looking at yesterday's raw response in Latest data.
290+
291+
**A firewall without CARP discovers no CARP addresses** rather than reporting an error.
292+
`get_vip_status` answers with an empty `rows` list and the message "Could not locate any
293+
defined CARP interfaces.", which is the normal answer for a single firewall, so the discovery
294+
rule takes the empty list.
295+
296+
**The pf ruleset item** polls every ten minutes on purpose. The response carries the full text
297+
of every rule, and rule changes are not a per minute concern.
298+
299+
## License
300+
301+
MIT, like this repository. The template is derived from `OPNsense by HTTP-JSON` in this same
302+
repository, with the greater part of the current content added on top. It carries its own name
303+
and its own uuids, so both can be linked to the same host and an import of one does not touch
304+
the other.
305+
306+
Maintained at https://github.com/Garfieldttt/opnsense-zabbix-template.

0 commit comments

Comments
 (0)