Monitor and configure OPNsense firewalls (HA-aware) from the PegaProx dashboard.
Wires an OPNsense firewall into the PegaProx admin panel. Read-only monitoring out of the box, write operations with audit evidence and explicit application/rollback outcomes, and a Prometheus /metrics endpoint for your existing monitoring stack.
- HA sync state (
pfsyncinterface, peer IP, version compatibility) - Per-interface throughput (RX/TX bytes, errors, drops, link state)
- pf state table utilization (current vs. limit)
- System: CPU, memory, load average, uptime
- Gateways: RTT, loss, up/down per gateway
- VPN: WireGuard peers, IPsec phase 1, OpenVPN sessions
- Services running/stopped
- Routing table, ARP, NDP neighbors
- Recent firewall log (limit-bounded)
- Cert inventory + expiry warnings (≤30 days)
| Writer | Endpoint | Surface |
|---|---|---|
AliasWriter |
/api/firewall/alias/* |
host/network/port/url/urltable/geoip/external |
RuleWriter |
/api/firewall/filter/* |
pass/block/reject per interface |
VerifiedFirewallWriter (DNAT) |
/api/firewall/d_nat/* |
destination NAT with canonical readback and revision checks |
DhcpSubnetWriter |
/api/kea/dhcpv4/*Subnet |
Kea DHCPv4 subnets |
NatWriter |
/api/firewall/source_nat/* |
outbound NAT |
OneToOneNatWriter |
/api/firewall/one_to_one/* |
BINAT / 1:1 NAT |
UnboundWriter |
/api/unbound/settings/{add,del,search}HostOverride |
DNS host overrides |
UnboundDomainWriter |
/api/unbound/settings/{add,del,search}Forward (type=forward) |
DNS domain overrides (forwarding) |
UnboundDotWriter |
same endpoint with type=dot | DNS-over-TLS upstreams |
WireguardPeerWriter |
/api/wireguard/client/* |
WireGuard peers (clients) |
DhcpReservationWriter |
/api/kea/dhcpv4/{add,del,search}Reservation |
Kea DHCPv4 static mappings |
Legacy writers use the lifecycle below. The new DNAT/rules/aliases routes additionally verify JSON acknowledgement, readback, revision conflicts and peer convergence; see management API.
validate → POST → apply/reconfigure → (optional) HA syncTo → audit
│
└─ on fail: rollback the orphan + record error
Audit log (state/audit.jsonl) — append-only JSONL with payload_sha256 per row: SHA-256 of the canonical-JSON sent to OPNsense. Correlates a known payload with the recorded operation without storing that payload; this is not a cryptographically authenticated log chain. An auditor replaying a known input can verify the historical write referenced that exact payload.
Destination NAT uses the native /api/firewall/d_nat/* API, qualified on OPNsense 26.1.2. Older or unsupported endpoints return an explicit error; the plugin does not rewrite XML configuration.
GET /api/plugins/opnsense/api/overview— single JSON snapshot for the Overview tabGET /api/plugins/opnsense/api/vpn— WireGuard, IPsec and OpenVPN status without a full overview requestGET /api/plugins/opnsense/api/network— interfaces + gateways + routes + ARP + NDPGET /api/plugins/opnsense/api/logs?limit=N— paginated firewall log tail (default 100, capped at 500)GET /api/plugins/opnsense/api/metrics— Prometheus text exposition (noprometheus_clientdependency)GET /api/plugins/opnsense/api/health— plugin liveness + config presence
Hash-routed (#overview, #network, #vpn, #logs, #nat, #firewall, #dns, #dhcp, #wg), ARIA tablist wrapped in <nav aria-label>, zero front-end dependencies:
| Tab | Content |
|---|---|
| Overview | system, HA sync, certs, interfaces (compact), gateways, services, VPN summary |
| Network | live traffic chart (stacked area, top-4 by throughput), interfaces with per-iface SVG sparklines + live RX/TX rates, gateways, routing table, ARP, NDP. Rates computed client-side from successive byte counters; 60-sample window. Per-iface drilldown modal (<dialog>) with RX/TX chart + neighbors + lazy-loaded firewall events filtered by iface. |
| VPN | full WireGuard / IPsec / OpenVPN tables (peer, pubkey/CN, endpoint, RX/TX, latest handshake) |
| Logs | firewall log tail with live filter (search src/dst/iface/rule + action chip pass/block/rdr/nat). Auto-poll 10 s |
| NAT | Destination NAT create/edit/delete, plus existing outbound NAT and 1:1 BINAT |
| Firewall | Filter rules and aliases create/edit/delete; explicit read-only state |
| DNS | three sub-sections — host overrides + domain overrides + DoT entries |
| DHCP | Kea DHCPv4 reservations (subnet UUID + IP + MAC + hostname) and subnets |
| WG peers | WireGuard peer list/create/delete (name, pubkey, tunnel address, keepalive, optional PSK) |
Theme-aware: PegaProx passes ?theme=corp-light|corp-dark|cloud and the plugin honours all three (the cloud value repaints it with the Modern view tokens — deep-blue surfaces, cyan accent). Tokens lifted from docker_swarm/swarm.html so the iframe blends with the host dashboard.
curl -sSL https://git.idkmanager.com/idkmanager/pegaprox-plugin-opnsense/raw/branch/main/install.sh | sudo bashRequirements:
- PegaProx 0.9.9.3+ at
/opt/PegaProx(native plugin frontend hook) - Python 3.11+
- HTTPS reachability from the PegaProx host to the OPNsense API
- An API key + secret on each OPNsense node (System → Access → Users → user → API keys)
Mirrors:
- Gitea (source of truth): https://git.idkmanager.com/idkmanager/pegaprox-plugin-opnsense
- GitHub: https://github.com/idkmanager/pegaprox-plugin-opnsense
- GitHub (personal mirror): https://github.com/alfonsokuen/pegaprox-plugin-opnsense
Edit /opt/PegaProx/plugins/opnsense/config.json:
{
"opnsense_hosts": [
{
"name": "lab",
"url": "https://10.0.0.1",
"api_key": "REPLACE_WITH_OPNSENSE_API_KEY",
"api_secret": "REPLACE_WITH_OPNSENSE_API_SECRET",
"verify_tls": true,
"ca_bundle_path": ""
}
],
"poll_interval": 30,
"read_only": false
}For HA pairs configure cluster_mode: "auto". Reads use the primary CARP VIP selection. New management writes require both peers reachable with complementary roles on every VIP, then verify the changed object on the peer. Mixed or unknown ownership blocks writes.
read_only: true disables every write endpoint (HTTP 403 from the route layer before the writer even runs). Useful as a guard rail in shared production environments.
Never commit credentials. Use SOPS or env-injected configs in production. See docs/INSTALL.md for the least-privilege OPNsense user recipe.
Every successful write records a single JSONL line in state/audit.jsonl:
{
"ts": "2026-05-10T22:30:06Z",
"user": "plugin",
"action": "dhcp_reservation.create",
"target": "91365bf5-bfc9-4ca5-8246-9b091c47d6d0",
"host": "lab",
"result": "ok",
"duration_ms": 164,
"detail": "v1.10.0 live smoke",
"payload_sha256": "0cf3b02debdb6d8abd3e6550267a3c33f8422cef5b96d3b7ee2880c505affeff"
}payload_sha256 is the hex SHA-256 of the canonical-JSON body sent to OPNsense (sorted keys, no whitespace). Deterministic across runs and independent of key order in the original dict. Delete operations leave it empty since they carry no payload.
sudo bash /opt/PegaProx/plugins/opnsense/uninstall.shThe uninstaller backs up the plugin directory to /tmp/pegaprox-opnsense-backup-<ts>.tar.gz before removal.
pip install -r requirements-dev.txt
pytest # default unit/regression suite
ruff check src tests # lintRead-only collectors + metrics:
export OPNSENSE_LAB_URL=https://10.0.0.1
export OPNSENSE_LAB_KEY=...
export OPNSENSE_LAB_SECRET=...
pytest -k liveWrite-path (mutates state, creates + cleans up):
OPNSENSE_ALLOW_WRITE=1 pytest tests/test_writers_live.pyGated by RUN_E2E=1. Walks the 8 tabs, asserts the ARIA state, and filters console errors:
pip install playwright && playwright install chromium
RUN_E2E=1 \
PEGAPROX_URL=https://pegasus.example.com \
PEGAPROX_USER=alfonso \
PEGAPROX_PASS=... \
pytest tests/test_e2e_smoke.py::test_e2e_login_and_visit_all_tabsAdds a write-path round-trip (creates + deletes a host override) when RUN_E2E_WRITE=1 is also set. Lab only — never against prod.
.
├── manifest.json # PegaProx plugin manifest (version, has_frontend, frontend_route)
├── __init__.py # entry point: register() / unregister(), 12 routes
├── opnsense.html # plugin UI: 9 tabs, verified management, theme-aware
├── install.sh / uninstall.sh
├── config.example.json
├── src/
│ ├── client/ # OPNsenseClient (HTTPS + retries + typed errors)
│ ├── collectors/ # read-only snapshot fns
│ ├── writers/ # 9 writers + AuditLog (with payload_sha256) + HAVerifier
│ ├── routes/ # build_*_payload functions per endpoint
│ └── metrics/ # Prometheus text-format exporter
├── tests/
│ ├── test_*_unit.py # unit/regression tests
│ └── test_e2e_smoke.py # Playwright browser e2e (opt-in)
├── fixtures/live/ # captured OPNsense API responses (sanitized)
└── docs/ # INSTALL / API / TROUBLESHOOTING
- Unit/regression tests run by default; live tests remain opt-in.
RUN_LOCAL_E2E=1enables browser ? Flask ? HTTPS simulator tests. ruff check— clean- axe-core: 0 violations across WCAG 2.0 A + AA on every tab (Overview / Network / VPN / Logs / NAT / DNS / DHCP / WG peers), live-verified at
pegasus.idkmanager.com - Live round-trips verified against OPNsense 26.1.2 lab: aliases, rules, source NAT, 1:1 NAT, Unbound host + domain + DoT, Kea reservation, WireGuard peer. Each round-trip records the SHA-256 in the JSONL.
- UI tokens lifted from PegaProx's
docker_swarm/swarm.htmlso the iframe blends with the host dashboard. Single deviation:--mutedbumped from#71717ato#a1a1aato clear AA contrast on--card.
- Unbound endpoint rename: 26.x collapsed
addDomainOverrideintoaddForwardwith atypediscriminator (forward|dot). The plugin uses the new endpoint and filters list results bytype. type=dotcoercion bug (upstream): 26.1.2 silently stores DoT entries withtype=forward. Plugin code is correct; the DoT filter will start surfacing rows automatically when upstream fixes it. Tracked in CHANGELOG.- Kea subnet management is available through the DHCP tab; advanced DHCP options remain GUI-managed.
one_to_oneapply path uses/applynot/reconfigure(different fromsource_nat).- Bare-root domain
.is rejected by Unbound DoT validation; use a real FQDN.
See CHANGELOG.md for the full version-by-version history.
MIT — see LICENSE.