August 2024 · 8 min read

Building a Security Distro

NullSec Linux v1.0 started as a Debian netinst ISO and a list of tools I kept reinstalling. Here's how it became a distro.

The bootstrap

We started with Debian 12 netinst, debootstrap, and a shell script. The script installed packages, configured the desktop, and copied dotfiles. That shell script is now 3,000 lines of Lateralus.

Tool selection

v1.0 shipped with 120 tools. The criteria: must be actively maintained, must work on Debian stable, must not duplicate another tool's primary function. We dropped 40+ candidates that didn't meet these bars.

The ISO build pipeline

We use live-build to create the ISO. The entire process is automated:

// build-iso.ltl
let config = load_config("nullsec.toml")

config.packages
    |> group_by(|p| p.category)
    |> each(|(cat, pkgs)| {
        println("Installing {cat}: {pkgs |> len()} packages")
        pkgs |> each(install_package)
    })

build_iso(config)
    |> sign_with_gpg(config.gpg_key)
    |> generate_checksums()
    |> upload_to_cdn()

Lessons learned

Building a distro is 10% package selection and 90% testing. Every tool needs to be tested on every release. Automated testing caught 15 broken tools in our first CI run that manual testing had missed.

Desktop customization

v1.0 shipped with XFCE because it was the fastest way to get a working desktop. But we didn't ship stock XFCE — we customized everything:

The testing pipeline

Automated testing was the difference between a toy project and a real distro. Here's what our CI runs on every build:

// test-suite.ltl — NullSec v1.0 CI tests

// 1. Boot test: does the ISO boot to a desktop?
let vm = qemu::launch(iso_path, ram: "2G", timeout: 120.seconds())
assert(vm.screenshot() |> ocr() |> contains("NullSec"))

// 2. Tool smoke tests: does every tool at least show help?
tools |> each(|t| {
    let result = vm.exec("{t.binary} --help 2>&1 || {t.binary} -h 2>&1")
    assert(result.exit_code == 0 || result.stdout.len() > 0,
           "Tool failed: {t.name}")
})

// 3. Network tests: do network tools work?
vm.exec("nmap -sn 127.0.0.1") |> assert_contains("Host is up")
vm.exec("curl -s https://example.com") |> assert_contains("Example Domain")

// 4. Desktop tests: do GUI apps launch?
["burpsuite", "wireshark", "ghidra"] |> each(|app| {
    vm.exec("timeout 10 {app} &")
    sleep(5.seconds())
    assert(vm.process_running(app), "GUI app failed to start: {app}")
})

This caught 15 broken tools in our first CI run. Tools that were technically installed but had missing shared libraries, wrong Python versions, or incompatible Java runtimes. Without automation, these would have shipped broken.

Community and feedback

We released v1.0 with zero marketing — just a Reddit post on r/netsec and r/hacking. The feedback shaped everything that came after:

v1.0 was downloaded 12,000 times in its first month. Not viral, but enough to validate the concept and build a community that would test v2.0 betas.

Lateralus is built by bad-antics. Follow development on GitHub or try the playground.