NixDaemon

NixOS pentest workstation as one flake — IceBreaker's successor
git clone https://git.daemon-sec.xyz/NixDaemon.git
Log | Files | Refs | README

2026-10-08-pentest-toolkit-design.md (18796B)


      1 # Pentest toolkit for NixDaemon — design
      2 
      3 **Date:** 2026-10-08
      4 **Status:** awaiting review
      5 **Author:** daemon-sec, with Claude
      6 
      7 ## Intent
      8 
      9 Turn this laptop into a Kali-equivalent offensive workstation, declaratively,
     10 for **HackTheBox CPTS prep**. Everything comes from the flake: no `curl | bash`,
     11 no tools living outside the store except the ones that must be mutable
     12 (VPN profiles, loot).
     13 
     14 Success criteria:
     15 
     16 1. `nh os switch` is the only install step. A fresh machine reaches the same kit.
     17 2. Tools are grouped into independently toggleable modules (`daemon.pentest.*`),
     18    so a category can be dropped in one line.
     19 3. Tools are reachable with no ceremony: `nxc`, `ffuf`, `secretsdump` work in any
     20    terminal, including under `sudo`.
     21 4. Windows/Linux/macOS agent binaries and offensive payloads are staged in one
     22    predictable tree, servable to a target in one command.
     23 5. The HTB loop — connect VPN, set target, fix clock skew, enumerate — is
     24    driven by a handful of commands that share state across terminals.
     25 
     26 Non-goals: C2 infrastructure beyond what ships in nixpkgs; anything targeting
     27 hosts outside HTB/authorised labs; Kali's full 600-package catalogue (curated
     28 instead); a second non-NixOS VM.
     29 
     30 ## Decisions
     31 
     32 These were delegated to me with "whatever is best for NixOS". Recorded with
     33 reasoning so they can be revisited.
     34 
     35 ### D1 — Tools install to `environment.systemPackages`, not `home.packages`
     36 
     37 **The single most important NixOS-specific decision.** Half this toolkit needs
     38 root: `nmap -sS` (raw sockets), `responder`, `mitm6`, `bettercap`, `tcpdump`,
     39 `masscan`. home-manager packages are on the *user's* PATH only, so
     40 `sudo nmap -sS` would fail with `command not found` — the classic
     41 Kali-refugee-on-NixOS trap. System packages behave like Kali's `/usr/bin`.
     42 
     43 Consequence: modules live in `modules/features/pentest/` as
     44 `flake.nixosModules.pentest-*`. Only genuinely user-scoped things (the `htb*`
     45 commands, the cheat card, shell integration) are home modules.
     46 
     47 ### D2 — Everything on PATH, per-category toggles, devshells as a bonus
     48 
     49 Friction matters more than PATH tidiness during CPTS. 1.8TB free, and the kit
     50 comes from the binary cache rather than compiling. Toggles are the escape hatch.
     51 
     52 `perSystem.devShells.pentest` (plus one per category) is declared anyway — ~10
     53 lines, and it makes the kit portable: `nix develop github:…#pentest` on any
     54 machine with Nix.
     55 
     56 ### D3 — Missing tools become pinned Nix derivations; a script re-pins them
     57 
     58 ~20 tools aren't in nixpkgs. Each gets a derivation in `_pkgs/` with a pinned
     59 `rev`/`hash`, so the toolkit rolls back with the system generation.
     60 `pentest-update` re-pins to latest on demand.
     61 
     62 Where a tool declares Windows support in nixpkgs, build it **from source** with
     63 `pkgsCross.mingwW64` rather than downloading a release blob. Verified available
     64 for `mimikatz`, `ligolo-ng`, `netexec`. Genuine provenance, no trusting strangers.
     65 
     66 Prebuilt .NET binaries (SharpCollection, Ghostpack, potato family) are the
     67 exception: building .NET offline in Nix is brittle, so those are pinned release
     68 assets with recorded hashes.
     69 
     70 ### D4 — Defaults: CPTS core + heavy GUI on; the rest written but off
     71 
     72 On: `recon ad bloodhound web pivot crack shells wordlists payloads python gui`.
     73 Off: `dfir reversing wireless cloud osint mobile`.
     74 Keeps the first build finishable in one sitting; each is one line to enable.
     75 
     76 ### D5 — Native NixOS options over bare packages, wherever they exist
     77 
     78 Not cosmetic — on NixOS the package alone is often *unusable*:
     79 
     80 | Option | Why the package alone is not enough |
     81 |---|---|
     82 | `programs.wireshark.enable` | Creates the `wireshark` group and the `dumpcap` capability wrapper. Without it you cannot capture without full root. |
     83 | `programs.proxychains.enable` | Manages `/etc/proxychains.conf`. `/etc` is read-only on NixOS, so the Kali habit of editing that file by hand simply fails. This module is what makes `proxychains` work at all. |
     84 | `security.wrappers` | Capabilities (`cap_net_raw`) instead of setuid, for raw-socket tools. |
     85 | `services.neo4j` + `services.postgresql` | BloodHound CE needs both; there is no `services.bloodhound` in nixpkgs, so this is wired by hand. |
     86 
     87 `rockyou.txt` ships gzipped in the store, so `wordlists.nix` produces a
     88 decompressed derivation and exports `$WORDLISTS`.
     89 
     90 ## Architecture
     91 
     92 Dendritic, matching the existing repo: every file under `modules/` is a
     93 flake-parts module declaring the outputs it owns, cross-referenced by **name**
     94 through `self`, never by path. Switches live in one `options.nix`; each category
     95 is gated with `lib.mkIf`.
     96 
     97 ```text
     98 modules/features/pentest/
     99 ├── options.nix       daemon.pentest.{enable,<cat>.enable} + assertions
    100 ├── core.nix          always-on base + $PAYLOADS/$WORDLISTS env + /etc/hosts helper
    101 ├── python.nix        the combined offensive Python env + suffix-free aliases
    102 ├── recon.nix         ad.nix  bloodhound.nix  web.nix  pivot.nix  crack.nix
    103 ├── shells.nix        wordlists.nix  payloads.nix  gui.nix
    104 ├── dfir.nix          reversing.nix  wireless.nix  cloud.nix  osint.nix  mobile.nix
    105 ├── vpn.nix           htbvpn@.service template + scoped sudo rule
    106 ├── time.nix          htb-time privileged helper + scoped sudo rule
    107 ├── devshells.nix     perSystem.devShells.pentest and per-category
    108 └── _pkgs/            ~20 pinned derivations (paths with /_ are skipped by import-tree)
    109 
    110 modules/home/
    111 ├── htb.nix           htbtarget htbvpn htbtime htbip htb new — the user-facing CLI
    112 ├── htb-shell.nix     zsh precmd integration: $TARGET/$BOX live in every terminal
    113 └── cheats/pentest.md `pentest-cheat`, in the existing cheat-card style
    114 ```
    115 
    116 ### Option tree
    117 
    118 ```nix
    119 daemon.pentest = {
    120   enable = mkEnableOption "the offensive toolkit";   # master switch, default false
    121   <category>.enable = ... ;                          # per D4 defaults
    122   payloads.windowsArches = [ "amd64" "x86" ];        # which .exe variants to build
    123   htb.vpnDir = "~/.config/htb/vpn";                  # where .ovpn profiles live
    124   htb.promptTarget = true;                           # show $TARGET in the prompt
    125 };
    126 ```
    127 
    128 Every category also implies `core`, so `$PAYLOADS`/`$WORDLISTS` always exist.
    129 An assertion fires if a category is enabled while `daemon.pentest.enable` is false.
    130 
    131 ## Components
    132 
    133 ### C1 — Tool modules
    134 
    135 Curated from what was verified present in the pinned nixpkgs (2026-10-08).
    136 
    137 - **recon** — nmap masscan rustscan naabu fscan nbtscan enum4linux-ng snmpcheck
    138   onesixtyone thc-ipv6 dnsrecon subfinder amass httpx dnsx katana gowitness
    139   eyewitness whatweb
    140 - **ad** — netexec impacket certipy bloodhound-py kerbrute responder mitm6
    141   coercer donpapi adidnsdump ldapdomaindump smbmap evil-winrm pypykatz bloodyad
    142   lsassy dploot masky pywerview krb5 openldap samba(smbclient) sshpass
    143   · custom: krbrelayx sprayhound
    144 - **bloodhound** — bloodhound-ce with neo4j + postgresql wired up
    145 - **web** — ffuf gobuster feroxbuster dirb nikto sqlmap commix wfuzz nuclei
    146   nuclei-templates wpscan joomscan dalfox arjun jwt-cli jwt-hack mitmproxy
    147   burpsuite zap
    148 - **pivot** — ligolo-ng chisel socat proxychains-ng(native opt) sshuttle gost
    149   frp iodine pingtunnel stunnel wireguard-tools openvpn
    150 - **crack** — hashcat hashcat-utils john hydra medusa crowbar crunch cewl hashid
    151   name-that-hash
    152 - **shells** — metasploit msfpc powershell pwncat rlwrap updog miniserve
    153   freerdp remmina rdesktop upx
    154 - **wordlists** — seclists wordlists rockyou(decompressed) exploitdb
    155 - **dfir** — volatility3 sleuthkit autopsy yara capa chainsaw hayabusa exiftool
    156   foremost testdisk chntpw binwalk · custom: velociraptor
    157 - **reversing** — ghidra radare2 rizin cutter gef pwntools one_gadget pwninit
    158   checksec flare-floss retdec patchelf
    159 - **wireless** — aircrack-ng hcxtools bettercap wireshark(native opt) termshark tcpdump
    160 - **cloud** — awscli2 azure-cli google-cloud-sdk kubectl trivy kube-hunter pacu
    161 - **osint** — theharvester recon-ng sherlock maigret holehe
    162 - **mobile** — apktool jadx dex2jar frida-tools objection scrcpy
    163 
    164 ### C2 — The Python toolkit (`python.nix`)
    165 
    166 Answers "a script built into the nixos config that can set up all the python tools".
    167 
    168 Three parts:
    169 
    170 1. **One combined interpreter.** `pentest-python` = `python3.withPackages` with
    171    every offensive library: impacket certipy pywerview dploot masky bloodhound
    172    ldapdomaindump pypykatz bloodyad lsassy minikerberos aiowinreg ldap3 dnspython
    173    scapy pwntools pycryptodomex requests rich. Any offensive script you download
    174    runs with `pentest-python foo.py` and its imports already resolve — no venv,
    175    no `pip install`.
    176 
    177    *Risk:* a version conflict inside one env (impacket is built against
    178    python3.14 here). Mitigation: if `withPackages` fails to resolve, fall back to
    179    per-tool standalone packages and a slimmer shared env. Decided at build time,
    180    not guessed.
    181 
    182 2. **Suffix-free aliases.** impacket installs 70 scripts as `secretsdump.py`,
    183    `psexec.py`, `GetUserSPNs.py`. A `mkSuffixFreeAliases` helper globs a package's
    184    `bin/*.py` and symlinks `secretsdump` → `secretsdump.py`, so both spellings
    185    work. Applied to impacket and any other `.py`-suffixed toolkit.
    186 
    187 3. **A discovery command.** `impacket` with no arguments lists all 70 scripts
    188    with fzf selection and `--help` preview; `impacket <name>` runs one. Same for
    189    `pentest-python --list`.
    190 
    191 ### C3 — Payload tree (`payloads.nix`)
    192 
    193 A derivation of symlinks at a stable path, exported as `$PAYLOADS`:
    194 
    195 ```text
    196 $PAYLOADS/
    197 ├── windows/{amd64,x86}/
    198 │   ├── privesc/potato/    JuicyPotato JuicyPotatoNG RoguePotato SweetPotato
    199 │   │                      GodPotato PrintSpoofer SharpEfsPotato EfsPotato
    200 │   │                      LocalPotato DeadPotato
    201 │   ├── privesc/printer/   SpoolSample SharpPrintNightmare PrintSpoofer
    202 │   ├── privesc/           winPEASx64 winPEASx86 Seatbelt SharpUp
    203 │   ├── ad/                Rubeus SharpHound Certify Whisker SharpView
    204 │   │                      ADCSPwn StandIn
    205 │   ├── creds/             mimikatz-2.2.0 mimikatz-2.1.1 LaZagne SharpDPAPI
    206 │   ├── agents/            ligolo-agent chisel nc
    207 │   └── misc/              SharpCollection (the full upstream set)
    208 ├── linux/{amd64,arm64}/   ligolo-agent chisel linpeas pspy socat nc
    209 ├── macos/{amd64,arm64}/   ligolo-agent chisel
    210 └── scripts/
    211     ├── ad/                PowerView PowerUp SharpView.ps1 Invoke-Mimikatz
    212     │                      PowerSploit nishang Invoke-Kerberoast
    213     ├── printer/           printerbug.py CVE-2021-1675.py Invoke-Nightmare.ps1
    214     └── privesc/           linux-smart-enumeration linux-exploit-suggester
    215                            PowerUp.ps1 Sherlock.ps1
    216 ```
    217 
    218 Multi-arch comes from `pkgsCross.mingwW64` (Windows) and `pkgsCross.aarch64-multiplatform`
    219 (Linux arm64) over the same source. Versioned duplicates live here as distinct
    220 filenames (`mimikatz-2.2.0.exe`, `mimikatz-2.1.1.exe`) rather than contending
    221 for one PATH name — the reason payloads are files, not commands.
    222 
    223 `payload-serve [port]` HTTP-serves the tree bound to `tun0`;
    224 `payload-serve --smb` runs the impacket `smbserver` equivalent;
    225 `payload-serve --list` prints the tree.
    226 
    227 ### C4 — Shared target state (`htbtarget`)
    228 
    229 The requirement is a target IP visible **across terminals**. Environment
    230 variables cannot be pushed into already-running processes, so:
    231 
    232 - **State** lives in `$XDG_STATE_HOME/htb/` (`target`, `name`, `vpn`), surviving
    233   reboots so a box can be resumed next day.
    234 - **Propagation** is a zsh `precmd` hook that re-reads the state file and exports
    235   `$TARGET`, `$RHOST`, `$IP`, `$BOX`. Every terminal picks up a change made in
    236   any other terminal **at its next prompt**.
    237 
    238 ```sh
    239 htbtarget 10.10.11.5 vintage.htb   # set IP (+ optional FQDN)
    240 htbtarget                          # print current
    241 htbtarget clear
    242 ```
    243 
    244 With a FQDN, it also manages a marked block in `/etc/hosts` through a privileged
    245 helper — necessary because Kerberos in CPTS AD boxes needs working name
    246 resolution, and `/etc` is read-only on NixOS.
    247 
    248 **Honest limitation, to be documented in the cheat card:** a shell that is
    249 mid-command when the target changes keeps the old value until its next prompt.
    250 Interactive use is unaffected.
    251 
    252 ### C5 — VPN control (`htbvpn`)
    253 
    254 Implemented as a **systemd service template** `htbvpn@.service`, not a
    255 backgrounded process — it survives closing the terminal, logs to the journal,
    256 and `down` reliably kills it.
    257 
    258 ```sh
    259 htbvpn list            # profiles in ~/.config/htb/vpn, active one marked
    260 htbvpn up lab_eu_free  # sudo systemctl start htbvpn@lab_eu_free
    261 htbvpn down            # stop whatever is up
    262 htbvpn status          # unit state + tun0 address
    263 htbip                  # just the tun0 IP, for pasting into payloads
    264 ```
    265 
    266 Profiles are **not** Nix-managed: they are per-account files that rotate, and
    267 `~/.config/htb/vpn/` is created for them. A sops-encrypted variant is available
    268 later if wanted. A scoped `NOPASSWD` sudo rule covers only
    269 `systemctl {start,stop,restart} htbvpn@*`.
    270 
    271 ### C6 — Clock skew control (`htbtime`)
    272 
    273 The Kerberos-skew problem: AD authentication rejects a clock more than ~5
    274 minutes off the DC, and on NixOS `systemd-timesyncd` is enabled by default and
    275 will **undo** any manual adjustment.
    276 
    277 `htbtime $TARGET`:
    278 1. Record prior state (is `timesyncd` active? what does `timedatectl` report for
    279    NTP? are `chrony`/`ntpd` running?) into `/var/lib/htb-time/state`.
    280 2. Disable the conflicts: `timedatectl set-ntp false`, stop `chrony`/`ntpd` if up.
    281 3. Sync to the target: `ntpdate -u <target>` (from `pkgs.ntp`, confirmed to ship
    282    `ntpdate` and `sntp`), falling back to `chronyd -q` if the DC refuses NTP.
    283 4. Report the resulting offset.
    284 
    285 `htbtime off` reads the state file and restores **exactly** what was there, then
    286 re-syncs to real time. `htbtime status` shows the current offset and mode.
    287 
    288 Two safeguards:
    289 
    290 - **A skew warning.** A large offset breaks TLS certificate validation, so `nix`,
    291   `git` and HTTPS may fail while skewed. `htbtime` says so explicitly rather than
    292   letting it become a confusing debugging session.
    293 - **Self-healing.** State lives in `/var/lib`, but a reboot re-applies the NixOS
    294   default anyway, so a forgotten `htbtime off` cannot permanently break the clock.
    295 
    296 Root side is `htb-time`, a fixed store script with a scoped `NOPASSWD` rule for
    297 `wheel` — mirroring the existing `fan-ec` / `fan` split in
    298 `modules/hosts/laptop/fan-cli.nix` and `modules/home/fan.nix`.
    299 
    300 ### C7 — Engagement scaffolding and cheat card
    301 
    302 ```sh
    303 htb new escape    # ~/htb/escape/{nmap,loot,creds,www,exploit}/ + notes.md template,
    304                   # sets the target if given, cds in
    305 htb ls            # boxes worked, newest first
    306 pentest-cheat     # recon | ad | pivot | transfer | crack | web | dfir | htb
    307 ```
    308 
    309 `pentest-cheat` follows the existing card mechanism exactly
    310 (`modules/home/cheats/*.md` + `cheat-render`), so it joins `nix-cheat`,
    311 `gpg-cheat` and `niri-cheat` with no new machinery.
    312 
    313 ### C8 — `pentest-update`
    314 
    315 Re-pins every `_pkgs/` derivation to its latest upstream `rev`/`hash` using
    316 `nix-prefetch-url`/`nurl`, rewrites the hashes in place, and prints a diff for
    317 review. Never auto-commits and never runs during a rebuild — pinning stays
    318 deliberate.
    319 
    320 ## Error handling
    321 
    322 - **Missing VPN profile** — `htbvpn up` lists what is available and exits 2.
    323 - **No target set** — any command needing `$TARGET` says which command sets it.
    324 - **`htbtime` without a reachable DC** — reports the failure and leaves NTP
    325   *disabled* with a clear warning plus the `htbtime off` hint, rather than
    326   silently half-applying.
    327 - **Unbuildable `_pkgs` entry** (upstream retagged, asset renamed) — that one
    328   derivation fails with the URL that broke; it must not take the rebuild down, so
    329   payload entries are assembled so a single failure is diagnosable in isolation.
    330 - **Python env conflict** — falls back to standalone packages (C2).
    331 
    332 ## Testing
    333 
    334 No unit-test framework applies to a package set, so verification is build- and
    335 behaviour-based, **per module as it lands**, not in one pass at the end:
    336 
    337 1. `nix flake check` and `nix build .#nixosConfigurations.nixos.config.system.build.toplevel`
    338    after each module — eval errors and missing attributes surface immediately.
    339 2. **Smoke test per category**: one `--version`/`--help` per tool, scripted, so a
    340    package that installs but cannot run is caught.
    341 3. **Payload tree**: assert every declared path exists and Windows artefacts
    342    report as PE binaries (`file`), Linux ELF ones as ELF.
    343 4. **`htbtarget` cross-terminal**: set in terminal A, confirm `$TARGET` appears in
    344    terminal B at its next prompt.
    345 5. **`htbvpn`**: unit starts, `tun0` gains an address, `down` removes it.
    346 6. **`htbtime`**: capture `timedatectl` before, run `htbtime` against a test NTP
    347    source, confirm NTP disabled and offset applied, run `htbtime off`, confirm
    348    `timedatectl` matches the original state byte for byte. This round-trip is the
    349    one that must not be hand-waved.
    350 7. `sudo nmap -sS` and `sudo responder -h` — the D1 guarantee, explicitly tested.
    351 
    352 ## Risks
    353 
    354 | Risk | Mitigation |
    355 |---|---|
    356 | First build is 40–60GB / a couple of hours | Mostly cache downloads. Phased: core + AD first, GUI last. |
    357 | Upstream release assets move, breaking `_pkgs` | Hashes pinned; `pentest-update` re-pins; failures isolated per derivation. |
    358 | `pkgsCross.mingwW64` fails for a given tool | Verified for mimikatz/ligolo-ng/netexec. Anything else falls back to a pinned release asset. |
    359 | Combined Python env conflicts | C2 fallback to standalone packages. |
    360 | Clock skew breaks TLS mid-engagement | Explicit warning + reboot self-heal (C6). |
    361 | 200 new names shadow an existing command | Smoke test diffs `PATH` collisions against the pre-change profile and reports them. |
    362 
    363 ## Phasing
    364 
    365 Each phase ends with a working, committed system.
    366 
    367 1. **Foundation** — `options.nix`, `core.nix`, `wordlists.nix`, `python.nix`. Proves the pattern.
    368 2. **CPTS core** — `recon`, `ad`, `web`, `pivot`, `crack`, `shells`.
    369 3. **HTB workflow** — `vpn.nix`, `time.nix`, `htb.nix`, `htb-shell.nix`. The commands.
    370 4. **Payloads** — `_pkgs/` derivations, cross-builds, the staging tree, `payload-serve`.
    371 5. **Heavy and optional** — `bloodhound`, `gui`, then `dfir`/`reversing`/`wireless`/`cloud`/`osint`/`mobile` (off by default).
    372 6. **Polish** — `pentest-cheat`, `pentest-update`, `devshells.nix`, README.
    373 
    374 ## Scope note
    375 
    376 Written for authorised lab use: HackTheBox CPTS preparation. The toolkit is
    377 ordinary defensive-security coursework equipment; it carries no targeting
    378 configuration and nothing here points at infrastructure the operator does not
    379 own or have explicit permission to test.