wren-linux / docs / maintaining.md (as plain text)
Keeping it working
Things outside Wren Linux change: download pages move, signing keys are replaced, packages get renamed, and new Whonix or Tor Browser versions can change what the scripts rely on.
bash test.shchecks it all without installing anything: every script parses and passes shellcheck; every Debian package still exists; every pinned signing key is still published, still has that fingerprint and isn't expiring; the newest release of each downloaded app can still be found and its signature verifies; the Debian and Firefox features the hardening relies on are still there; and the saved downloads still verify.bash test.sh --offlineskips the network. Run it now and then, and before a reinstall.pins.shholds every fingerprint and the outside versions the scripts were written against, each with the date it was last checked by hand (# checked: YYYY-MM-DD).test.shwarns when one is over a year old, and when Tor Browser or Whonix reach a new major version.bash tools/test-apt-markers.sh(run bytest.sh) checks the package bookkeeping behind--removeagainst fakes of apt, so it changes nothing.update.shdecides which scripts to rerun from the files each one uses: the script itself, every"$WREN_DIR/bin|data|vendor|lib/..."it names, everyinstall_bin NAME, any pinned name frompins.shit (or a library it sources) uses, and the files on a# files: bin/x data/yheader line. Put a# files:line in a script that gets a file installed by a library function, where the script's own text doesn't show it (the Landlock wrapper,bin/no-abstract-sockets, is the example). The shared librarieslib.shloads for everyone are only reported, never counted.tools/test-update.shchecks this against a pretend server.tools/test-*.sh(run bytest.sh) cover the parts that guard something: the signature checks (with throwaway expired and revoked keys), the sshd settings check, old package lists, the media sandbox, andbin/no-abstract-sockets(needs Linux 6.12; skipped on an older kernel).- shellcheck on every change: run
bash test.shbefore you commit (it runs shellcheck; needssudo apt install shellcheck). There is no CI service (Wren Linux isn't on GitHub): runbash test.sh --offlinebefore you publish; the steps for a clean Debian 13 are at the end of this page. Settings are in.shellcheckrc. There is deliberately no git hook for it: a hook is a file in the project that runs as you on every commit, and the project's files can come from someone else.
Signing Wren Linux's commits
update.sh runs what it brings in, with sudo. To make sure it is yours, sign
your commits and pin the key: from then on every Wren Linux computer brings in a
change only when the newest commit carries a good signature from that key
(update_signed in lib/update.sh, tested by tools/test-update-signed.sh
with throwaway keys). One signed commit on top vouches for everything under
it, since each commit's id covers the ones before it.
To publish, use bash tools/git-push.sh: it sets up signing and the pin the first
time, reminds you to run bash test.sh --offline first (no CI looks at what is pushed), signs the newest
commit if it isn't (with an empty commit on top), checks that signature as a computer updating would,
and asks before it pushes. It takes no options: one it doesn't know is refused. Computers with
the pin refuse an update whose newest commit isn't signed.
The easy way to make the key: bash extras/signing-key.sh in a git copy (the package leaves it out; wrenctl install wren-dev gets a copy), then wren-key with a USB
stick mounted, just after a restart, with the network off and nothing else open (it checks, and
refuses otherwise: so do wren-key renew and restore; the network can go back on for the push). Its first run explains and asks, makes the key in two parts
(a primary key that only certifies, in an encrypted backup on the stick and
nowhere else; and a signing subkey, kept here by its own user with no network,
that your own user can't read), and offers to set git up to sign every commit
and to pin the key. Then commit what wren-key pin changed, signed, and
push it: computers that update to it take it unchecked (their copy has no pin
yet); every update after that is checked. On a new computer, or after
reinstalling, don't make a second key: wren-key restore BACKUP takes it
from the backup on your stick. man wren-key and
extras/signing-key.md have the rest, the steps by
hand included. In short, by hand:
- Make the key on a computer you trust, best one that is offline: a primary
key that only certifies, and a signing subkey for every day.
gpg --quick-generate-key "Wren Linux signing key <you@example.org>" ed25519 cert never, thengpg --quick-add-key FINGERPRINT ed25519 sign 2y. Keep a revocation certificate and a backup of the primary key apart from the computer (an encrypted stick, and on paper withpaperkey). - Put the public half in the repository and pin the primary key's
fingerprint (a signature by a subkey counts for its primary key, so the
subkey can be renewed or replaced without a new pin):
gpg --export --armor FINGERPRINT >data/wren-signing-key.asc, andWREN_SIGNING_FPR=FINGERPRINTinpins.sh.bash test.shchecks the file is exactly that key and can still sign. - Sign from then on:
git config commit.gpgsign trueandgit config user.signingkey FINGERPRINTin~/wren. Check one withgit log --show-signature -1. - Commit and push all of this as one signed commit.
A new subkey, or a renewed one (wren-key renew BACKUP): export the key
again into data/wren-signing-key.asc (wren-key pin) and commit that
with the old subkey's signature, before it expires. A key that has
expired or been revoked fails the check, and updates stop until a copy with
the new key is brought in by hand.
What Wren Linux adds to Debian
docs/what-wren-adds.md lists each change. tools/test-what-wren-adds.sh keeps its Wren Linux side
true, and fails when a new base part that hardens something has no row. It can't check the Debian column: measure
that again on a plain install at each new Debian, with /usr/sbin/sysctl on a real host (a container has
network settings of its own), the kernel's /boot/config-*, and the live image's package list.
Trying it for real
Tests with pretend programs show what you believed the real ones do, and have passed over real bugs. These need a throwaway computer or VM, and root, and have caught what they missed:
- A DHCP client: in network namespaces, joined by a veth pair, with
dnsmasq --log-dhcpas the server, whose log says what each client sent. Give the client a fresh/runand/var/lib(unshare -m), and/etc/netns/NAME/resolv.confso the real one isn't touched: dhcpcd's control socket isn't per namespace, so don't stop one by name. - NetworkManager: in a privileged container (
podman run --privileged), with dbus and udev running, on a veth it's told to manage, since it leaves those alone by default. - Whonix: unpack the real image, boot a qcow2 overlay of it with QEMU, and over QMP press
its power button (
system_powerdown) and take screenshots (screendump). Your user needs thekvmgroup. Check the disk withdebugfs -R featuresande2fsck -fnonce it's off. - A window: a headless sway (
WLR_BACKENDS=headless WLR_RENDERER=pixman), asked for its windows withswaymsg -t get_tree. - A clean Debian 13: the recipe at the end of this page.
And run a command as a normal user: /usr/sbin isn't on that PATH, which testing as root hides.
Making a release
A release is a signed tag on what's already published. Its name is the year and month
(2026.11, then 2026.11.1), and its message is the release notes.
wren-release --tag # publish, mark it as a release, install it here
bash tools/tag-release.sh # only mark what's already published
bash tools/publish-image.sh # its install image, signed, on the server
tag-release.sh writes the notes from the commits since the last release, for you to edit,
signs the tag, checks it with the pinned key alone, and asks before pushing. Write the notes
for people who use Wren Linux: what changed for them, and what they should do.
publish-image.sh needs a commit with such a tag. It builds with
make-install-iso.sh --public, which puts nothing of this computer's on the image, signs
SHA256SUMS, checks that, and copies them to /srv/wren-images/VERSION on the server, as
images/VERSION/ on the site, beside RELEASE-NOTES.txt. It never replaces what's there.
The image hands out Debian's programs, many under the GPL, which asks that their source be offered
too. So Debian's source for that exact image goes beside it (about 3 GB), checked against Debian's
signed list, with a SOURCE.txt, and without it nothing is published. Keep each image's source
up for as long as the image is. This is a good-faith reading of the licence, not legal advice.
A server set up before this needs setup-vps.sh run again for that folder.
Neither can run by itself. The key signs only after your sudo password, on purpose: a script that could sign alone means anything running as you could too.
The package's version still comes from its commit (0~git...), so a later commit always
sorts higher, whether or not it's a release. The image isn't built byte for byte the same
twice: anyone can rebuild the package on it and compare, but only check the image's signature.
The Wren Linux package
bash tools/build-deb.sh builds wren-linux_VERSION_all.deb from a commit
(--commit REV, by default HEAD; never from files that aren't committed).
--version sets the version; without it the version is 0~git, the commit's
time and its id, so a later commit always has a higher version. tools/test-build-deb.sh checks it.
- What it installs: the scripts in
/usr/share/wren, owned by root and writable by no one else; thewrenctlcommand in/usr/bin; its man page and Tab completion; the licenses. Nothing outside/usr, nothing setuid. - What it runs when installed: nothing. It has no install scripts, so
installing it changes no setting. The desktop is set up as before, as you:
wrenctl part NAME,wrenctl install NAME,bash /usr/share/wren/install-base.sh. - Why: with the scripts root's, a program running as you can no longer
change one and wait for you to run it with sudo. The
wrenctlcommand uses/usr/share/wrenwhenever it is there (WREN_DIR=~/wren wrenctl ...uses your own copy instead). - The same file every time: every date in it is the commit's, so two builds of one commit are the same, byte for byte; the build prints the SHA-256. Publish that next to the package: anyone can build it again and compare.
- lintian checks it when installed (
wrenctl install wren-dev); it isn't needed to build. Debian's debhelper isn't used: the package is files only, anddpkg-debwith a short script is easier to read and to build the same way every time.
On the computer you publish from, wren-release (or bash tools/release.sh) does it all, stopping at the
first step that fails: git-push.sh, the package in a new private folder, `sudo apt
installof that one file,wrenctl --update.--local` installs the last commit
without pushing. Don't build into /tmp and install by a wildcard: every user can
write there, the sandboxes' too, and apt installs as root.
Install it on another computer with sudo apt install ./wren-linux_VERSION_all.deb.
apt brings a newer one (the same way, or from the signed apt repository
below); wrenctl update then runs again the parts and extras whose files
changed, as it does for a git copy. Without git it compares the files with a
record it keeps after each run (~/.local/state/wren/applied-files, and a
copy of pins.sh); the first run takes what is set up as up to date
(wrenctl update --all offers everything). tools/test-update-package.sh
checks it.
The signed apt repository
bash tools/make-apt-repo.sh --key FINGERPRINT --out FOLDER wren-linux_*.deb
makes an apt repository (pool/, dists/trixie/) of Wren Linux's packages and
signs it, on your own computer, with your key (the pinned WREN_SIGNING_FPR
once it is set; gpg asks for the passphrase or the token). Upload FOLDER as it
is to any web server or mirror (rsync -a --delete FOLDER/ server:/var/www/debian/);
tools/test-apt-repo.sh checks it with throwaway keys and Debian's own apt.
- The server can't change a package. apt checks every file against
InRelease, andInReleaseagainst your key. Whoever runs the server can only hold updates back. - Nor serve an old one for long:
InReleaseis good for 14 days (--valid-days N, 1 to 90), after which apt refuses the repository. Sign it again within that time, even when nothing changed. - Only Wren Linux's packages go in (
wren-linuxandwren-*); the script refuses anything else.
On a Wren Linux computer, wrenctl part wren-repo adds it, once pins.sh
names both the key (WREN_SIGNING_FPR) and the address
(WREN_REPO_URL, https:// or an .onion address); until then it changes
nothing. The key comes from the copy's own data/wren-signing-key.asc,
only if it is exactly the pinned one, and is trusted for that repository
alone (Signed-By). An apt pin lets only the package wren-linux come from it,
so even someone with your key or the server couldn't replace a Debian package
or add another (tools/test-wren-repo.sh checks the pin with Debian's own
apt). unattended-upgrades then installs Wren Linux's updates like Debian's. It
is not in the base list yet: add it to PARTS in packages.sh once the
repository is live.
The website
site/ is the website, plain HTML with no JavaScript, and tools/build-site.sh
builds it from one commit, with a browsable copy of every file
(bash tools/build-site.sh --preview builds this copy's pages, committed or
not, into ~/Downloads/wren-site-preview and opens them in the sandboxed
Firefox; the server's headers are seen only on the test VM). tools/setup-vps.sh sets up the server,
tools/publish-site.sh puts the site and the git mirror on it from a signed
commit only, and tools/try-vps.sh tries it all in a local VM first: see
vps.md.
Other scripts
prep/setup-sudo.sh USER(as root): installs sudo and adds USER to the sudo group.status.sh: what's installed, the Firefox hardening, sway snippets, how each Wi-Fi device is set up, and a warning if/,/homeor swap isn't encrypted.prep/make-install-iso.sh: an automatic Debian 13 install image with Wren Linux on it (see the README, "Before you reinstall"). What runs inside the installer isdata/install-iso-*;tools/test-install-iso.shchecks it.tools/try-install-iso.shinstalls from a built image in a throwaway QEMU VM.prep/fetch-offline-debs.sh,prep/fetch-tor-browser.sh: download things you need before a reinstall (see the README).tools/pack-usb.sh /path/to/stick: puts the last commit andoffline/on a stick, without personal files, labels the stickWREN(only a removable drive, and it asks first) and writesSTART-HERE.txt.start.sh: what a user runs from the stick on a fresh install (sudo mount -L WREN /mnt && bash /mnt/wren/start.sh).
Running the suite on a clean Debian 13
There is no CI service: the suite is run by hand, before a release and on every commit that is
handed over. To run it on a clean Debian 13, in a container that can make firewall rules in
network namespaces and start a headless sway (so --privileged):
podman run --rm -it --privileged debian:trixie bash # or docker; pin the image by its digest
apt-get update
apt-get install -y --no-install-recommends git sudo shellcheck gnupg gpgv gpg-agent acl sway foot rsync nftables xclip cage socat keepassxc lintian python3 wget iproute2 util-linux procps ca-certificates file xz-utils bsdextrautils openssh-client apparmor
# a normal user with sudo: the scripts refuse to run as root
useradd -m tester
echo 'tester ALL=(ALL) NOPASSWD:ALL' >/etc/sudoers.d/tester
# put the repository in tester's home (git clone, or copy it in), make it theirs, then:
su tester -c 'cd ~/wren-linux && PATH=$PATH:/usr/sbin bash test.sh --offline'
What each is for: gpg-agent makes and checks keys, nftables runs the firewall rules, lintian
checks the package, sway and foot give the window tests a headless desktop. test.sh runs
shellcheck over every shell script and checks the Python commands in bin/ as Python. Run
it with no locale set (env -u LANG -u LC_ALL) as a container does: a check once passed under
en_US.UTF-8 and failed without it.