Firewall Dashboard is a single-binary web application for inspecting firewall traffic, rule counters, and authentication state. It embeds a Svelte frontend in a Go server and exposes a small HTTP API for operational diagnostics.
PF on FreeBSD and OpenBSD is the primary deployment target. Linux nftables is supported as an experimental backend and has less feature coverage.
- Embedded web UI and JSON API in one Go binary
- PF traffic, state, and rule-counter visibility on FreeBSD and OpenBSD
- nftables rule and connection inspection on Linux
- Optional OpenID Connect authentication
- Native FreeBSD and cross-platform CI builds
- Linux systemd, FreeBSD rc.d, and FreeBSD Ports packaging assets
- Go 1.25+
- Node.js 20.19+ or 22.12+
npmjustcommand runner
Runtime dependencies depend on the selected backend:
| Platform | Backend | Required commands |
|---|---|---|
| FreeBSD / OpenBSD | pf |
pfctl, tcpdump |
| Linux | nftables |
nft, conntrack |
| Any | mock |
None |
Build the UI and server from the repository root:
just build
./pf-dashboardOpen http://localhost:8080.
The server binds to 127.0.0.1:8080 by default. To expose it through a reverse
proxy or on another interface, set server.addr explicitly.
just build runs the frontend build and embeds ui/dist into the binary. For
frontend-only development, start Vite separately:
cd ui
npm ci
npm run devThe Vite development server proxies /api requests to the Go server.
just build # build the UI and application binary
just test # build the UI and run Go tests
just run # build and start the application
just build-linux # cross-compile a Linux amd64 binary
just build-freebsd # cross-compile a FreeBSD amd64 binary
just screenshots # regenerate README/marketing screenshots with fake data
just release-tarball # create a source tarball with embedded UI assets
just docker-test # run tests in the Docker test stage
just docker-build # build the release Docker imageThe screenshot suite renders deterministic fake firewall data in headless Chrome. It also checks each rendered page for fixture-specific content before writing the image, making it useful as a lightweight browser integration test.
| WAN bandwidth | PF/ALTQ QoS |
|---|---|
![]() |
![]() |
Run just screenshots to regenerate every image. Set CHROME_BIN if Chrome or
Chromium is not available under a common executable name.
Configuration precedence is defaults, TOML configuration file, environment
variables, then command-line flags. Pass a TOML file explicitly with
--config /usr/local/etc/pf-dashboard.toml. Environment variables use the
PFCTL_DASHBOARD_ prefix; nested keys use underscores, for example
PFCTL_DASHBOARD_SERVER_ADDR maps to server.addr.
Defaults are OS-aware:
| Operating system | Default backend |
|---|---|
| FreeBSD / OpenBSD | pf |
| Other platforms | mock |
Supported backends:
| Backend | Purpose |
|---|---|
pf |
FreeBSD and OpenBSD PF integration |
nftables |
Linux nftables integration |
mock |
In-memory data for local development and testing |
The application exits during startup when a selected backend is unavailable on the current platform or its required commands are absent.
# Bind to all interfaces on port 8081.
./pf-dashboard --server.addr=0.0.0.0:8081
# Load settings from a TOML file.
./pf-dashboard --config /usr/local/etc/pf-dashboard.toml
# Use the Linux nftables backend.
PFCTL_DASHBOARD_FIREWALL_BACKEND=nftables ./pf-dashboard
# Enable detailed firewall command logging.
./pf-dashboard --firewall.debug
# Change the client traffic refresh interval.
./pf-dashboard --server.refresh.traffic_interval_ms=1000
# Enable HTTP request logging.
./pf-dashboard --server.http_log
# Trust forwarded client IP headers from a reverse proxy.
./pf-dashboard --server.trusted_proxies=127.0.0.1/32
# Tune backend resource limits.
./pf-dashboard --firewall.cache_ttl_ms=1000 \
--firewall.command_timeout_ms=5000 \
--firewall.max_concurrent_commands=2 \
--firewall.max_streams=4
# Read blocked packets live from a custom PF log interface.
./pf-dashboard --firewall.pf.blocked_source=live \
--firewall.pf.pflog_interface=pflog0
# Read blocked packets from a pflogd capture file instead.
./pf-dashboard --firewall.pf.blocked_source=file \
--firewall.pf.pflog_path=/var/pf/pflog
# Select one interface for the optional vnStat bandwidth tab.
./pf-dashboard --vnstat.interface=em0Example TOML configuration:
[server]
addr = "127.0.0.1:8080"
[firewall]
backend = "pf"
[firewall.pf]
blocked_source = "auto"
pflog_interface = "pflog0"
pflog_path = "/var/pf/pflog"
[vnstat]
interface = "em0"
[qos]
# Read-only PF/ALTQ queue monitoring. Enabled by default when supported.
enabled = trueThe FreeBSD example is also available at
packaging/freebsd/pf-dashboard.toml.sample.
Authentication defaults to none, which assumes an upstream reverse proxy
controls access. Do not expose an unauthenticated instance directly to an
untrusted network.
Set auth.mode to oidc to use the OpenID Connect authorization-code flow.
OIDC configuration is best supplied through environment variables:
PFCTL_DASHBOARD_AUTH_MODE=oidc \
PFCTL_DASHBOARD_AUTH_OIDC_PROVIDER_URL=https://id.example.com/realms/main \
PFCTL_DASHBOARD_AUTH_OIDC_CLIENT_ID=pf-dashboard \
PFCTL_DASHBOARD_AUTH_OIDC_CLIENT_SECRET=replace-me \
PFCTL_DASHBOARD_AUTH_OIDC_REDIRECT_URL=https://dashboard.example.com/auth/callback \
./pf-dashboardThe required OIDC settings are the provider URL, client ID, client secret, and redirect URL. The login flow uses PKCE and stores only an opaque session ID in the browser. Sessions are held in memory and are cleared when the process restarts.
Optional authorization constraints can require an exact subject ID, membership in at least one configured group, and/or a verified email from an allowed domain. When more than one constraint type is configured, all configured types must match:
PFCTL_DASHBOARD_AUTH_OIDC_ALLOWED_SUBJECTS=user-id-1,user-id-2
PFCTL_DASHBOARD_AUTH_OIDC_ALLOWED_GROUPS=firewall-admins,network-operators
PFCTL_DASHBOARD_AUTH_OIDC_ALLOWED_EMAIL_DOMAINS=example.comOptional settings also include scopes and session-cookie controls. Post-login redirects are restricted to paths on the dashboard origin.
When OIDC is enabled, the application provides /auth/login, /auth/callback,
and /auth/logout. Authentication status is available at /api/auth/me.
The process needs permission to query the firewall and read traffic data. Grant only the capabilities or device access required by the selected backend.
sudo apt install libcap2-bin
sudo setcap 'cap_net_admin,cap_net_raw+ep' /path/to/pf-dashboardFor systemd deployments, configure AmbientCapabilities=CAP_NET_ADMIN CAP_NET_RAW and CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW. Remove
capabilities with sudo setcap -r /path/to/pf-dashboard.
The dashboard does not need to run as root. PF itself should still be enabled and configured by an administrator, while the dashboard runs as a dedicated unprivileged account.
Create the account and add it to the PF access group:
sudo pw groupadd pf-dashboard
sudo pw useradd pf-dashboard -g pf-dashboard -d /var/empty \
-s /usr/sbin/nologin
sudo pw groupmod pf -m pf-dashboardThe pf group must exist on the system. If it does not, create an equivalent
dedicated group first and use that group consistently in the device and log
permissions below.
Grant that group access to PF and BPF devices in /etc/devfs.conf:
perm pf 0660
perm bpf* 0660
own pf root:pf
own bpf* root:pf
Apply the device permissions and verify them:
sudo service devfs restart
ls -l /dev/pf /dev/bpf*The account also needs to read the pflog file. Start pflogd, then make sure
the file is group-readable; repeat this after log rotation if your rotation
configuration resets ownership:
sudo sysrc pf_enable=YES
sudo sysrc pflog_enable=YES
sudo service pflog start
sudo chgrp pf /var/log/pflog
sudo chmod 0640 /var/log/pflog
sudo -u pf-dashboard pfctl -s state
sudo -u pf-dashboard tcpdump -n -e -tttt -r /var/log/pflog -c 1Use pf_dashboard_user and pf_dashboard_group in the supplied rc.d script:
sudo install -m 0555 packaging/freebsd/rc.d/pf_dashboard \
/usr/local/etc/rc.d/pf_dashboard
sudo sysrc pf_dashboard_enable=YES
sudo sysrc pf_dashboard_user=pf-dashboard
sudo sysrc pf_dashboard_group=pf-dashboard
sudo sysrc pf_dashboard_app_flags='--server.addr=127.0.0.1:8080 --firewall.backend=pf'
sudo service pf_dashboard startFor access from another host, bind to the required interface instead of
127.0.0.1, and put authentication or a reverse proxy in front of the
dashboard.
Blocked traffic is available only for PF rules that include the log option,
for example block log all. A plain block all rule will block packets but
will not produce entries for the dashboard to display.
Blocked traffic rows can generate a narrowly scoped pass ... quick suggestion
for copying into a PF configuration. Review the generated interface, direction,
addresses, destination port, and rule ordering before applying it; the dashboard
never changes PF configuration itself.
PF log collection defaults to firewall.pf.blocked_source=auto: it reads live
blocks from pflog0 and merges them with /var/log/pflog when the capture
file exists. Set the source to live to use only the configured
firewall.pf.pflog_interface, or file to use only the configured
firewall.pf.pflog_path. This supports layouts such as pflog0 with
/var/pf/pflog; pflogd is needed only when using the file source.
At startup, the service logs the selected blocked source, interface, and
capture path. Set firewall.debug=true to also log every pfctl and tcpdump
subcommand, including the live blocked collector and browser traffic streams.
When vnstat is installed, the dashboard adds a live-refreshing Bandwidth tab
with interface totals, the latest five-minute RX/TX rate, and a graph of up to
four hours of five-minute samples. The graph is shown only when vnStat has
history for the selected interface. Interfaces can be viewed individually,
bookmarked, or browsed using the WAN and LAN/VLAN filters. The tab is absent
when the executable is not available. Start vnstatd so its database is
populated, then optionally limit collection to one interface with vnstat.interface (or
PFCTL_DASHBOARD_VNSTAT_INTERFACE). Set vnstat.enabled=false to disable the
integration. The refresh cadence follows server.refresh.traffic_interval_ms;
the displayed rates remain five-minute averages provided by vnStat.
On FreeBSD PF systems with ALTQ queue reporting, the dashboard adds a
live-refreshing QoS tab. It runs pfctl -s queue -v read-only and displays each
queue's configured bandwidth, scheduler, packet and byte counters, drops, and
current queue depth. The tab is hidden when the platform or kernel does not
support the command. Set qos.enabled=false to disable probing and collection,
or set qos.binary when pfctl is outside the service's PATH.
GoReleaser builds archives and Debian/RPM packages using the project’s systemd unit and sample environment file:
goreleaser release --snapshot --cleanPackages install the binary at /usr/bin/pf-dashboard, the service unit, and a
sample environment file at /etc/default/pf-dashboard. After installation:
sudo systemctl daemon-reload
sudo systemctl enable --now pf-dashboardSet deployment-specific configuration in /etc/default/pf-dashboard or
/etc/sysconfig/pf-dashboard, then restart the service.
Install the supplied rc.d script and enable the service:
pf-dashboard config rc.d | sudo install -m 0555 /dev/stdin /usr/local/etc/rc.d/pf_dashboard
sudo sysrc pf_dashboard_enable=YES
sudo service pf_dashboard startpf-dashboard config rc.d writes the bundled service script to stdout without
starting the dashboard or requiring firewall access. The repository copy at
packaging/freebsd/rc.d/pf_dashboard remains available for packaging.
Use packaging/freebsd/rc.conf.sample as a
starting point for service settings. The rc.d script supports command, flags,
environment, user, and group overrides through pf_dashboard_* variables.
A local port skeleton and poudriere workflow are documented in
docs/freebsd-porting.md.
The HTTP API is documented in openapi.yaml. Key endpoints
include traffic (/api/blocked, /api/passed, /api/traffic), rule counters
(/api/rules), refresh configuration (/api/config/refresh), and the PF live
traffic stream (/api/stream/traffic).
Generate a shell completion script from the built binary:
./pf-dashboard completion bash > /etc/bash_completion.d/pf-dashboard
./pf-dashboard completion zsh > "${fpath[1]}/_pf-dashboard"
./pf-dashboard completion fish > ~/.config/fish/completions/pf-dashboard.fish

