pebble.
Pebble / Docs / Operating

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

Enabling it#

  1. Make sure the pebble user exists (pebble bootstrap creates it).

  2. 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-only
  3. Enable the interface in pebble.yaml and 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: 30m

    pebble apply validates the address (WAN addresses are refused), generates the PF rule that admits allow_from, writes active/web.json, and starts pebble_agent and pebble_web. It also refuses a change that would lock out the browser you are using.

  4. Open https://192.168.10.1:8443/. On first start a self-signed certificate is created; pebble web cert prints its SHA-256 fingerprint, which you should compare with the one your browser shows before accepting it. Use cert_file/key_file for your own certificate, or pebble web cert --force to regenerate (for example after changing allowed_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:

  1. The edit is made to a copy of pebble.yaml, preserving comments.
  2. 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.
  3. 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.
  4. 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.