Automated deploy scripts for security workstations — Kali on hardware you sit at, Kali on a headless box you reach remotely, and a Tails live session stocked with pentest tooling over Tor.
For authorized engagements, CTFs, and lab work only.
| Script | Target | Desktop | Access |
|---|---|---|---|
kali-deploy-physical |
Physical laptop you sit at | i3 + polybar + kitty, picom on glx | local console, plus sshd |
kali-deploy-vm |
Hyper-V guest reached over a remote agent | i3 + polybar + kitty, no compositor | ScreenConnect / ConnectWise Control agent |
kali-deploy-remote |
Headless box / cloud teamserver | none | Tailscale SSH + hardened OpenSSH + ufw |
kali-deploy-wsl |
Kali as a WSL2 instance on a Windows host | none — Windows Terminal draws the shell | none; you enter with wsl.exe |
test-with-tails (beta) |
Tails OS live session | Tails' own GNOME | everything over Tor; nothing persists |
kali-deploy-vm is configuration only — it installs no tools, on the basis that
a stock Kali image already ships kali-linux-default. Use kali-deploy-physical
if you want the kali-tools-* metapackage set.
Superseded Ubuntu scripts live in archive/. SPEC.md covers the
design reasoning behind the two Kali scripts.
A Kali-first deploy for a box you sit at, physical or virtual: the full tool set, the i3 desktop, and a hardening phase kept deliberately out of the tooling's way.
sudo ./kali-deploy-physicalOptions:
--dry-run show what would happen; change nothing
--mac-stable clone a per-network MAC (off by default)
--mac-random clone a fresh MAC per activation (off by default;
breaks long-lived connections)
--skip <phase> skip a phase (repeatable)
--only <phase> run only these phases (repeatable)
--list-phases print phase names
--print-packages print every package the script can install
--print-checked-binaries
print every binary the verification pass ticks
-h, --help
Phases: base tools desktop harden i3 polybar kitty shell dots wordlists
Re-running is always safe — every phase is idempotent, so a run that dies partway
can be resumed by running it again (or narrowed with --only). A full transcript
goes to /var/log/kali-deploy-physical-<timestamp>.log.
Tools come from Kali's own kali-tools-* metapackages rather than a hand-listed set
of package names — they're curated, dependency-resolved, and stable across rolling.
Edit TOOL_METAS at the top of the script to change scope.
Included: information-gathering, vulnerability, web, database, passwords, exploitation, post-exploitation, sniffing-spoofing, windows-resources, reverse-engineering, 802-11, wireless, bluetooth, rfid.
Not included: kali-tools-sdr (gnuradio is a very large dependency tree). Add it to
TOOL_METAS if you want it.
EXTRA_PKGS names the things the metapackages don't cover — including the AD and web
tooling (netexec, mitm6, certipy-ad, enum4linux-ng, bloodhound.py, ffuf,
gobuster), which is not in the kali-tools-* closure. Don't assume the ISO will
supply them; see the Testing section.
The desktop is kali-desktop-i3, which is a complete desktop — i3, lightdm, polybar,
kitty, picom, feh, network-manager, betterlockscreen — with the i3 session registered
with the display manager. On top of that the script writes the i3, polybar, and kitty
configs carried over from archive/ubuntu-autodeploy-v5.
Kali stages its own i3/polybar/kitty configs under /usr/share/i3-dotfiles/ and
never copies them into $HOME, so they don't collide with these. Worth a look if you
want to borrow from them.
Claude Desktop is installed from Anthropic's own signed apt repo, so it updates with
everything else on apt upgrade. Its signing key is checked against the published
fingerprint (31DD DE24 DDFA B679 F42D 7BD2 BAA9 29FF 1A7E CACE) before the repo is
registered — a key fetched over TLS only proves the CDN served it, and this grants
root-level package trust. A mismatch means no repo rather than a repo trusted anyway.
Note that Linux support is beta and upstream tests Ubuntu 22.04+ and Debian 12+, not
Kali. It resolves and runs, but it's not a combination Anthropic tests, so the install
is non-fatal. The script also adds you to the kvm group, which Cowork needs for the
QEMU guest it runs tasks in — that takes effect at your next login.
Discord is installed here too, from the vendor .deb — it isn't in Kali's repos or
Debian's, the client being proprietary. That means no apt source and so no updates,
and Discord refuses to launch once it is behind the current build. When that happens,
re-run the phase (--only desktop) after removing the installed package, or fetch the
.deb by hand. The install is non-fatal: no network gets you a desktop and a Mod+d
that does nothing, not a failed deploy.
Deliberately thin. This is a pentest box, and the controls a general-purpose baseline applies are the same ones that break the tooling installed alongside them — so the phase does less than its name suggests, on purpose.
ufwis not enabled, and an earlier run's enabled state is undone. A default-deny inbound policy lets every listener here — Responder,ntlmrelayx, a msfconsole handler,impacket-smbserver,python3 -m http.server— bind successfully and then catch nothing. That failure gets debugged from the target end, mid-engagement, long after anyone remembers a firewall is involved.sshdis left running. The box is reached remotely, and a remote-access agent depending on X, the display manager and a vendor relay is a single point of failure; sshd is the way back in, and the pivot path besides.bluetooth.serviceis left running — the BT tools this script installs drivebluetoothd, so disabling it disabled the tooling.avahi-daemonandcups/cups-browsedoff. This one is a port-conflict fix rather than hardening: avahi holds UDP 5353 and cups holds 631, and Responder needs 5353 free to poison mDNS. It binds what it can and stays quiet about the rest, so the symptom is a capture that never happens.- MAC cloning off unless asked for —
--mac-stablefor a per-network MAC,--mac-randomfor a fresh one per activation. See the caveat below. - The idle screen lock stays on — 5 minutes, plus
xss-lockon suspend andMod+Escapeon demand.kali-deploy-vmdisables the same timer, which is the right call there; this script is for bare metal that travels, and an unattended unlocked laptop is the threat that actually applies to it.
The verification pass ends with an ss -tulpn listing of everything actually
listening. Read that rather than trusting the toggles.
If a specific engagement wants a firewall policy, set it deliberately:
fw-status # sudo ufw status verbose
fw-open 8000/tcp # sudo ufw allow 8000/tcpClones the owlshells/dots shell kit into
~/dots and runs its install.sh, which symlinks ~/.bash_aliases and adds a
guarded hook to ~/.zshrc. It then seeds the command-recall index from any logs
already on the box, so the shell is useful on the first login rather than the tenth.
dots is public, so a freshly imaged box with no credentials at all still gets it.
The authenticated transports are tried first anyway, because DOTS_REPO can point
at a private fork; the phase skips rather than fails if every one is unreachable
— a shell kit is never a reason to redo an install:
| Order | Transport | Needs |
|---|---|---|
| 1 | DOTS_TOKEN |
sudo DOTS_TOKEN=<pat> ./kali-deploy-... |
| 2 | SSH | a key already on the box, agent or on disk |
| 3 | gh |
gh auth login, if gh is present — it ships from GitHub's own apt repo, not Kali's, so a stock box will not have it |
| 4 | anonymous HTTPS | nothing — the path a stock box actually takes |
A token is passed as a per-invocation http.extraHeader, never embedded in the
remote URL — a URL credential gets persisted into .git/config and echoed back in
git's error output, and this script's transcript goes to /var/log. git is also
pinned to BatchMode/GIT_TERMINAL_PROMPT=0 so it can never sit waiting for input
on a box nobody is standing in front of.
Finish it later with sudo DOTS_TOKEN=<token> ./kali-deploy-<physical|remote> --only dots. On a freshly imaged box DOTS_TOKEN or a restored SSH key are the
only transports that exist: gh is not in Kali's repos, and adding GitHub's apt
source would mean a second third-party repo when Tailscale is the one declared
exception.
Set DOTS_REPO=owner/name to point the phase at a different kit.
The zshrc block the shell phase writes defines serve, ports, listening,
myip, WORDLISTS/SECLISTS and the encoders only when dots is absent. dots
provides richer versions, and alias serve would otherwise permanently shadow its
serve() function, since zsh resolves aliases before functions. extip is kept
unguarded as its own name, because "what the internet sees" is a different question
from dots' local-interface myip.
On the remote script install.sh is invoked with --no-tmux: that box's
~/.tmux.conf is the headless SSH one written by the tmux phase, which suits it
better than the repo's — and leaving it a real file stops write_file's cat >
from writing into the dots checkout through a symlink.
Some things can only be checked on the hardware, and the script prints them at the end rather than claiming they passed:
-
Pick the i3 session at the LightDM greeter. Kali defaults to Xfce; i3 is not automatic.
-
Confirm polybar renders and the battery module finds
BAT0. -
Mod is the Windows/Super key.
Mod+Returnkitty,Mod+bfirefox,Mod+cClaude Desktop,Mod+dDiscord,Mod+pdmenu,Mod+Escapelock. Discord hasMod+dbecause it is the mnemonic one; dmenu moved toMod+p, which is where dwm has always launched it from.Mod+Shift+cis still i3's reload — a separate binding, not a conflict withMod+c. -
Two fingers up on the touchpad should now scroll you down the page, not up — the desktop phase writes
/etc/X11/xorg.conf.d/40-touchpad.confto invert libinput's default. X reads that once at server start, so it is in effect from the first i3 session and not before. -
MAC cloning is off unless you passed
--mac-stableor--mac-random; theMAC:line in the verification summary says which. If you did enable it and a captive portal or MAC-allowlisted network rejects you, that's why —sudo rm /etc/NetworkManager/conf.d/00-macrandomize.conf.--mac-randomadditionally breaks anything holding a long-lived outbound socket: a fresh MAC per activation means a new DHCP lease and a new IP on every reconnect, so a remote-access agent (ConnectWise Control, Tailscale, an SSH reverse tunnel) drops with "the network connection has been disconnected".--mac-stablekeeps a per-network address, so the lease survives.On a VM neither is safe without host cooperation. A hypervisor's vSwitch forwards only frames whose source MAC it assigned to the vNIC. Hyper-V exposes this as Settings → Network Adapter → Advanced Features → Enable MAC address spoofing, off by default, and with it off any cloned MAC — stable or random, the switch doesn't distinguish — gets the guest's traffic dropped, and it stays dropped across reboots because each boot re-applies it. If a VM deployed by an older version of this script has no network, that's the first thing to check:
ip link show # active MAC vs. the one Hyper-V assigned sudo rm /etc/NetworkManager/conf.d/00-macrandomize.conf sudo systemctl restart NetworkManagerRe-running the deploy does the same thing for you.
-
Verify monitor mode before relying on it:
sudo airmon-ng start wlan0.
Kali's own zsh config is left intact; the script appends a marker-guarded block with
PATH, wordlist variables, aliases, and lfcd — a wrapper around the lf file manager
that leaves you in whatever directory you quit from. Oh My Zsh is deliberately not
installed — Kali already ships syntax highlighting and autosuggestions, and OMZ only
adds startup cost. To remove, delete the block between the kali-deploy-physical
markers in ~/.zshrc.
For Kali as a Hyper-V guest that you reach with a remote-desktop agent instead of sitting at it. Configuration only — no tools, no firewall, no dotfiles.
sudo ./kali-deploy-vm
sudo SCREENCONNECT_URL='https://<you>.screenconnect.com/Bin/ConnectWiseControl.ClientSetup.deb?e=Access&y=Guest&t=' \
./kali-deploy-vm --only screenconnectPhases: base hyperv desktop i3 polybar kitty shell screenconnect
Options are the same grammar as the other two, plus --autologin and
--compositor <xrender|glx|egl>.
The split is deliberate: kali-deploy-physical is for hardware you sit at, and
stays that way. It hardcodes picom --backend glx, which is correct on a real
GPU and has worked on that deploy for as long as it's existed — there's no VM
accommodation in it, and nothing here asks it to grow one. Pick between them on
the target, not on the options.
If you want the tools, use kali-deploy-physical; this script installs none.
No compositor. A Hyper-V guest has no hardware GLX — Mesa falls back to
llvmpipe. picom --backend glx fails after it has already mapped the composite
overlay window, and that overlay is full-screen, opaque, and never made
input-transparent: a black screen that swallows every keystroke, with i3 running
fine underneath. Beyond that, on a box reached by screen-scraping, compositing
buys nothing you can see and costs bandwidth on every frame. --compositor xrender opts back in with the backend that has no GL dependency.
No idle lock, no DPMS blanking. On a remote box a blanked screen is
indistinguishable from a hung one, and an idle lock you didn't ask for means
typing a password down a laggy link to get back into a session you never left.
$mod+Escape still locks manually.
Nothing that assumes hardware. The battery module, the wireless module and
the backlight keys are emitted only if /sys says the hardware is there — so the
bar has no permanently-blank BAT0 and no permanent "wlan down". These are
probed, not assumed, so the script is still correct on bare metal.
Two smaller ones that matter in practice: sync_to_monitor no in kitty.conf
(vsync against a display that isn't real is what makes kitty paint late or appear
to hang on software rendering), and $mod+Shift+Return bound to xterm as a
fallback, because kitty wants OpenGL 3.3 and a tiling WM whose only terminal
binding is broken is a box you can't use.
It asks you for the URL. Run the script with nothing set and the phase prompts, reads it back for proofreading, and lets you re-enter up to three times:
[+] The ScreenConnect agent needs the installer URL from your instance.
[+] Access -> Build Installer -> Linux -> copy the .deb link.
[+] No clipboard in the guest yet? Two ways round it:
[+] - VMConnect menu: Clipboard -> Type clipboard text
[+] - or serve the .deb off the host and type a short LAN URL
URL: https://acme.screenconnect.com/Bin/...
You typed:
https://acme.screenconnect.com/Bin/ConnectWiseControl.ClientSetup.deb?e=...
Correct? [Y/n]
[+] got a URL for https://acme.screenconnect.com
The proofreading matters because this is the phase that bootstraps the clipboard — until the agent is up, a Hyper-V guest in a basic VMConnect session has no path from the host clipboard, and the URL is ~150 characters of session parameters. If you have to type it by hand, a one-shot prompt that only fails at download time is not usable. Surrounding whitespace is trimmed, since Type clipboard text can pick up a stray leading space or trailing newline.
The read-back goes to /dev/tty, not through stdout — stdout is teed to
/var/log and the URL carries session parameters (y=, c=, s=), so echoing it
into the transcript would defeat every other guard here. Only the host is logged.
The URL is also never passed through the run() helper (which prints its
arguments under --dry-run), and it's scrubbed out of curl's stderr before any
error is printed.
If you'd rather not type it at all: serve the .deb from the Windows host
(python -m http.server 8000) and type a short LAN URL instead — the phase takes
any URL, not just a Control one.
The prompt appears only when stdin is a terminal and not under --dry-run.
SPEC.md rules out interactive prompts because a prompt that hangs an unattended
run is the exact failure the whole spec exists to avoid — so from cron, a pipe, or
CI this falls straight through to the skip path instead of waiting. Pre-supply it
to skip the question entirely:
| Variable | Meaning |
|---|---|
SCREENCONNECT_URL |
ClientSetup URL from Access → Build Installer → Linux |
SCREENCONNECT_DEB |
path to an already-downloaded .deb; takes precedence |
Same variable names kali-deploy-physical uses, so a box that gets one script can
be handed the other unchanged.
Press Enter alone and the phase skips and prints what to do — it is never a reason for the run to fail, and the desktop is already up by the time it runs.
The phase installs the X libraries the agent links against before the .deb,
because a missing one produces an agent that installs cleanly, starts cleanly, and
shows you a black rectangle. libicu is resolved dynamically since its version
suffix moves with every Debian import.
This phase takes a .deb only — prefer the one your Control server builds.
ClientSetup.sh takes its RPM branch whenever rpm(1) exists, and on Debian that
fails: ConnectWise's noarch RPMs ship unsigned and without file digests, while
Debian's rpm defaults %_pkgverify_level to all, so the install dies on
does not verify: no digest. A .deb skips the branch entirely.
Off by default. With a greeter, a box that reboots comes back to a locked login
screen the agent can still show you — you lose a password prompt. With
--autologin it comes back to an unlocked desktop holding your keys, your loot and
your shell history, reachable by anyone who reaches the agent. It's offered because
some agent configurations only attach to an active user session, but it isn't a
choice to make on someone's behalf.
Host-side on a Gen 2 VM — the guest's hyperv_drm takes its mode from the host and
xrandr can't add modes it was never offered. From an elevated PowerShell prompt,
VM powered off:
Set-VMVideo -VMName <name> -HorizontalResolution 1920 -VerticalResolution 1080 -ResolutionType SingleCtrl+Alt+F3 for a TTY, log in, unstick (aliased by the shell phase — it's
pkill picom; i3-msg restart), then Ctrl+Alt+F7. That path works regardless of
what's on the display, so you're never actually locked out.
For a box you reach over Tailscale rather than sit at. Installs the Kali tool set with
no GUI, joins a tailnet with Tailscale SSH as the primary auth path, hardens OpenSSH as
a fallback, and restricts inbound to the tailscale0 interface.
sudo ./kali-deploy-remoteIt takes the same flags as the physical script — --dry-run, --skip, --only,
--list-phases, --print-packages, --help.
Phases: base tools tailscale ssh firewall tmux shell dots wordlists
Environment knobs: TS_AUTHKEY, TS_ADVERTISE_TAGS, SSH_PUBKEY, SKIP_TAILSCALE=1,
SKIP_UFW=1. The last two are equivalent to --skip tailscale / --skip firewall and
are kept so older invocations don't break.
It will not disable password auth or enable the firewall unless it can prove you have another way in (an installed key, or Tailscale already up) — a box that can't reach itself is worse than one with password auth on.
Like the physical script it keeps Kali's own zsh and appends a marker-guarded block —
including lfcd, the lf file-manager wrapper that leaves you in whatever directory
you quit from. Oh My Zsh and the agnoster theme are deliberately not installed.
Agnoster needs powerline glyphs in the terminal you're connecting from, so over SSH
from anything unconfigured it renders as boxes.
Your shell auto-attaches to a main tmux session on interactive SSH login, so a
dropped connection never kills your work. For a plain shell — the day tmux itself is
what's broken — connect with NO_AUTO_TMUX=1.
A full transcript goes to /var/log/kali-deploy-remote-<timestamp>.log.
See TMUX-TAILSCALE-CHEATSHEET.md.
Use kali-deploy-wsl. This script has no WSL handling at all —
no detection, no default skips, no WSL-only packages. Run it on a WSL box and it will
install a tailscaled and a listening sshd, because that is what you asked for.
Kali as a WSL2 instance on a Windows host: the box you sit at all day and enter with
wsl.exe from Windows Terminal.
sudo ./kali-deploy-wslWSL support used to be a detected profile inside kali-deploy-remote. It moved because
that script is its access layer — Tailscale SSH, a hardened OpenSSH, and a default-deny
ufw — and on WSL all three are wrong, so running it there meant running it with the whole
thing switched off. A target that needs a script's defining feature disabled is a
different target, not an option of that one, which is the split SPEC.md already argues
for. Here the access layer is absent, not skipped: no tailscale, no sshd, no
ufw, and no phases for them.
The move is complete in both directions: kali-deploy-remote no longer contains
is_wsl() or any WSL branch, and the suite's R8 was inverted to pin that — it creates
/run/WSL, runs the remote script, and asserts nothing changes. A removed special case
needs a test more than a present one does.
| Left out | Why |
|---|---|
| i3 / polybar / picom | No window manager. WSLg gives each GUI application its own Windows window, so there is no session for a WM to own and kali-desktop-i3 installs a stack that cannot start. GUI applications do work — see the wslg phase. |
| kitty | The terminal is a Windows application. Configuring a Linux terminal emulator would configure the wrong machine. |
| font probing | Same reason — fc-list here answers a question about the guest while Windows draws the glyphs. |
| tailscale / ssh / ufw | Run Tailscale on the Windows host; the tailnet is reachable from inside without a second tailscaled. WSL2 is NAT'd behind the host, so inbound is Windows Firewall's business. |
| MAC cloning, monitor mode, bluetooth | The adapters belong to Windows and WSL2 sees none of them. The 802-11, wireless, bluetooth and rfid metapackages are excluded from --with-metas for the same reason. |
A wslconf phase — the only phase in this repo that configures the boundary with the
Windows host rather than anything inside Kali. It writes /etc/wsl.conf with systemd,
[interop], and the automount metadata option, that last one being the fix for
every file under /mnt/c reading as 0777 root:root — which makes chmod a no-op, stops
ssh accepting a key stored there, and shows the whole tree as mode-changed in git.
The phase is non-destructive by construction: wsl.conf is routinely hand-edited, so it
creates the file, appends a section that is entirely absent, and never rewrites a
section that is already there. Where a section exists but lacks a key, it prints the
line to add rather than editing around it. Changes need wsl --shutdown from Windows,
which the script says rather than pretends to do.
Kali under WSL runs GUI applications fine, but renders them on the CPU out of the box, and the symptom looks like a broken display rather than a missing driver:
[GFX1-]: glxtest: libpci missing
[GFX1-]: glxtest: libEGL missing
[GFX1-]: glxtest: EGL test failed
[GFX1-]: No GPUs detected via PCI
Every one of those lines is a missing package, not a missing GPU. Windows is passing
the GPU through — it appears as /usr/lib/wsl/lib/libd3d12.so — but Kali ships no Mesa
userspace, and Mesa's d3d12 gallium driver (in libgl1-mesa-dri) is the bridge.
libpci3 is what lets glxtest enumerate the bus at all, which is why its absence reads
as "No GPUs detected".
So the phase installs libgl1, libegl1, libglx-mesa0, libgl1-mesa-dri, libpci3,
mesa-utils and firefox-esr — gated on /mnt/wslg, since without a way to display a
window that is ~200MB of driver for nothing. Firefox is named rather than inherited:
it is on a stock image but not on a netinst, and it is firefox-esr — plain firefox has
no candidate on Kali, so naming the obvious one installs nothing silently.
The phase also compiles the dconf system db. Kali's /etc/dconf/profile/user declares
system-db:local but never builds it, so every GTK app opens with an alarming
unable to open file '/etc/dconf/db/local' ... expect degraded performance. Cosmetic, and
noisy enough to send you debugging a display problem you don't have.
Verification reads the renderer, not the package list — d3d12 means the GPU bound,
llvmpipe means Mesa fell back to software and every window will feel like it.
Opening a page from inside WSL uses the Windows browser. Two real executables land in
/usr/local/bin — not shell aliases, so scripts, sudo, and non-zsh shells get them too:
| Command | What it is |
|---|---|
edge <url|file> |
Edge on the host. Probes both Program Files locations at run time, so a Windows update can't stale it. Arguments naming a real file are translated to \\wsl.localhost\… paths; URLs and flags pass through. |
wsl-browser <url|file> |
The host's default handler, whatever that is. |
$BROWSER points at wsl-browser, which is the part an alias can't do: gh,
git web--browse, xdg-open and Python's webbrowser all exec $BROWSER, and a child
process can't call a zsh function. Neither is gated on WSLg — msedge.exe is a Windows
process needing interop, not a Linux display, so both work on WSL1 too.
Firefox stays, as the deliberate exception. It's the browser for anything going through Burp or ZAP, and the reason isn't speed:
- Burp's CA cert stays in a Linux profile instead of the Windows system store, where it would make your daily-driver browser trust an intercepting CA
/etc/hostsvhost entries you add in Kali actually resolve — Edge uses Windows' resolverproxychainsandtorsocksdon't apply to a Windows process at all- Engagement traffic never touches a browser signed into your personal account and syncing history to it
The rest lands in the shell block: pbcopy / pbpaste against the Windows clipboard,
open (via wslview if you have it, else explorer.exe), cdwin and winhome for the
host profile directory, and winhost for the host's IP as seen from inside. tmux copy-mode
y pipes to clip.exe, so a selection lands on the clipboard you are actually going to
paste from.
wslu is deliberately not installed: Kali carries no such package, and listing a name
that never resolves is what T1 exists to reject. Adding Debian's repo to a rolling release
is ruled out by SPEC.md. explorer.exe covers the gap, and wslview is probed at
runtime in case you install it by hand.
The headless set plus ffuf, gobuster and sqlmap — on a workstation those are
baseline, not an extra. --with-metas additionally installs the ten software-only
kali-tools-* metapackages for the breadth kali-deploy-physical gives a laptop; it is
off by default because it is a multi-gigabyte install onto a disk that is a growable file
on the host's NTFS volume.
The reverse of kali-deploy-remote, where a dropped SSH connection kills your work so
attaching every login to a persistent session is a straight win. Here every Windows
Terminal tab is a fresh login shell, and auto-attaching them all to one session means the
second tab steals the first one's panes. Set AUTO_TMUX=1 for the remote behaviour.
Install a Nerd Font on the Windows side and select it in the Windows Terminal profile.
Nothing inside the instance can check this, so the script says so instead of ticking it;
VINNY_ASCII=1 is the fallback.
Stocks a Tails live session with the tools it doesn't ship: Nmap (and Ncat),
Impacket, NetExec, Sliver, Burp Suite Community, proxychains, neovim,
build-essential, an Apache server, and a staged copy of Mythic. The awkward part is that every fetch has
to go through Tor, as the amnesia user — dealing with those quirks, and the
permissions problems that come with them, is the reason the script exists.
sudo ./test-with-tailsBefore you run it
- Set an administration password at the Tails greeter when you log in. Without one there is no root, and this must be run from a root terminal.
- If
git clonefails over Tor,wgetthe raw file instead andchmod +xit.
After it finishes
- Burp Suite is downloaded but not installed — run the GUI installer yourself:
~/burpsuite.sh. It's the current Community build, ~380 MB, which makes it the slow part of the run over Tor. - Mythic is cloned to
~/Mythicbut not built or started. See below. - Tails is amnesic. Unless you are running from Persistent Storage, all of this is gone at the next boot and the script has to run again, over Tor.
Operating notes
- All traffic goes through Tor, and Tor carries neither ICMP nor UDP. Nmap has
to run with
-Pn, wrapped inproxychainsortorify. - Standing up the Apache server opens a listener on your Tails session — it pokes a
hole in your persec. It is started but not enabled (nothing survives the session
anyway);
sudo systemctl stop apache2if you don't need it.
Two C2s, and only one of them actually runs here.
sliver-server installs to /usr/local/bin — one static Go binary, ~270 MB over
Tor, arch-suffixed asset (sliver-server_linux-amd64). It works offline once it's
down: first run unpacks its embedded toolchain rather than fetching one, so implant
compilation needs no network at all. Two limits worth knowing before you rely on it:
-
torifydoes nothing for Sliver itself. torsocks isLD_PRELOADover libc, and Go makes raw syscalls straight past it — so Sliver's traffic goes direct and the Tails firewall drops it. Anything that reaches out (armory install) needs Go's own proxy support instead:HTTPS_PROXY=socks5://127.0.0.1:9050 sliver-server
-
Implants can't call back to a Tails box without an onion service you set up by hand — and Tails regenerates
torrcevery boot, so that's per-session work. As a local implant builder and workbench it's fine; as a callback server it isn't.
Mythic is staged, not installed. It's a Docker stack, and two things about Tails
get in its way. The Docker daemon runs as root, and root's traffic is not torified — the
Tails firewall drops it, so image pulls fail, and so does make, which pulls its
builder image from ghcr.io. And the images run to several GB against a filesystem
that is RAM. So the script installs the daemon and clones the repo:
cd ~/Mythic && sudo make # builds mythic-cli
sudo ./mythic-cli start # brings the stack upBoth of those need egress the Tails firewall doesn't give the daemon, so bringing
Mythic up is a decision you make with a plan for that, not something the script does
behind your back. Sliver is the one to reach for on a live session; the better use
of Mythic from Tails is as an operator, pointing a browser at a Mythic you already
run elsewhere (https://<server>:7443), which needs no Docker here at all.
NetExec installs as nxc in /usr/local/bin — that's the command name, not
netexec. Its Linux build comes from a GitHub release asset, and because the newest
release doesn't always carry one (v1.5.1 ships no assets, so /releases/latest/
404s) and the asset name has changed shape across versions, the script asks the API
for the newest release that actually has a Linux build rather than hardcoding a URL.
If it can't find one it says so and points you at the releases page.
Still a beta. It was imported as written rather than rewritten, so it has no
phase engine, no --dry-run, no verification pass, and no idempotency guarantee —
none of what makes the Kali scripts safe to re-run or testable in a container. A
failed step is silent; read the output. Treat it as a convenience for a throwaway
live session, not as a deploy you'd trust unattended.
Deploy scripts are hard to test because the failure mode is a half-configured machine.
tests/ validates both Kali scripts against a real Kali container before either
touches hardware:
tests/run-tests.sh # static analysis + container suite
tests/run-tests.sh --static-only # bash -n + shellcheck onlyCovers, for each script: every package name resolving to an installation candidate,
full dependency resolution via apt --simulate, every binary the verification pass
ticks actually being delivered by that package set, --dry-run mutating nothing, the
config phases running for real with correct ownership, i3 -C / tmux validating the
generated configs, idempotency across repeated runs, backups surviving a re-run,
graceful degradation with no systemd, the SSH and firewall lockout guards, injected
bad-package handling, and argument validation.
The suite reads its package list from --print-packages and its expected-binary list
from --print-checked-binaries, so neither can drift out of sync with the script.
test-with-tails gets the static checks (bash -n, shellcheck) and nothing more.
Tails publishes no container image, and the script's install path depends on Tor and
on the live session's amnesia user — neither of which a container reproduces, so a
container pass would prove nothing about the run that matters.
That second one exists because of a real bug: the physical script printed a green tick
for seven tools it never installed. Four of them (netexec, bloodhound.py, ffuf,
gobuster) happened to be in the ISO's kali-linux-default, so on a stock install they
looked fine — luck, not intent, and gone on a netinst. The other three (mitm6,
certipy-ad, enum4linux-ng) were missing on every path. A red X at the end of a
40-minute deploy, on the day you need the box, is how you would have found out.
Requires docker. It pulls kalilinux/kali-rolling and
koalaman/shellcheck and modifies nothing on the host.
CI runs the static checks on every push. The full container suite runs weekly on a schedule, on pull requests, and on manual dispatch — Kali is a rolling release, so a metapackage can be renamed or a new dependency conflict introduced without anything in this repo changing, and the scheduled run is what catches that before a deploy does.
archive/ubuntu-autodeploy-v5 is the last Ubuntu version and the source of the i3
desktop and the apt-resilience helpers the Kali scripts inherit. It fixed a series of
real v4 deploy failures — a package with no installation candidate aborting the whole
apt batch under set -e, setcap on a venv symlink, gunzip on a file that wasn't
gzip — which is why none of these scripts use set -e. See archive/ for
the full story.
MIT — see LICENSE.