Security
Pebble is security-sensitive infrastructure software. This document states the threat model, trust boundaries and the protections that exist — and is explicit about which have been tested and which rest on platform assumptions.
Trust boundaries and attacker-controlled input#
| Input | Trust | Handling |
|---|---|---|
| Internet traffic | hostile | PF default deny on ingress; no service listens on the WAN except WireGuard UDP; anti-spoofing; tagged port forwards |
| LAN clients | partly trusted | may use DNS, DHCP, ping and (if allowed) SSH on the router; everything else to the router is dropped; cannot reach other sites without rules |
| WireGuard peers | partly trusted | traffic accepted only from the peer's allowed IPs (WireGuard) and only where rules permit (PF); peer networks are anti-spoofed on other interfaces |
| DHCP client data (host names, client IDs) | hostile | dnsmasq stores it; pebble dhcp leases strips control characters; client host names are never published in DNS |
| Firewall IP list feeds | hostile | HTTPS and size limits as for blocklists; IPv4 entries only, nothing broader than /8, entries overlapping the router's own networks dropped; checked by pfctl -n before the live table is swapped; previous list kept on failure; pass rules from lists warned about |
| Blocklist feeds | hostile | HTTPS only, size/time/redirect limits, decompression limit, strict name syntax, never executed or interpreted, validated before reload |
| OPNsense exports | hostile | bounded XML parser, DTDs and entities refused, depth/element/size limits, no network access, secrets never copied into ordinary output |
pebble.yaml |
administrator | strict schema; every value validated and restricted to characters that are safe in the target syntax |
| Generated files, manifests | root-owned | manifests re-validated before use: only Pebble-owned paths and well-formed store names are accepted |
| Backups to restore | semi-trusted | only known regular-file members accepted; no links, directories or traversal |
| Web interface requests | hostile until signed in | Host allowlist, argon2id sign-in with throttling, CSRF tokens and origin checks, strict CSP, body limits; roles enforced again by the agent (web.md) |
| Agent socket clients | local, root or pebble only |
peer credentials checked at accept; structured JSON with unknown fields refused; inputs validated; diagnostics rate-limited |
| System log lines | hostile (any local process can log) | parsed with anchored patterns into bounded events; never executed or interpreted; control characters stripped |
Privileges#
pebbleruns as root only for commands that change the system (apply,confirm,rollback,dns blocklist update,vpn genkey).validate,plan,firewall explain/test,importrun unprivileged.- Unbound runs chrooted as
unbound; dnsmasq drops privileges as packaged. - No Pebble process is needed for routing. The optional
pebble_agent(root) andpebble_web(userpebble) run only when monitoring or the web interface is enabled. The web process holds no privileges; it can only request the agent's fixed, validated operations over a Unix socket restricted to root andpebbleby file mode and by peer credentials. There is no generic command execution. See web.md.
Engineering controls#
| Control | Implementation | Tested |
|---|---|---|
| No shell interpolation | commands run with exec and argument vectors; rc.d scripts read generated files with read, never eval |
unit test: non-allowlisted executable refused |
| Fixed executable paths | allowlist of absolute paths in internal/platform/exec.go; fixed PATH and LC_ALL=C |
yes |
| Timeouts and bounded output | every command has a timeout; captured output is capped | yes |
| Atomic writes | temp file in the same directory, fsync, rename, directory fsync | yes |
| Symlink safety | Pebble never follows or replaces a symlink at a path it writes or reads; key files must be regular files | yes |
| Path traversal | owned-path allowlist; store names fNNN; key file names restricted; backup members allowlisted |
yes |
| Protected keys | private/preshared keys only in /usr/local/etc/pebble/keys (0700), files 0600 root; loaded with wg set private-key <file>; never in YAML, generated files, generations, logs or diagnostics |
yes (generation and import outputs checked for key material) |
| Escaping | names limited to [a-z0-9_-.]; TXT and descriptions exclude quotes/backslashes/control characters; validated before rendering |
yes (fuzzed) |
| No WAN exposure by accident | DNS never offered to WAN-only networks; SSH from WAN requires allow_wan; port forwards produce "dangerous" warnings in imports |
yes |
| No default-pass window | block all is the first rule; pfctl -f swaps rulesets atomically; forwarding is enabled after PF at boot and only if PF is enabled |
policy unit-tested; boot ordering not yet tested on FreeBSD |
| No open resolver | Unbound binds to loopback/LAN/tunnel only; access-control refuse by default; PF allows DNS only from internal networks |
config unit-tested; integration test written |
| Rollback independent of the process | cron watchdog + daemon(8) timer + boot recovery |
simulated crash tests; not yet on FreeBSD |
| Web interface | sessions, CSRF, CSP, Host validation, throttling, role checks in two processes | yes (internal/web, internal/agent tests) |
| Bounded monitoring | journal size and retention limits; no packet capture; no DNS query logging | yes |
| No telemetry | Pebble makes no network connections except feed downloads you configure and pkg when you run pebble update check |
by inspection |
| No insecure updates | Pebble never installs updates; pkg verifies repository signatures |
by design |
| Privacy | no query logging by default; quiet-dhcp; diagnostics are aggregate |
by inspection |
Fuzz targets: configuration parsing, network/port parsing, DNS name
normalisation, feed parsing and OPNsense import (make fuzz). Fuzzing has
already found and fixed one crash (a nil dereference on exports without an
<OPNsense> section).
Assumptions about the platform#
These protections depend on FreeBSD behaving as documented; they were checked against FreeBSD 15.0 sources but not yet on a running system:
- PF evaluates
quickrules as written, supportstag/taggedwith old-stylerdr, and(self)/($if)dynamic addresses follow address changes. rc(8)re-readsrc.confon SIGALRM (used by boot recovery).dhclienthonourssupersedein the file given with-c.unbound-checkconf,pfctl -nanddnsmasq --testreject invalid files.flock(2)locks are released when a process dies.
Known limitations#
pebble firewall testmodels Pebble's own rules; it does not see the administrator anchor or existing states.- WireGuard endpoint host names are resolved through the router's own resolver; a compromised upstream DNS cannot impersonate a peer (keys are authenticated) but can stop it from being reached.
pool.ntp.orgis exempt from DNSSEC validation (see dns-filtering.md).- A malicious LAN client can still exhaust DHCP leases or send heavy DNS load; Pebble does not add rate limiting in version 1.
- Backups made with
--include-keyscontain secrets; they are written with mode 0600. - WireGuard's handshake uses X25519, which a future quantum computer could
break; traffic recorded today could then be decrypted. A per-peer
pre-shared key (
preshared_key_file,pebble vpn genkey --psk) is WireGuard's mitigation: use one for every site-to-site peer whose traffic must stay confidential for years. VPN providers set their own policy. - Every web user, including viewers, can see each device's open connections (remote addresses, ports, byte counts). Pebble records none of this: it is read from PF's state table when a page is viewed.
- Per-device blocking and VPN routing are keyed on a device's reserved IPv4 address. A device that sets another address on itself escapes them; they are a household control, not a security boundary against a hostile device.
- A VPN exit's provider sees the assigned devices' traffic. Its private key comes from the provider's configuration file and is stored like any other WireGuard key.
Reporting vulnerabilities#
Please report security issues privately to the maintainer ([email protected]) rather than in a public issue.