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.

Blubie, the Blunix cat, sits beside a round magnifying glass on a stand, ready to look closer.
Look closer. Say what you saw. Change nothing.

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.

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.

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.

The five image helpers, how to run them, and what they do.
HelperCommandWhat 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.

  1. Get the public key

    Download it from blunix.io/keys/blunix-releases.asc. The same file is keys/blunix-releases.asc in 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
  2. 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 --fingerprint printed 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]>
  3. Check the signature

    Download SHA256SUMS and SHA256SUMS.asc from the release, next to the files you want.

    gpg --verify SHA256SUMS.asc SHA256SUMS

    Accept it only if gpg reports a good signature and names the fingerprint above. Anything else means stop.

  4. Check the files

    sha256sum -c --ignore-missing SHA256SUMS

    Each 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.