Tools
Small tools that say what they checked.
These are the commands that ship with blunix. Each one does one job. Each one says what it did not check. Most print JSON, so a screen reader reads the same words a script parses.
Every output on this page came from running the tool. We captured them on Debian 10 and on a Mac, and each block says which. Local paths are shortened.

Get them
The tools live in the blunix-utils repository on GitHub. A copy is synced into the blunix distribution repository, and new images include the launchers.
Run from a checkout
You need Python 3.9 or later and PyYAML. Commands that encrypt or decrypt use Debian's age. The disk and service checks use Linux tools such as lsblk and systemctl. Missing tools are reported as unavailable, not as errors.
Debian 10 ships Python 3.7, which is too old. On Debian 10, use a newer Python. We tested with Python 3.11 in a private venv, with no system changes. Debian 11 ships Python 3.9 and Debian 12 ships 3.11, so on Debian 11 and later the system Python works.
git clone https://github.com/afterdarksys/blunix-utils.git
cd blunix-utils
./apply/blunix doctor
./apply/gitbuild --help
blunix --help, blunix -h, and blunix help print the command list and exit 0. Bare blunix with no command runs the installer bootstrap. Output, captured on Debian 10:
usage: blunix COMMAND
doctor [--root ROOT] executables available and filesystem capacity
security audit [--root ROOT] protected-path ownership and permission checks
integrity check [PRODUCT] [--root ROOT]
Gitbuild file and link drift
admin status selected systemd service states
disk inspect read-only block device and mount metadata
support collect --output FILE [--root ROOT]
mode-0600 JSON report, no secrets, logs or addresses
proxy help build-proxy for static-address and netboot LANs
gitbuild --help source builds and installed product checks
disk check|render, net render, access boot|render, ai apply, gui apply,
tools apply, node apply|boot image and installer steps (see blunix.io/tools)
profile check --file FILE check a Profile or Node document (YAML or JSON)
profile render --file FILE --label LABEL --hostname NAME [--target DISK] [--format yaml|json]
print the Node document for one machine
With no command, blunix runs the installer bootstrap.
The two profile lines were added after that capture and were captured on the Mac.
Install as a package with Gitbuild
The repository carries its own Gitbuild recipe. Gitbuild builds one explicit ref and records the commit it resolved. It installs under /usr/local/afterdarksys/blunix-utils and links the commands into /usr/local/bin. The repository has no release tags yet, so name a full commit.
./apply/gitbuild prepare afterdarksys/blunix-utils --ref FULL_COMMIT_SHA --output ./build/blunix-utils
# Read build/blunix-utils/bundle.json before you install.
sudo ./apply/gitbuild install ./build/blunix-utils
gitbuild verify blunix-utils
The image builders are not part of this package. They stay as scripts in the source tree.
Host checks
Six read-only commands. They print JSON with a status field. The exit code is 0 when the status is ok and 1 for anything else. They are narrow on purpose. None of them is a full hardening audit, a signature check, a hardware test, or a repair tool. None of them changes the system.
The examples below ran on a macOS workstation against a scratch directory we made to stand in for a mounted image. In it, etc/shadow had mode 0644 and was owned by an ordinary user, so there is something to find. The scratch path is shown as /work/demo-root.
blunix doctor
Says which helper programs exist on the machine running the check, and how much space and how many inodes are free under the root you name.
blunix doctor [--root ROOT]
It does not test whether those programs work, and it is not a health check. The program list is about the machine you run it on, even with --root. Only the free space follows --root.
Output, captured on Debian 10 as an ordinary user. age and git were not installed on that machine.
{
"status": "ok",
"root": "/",
"tools_on_operator_host": {
"age": false,
"git": false,
"lsblk": true,
"findmnt": true,
"systemctl": true,
"ip": true,
"ss": true,
"journalctl": true,
"sha256sum": true
},
"root_free_bytes": 10180431872,
"root_free_inodes": 908565,
"note": "Tools are checked on the machine running this command; --root changes only the free-space figures. Not a full system health assessment."
}
blunix security audit
Checks the owner and permission bits of four protected paths: /etc/shadow and /etc/gshadow (at most 0640), /root/.ssh (at most 0700), and /usr/local/afterdarksys (at most 0755). Each must be owned by root.
blunix security audit [--root ROOT]
It reads metadata only, never file contents. A symlink anywhere in a protected path is reported and not followed, so a mounted image cannot point the check outside itself. A path that does not exist is skipped. If none of the four exist, the status is unavailable.
Output, captured on the Mac against the scratch root. Exit code 1.
{
"status": "findings",
"scope": "permission and ownership checks only; no configuration bodies read",
"checked": [
"/etc/shadow",
"/root/.ssh"
],
"findings": [
{
"path": "/etc/shadow",
"issue": "excess permissions",
"mode": "0o644"
},
{
"path": "/etc/shadow",
"issue": "not owned by root"
},
{
"path": "/root/.ssh",
"issue": "not owned by root"
}
]
}
Run as an ordinary user on a live host, some protected paths cannot be read at all. The audit does not crash. It reports each one as a finding, such as {"path": "/root/.ssh", "issue": "unreadable metadata"}, sets the status to findings, and exits 1. blunix support collect carries the same finding in its report. Run as root for a full answer.
blunix integrity check
Compares every product that Gitbuild installed against its install receipt. It reports changed, missing, and extra files, and command links that no longer point where they should. Name one product to check only that one.
blunix integrity check [PRODUCT] [--root ROOT]
It covers Gitbuild products only, not Debian packages. The receipt is a local baseline written at install time. It is not a signature. Someone who can rewrite the product and its receipt together can pass this check.
Output, captured on the Mac. With nothing installed, the list is empty and the status is ok.
{
"status": "ok",
"scope": "gitbuild packages only; receipts are local baselines, not signatures",
"products": []
}
Asking for a product that is not installed gives "status": "error" for that product, a reason that says why, and exit code 1. Output of blunix integrity check not-a-product, captured on Debian 10:
{
"status": "findings",
"scope": "gitbuild packages only; receipts are local baselines, not signatures",
"products": [
{
"product": "not-a-product",
"status": "error",
"reason": "gitbuild: product is not installed"
}
]
}
blunix admin status
Reads the state of five services through systemctl show: ssh, systemd-resolved, systemd-networkd, blunix-bootstrap, and blunix-access. For each it keeps LoadState, ActiveState, SubState, and Result. A failed unit is a finding.
blunix admin status
It does not start, stop, or restart anything. It does not read the journal. It always looks at the running host, so it takes no --root.
Output, captured on Debian 10, trimmed after the first two services. That machine is plain Debian, so blunix-bootstrap and blunix-access are not installed. Each shows "LoadState": "not-found" and "status": "unavailable", which makes the overall status unavailable and the exit code 1. On a blunix host those two services exist.
{
"status": "unavailable",
"services": {
"ssh.service": {
"status": "ok",
"properties": {
"Result": "success",
"LoadState": "loaded",
"ActiveState": "active",
"SubState": "running"
}
},
"systemd-resolved.service": {
"status": "ok",
"properties": {
"Result": "success",
"LoadState": "loaded",
"ActiveState": "inactive",
"SubState": "dead"
}
},
...
blunix disk inspect
Lists block devices through lsblk: kernel name, type, size in bytes, read-only flag, removable flag, and mount points.
blunix disk inspect
It does not read SMART data, check a filesystem, or touch a partition table. It takes no --root, because disks belong to the running host.
It works with the older lsblk on Debian 10 and 11. Each device always has a mountpoints list, with null when nothing is mounted. Output, captured on Debian 10 in a throwaway VM, trimmed after the first two devices. The rest were the root partition, swap, and a CD drive.
{
"status": "ok",
"devices": [
{
"kname": "sda",
"type": "disk",
"size": 21474836480,
"ro": false,
"rm": false,
"mountpoints": [
null
]
},
{
"kname": "sda1",
"type": "part",
"size": 536870912,
"ro": false,
"rm": false,
"mountpoints": [
"/boot/efi"
]
},
...
blunix support collect
Writes one JSON report that you can send to someone helping you. It holds the doctor, security audit, and integrity check results. Nothing else.
blunix support collect --output report.json [--root ROOT]
The report never contains configuration file bodies, journal logs, credentials, environment variables, process arguments, or network addresses. The report lists those omissions inside itself, so the reader knows they were left out on purpose. The file is created with mode 0600. It will not overwrite an existing file, and it will not follow a symlink at that name. Reports over 1 MiB are refused.
Output, captured on the Mac against the scratch root. The command prints a short receipt.
{
"status": "ok",
"output": "/work/report.json",
"bytes": 1413
}
The file mode, and the end of the report:
-rw------- 1 you staff 1413 Oct 3 06:17 report.json
"omitted": [
"configuration bodies",
"journal logs",
"credentials",
"environment variables",
"process arguments",
"network addresses"
]
Running it again with the same name refuses, rather than replacing the first report:
blunix: diagnostic failed (FileExistsError)
Gitbuild
Builds software from source at one named Git ref, then installs it as a product with a receipt. It handles Python, Go, Rust, Bash, Node, PHP, and custom recipes. Run it as gitbuild or blunix gitbuild. The full guide is docs/gitbuild.md in blunix-utils.
usage: gitbuild [-h] {prepare,build,install,remove,list,verify,doctor} ...
Build and manage After Dark source packages
positional arguments:
{prepare,build,install,remove,list,verify,doctor}
prepare (build) check out a GitHub ref and create an install bundle
install install a prepared bundle and record its file baseline
remove remove an installed product
list list installed products
verify compare an installed product with its baseline
doctor report available build tools
options:
-h, --help show this help message and exit
gitbuild prepare
Checks out a repository, builds the ref you name, and writes a bundle: bundle.json plus a payload/ tree with a SHA-256 and mode for every file. A tag or branch is resolved to a commit, and the commit is recorded. build is another name for the same command.
gitbuild prepare OWNER/REPO --ref REF --output DIR [--manifest RECIPE.yaml]
It accepts only repositories owned by afterdarksys or straticus1. It refuses to run as root. The output directory must not exist, and a failed build leaves no bundle. It does run the repository's build code with your permissions and network access, so build untrusted code in a disposable machine. Bundles are integrity checked, not signed.
Output, captured on the Mac. A repository outside the two owners is refused before anything is fetched.
gitbuild: repository must belong to straticus1 or afterdarksys
gitbuild install
Installs a bundle under /usr/local/afterdarksys/PRODUCT and publishes relative links in /usr/local/bin, sbin, lib/PRODUCT, and etc/PRODUCT. It checks the file inventory before writing and again after copying. Installing a newer bundle is the upgrade. Local edits to configuration survive. A new default for an edited file is saved beside it as .gitbuild-new.
gitbuild install BUNDLE [--root ROOT]
It runs no code from the repository and no install hooks. It does not start services or write systemd units. It refuses to overwrite files it does not own, special permission bits, and links that leave the product. With --root it assembles a target filesystem instead of the running system.
gitbuild list and gitbuild verify
list shows installed products with their repository, commit, and status. verify compares one product with its receipt and exits 1 if anything differs.
gitbuild list [--root ROOT]
gitbuild verify PRODUCT [--root ROOT]
Output, captured on the Mac against the empty scratch root:
$ gitbuild list --root /work/demo-root
[]
$ gitbuild verify example --root /work/demo-root
gitbuild: product is not installed
On a real product, verify prints product, a changed list, and a links_changed list. In the project's test fixture, one file is edited and one command link is deleted after install. verify then names bin/demo under changed and sbin/demod under links_changed, and exits 1.
gitbuild remove
Removes a product and its links. Configuration stays, and the product is listed as config-only. Add --purge to delete the configuration too.
gitbuild remove PRODUCT [--purge] [--root ROOT]
If a step fails, the product and links are rolled back. That covers errors, not a power cut. It is not a multi-version rollback system.
gitbuild doctor
Says whether the programs a build system needs are on the path.
gitbuild doctor [--system {bash,custom,go,node,php,python,rust}]
It checks that programs exist, and that python3 -m pip runs for Python. It does not check versions or native libraries. Output, captured on the Mac:
{
"tools": {
"git": "/usr/local/bin/git",
"go": "/usr/local/bin/go",
"python3": "/usr/local/bin/python3"
},
"missing": [],
"note": "Checks executable availability; recipes may need additional headers and tools."
}
Build profiles
A profile is a node document without a hostname, a label, or a disk target, so one profile serves many machines. It is YAML or JSON with kind: Profile. distribution is optional and only debian is accepted today.
- profile check reads a Profile or a Node and checks every model it names. It writes nothing.
- profile render adds a label and a hostname and prints the Node document for one machine, in YAML or with
--format json. The result is checked like any node document before it is printed. - The portal imports a JSON Profile or Node into its build form. For a YAML profile, render it with
--format jsonfirst.
Output, captured on the Mac, for a profile named lab-speech:
$ blunix profile check --file lab-speech.yaml
blunix: profile lab-speech accepted
$ blunix profile render --file lab-speech.yaml --label ada --hostname ada-1
apiVersion: blunix.dev/v1
kind: Node
name: ada
hostname: ada-1
disk: metal-luks
network: dhcp-any
access: full-speech
ai: default
update:
url: https://updates.blunix.io/blunix
channel: stable
sysexts: []
A profile that names another distribution is refused:
blunix: refused distribution
blunix proxy
The build-proxy, for a LAN of machines with static addresses or netboot. You describe the machines in one site file. It publishes one encrypted build per machine and serves those installs on the LAN. The build-proxy drawing on the architecture page shows how the parts connect.
usage:
blunix proxy init [--api URL] [--key-file PATH] [--listen HOST:PORT]
blunix proxy plan SITE.yaml [--check] [--explain]
blunix proxy publish SITE.yaml [--out DIR]
blunix proxy serve [--site SITE.yaml] [--media DIR [--sums PATH]] [--listen HOST:PORT] [--advertise HOST:PORT]
blunix proxy dnsmasq SITE.yaml [--interface IF] [--range START,END] [--proxy HOST:PORT] [--tftp-root DIR]
All commands take --config PATH (default ~/.config/blunix/proxy.yaml).
The account key is read from its key file, never from the command line.
- init saves the API address, the listen address, and the path to your account key file. The key is read from a mode 0600 file or typed at a hidden prompt. Putting it on the command line is refused.
- plan reads the site file and says, one sentence per machine, what publish would do. It sends nothing. Add
--checkto ask the API which labels are already yours, and--explainto list every setting with where it came from: the machine, the site defaults, a profile, the site's boot section, or the built-in value. - publish makes a new key for each machine, encrypts that machine's document with age, and uploads only the ciphertext. Each key is written to
keys.txt(mode 0600) before its upload, so a lost answer from the server never loses a key. It stops if a label belongs to another account. - serve relays build documents for
blnx.iobuild hosts only, and serves netboot files. It never serves keys. With--mediait checks the media againstSHA256SUMSfirst. - dnsmasq prints a dnsmasq configuration with one address reservation per machine. It prints to standard output and changes nothing.
The site file can name build profiles, set interface patterns, and tune netboot (installer access profile, consoles, extra kernel arguments, lease time, TFTP root) in a boot: section, so you can try configurations before settling on one. The rules are in docs/proxy.md.
What it does not stop: a person who can read keys.txt. Print the install cards and delete the file. Netboot is for trusted LANs until images are signed.
Output of plan, captured on the Mac. The site file had two DHCP machines, ada and grace, with example MAC addresses.
blunix proxy: machine 00:50:56:aa:bb:01 gets label ada (https://ada.blnx.io/), hostname ada-1, network by dhcp, disk metal-luks, access regular.
blunix proxy: machine 00:50:56:aa:bb:02 gets label grace (https://grace.blnx.io/), hostname grace-1, network by dhcp, disk metal-luks, access full-speech.
blunix proxy: publish would reserve each label, encrypt one document per machine with a new key, upload it as a new version, and write the keys to keys.txt. Running publish again uploads new versions.
Trying to pass a key as a flag, also captured on the Mac:
blunix proxy: the account key is read from its key file, never from the command line
Image helpers
Scripts in image/ that the release build runs. They are source-tree tools, not part of the installed package. The scanners print a fixed line on failure. They never print a secret, or the path or byte offset in the image where they found one.
| Helper | Command | What it does |
|---|---|---|
| scan-root | image/scan-root.py [--release] MOUNTED_ROOT |
Walks a mounted image for the build's test secrets and for vendor AI binaries. It does not follow links out of the mount. With --release it also refuses the test fixture, any unlocked account, a root SSH key, an sshd that allows root or password login, a shipped sshd host key, and an image with sshd but no host key generation. |
| scan-raw | image/scan-raw.py DISK.raw or DISK.raw.zst |
Reads every byte of the disk image, free space included, because a deleted file keeps its bytes. It refuses private key bodies, age secret keys, the test fixture's age header, and the test secrets. |
| seal-root | image/seal-root.py MOUNTED_ROOT |
Test images only. Puts the encrypted test fixture and the test root password onto a mounted root. It reads both secrets from files, never from arguments or the environment. |
| redact-serial | image/redact-serial.py [--check] VM_DIR |
Removes test secrets from a VM's serial and boot logs, then keeps only the expected boot lines in summary.txt. With --check it only reports whether the boot evidence is complete. |
| gpl-sources | image/gpl-sources.py VERSION STATUS... |
Turns the dpkg status files of a release into SOURCES.md: every Debian source package at its exact version, a snapshot.debian.org link for each, and the written source offer. It reads files only and fetches nothing. |
The scanners fail closed. On a checkout without the build's test secrets, they cannot know what to look for, so they refuse. Captured on Debian 10, exit code 1:
$ python3 image/scan-root.py /
blunix: scan failed: build/root-password is missing or unreadable
The message names the build secret file that is missing, never its contents. scan-raw.py prints the same kind of line.
On a build machine, the failure lines are fixed sentences, taken from the code and its tests: plaintext secret leaked into the image, vendor binary in image, test fixture in a release image, unlocked account in a release image, and others of the same shape. A clean scan prints nothing and exits 0.
Output of gpl-sources.py, captured on the Mac from a two-package status file we wrote for the example. The version number is only an example.
# Source for Blunix 0.2.0
The Blunix 0.2.0 installer ISO, disk image and netboot media contain binary packages from Debian 13 (trixie). Those packages keep their own licenses, many of them the GNU GPL. Blunix's own code is licensed separately; see LICENSE in the repository.
## Where the source is
Every source package below is in the Debian archive at the exact version shipped. The link opens that version on snapshot.debian.org. On a Debian system, `apt-get source NAME=VERSION` fetches the same thing.
## Written offer
For at least three years after this release, and for as long as we offer support for it, After Dark Systems will give anyone a complete copy of the corresponding source code for any GPL- or LGPL-licensed package in this release, on request, for no more than our cost of physically performing the distribution. Ask through https://github.com/afterdarksys/blunix/issues and name the release and the package.
## Source packages (2)
| Source package | Version | In |
|---|---|---|
| [bash](https://snapshot.debian.org/package/bash/5.2.37-2/) | `5.2.37-2` | disk |
| [glibc](https://snapshot.debian.org/package/glibc/2.41-12/) | `2.41-12` | disk |
Verify a release
v0.1.1-rc.2 is the first signed release. v0.1.0 remains an unsigned test release: its checksums prove the bytes match, not who built them.
From the first signed release on, SHA256SUMS comes with SHA256SUMS.asc, a detached signature made with the release key. Signing happens offline, on the maintainer's machine. The build server never holds the key.
-
Get the public key
Download it from blunix.io/keys/blunix-releases.asc. The same file is
keys/blunix-releases.ascin the blunix repository and in blunix-utils, so you can check one against the other.curl -fsSLO https://blunix.io/keys/blunix-releases.asc gpg --show-keys --fingerprint blunix-releases.asc gpg --import blunix-releases.asc -
Check the fingerprint
Compare it with this page and with the repository. They must match in full: 62F7 36BE A2AB 2E1F A16D 5138 BCB3 426C 090A DF92. This is what
gpg --show-keys --fingerprintprinted for the file:pub ed25519 2026-10-02 [SC] [expires: 2028-10-01] 62F7 36BE A2AB 2E1F A16D 5138 BCB3 426C 090A DF92 uid Blunix Release Signing <[email protected]> -
Check the signature
Download
SHA256SUMSandSHA256SUMS.ascfrom the release, next to the files you want.gpg --verify SHA256SUMS.asc SHA256SUMSAccept it only if gpg reports a good signature and names the fingerprint above. Anything else means stop.
-
Check the files
sha256sum -c --ignore-missing SHA256SUMSEach file you downloaded must say OK. The installer also checks the disk image against its sha256 before it writes a disk.
There is no signed release yet, so there is no signature output to show here. The releases are listed on the release log.
What is different here
Plain facts about how these tools and the install around them behave. Each one is in the code today.
- Access starts at the boot menu. Entries 1 to 5: full speech, console speech, large print, regular, advanced. Both speech entries also turn on braille. The access page has the details.
- Prompts are one sentence.
blunix: build hostname.
blunix: key. Type it. It will not be spoken.
The hostname is read back, and silence does not fetch. - The build key is made in your browser. Only ciphertext is uploaded. A wrong key says
blunix: could not decrypt. Nothing applied.
- Secrets stay off the command line.
blunixrefuses--passphrase.blunix proxyrefuses an account key as an argument and reads it from a 0600 file. - Release builds come only from signed tags. The build server pulls from GitHub, accepts no inbound connections except admin SSH, and holds no signing key. Unsigned tags are refused.
- Release images are scanned before they ship. The mounted root and every raw byte of the disk, free space included.
- Support reports leave the private parts out. No configuration bodies, logs, credentials, environment, process arguments, or network addresses. The report says so inside itself.
- Every Gitbuild product can be checked. Its receipt records each file's SHA-256 and mode, and
blunix integrity checkcompares them on demand. - Every release carries its GPL source list.
SOURCES.mdnames each Debian source package and version, with a written offer. - The checks say what they are not. Every report carries its scope, so nobody mistakes a permission check for a full audit.