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:
- Theme — custom dark GTK theme with green-on-black terminal colors. Feels like a proper hacking environment without being unusable.
- Panel — single top panel with workspaces, system tray, and a quick-launch menu categorized by pentest phase (Recon, Scan, Exploit, Post, Forensics, Report).
- Wallpaper — custom artwork featuring the NullSec logo. Three variants: minimal, matrix rain, and topographic map.
- Terminal — pre-configured with Zsh, Oh My Zsh, custom prompt showing current engagement/scope, and aliases for common tool chains.
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:
- "Why Debian and not Arch?" — this came up so often that it became the primary motivation for the v2.0 rebuild
- "XFCE feels dated" — led directly to building the Nullkia desktop environment
- "Missing [tool X]" — we received 200+ tool requests, which we triaged using the selection criteria described in our tool curation post
- "Boots slow" — profiled the boot sequence, found systemd service ordering issues, cut boot time from 35s to 22s
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.