pebble.
Pebble / Docs / Start

Installation

Requirements#

Build prerequisites (only if building from source): Go 1.26 or later, make, git. The Makefile builds with exactly the Go release named in it (GOTOOLCHAIN, currently go1.26.9, which go downloads if needed): the release checked with govulncheck (make lint runs it when installed). make GOTOOLCHAIN=local uses the installed Go instead. Builds are reproducible (-trimpath, no toolchain VCS stamping, CGO disabled, build date taken from the last commit). Cross-compiling for FreeBSD works from any platform:

make build-freebsd            # bin/pebble-freebsd-amd64
scripts/build.sh freebsd      # runs vet and tests, then dist/pebble-<version>-freebsd-amd64.tar.gz + .sha256
scripts/build.sh package      # on FreeBSD: dist/pebble-<version>.pkg via pkg-create(8)

Dependencies: go.yaml.in/yaml/v3 (YAML parser; the standard library has none) and golang.org/x/net (DNS message encoding for diagnostics). Both are pinned in go.sum.

Bootstrapping from a clean system#

The binary carries everything else it needs. Copy it to the machine (for example with scp, or from a USB stick) and run, as root:

./pebble bootstrap [--dry-run] [--packages DIR] [--skip-packages] [--force]

Steps (each reports done, ok when already in place, or would in a dry run):

Step Action
release FreeBSD 15 or later, a RELEASE or STABLE build, amd64, root, /usr/bin/wg present
disk space 100 MB free under /usr/local, 200 MB under /var
pkg bootstraps pkg(8) if the system has never used it
packages installs unbound and dnsmasq (pkg install, or pkg add from --packages DIR)
kernel modules kldload -n pf pflog if_wg — loading pf does not filter anything until a ruleset is applied
directories creates Pebble's directories (keys and state 0700) and an empty pf.local.conf
files installs /usr/local/sbin/pebble, rc.d/pebble, rc.d/pebble_recovery and the examples, replacing older copies
DNSSEC trust anchor creates /usr/local/etc/unbound/anchor/root.key inside Unbound's chroot with unbound-anchor (which falls back to its built-in root key when offline; an anchor at the old location is moved instead). Only anchor/ and the key belong to the unbound user; the chroot, which holds Pebble's configuration, belongs to root
PF at boot sysrc pf_enable=YES

It then lists the network interfaces. Add --wan, --lan, --lan-address, --hostname and --timezone to write /usr/local/etc/pebble/pebble.yaml (an existing file is left alone), and --apply to deploy it straight away; finish with pebble confirm.

Without --apply, bootstrapping changes exactly one rc.conf setting, pf_enable="YES" (with sysrc), and configures no interface, loads no ruleset and starts no service. Until a configuration is applied, pf_rules still points at the (normally absent) /etc/pf.conf, which rc.d/pf skips at boot, so nothing is filtered or blocked. Everything else — the ruleset, forwarding, Unbound, dnsmasq, WireGuard — is enabled by the managed rc.conf block written by pebble apply, so it is covered by the usual validation and rollback protection. The pf_enable line agrees with that block and does not trigger a "shadowed setting" warning.

Installing with the script#

scripts/install.sh finds the binary in a release tarball, verifies its checksum if a checksum file is present, and runs pebble bootstrap with the same options:

sh scripts/install.sh               # online
sh scripts/install.sh --offline DIR # pkg add from DIR/*.pkg
sh scripts/install.sh --dry-run     # show what would happen

Other options (for example --wan igb0 --lan igb1 --apply) are passed to pebble bootstrap.

Verifying a release#

Releases are signed with an Ed25519 key, using only OpenSSL (part of the FreeBSD base system). Get the project's public key from the repository (packaging/release-signing.pub), not from the tarball: a tarball someone has tampered with would carry their key too.

Before unpacking, check the download:

openssl pkeyutl -verify -pubin -inkey release-signing.pub -rawin \
    -in pebble-<version>-freebsd-amd64.tar.gz.sha256 \
    -sigfile pebble-<version>-freebsd-amd64.tar.gz.sha256.sig
sha256 -c "$(cut -d' ' -f1 pebble-<version>-freebsd-amd64.tar.gz.sha256)" pebble-<version>-freebsd-amd64.tar.gz

Then let the installer check the binary inside it; it refuses to continue if the signature does not verify:

sh scripts/install.sh --pubkey /path/to/release-signing.pub

Maintainers: create the key once, on a machine you trust, and keep the private half offline:

openssl genpkey -algorithm ed25519 -out pebble-release.key   # private: keep offline, mode 0600
openssl pkey -in pebble-release.key -pubout -out packaging/release-signing.pub

Commit packaging/release-signing.pub, then sign each release with PEBBLE_SIGNING_KEY=/path/to/pebble-release.key scripts/build.sh freebsd. Unsigned builds print a warning. If the private key is ever exposed, publish a new public key and say so prominently: every earlier signature should then be distrusted.

Preparing an offline installation#

On a machine with network access and the same FreeBSD release and ABI:

pkg fetch -d -o /tmp/pebble-pkgs unbound dnsmasq

Copy /tmp/pebble-pkgs/All and the pebble binary to the router, then run ./pebble bootstrap --packages /path/to/All. On a system where pkg(8) itself has never been used, install it first with pkg add /path/to/All/pkg-*.pkg.

First deployment (local console)#

pebble interfaces
pebble init --wan igb0 --lan igb1 --lan-address 192.168.10.1/24 --timezone Europe/London
vi /usr/local/etc/pebble/pebble.yaml
pebble validate && pebble plan --diff
pebble apply
pebble confirm

Why the console: the first apply rewrites interface configuration and loads a default-deny firewall. Pebble refuses to apply a policy that would block your current SSH session, and rolls back automatically if you do not confirm, but the console is the only access that cannot be affected.

After confirming, reboot once to check that everything comes back:

shutdown -r now
pebble status && pebble diagnose

Uninstalling#

See the README. The three modes are separate on purpose: removing the tool never stops the router; stopping routing and removing configuration each require explicit confirmation.