Web interface
Pebble's web interface is optional and disabled by default. Routing, filtering, DHCP, DNS and WireGuard never depend on it: if it stops, or is never enabled, the router keeps working exactly as before.
Status. In use on a FreeBSD 15.1 home router, and tested against a simulated router (
--dev-fixtures) in development.
How it is built#
browser ──HTTPS──▶ pebble web serve (user "pebble", no privileges)
│ Unix socket /var/run/pebble/agent.sock (0660 root:pebble)
▼
pebble agent (root; fixed operations only)
│
▼
the same compiler, transaction engine and collectors as the CLI
pebble web serverenders pages on the server (Gohtml/template) and holds browser sessions. It runs as the unprivilegedpebbleuser and can do nothing except ask the agent for operations the agent defines. It never sees password hashes and cannot say who is calling: see below.pebble agentis the privileged boundary. It accepts connections only from root and thepebbleuser (checked with the kernel's peer credentials,LOCAL_PEERCRED), exposes a fixed list of operations with structured request and response types, validates every input, and has no generic command execution. Changes go through the same validation, planning and transactional apply aspebble apply; the CLI and the web interface cannot diverge.- The agent decides who is calling. At sign-in the web process passes the user name and password to the agent, which checks them against the root-only accounts file and returns a session token signed with a key that exists only in its memory (HMAC-SHA-256). The token carries the user, the role, a tag of the password in force, the browser's address (for the lock-out check) and a 12-hour expiry. Every later request carries the token, and the agent derives the user and role from it alone, re-checking the account each time: removing an account or changing its role or password ends its sessions at once, and signing out revokes the token. A compromised web process can therefore act only through sessions that are open, never as an administrator it invents. Restarting the agent signs everyone out.
- Nothing is loaded from the Internet: CSS, JavaScript and the icon are embedded in the binary. Every page works without JavaScript; the script only adds live refresh (with a pause button), copy buttons and confirmation prompts.
Enabling it#
Make sure the
pebbleuser exists (pebble bootstrapcreates it).Add at least one account. There is no default password.
pebble web user add alice --role admin pebble web user add bob --role viewer # read-onlyEnable the interface in
pebble.yamland apply:management: web: enabled: true # The default listens on 127.0.0.1:8443 only: reach it through an # SSH tunnel (ssh -L 8443:127.0.0.1:8443 router). listen: 192.168.10.1:8443 # or the LAN / a WireGuard address allow_from: [192.168.10.0/24] # who may connect (PF rule) allowed_hosts: [router.home.arpa] session_timeout: 30mpebble applyvalidates the address (WAN addresses are refused), generates the PF rule that admitsallow_from, writesactive/web.json, and startspebble_agentandpebble_web. It also refuses a change that would lock out the browser you are using.Open
https://192.168.10.1:8443/. On first start a self-signed certificate is created;pebble web certprints its SHA-256 fingerprint, which you should compare with the one your browser shows before accepting it. Usecert_file/key_filefor your own certificate, orpebble web cert --forceto regenerate (for example after changingallowed_hosts).
Remote administration: prefer a WireGuard tunnel and listen on the tunnel address. The web interface is never offered on the WAN.
Roles#
| Role | Can |
|---|---|
viewer |
see every page, test DNS names and firewall flows, preview plans |
admin |
also apply, confirm and roll back changes, update lists, run ping, traceroute and the guided WireGuard test |
The agent enforces roles independently of the web process. Removing an account, or changing its role or password, ends its sessions at their next request.
Making changes#
Every change, whether from a form (a firewall rule, a DNS feed, a DHCP reservation made from a lease, a WireGuard peer) or from the YAML editor, follows one path:
- The edit is made to a copy of
pebble.yaml, preserving comments. - The agent validates, compiles and plans it. You see the configuration diff, every generated file that changes, lint warnings (public exposure, duplicate or shadowed rules, unexpected inter-site access) and whether the change can interrupt connectivity.
- Applying is bound to exactly the text you reviewed: an edit made after the review requires a new review, and a change made meanwhile by someone else (or the CLI) is refused rather than overwritten.
- A change that can interrupt connectivity is applied with rollback
protection armed by the router (cron watchdog,
daemon(8)timer and boot recovery). Every page then shows the deadline with Confirm and keep and Roll back now. Closing the browser or stopping the web service does not prevent the rollback; there is no browser-side timer.
The Devices page shows which way each device reaches the Internet and lets
an administrator change it for a reserved device, through the same review:
the WAN, a VPN provider exit (role: egress, see
wireguard.md), or Blocked — no
Internet, while the LAN, the router and WireGuard sites still work. A
dynamic device is changed by making it static with Internet via set.
See networking.md.
The Devices page also shows each device's current upload and download
rate, refreshed every five seconds; following it opens a live view of that
device's open connections (remote address, state, age, bytes and current
rate; administrators only, viewers see the totals), refreshed every two seconds.
With dns.query_log on, the same page lists the device's recent DNS
lookups and whether each was answered or blocked (administrators only;
viewers see counts). Both come from PF's state table
(pfctl -vss, sampled at most every two seconds however many people are
watching; a request never waits for a sample in progress, and a table of
more than 50,000 states is not listed), counting each connection where it enters the router, so traffic
through a VPN exit is included. Traffic between two LAN devices never
reaches the router and is not shown.
Private keys are never shown or entered. Peers are added with their public
keys; pebble vpn genkey and pebble vpn show on the router handle key
material.
Security controls#
| Control | Implementation |
|---|---|
| Passwords | argon2id (19 MiB, 2 passes), PHC strings in /usr/local/etc/pebble/web-users (0600 root), verified only by the agent; at most two verifications at once |
| Identity | the agent verifies passwords and issues signed session tokens; the web process forwards them and cannot assert a user, role or client address |
| Sessions | 256-bit random IDs held in memory only; HttpOnly, SameSite=Strict, Secure and the __Host- prefix under TLS (plain HTTP, loopback only: a port-specific name, pebble-<port>, though browsers still share cookies across localhost ports, so prefer TLS); idle (session_timeout) and 12-hour absolute expiry; never in URLs |
| Sign-in throttling | 5 attempts per client per 5 minutes (counted before the password check); while 30 failures a minute arrive from anywhere, only clients without failures may try; unknown user names cost the same time as wrong passwords |
| CSRF | per-session token on every form, plus Origin and Sec-Fetch-Site checks |
| XSS | contextual escaping by html/template; CSP default-src 'none'; script-src 'self'; style-src 'self', no inline script or style |
| DNS rebinding | requests are refused unless Host is the listen address, the host name or an allowed_hosts entry |
| Framing, sniffing, referrers | frame-ancestors 'none', X-Frame-Options: DENY, nosniff, Referrer-Policy: no-referrer |
| Transport | TLS by default with HSTS; plain HTTP is refused unless the listen address is loopback |
| Resource limits | 2 MiB request bodies, 16 concurrent requests, header and read timeouts; the agent allows 8 concurrent requests plus 2 reserved for changes, confirm, rollback and sign-in (so busy dashboards cannot block them), one change at a time, and rate-limits diagnostics |
| Open redirects | the post-sign-in destination must be a local path |
| Exports | the event export masks public addresses, MAC address device parts and WireGuard keys |
Development preview#
To work on the interface away from a router:
pebble web serve --dev-fixtures examples/site-to-site.yaml --listen 127.0.0.1:8099
This runs the agent in-process against a simulated FreeBSD router and
prints a one-time password. Every page carries a "Development fixture
data" banner; none of the figures describe a real network.
pebble agent --dev-fixtures FILE and pebble web serve --settings FILE --socket PATH run the two halves separately.