Tracks FreeBSD configuration in git and records the installed package set, so a host can be inspected, reverted, or rebuilt from the repository.
Ownership, permissions and flags are stored in an mtree(8) spec alongside the
files. Secrets are encrypted with age(1). Default tracked paths are /etc,
/usr/local/etc, /root/.ssh, /var/cron/tabs and a few files under /boot.
The name is moor, as in mooring point.
git and age from packages. Everything else is base. Tested on FreeBSD 15.1.
make install
cp /usr/local/etc/bmoor.conf.sample /usr/local/etc/bmoor.conf
$EDITOR /usr/local/etc/bmoor.conf
bmoor init
Put the repository on its own dataset first. Inside the boot environment, activating an older BE reverts the git history with it:
zfs create -o mountpoint=/var/db/bmoor -o compression=zstd zroot/bmoor
bmoor status # what would be committed
bmoor commit -m "message" # exits 1 if nothing changed
bmoor be [name] # boot environment
bmoor restore --help
History is plain git:
git -C /var/db/bmoor/repo log -p tree/etc/rc.conf
git -C /var/db/bmoor/repo diff HEAD~5 -- tree/usr-local-etc
periodic(8) commits daily via 410.bmoor. The pkg triggers write
/var/db/bmoor/dirty and do not commit.
repo/tree/<dir>/ mirrors of the tracked directories
repo/secrets/<dir>/ age-encrypted files, *.age
repo/files/ individually tracked files
repo/manifest/ mtree specs, package and system state
repo/restore.sh standalone restore script
EXCLUDES and SECRETS are read by both tar(1) and mtree(8). Only two
forms behave the same in both:
*.sample basename glob, no '/'
./ssl/certs literal path anchored at './', relative to each DIRS entry
These match nothing in at least one of the two, and do so silently:
/etc/passwd leading '/'
**/*.sample '**'
etc/make.conf unanchored path
./ssl/* path plus wildcard
bmoor warns about all four at sync.
Files matching SECRETS stay out of the mirror and are stored as age(1)
blobs under repo/secrets/. Re-encryption is driven by an HMAC of the
plaintext, keyed by DIGEST_KEY, so unchanged secrets do not produce a new
blob on every commit.
Use at least two recipients in recipients.txt. Keep IDENTITY and
DIGEST_KEY off the repository; without them nothing decrypts.
restore.sh is copied into the repository on every commit and runs standalone.
sh restore.sh --help
sh restore.sh --root /tmp/t --no-pkg # into a scratch tree
Order: passwd database and pkg config, package replay, then the full tree and
metadata. pkg lock is applied last.
- Package versions are not restorable. Restore installs the recorded names at
whatever version the repository serves.
pkg.freebsd.orgkeeps no version history. - Packages with
%R = unknown-repositorycannot be restored. Warned at capture. - The version gate is exact. A
15.1-RELEASE-p1capture will not restore ontop2without--force. base/*packages are recorded but never replayed.- Set
REMOTE, or the repository only survives as long as the disk. - pkg writes
/var/backups/pkg.sql.zstnightly via411.pkg-backup. Include/var/backupsin off-host copies.
- ACLs, extended attributes, hardlinks, device nodes, mtimes. ACLs and hardlinks are detected and warned about.
- ZFS snapshots at capture time.
- Automatic boot environments before
pkg upgrade. Runbmoor be && pkg upgrade.
No FreeBSD port exists. Its pkg integration is a plugin, which cannot load
under pkg-static, and ports set PKG_BIN=pkg-static. Its pkgng shell paths
use GNU xargs -d and GNU sed -i. It does not record the package set.
make test # 45 assertions, unprivileged
make lint # sh -n over every script
BSD-2-Clause. See LICENSE.