Installation
Requirements#
- FreeBSD 15.1-RELEASE or later, amd64 (the installer refuses older
releases unless
--forceis given; FreeBSD 14 lacks a base-systemwg(8)at/usr/bin/wg). - Root access and a local console for the first deployment.
- 100 MB free under
/usr/localand 200 MB under/var. - Packages
unboundanddnsmasq(installed by the installer from the configured pkg repository, or offline from.pkgfiles).
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.