Wren Linux

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.

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:

  1. 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, then gpg --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 with paperkey).
  2. 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, and WREN_SIGNING_FPR=FINGERPRINT in pins.sh. bash test.sh checks the file is exactly that key and can still sign.
  3. Sign from then on: git config commit.gpgsign true and git config user.signingkey FINGERPRINT in ~/wren. Check one with git log --show-signature -1.
  4. 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:

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.

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.

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

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.