Skip to content

Repository files navigation

Firewall Dashboard

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.

Highlights

  • 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

Requirements

  • Go 1.25+
  • Node.js 20.19+ or 22.12+
  • npm
  • just command runner

Runtime dependencies depend on the selected backend:

Platform Backend Required commands
FreeBSD / OpenBSD pf pfctl, tcpdump
Linux nftables nft, conntrack
Any mock None

Quick Start

Build the UI and server from the repository root:

just build
./pf-dashboard

Open 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 dev

The Vite development server proxies /api requests to the Go server.

Common Commands

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 image

Screenshots

The 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.

PF traffic overview

WAN bandwidth PF/ALTQ QoS
WAN bandwidth PF ALTQ queues

Run just screenshots to regenerate every image. Set CHROME_BIN if Chrome or Chromium is not available under a common executable name.

Configuration

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=em0

Example 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 = true

The FreeBSD example is also available at packaging/freebsd/pf-dashboard.toml.sample.

Authentication

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-dashboard

The 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.com

Optional 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.

Permissions and Platform Setup

The process needs permission to query the firewall and read traffic data. Grant only the capabilities or device access required by the selected backend.

Linux nftables

sudo apt install libcap2-bin
sudo setcap 'cap_net_admin,cap_net_raw+ep' /path/to/pf-dashboard

For 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.

FreeBSD PF

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-dashboard

The 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 1

Use 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 start

For 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.

vnStat Bandwidth

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.

QoS Monitoring

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.

Deployment and Packaging

Linux packages

GoReleaser builds archives and Debian/RPM packages using the project’s systemd unit and sample environment file:

goreleaser release --snapshot --clean

Packages 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-dashboard

Set deployment-specific configuration in /etc/default/pf-dashboard or /etc/sysconfig/pf-dashboard, then restart the service.

FreeBSD rc.d

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 start

pf-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.

FreeBSD Ports

A local port skeleton and poudriere workflow are documented in docs/freebsd-porting.md.

API and Shell Completion

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

About

Golang service that can run on your freebsd PF firewall to monitor traffic (pass, blocked), and rule sets

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages