What it is
A manual? At two in the morning? With TABLES?
slag.sh is a single bash script for retiring a Linux server: taking it out of service so that nothing it held can be recovered or used. It has three phases, and you run them in order:
- inventory lists what lived on the host: processes, services, their configs and state, environment secrets, users' keys and dotfiles. It writes a directory and a tarball.
- checklist turns the inventory into
ROTATION-CHECKLIST.md, the credentials and identities you must rotate or revoke. - wipe destroys the data on whole disks and proves, by reading every byte back, that they hold nothing but zeros. It ends with a receipt.
The wipe never runs on its own. slag.sh all runs the first two phases and stops, and prints the wipe command for you to run yourself once the inventory is off the machine.
The reasoning behind the order: no tool inside a machine can prove that every copy of its data is gone. Provider snapshots, storage-layer copies and retired SSD cells are out of reach. So the wipe does what can be proven, the receipt says what that was, and the checklist makes anything recovered from elsewhere worthless.
Quick start
Boot the server into the provider's rescue system if it has one (see rescue mode or in place). As root:
# slag.sh inventory --out /root/slag.out # slag.sh checklist --out /root/slag.out # scp -r /root/slag.out /root/slag.out.tar.gz you@elsewhere: # slag.sh wipe --target /dev/sda --target /dev/sdb --out /root/slag.out
- The wipe lists every target with its model, serial and size and asks you to type the device names (sda sdb).
- It starts itself as the systemd unit
slag-wipeand your terminal follows its log. If the SSH session drops, the wipe continues; reconnect and runjournalctl -fu slag-wipeortail -f /run/slag.*/slag.log. - When it finishes, copy the receipt (
/run/slag.*/RECEIPT.txt, also printed on the console) off the machine. After an in-place wipe a new SSH login usually fails, so add--serveor--send-receiptto the wipe; see getting the receipt off the machine. - Work through
ROTATION-CHECKLIST.md, delete the provider's snapshots and backups of the server, and destroy the instance.
The inventory directory holds secrets: it copies process environments, which is where API keys and database passwords tend to live. It is created readable by root only (directory 700, tarball 600); if --out names an existing directory others can read, the script warns. Move it somewhere safe, and shred it once rotation is done.
Requirements
- Linux, bash 4 or later, run as root for
inventoryandwipe.selftestand--helpneed no root. - For the wipe: coreutils (
dd,od), diffutils (cmp), util-linux (lsblk,blockdev,blkdiscard,wipefs), e2fsprogs (filefrag), findutils. - Recommended: a static busybox. The wipe copies it into RAM alongside everything else.
- For
--serve: a static busybox with thehttpdapplet. For--send-receipt: curl, and for https a CA bundle. Each option is refused before the confirmation if what it needs is missing. - Optional, used when present:
nvme-cli(NVMe sanitize and format),hdparm(ATA secure erase, HPA check),sg3-utils(SCSI sanitize),mdadmanddmsetup(stopping RAID and device-mapper devices built on the targets). A step whose tool is missing is recorded asunsupported, never silently skipped. - systemd for the detached run. Without it the wipe detaches with
setsid.
inventory
slag.sh inventory [--out DIR]. Default output: /tmp/slag.<timestamp> plus a .tar.gz of it.
- Processes: for each running process, its executable, working directory, command line, environment (marked "secrets likely") and open files.
- systemd services: each unit's file, drop-ins, environment files, working, state, configuration, log and cache directories.
- Packages: package-owned config files, which of them were modified, and files written since the system was provisioned.
- Containers: if Docker is installed, each container's mounts and environment.
- Users: each user's
.ssh,.gnupg, cloud credentials, shell history,.netrc,.pgpassand similar. - Always sensitive:
/etc/shadow,/etc/ssh,/etc/ssl/private,/etc/letsencrypt, cloud-init state, the journal, swap.
Every path it finds goes into meta/paths.txt. The wipe reads that list for its per-file proof when you pass the same --out.
checklist
slag.sh checklist [--out DIR] reads the inventory and writes ROTATION-CHECKLIST.md in it, with checkboxes for:
- SSH host keys to remove from clients'
known_hosts, user keys to rotate, SSH certificates to revoke; - TLS certificates to revoke (not just reissue) and ACME account keys;
- every environment variable on a running process whose name looks like a secret (
KEY,TOKEN,SECRET,PASS,DSNand so on), listed by name; - database passwords, when a database was running;
- the host's entries in directories, monitoring, config management and DNS, and its access to backup storage;
- provider-side work: delete every snapshot and image of the server, then destroy it.
wipe, step by step
slag.sh wipe --target /dev/DISK [--target ...] [options]. The steps run in this order:
Check the targets
Each
--targetis resolved (symlinks such as/dev/disk/by-id/...are followed) and must be a whole disk. Partitions, LVM and device-mapper volumes, RAID devices,/dev/mappernames and duplicates are refused; loop devices only with--allow-loop. If any disk on the machine is not a target, the wipe refuses and lists it, unless you pass--allow-partial. With hardware erase on, every namespace of a targeted NVMe controller must also be a target, because a sanitize erases them all.Confirm
The prompt shows each target's model, serial and size. You type the device names. Without a terminal on stdin,
--yesis required instead.--serveand--send-receiptare checked before this prompt, and the receipt URL is printed above it, on your terminal and on the console.Copy into RAM and self-test
bash, the script, busybox and every tool the wipe calls are copied into a fresh tmpfs with their shared libraries and loader, so nothing afterwards runs from the disks being wiped. A self-test runs there: the tools start, the verify catches a short read and a non-zero byte, zeroize clears a test file, and the strike zeroes exactly the blocks of a test file that hold planted keys. If anything fails, the wipe stops before touching a disk.
Detach
The wipe runs as the systemd unit
slag-wipe(or undersetsid), with a locale and time zone that need nothing from disk. Your terminal follows the log and exits with the verdict. With--serve, the receipt server starts now, from the tmpfs, as its own unitslag-serve.Map the sensitive files
While the filesystems are still mounted, each sensitive file on a target is mapped to its extents on the disk. See per-file proof.
Quiesce
Swap off, then a check that no swap remains on a target. Services that hold files open for writing on the targets are stopped, and journald is told to let go of
/var. Then sync, an emergency read-only remount of every filesystem, the partitions under the target mounts set read-only at the block layer, and RAID, device-mapper and bcache devices built on the targets stopped from the top down. Anything still writable, swapped to, or stacked on a target makes the verdict FAIL.Strike: keys first
Every target, before any is erased: the mapped sensitive-file extents are zeroed, then every byte is read and each block holding a key signature is zeroed and read back as zero. Live files, deleted files, journal and swap alike. If the run is cut short after this, the keys are already gone. See the strike.
Hardware erase, then wait
Every target is hardware-erased before any of them is zeroized. NVMe: sanitize with crypto erase, then block erase, sent once per controller, with the sanitize log polled until it finishes (
--hw-timeout); if sanitize is unsupported, an NVMe format with secure erase. ATA: secure erase, unless the drive is frozen or already has a password, which is recorded as such. If an ATA erase fails after a password was set, the password is removed again. SCSI and SAS: sanitize where supported. Virtual disks have no hardware erase and say so.For each target: HPA check and wipefs
On ATA disks, a host protected area that hides sectors fails the run, with the
hdparmcommand that removes it. Thenwipefsclears filesystem and partition signatures.Zeroize
Every block is read and zeros are written only where a block is not already zero. See zeroize.
Fence (in place only)
When the target is the running root disk, the whole disk is set read-only right after it is zeroized, so the running kernel cannot write to it before the verify.
Verify
Every byte is read back with direct I/O and compared with zeros. It passes only if exactly the device's size was read and every byte was zero. A read error or a short read is a FAIL. With
--sample N, only the first block, the last block and N random 1 MiB blocks across the whole disk are read, and the receipt says the verify was sampled.Re-read the file extents and signatures
The mapped file extents are read again and must be zero. After a 10 second settle, the offsets where a signature or partition start was found before the wipe are read again too.
Pattern (only if asked)
With
--pattern NAME, and only after every target has verified zero: each pass of the named method over every byte, then the last pass read back in full. See overwrite patterns.Reclaim (only if asked)
With
--reclaim, after every target has verified: discard, then verify again. See reclaim.Receipt
Every step and its outcome, written to the console, the log and
RECEIPT.txtin the tmpfs, with its SHA-256 on the console. With--serveit is published at the receipt URL; with--send-receiptit is uploaded once. See the receipt and getting it off the machine.
Zeroize
I only kept the zeros that cover my backing file. The old data is IN the backing file. Delete it too, or it's a snapshot with extra steps!
Zeroize brings every block of a target to zero and leaves alone the blocks that already read zero. It does not write every block.
It reads the disk in 16 MiB chunks with direct I/O. A chunk that reads all zero costs one read and one compare. When a chunk holds data, it finds each 64 KiB piece that is not zero, joins neighbors into a single write, and writes zeros over them in place. Every read and write is checked for its exit status and byte count.
When the disk has bad spots
A 16 MiB chunk that cannot be read is zeroed whole, since its contents are unknown, and the pass carries on. A write error is recorded and the pass carries on. At the end the step is failed, with the number of chunks that had read or write errors and the first offset of each, and the device fails. A dying disk still gets every block zeroed that can be reached. After 64 failed chunks in a row the pass stops (the device has probably gone away) and the receipt says how many bytes were left.
Why not write zeros over everything
On thin storage (a qcow2 image, an LVM thin volume, a Ceph or SAN volume at a provider) the approaches end differently:
- Zeros everywhere allocates every block. In the lab, a 10 MiB qcow2 overlay of a 3 GiB disk grew to the full 3 GiB.
- Discard, then zeros is worse. The discard frees the blocks that held the data, and the zeros land on newly allocated blocks. The old contents stay in the storage's free space, never overwritten.
- Zeroize writes only where data was. A block that held data is already allocated, so the zeros go over it where it sits. A block that reads zero is unallocated or already zero: nothing in it to destroy, and writing it would only allocate space.
In the lab, a 512 MiB thin raw target held 536,875,008 allocated bytes before the wipe and exactly 536,875,008 after: every block of data was overwritten where it was, and nothing new was allocated.
Overlays and backing files
A qcow2 overlay, or any copy-on-write layer over a base image, is a snapshot by another name. Zeros written over data that lives in the base cannot go into the base, so the overlay stores them itself and grows by that much. The old data stays in the base file. In the lab, the root disk's overlay went from 10.4 MiB to 1.17 GiB: zeroize wrote 1,258,225,664 bytes, all of them over data that came from the base image. (The old wipe grew the same overlay to the full 3 GiB.) The guest sees zeros; the base image still holds the original. Delete or wipe the base image at the provider, exactly as you would a snapshot.
What zeroize does not reach
- SSD flash cells. The controller sends each write to a fresh cell, so no write reaches a worn or retired cell. The hardware erase covers those.
- Snapshots and copies on the storage side. Delete them at the provider.
Speed
Empty space is fast: reading is the cost, about 12 ms per 16 MiB chunk on top of the disk's read time. Space full of data is slower, because each 64 KiB piece is checked separately, roughly 1 ms each. A completely full 1 TB disk adds a few hours of CPU time to the read. Mostly empty disks, the usual case for a server, are dominated by read speed.
--passes N is accepted for old command lines and ignored with a warning: a second zeroize would find every block zero and write nothing.
Reclaim
With --reclaim, after every target has verified, the wipe discards each target (blkdiscard) to give space back to thin storage and send a TRIM to an SSD, then verifies every target again in the same mode as the first verify. On an in-place wipe, the disk's read-only fence is lifted for the discard and set again before the second verify.
- Reclaim runs only after zeroize and the first verify, so the blocks it frees already hold zeros.
- If any target failed its first verify, reclaim is skipped and recorded as skipped.
- A discard that is unsupported or errors is recorded, but does not by itself fail the device. The second verify decides.
- Some SSDs do not promise that a discarded block reads back zero. If the second verify finds data, the receipt says the device does not read zeros after a discard, and that device fails, because you asked for the reclaimed state to be proven.
Without --reclaim, nothing is ever discarded.
In the lab, --reclaim on an in-place wipe shrank the root overlay from 11,083,776 bytes to 598,016, and a thin raw target's allocation went from 536,875,008 bytes to 0, with the second verify passing on both.
Per-file proof
Before quiescing, the wipe maps sensitive files to the places they sit on the target disks:
- a built-in list:
/etc/shadow, the SSH host keys,/etc/ssl/private,/etc/letsencrypt,/etc/wireguard,/etc/krb5.keytab,/root/.sshand each user's.ssh,/var/lib/tailscale; - plus every path in the inventory's
meta/paths.txtwhen you pass--out DIR(or setSLAG_OUTDIR).
Directories expand to the files under them. Only filesystems on a target disk are walked: a listed path on another filesystem is skipped, and the walk stops at mounts that leave the targets, such as /proc, /sys and tmpfs. Up to 20,000 files on the targets are mapped; the receipt says if that cap was reached. Each file's physical extents come from the filesystem (filefrag), converted to byte offsets on the disk using the partition's start.
After the verify, exactly those extents are read again with direct I/O and must be zero. The receipt gives the number of files, extents and bytes re-read, and names any file whose extents were not zero. It never shows a file's contents.
Some files cannot be mapped this way: data stored inline in the inode, delayed-allocation, encrypted or compressed extents, and files on LVM, RAID, multi-device btrfs or overlay filesystems, where an extent is not a simple offset on one disk. The receipt counts them as unmapped with the reason. They are still covered by the full verify; they just lack their own line.
The strike: keys first
You read the whole disk just to find my SSH key? Before the wipe even starts? Even the copy I deleted in 2019?
A full wipe of a big disk takes hours. If the run is cut short (the session drops, the provider kills the instance, the power goes), whatever the zeros had not reached is still there, and that may include the keys. The strike destroys the keys first.
It runs on every target after quiesce and before any hardware erase:
- Mapped files. The extents of the mapped sensitive files are zeroed. (From a rescue system nothing is mounted, so only the scan runs.)
- Raw scan. The whole device is read in 16 MiB chunks, each read running 4 KiB into the next so a key across a chunk edge is found whole. Where a key signature matches, its 64 KiB block is zeroed, along with the following blocks out to the pattern's span and at least 4 KiB past the match.
Because the scan reads the raw device, not files, it finds keys wherever they sit: live files, deleted files still in free blocks, journal blocks, old copies of a rewritten key, and swap. Each struck range is read back with direct I/O and must be zero. As in zeroize, zeros go only to blocks that are not already zero, so thin storage allocates nothing. The receipt gives, per device, the hits per pattern and the ranges and bytes struck. It never shows the matched bytes.
| Pattern | Matches | Span |
|---|---|---|
pem-private | -----BEGIN/END … PRIVATE KEY-----: RSA, EC, PKCS#8, encrypted, OpenSSH, and GPG PRIVATE KEY BLOCK | 128 KiB |
openssh-key | openssh-key-v1, raw (memory, swap) or base64 | 128 KiB |
putty-key | PuTTY-User-Key-File-N: | 128 KiB |
age-key | AGE-SECRET-KEY-1… | 64 KiB |
shadow-hash | crypt(3) hashes in shadow form: $1$, $2b$, $5$, $6$, $y$, … | 64 KiB |
wireguard-key | PrivateKey = <44-character base64> | 64 KiB |
tailscale-key | privkey:<64 hex>, "PrivateNodeKey" | 128 KiB |
jwk-private | a JWK private member "d": "…" (ACME account keys) | 64 KiB |
luks-header | LUKS and LUKS2 backup header magic, on a 4 KiB boundary only | 16 MiB |
aws-key | AKIA…/ASIA… key ids, aws_secret_access_key | 64 KiB |
url-credential | scheme://user:password@ (database URLs, .env files) | 64 KiB |
The patterns are greedy on purpose: a false positive only zeroes a block a few minutes early that zeroize clears anyway. The 16 MiB LUKS span covers the header and keyslot area; with those gone, the volume's data is ciphertext with no key. LUKS matches must sit on a 4 KiB boundary, as every real header does, because cryptsetup's and blkid's libraries carry the same bytes as a constant; those copies are counted in the receipt and left for zeroize.
Why keys
NIST SP 800-88 Rev. 2 (September 2025) points to IEEE 2883 for sanitization techniques and keeps one of its own: cryptographic erase, because destroying the key is decisive. IEEE 2883 sorts methods into Clear (overwrite every addressable block), Purge (the device's own sanitize commands, which also reach spare and remapped areas) and Destruct. Keys and the headers that wrap them are the data whose early loss makes the rest worthless or replaceable.
The strike adds no assurance to a finished wipe. Zeroize plus the full verify is Clear, and the verdict rests on the verify. A strike range that does not read back zero is recorded as failed, but the verdict still comes from the verify.
Options
--no-strike: skip it.--strike-only: strike, write the receipt and stop. No hardware erase, no zeroize, no verify. The disks are not wiped: the verdict isSTRIKE-ONLYand the exit status 3.--strike-patterns FILE: add your own, one per line,NAME<TAB>SPAN_KIB<TAB>ERE(span a multiple of 64; the pattern isgrep -Eon raw bytes). Every line is checked before the confirmation.
On a real machine
A Linode nanode (Ubuntu 24.04, 24.5 GiB disk and 496 MiB swap) with keys planted live, deleted, and raw in swap, wiped by slag from the rescue system: the strike scanned the disk in 2 minutes 27 seconds and the swap in 3 seconds. The keys were gone about two and a half minutes after launch, before zeroize had started. The verdict was PASS, and a second rescue boot read both disks as zero.
It costs one extra full read of each target. Chunks that read all zero are skipped after one compare.
What it cannot reach
The same places no overwrite reaches: SSD spare and remapped flash pages (the hardware erase covers those on bare metal), sectors a hard disk has reallocated, and provider snapshots and backups. See what it cannot prove.
Overwrite patterns
Some policies still name a multi-pass overwrite method: DoD 5220.22-M, Gutmann, VSITR, HMG IS5 and others. --pattern NAME runs one of them after the normal wipe has proved every target all zero, and leaves the method's last pass on the disk.
$ slag.sh patterns # the list below $ slag.sh wipe --target /dev/sda --pattern dod3 # zeroize, verify, then DoD 3-pass
None of the current standards need it. NIST SP 800-88 Rev. 2 and IEEE 2883-2022 treat one overwrite of every addressable location, verified, as Clear, and the zero pass with its full read-back already meets that. A pattern adds time and writes; use it when a policy names one.
What a pattern run does
- Everything a plain wipe does, unchanged: strike, hardware erase, zeroize, the full zero verify (
VERIFIED: /dev/X reads back all zero, …) and the re-check, for every target. - Only if every target verified zero: for each target, each pass of the pattern over every byte, from byte 0 to the device's size. A pass is a byte pattern (1 or 3 bytes, repeated in phase across the whole disk) or random data from
/dev/urandom. - The last pass is read back in full with direct I/O. Its SHA-256 must equal the SHA-256 of exactly the bytes written for that pass. On success the log prints
PATTERN VERIFIED: /dev/X holds the last pass of NAME, SIZE bytes, sha256 HASH.
The receipt gets a pattern: header line and one pattern: line per device with the SHA-256 the disk must hash to. Anyone holding the disk can check it later: sha256sum /dev/X must print the receipt's value. That proves the disk holds exactly the last pass and nothing else, and it works for a random last pass too, without knowing the random data.
The verdict is PASS, exit 0, when both proofs hold, and the verdict sentence says the disks now hold that pass, not zeros. A failure in any pass or in the read-back makes that device and the run FAIL, exit 1. A run without --pattern prints exactly what it printed before.
harmon
harmon is zeroize (verified), then one random pass, then the bitwise complement of that random pass (R INV). INV reads each block back from the disk just ahead of rewriting it as its inverse (byte b becomes 255 − b), so no copy of the random data is kept anywhere.
The methods
R is a random pass, INV the complement of the previous pass. Hex values are the bytes written, repeated.
| Name | Passes | Sequence | Source |
|---|---|---|---|
dod3 | 3 | 00 FF R | US DoD 5220.22-M (E): a character, its complement, random |
dod7 | 7 | 00 FF R R 00 FF R | US DoD 5220.22-M (ECE): (E), one random pass, (E) again |
gutmann | 35 | R×4 55 AA 924924 492492 249249 00 11 22 … FF 924924 492492 249249 6DB6DB B6DB6D DB6DB6 R×4 | Gutmann, USENIX Security 1996 |
schneier | 7 | FF 00 R R R R R | Schneier, Applied Cryptography (2nd ed., 1996) |
vsitr | 7 | 00 FF 00 FF 00 FF AA | German BSI VSITR |
ops2 | 7 | 00 FF 00 FF 00 FF R | RCMP TSSIT OPS-II (Canada; replaced by ITSG-06) |
itsg06 | 3 | 00 FF R | CSEC ITSG-06 (Canada, 2006) |
hmg-baseline | 1 | 00 | UK HMG IS5 Baseline |
hmg-enhanced | 3 | 00 FF R | UK HMG IS5 Enhanced |
afssi5020 | 3 | 00 FF R | US Air Force AFSSI-5020 |
ar380-19 | 3 | R 00 FF | US Army AR 380-19 |
navso | 3 | 00 FF R | US Navy NAVSO P-5239-26 |
gost | 2 | 00 R | Russia GOST R 50739-95 |
pfitzner | 33 | R×33 | Roy Pfitzner |
random | 1 | R | one random pass |
harmon | 2 | R INV | Harmon: zeroize (verified), random, then the complement of that random pass |
Gutmann and Schneier are exact from the source. The rest are the conventional readings that wiping tools such as nwipe and Eraser implement; several of the military documents are withdrawn or hard to obtain. Where a method says "a character" without fixing it, slag.sh uses 00 and its complement FF. Gutmann designed his patterns for early-1990s drive encodings and has said the full 35 passes were never needed for later drives. Your policy's own text wins over this table.
Limits
- Rescue systems only. In place, the disks are set read-only before the zero verify, so nothing can follow it.
--patternis refused before the confirmation when a filesystem on a target is mounted. - Not with
--reclaim(it would discard the pattern) or--strike-only(it does not wipe). - Thin storage becomes fully allocated: every block is written. On a VPS this can cost money and is never needed for Clear.
- Time: each pass is one full write of every target, plus one full read at the end. Gutmann on a 1 TB disk at 200 MB/s is about 50 hours.
- With
--sample, the zero verify is sampled; the pattern read-back is always full. - A pattern reaches only what the host can address, like the zero pass. See what it cannot prove.
The receipt
Every step records one outcome:
| Outcome | Meaning |
|---|---|
ok | The step ran and did what it says. |
unsupported | The device or system has no way to do this (a virtual disk has no hardware erase; a tool is not installed). |
frozen | An ATA drive's security is frozen by the firmware, so secure erase cannot be sent. |
failed | The step ran and did not succeed. Most failures make the device's verdict FAIL. |
skipped | Not attempted, with the reason (for example, reclaim after a failed verify). |
A real receipt from the lab, verbatim, for an in-place wipe of a VM's running 3 GiB root disk:
===================== SLAG WIPE RECEIPT =====================
tool: slag.sh 1.1.0
host: slaglab
started: 2026-10-30T17:57:56Z
finished: 2026-10-30T17:58:26Z
run: in-place (re-exec from tmpfs /run/slag.20261030T175756Z)
zeroize: conditional zero pass: every block read (direct I/O, 16 MiB chunks); zeros
written only to 64 KiB blocks not already zero, in place; no discard before it
verify: full read-back of every byte
reclaim: not requested; nothing was discarded
files: 8 files on the targets mapped to 8 extents (built-in list; 0 empty; 0 not on a target)
caveat: in-place: the running kernel still holds the target filesystems (read-only,
on devices set read-only). This receipt covers the disk as of the re-check
10s after the verify; a write the kernel makes after that is not covered.
device /dev/vda
model=? serial=slagroot size=3221225472 bytes
signatures ok 10 offsets noted (signatures and partition starts)
hw-erase unsupported no hardware erase path for vda (virtual device?)
wipefs ok /dev/vda: 2 bytes were erased at offset 0x000001fe (PMBR): 55 aa
zeroize ok conditional zero pass: 3,221,225,472 of 3,221,225,472 bytes read; 1,258,225,664 bytes written where a block was not already zero (19199 blocks of 64 KiB in 600 writes, in 93 of 192 16 MiB chunks)
fence ok
verify ok full read-back: 3221225472 of 3221225472 bytes read, all zero
file-extents ok 8 files, 8 extents, 32,768 bytes re-read, all zero
recheck ok 5 signature blocks still zero
zeroized: 3,221,225,472 bytes read, 1,258,225,664 bytes written where a block was not already zero; every block verified zero.
verify: full: 3221225472 of 3221225472 bytes read, all zero
files: 8 files, 8 extents, 32,768 bytes re-read, all zero
device verdict: PASS
host
file-map ok 8 files on the targets mapped to 8 extents (built-in list; 0 empty; 0 not on a target)
swapoff ok
swap-check ok no active swap on the targets
journald-relinquish ok
stop:unattended-upgrades.service ok
sync ok
remount-ro ok read-only: /boot/efi /
fence:vda1 ok
fence:vda15 ok
holders ok no md/dm/bcache device is built on the targets
VERDICT: PASS - the targeted devices read back as zero, every byte; no filesystem on them remains (in-place: as of the re-check 10s after the verify; the running kernel can still write later)
==============================================================
The receipt goes to the console (or the serial console), the log and RECEIPT.txt in the tmpfs, and its SHA-256 to the console. The tmpfs is gone when the machine powers off, so copy the receipt off first.
Getting the receipt off the machine
After an in-place wipe, new SSH logins usually fail. sshd starts a fresh copy of itself from the disk for every connection, and the disk is now zeros. In the lab, a new login after the wipe ended with Connection closed. Without help, the receipt is then only on the console, or in an SSH session that was already open. Two options fix that, and you can use both.
--serve ADDR:PORT
The wipe serves the receipt over plain HTTP from the tmpfs, with busybox httpd:
# slag.sh wipe --target /dev/vda --serve 10.8.0.5:8099 [slag] RECEIPT URL: http://10.8.0.5:8099/6ac9bef95959a4178fa0c4db1a61554d/ ... Type the device names to wipe, separated by spaces ("vda"): vda
ADDRis an IP address on this machine (IPv6 in brackets:[2001:db8::1]:8099) or a name that resolves to one. A name is resolved before the wipe, while name lookup still works. An address that is not on this machine, a missing or bad port, or a missing busyboxhttpdis refused before the confirmation.- Everything lives under one random path, a 32-character token from
/dev/urandom:slag.log(live, so you can watch the wipe), andRECEIPT.txt,RECEIPT.txt.sha256andVERDICTonce they are written. Nothing else is reachable./and/<token>/return an empty page, not a listing; any other path returns 404; there is no CGI. - The URL is printed before the confirmation (your terminal and the console), in the receipt, and again at the end, next to the receipt's SHA-256.
- The server runs as the systemd unit
slag-serve(without systemd, undersetsidand busyboxtimeout), only from the tmpfs, so it outlives the wipe. It stops at power-off, or after--serve-for(default 24 hours). - The receipt records
serve okwith the URL, orserve failedwith the reason. If nftables, iptables or ufw has input rules that may block the port, the wipe warns and the receipt says so. It never changes your firewall.
$ curl -O http://10.8.0.5:8099/6ac9bef95959a4178fa0c4db1a61554d/RECEIPT.txt $ curl -O http://10.8.0.5:8099/6ac9bef95959a4178fa0c4db1a61554d/RECEIPT.txt.sha256 $ sha256sum -c RECEIPT.txt.sha256 RECEIPT.txt: OK
The pages show the host name, disk models and serial numbers, and file paths. They hold no secrets, but they travel as plain HTTP. Use a private, mesh or VPN address, and compare your copy with the SHA-256 printed on the console, not only with the .sha256 file served next to it.
--send-receipt URL
After the receipt is written, the wipe uploads it once: an HTTP POST with the receipt as the raw body and Content-Type: text/plain.
# slag.sh wipe --target /dev/sda --send-receipt https://ntfy.example.net/slag \
--send-receipt-header "Authorization: Bearer $TOKEN"
httpandhttpsonly, checked before the confirmation. Credentials inside the URL are refused; pass them as headers with--send-receipt-header 'Name: value', which you can repeat. Header values and the URL's query string are never printed or logged, only header names.- curl, its libraries and the CA bundle are copied into the tmpfs, and the URL's host is resolved before the wipe and pinned with
curl --resolve, because name lookup and the certificate store are gone once the disk is zeros. - Connect timeout 10 seconds, 60 seconds per try, up to 3 retries.
- The body is the receipt as written. The outcome,
send-receipt okwith the HTTP status orsend-receipt failedwith the reason, is then appended to the localRECEIPT.txtas one line after the closing line. So the local copy is exactly the uploaded body plus that line, and the receipt'ssent to:line says so. - A failed send never changes the verdict.
Options
| Option | Phase | What it does |
|---|---|---|
--out DIR | all | Inventory directory (default /tmp/slag.<time>). For wipe, its meta/paths.txt feeds the per-file proof. |
--target DISK | wipe | A whole disk to wipe. Repeatable and required. |
--reclaim | wipe | Discard every target after it verifies, then verify again. |
--sample N | wipe | Verify the first block, the last block and N random 1 MiB blocks instead of every byte. Proves only those blocks. |
--no-hw-erase | wipe | Skip ATA, NVMe and SCSI hardware erase. |
--hw-timeout S | wipe | Longest wait for an NVMe sanitize (default 7200 seconds). |
--allow-partial | wipe | Allow disks on the machine to stay untargeted. They are listed. Does not cover NVMe namespaces a sanitize would reach. |
--allow-loop | wipe | Accept loop devices as targets. For testing. |
--pattern NAME | wipe | After every target has verified zero, write a legacy overwrite method and read its last pass back. The disks then hold that pass, not zeros. Rescue systems only. See overwrite patterns. |
--dry-run | wipe | Print every command the wipe would run. Touches nothing. |
--yes | wipe | Skip the typed confirmation. Required when stdin is not a terminal. |
--serve ADDR:PORT | wipe | Serve the receipt, its SHA-256, the verdict and the live log over plain HTTP from the tmpfs at http://ADDR:PORT/<random token>/. ADDR must be on this machine. See getting the receipt off the machine. |
--serve-for D | wipe | How long --serve runs: N, Ns, Nm or Nh (default 24h). It also stops at power-off. |
--send-receipt URL | wipe | POST the receipt once to an http or https URL after it is written. A failed send does not change the verdict. |
--send-receipt-header 'Name: value' | wipe | An extra header for --send-receipt, such as an auth token. Repeatable. Only the name is ever printed. |
--passes N | wipe | Ignored, with a warning. Kept so old command lines still run. |
-h, --help | all | Show the built-in help. |
Commands: inventory, checklist, wipe, selftest, patterns (list the --pattern methods), and all (inventory plus checklist, then it stops).
Exit status
0: every target passed.1: any failure, including a refused target, a verify that found non-zero data, or a verify that could not read every byte it was asked to read.2: the terminal following a detached wipe lost track of it before a verdict appeared. The wipe may still be running; checksystemctl status slag-wipeand the receipt.3:--strike-onlyfinished. The disks are not wiped.
What it cannot prove
A tool that tells me what it CAN'T do? Is this... is this what trust feels like?
A full verify proves:
- every byte of each targeted disk, as this machine sees it, reads back as zero;
- no filesystem or partition table remains on the targets;
- the extents of each mapped sensitive file read zero;
- every block that held data was overwritten in place, and blocks that read zero were not written.
Nothing inside the machine can prove:
- that provider snapshots and backups are gone;
- that the storage layer kept no copy of old blocks;
- that SSD cells the controller no longer exposes are clean (the hardware erase is for that);
- anything about sectors hidden by an ATA device configuration overlay (a host protected area is checked, and fails the run);
- for an in-place wipe of the running root disk, that the kernel writes nothing after the final re-check.
Everything on the second list is why the rotation checklist exists. Rotate every credential that lived on the host, so that a block recovered from anywhere is worthless.
Rescue mode or in place
Run the wipe from the provider's rescue system whenever there is one. The targets are then not the running system, nothing is mounted from them, and every step works as described.
An in-place wipe of the running root disk works and is tested, but the kernel still holds the root filesystem. The wipe remounts it read-only, sets its partitions and then the whole disk read-only, and re-reads the signature offsets 10 seconds after the verify. In the lab, the kernel logged filesystem errors after the receipt, and the disk stayed all zero.
That fence exists because of something the lab caught: before it, an in-place wipe passed its verify, and then ext4's error handling rewrote the filesystem's superblock onto the zeroed disk. Remounting read-only does not stop that write; a read-only block device does. Kernels 5.10 and older may not honor a read-only device for that write, so on those only the re-check stands between the verify and a late write.
selftest and dry runs
slag.sh selftest copies the wipe's tools into a temporary directory and runs the same self-test the wipe runs before touching a disk. It needs no root and touches no disk. Run it on a new image before you need it.
slag.sh wipe --target ... --dry-run runs the target checks and prints every command the wipe would run, without the confirmation and without writing anything.
Retire a machine
One word? I type ONE word and go home, and it still won't delete anything it hasn't checked from the outside? I'm calling my mother.
slag retires a machine: it drives slag.sh for you, from your own machine, against a machine you can reach over SSH as root. It runs the inventory, the checklist and a wipe of every whole disk, then pulls the receipt back and checks its hash. With a cloud provider's API token it wipes from the provider's rescue system, checks the disks again from outside the machine, and can then delete the instance. It lives in the repository as slag/slag.py and needs only Python 3's standard library and OpenSSH.
$ slag/slag.py --dry-run 203.0.113.10 # show everything, touch nothing $ slag/slag.py 203.0.113.10 $ slag/slag.py --provider linode --token-file ~/linode.token 203.0.113.10 $ slag/slag.py --provider linode --token-file ~/linode.token --pattern dod3 --delete 203.0.113.10
slag is new and its flags may still change. slag/slag.py --help is the authority for your copy.
Before it connects
slag refuses these before any SSH connection is made:
- hosts in your fleet list;
- the machine you are running it on;
- loopback and link-local addresses;
- private and mesh addresses, unless you pass
--allow-private; - anything in the local protected list,
slag/protected.txt: addresses, networks and hostnames, one per line. Start it fromslag/protected.txt.example.
What it shows you
Over SSH, as root: the hostname, the OS, every disk with its size, model and serial number, and the listening services.
With a provider (--provider linode or --provider auto, and a token from --token-file or LINODE_TOKEN), it also shows the instance's label, region, disks, backups and snapshots. The token can be for any account, not only yours. If the IP is not an instance on that token's account, slag says so and carries on as a plain SSH slag.
One confirmation, then walk away
You type one word: the instance's label for a provider target, or the machine's hostname for a plain target. slag then detaches and runs in the background on your machine. You can close the terminal.
A token without --provider is refused: slag never ignores a token and quietly wipes in place.
Provider targets: the rescue flow
A provider target is wiped from the provider's rescue system, where nothing of the instance is mounted, so the running kernel cannot write to the disks during or after the wipe.
Prepare in the normal boot
Copy
slag.shand check its hash, plant marker files, run the inventory and the checklist, and pull both back, hash-checked, before anything is destroyed.Rescue boot
The rescue system boots with every disk and volume of the instance attached. If the provider no longer lists exactly the disks and volumes shown at the confirmation, slag stops before rebooting the instance.
Pin the rescue system
Over the provider console (on Linode, Lish: your SSH key must be in the Lish keys of your profile), slag installs your key for root, turns password logins off, checks that with
sshd -T, and reads sshd's host key. Only that host key is accepted for the SSH connection that follows.Selftest and check the disks
slag.shis copied again, hash-checked, and self-tested. The rescue system must show exactly the instance's disks, at the sizes the provider reports, with nothing mounted. Its own boot media is never a target.Wipe
slag.sh wipeof those disks, with--yes --serve(and--send-receiptif a notify URL is configured, and--patternif you gave one). slag polls for the verdict, then fetches the receipt and checks its hash.Prove it from a second rescue boot
The rescue system boots again, and from the console slag reads each disk once:
teefeedscmp -n SIZEagainst/dev/zerowhile the marker search reads the same stream. Every disk must read zero and hold no marker. The device map must be the one the wipe ran against, and each disk the size shown at the confirmation: an empty or missing device never counts as zero.Delete (only with --delete)
Backups and snapshots are cancelled and the instance is deleted, but only after a PASS verdict, a receipt whose hash matches, and a passing check from the second rescue boot, and only if the provider still lists exactly the disks that check read. If anything is missing, slag stops and notifies you instead of deleting. Without
--delete(older name:--delete-when-done), the instance is left in its rescue system.Notify and summarize
A notification goes out with the instance label as the title, and
SUMMARY.txtis written with the rest of the run in~/burns/<name>-<time>/.
--in-place wipes a provider target the old way, from its running system, with the same in-place check as a plain target.
Plain targets: in place
Without a provider, slag copies slag.sh, plants markers, runs the selftest, the inventory and the checklist, then wipes every whole disk in place with --serve, fetches the receipt, checks its hash, and notes whether a fresh SSH login still works. Before the confirmation it refuses a disk with a filesystem directly on it or one holding the running root filesystem, because the kernel can still write to it after the zero pass; --in-place-anyway overrides that. Snapshots, backups and deleting the machine are left to you. Either way, work through ROTATION-CHECKLIST.md.
--pattern: proof by SHA-256
--pattern NAME (rescue mode only) has slag.sh write that overwrite pattern after the zero verify. The name is checked against the pinned slag.sh before the confirmation, and refused for an in-place run. The disks then hold the last pass, not zeros, so the proof changes with it: on the second rescue boot, each disk's SHA-256 must equal the one slag.sh read back for that device in its receipt. For a fixed-byte last pass (vsitr ends with AA) it must also equal the hash slag computes from the pattern itself. For a random last pass (R, INV) the marker search still runs and must find nothing. A disk with no matching receipt line fails; there is no fallback to the zero check. --delete waits for this proof.
The receipt URL
Before the confirmation, and again when slag detaches, it prints where slag.sh will serve the receipt (http://ADDRESS:PORT/<token>/) and what happens once slag has it. --serve-for DURATION (N, Ns, Nm or Nh) limits how long slag.sh serves it; slag.sh's default is 24 hours. In rescue mode the second rescue boot ends it in any case.
The outcome
The outcome reads plain PASS only when everything you asked for happened: the receipt passed, the proof from the second rescue boot passed (rescue mode), and the delete ran (with --delete). Otherwise it says what did not, for example PASS receipt, but proof skipped, and the run exits 1.
Keys with a passphrase
You do not need an ssh-agent. Before anything else, slag checks the keys it will use. A key with a passphrase is added once with ssh-add, on the terminal; with no agent at all, slag starts its own for this run only and stops it when the run ends. Without a terminal, slag refuses and names the key that needs unlocking.
Resume
slag.py --resume RUN_DIR continues a run from its STATE.json. It never starts a wipe twice, and never deletes without a recorded PASS, matching hashes and a passing check from the second rescue boot.
Many machines: slagctl
slag/slagctl.py runs slag over a list of machines in two stages, so a mass wipe never runs off a hand-typed host list.
$ slag/slagctl.py discover --hosts hosts.txt --provider linode --token-file ~/linode.env --out fleet-wipe.json $ slag/slagctl.py wipe --manifest fleet-wipe.json --provider linode --token-file ~/linode.env --delete-when-done --pattern harmon --jobs 2
discover touches nothing. It runs slag.py --dry-run on each host, so every refusal above applies, and you approve each host it accepts. The manifest records what slag found, not what you typed: hostname, machine-id, disks with sizes and serial numbers, the provider instance, rescue or in-place, and the confirmation word.
wipe runs only the manifest, two hosts at a time by default, after you type WIPE N for its N hosts. Each host goes through slag with the manifest entry as a pin, and slag refuses a host that no longer matches it (rebuilt, a new disk, an IP handed to someone else), in the same run that would wipe it. A rescue host is never wiped in place: without a provider token it is not started at all. --pattern NAME is checked against slag.sh before the batch is released and passed to each rescue host; an in-place host is not started with it, because slagctl never drops the pattern. A host that stops is listed in the fleet receipt with the line that resumes it; slagctl never retries it another way.
Providers
Providers are plugins in slag/providers/. Linode is the first. Adding one is one file: it looks an IP up on the account, and supplies the rescue boot, the console, the backup and snapshot cleanup and the delete. A step a provider cannot do is recorded as unsupported.
The slag appliance
For a machine in front of you, or one you can only reach by its network boot: the slag appliance boots straight into slag, a menu that picks disks and runs slag.sh, with nothing unsigned anywhere in the chain. It is one Alpine kernel and one initramfs holding the whole appliance: OpenRC, the slag menu, slag.sh, the disk tools, ethernet drivers and dropbear. It runs from RAM; no disk is mounted.
The chain of trust
- A small iPXE micro-boot carries one pinned root certificate, the slag netboot CA, and nothing else. From the moment it starts, nothing unverified may run.
- It fetches
https://pxe.slag.sh/boot.ipxeand its signature over TLS pinned to that root. A server with a certificate from any other CA is refused. - The boot script, the kernel and the initramfs are each verified as signed by the code-signing certificate
itys-burn-netboot-codesign. The web server's own TLS key cannot sign a boot image, even though the same root issued it. A changed byte, a wrong signer or a foreign CA stops the boot before any kernel runs.
The menu
Every micro-boot shows the same menu:
slag - retire this machine Network: latest signed build from pxe.slag.sh (default, 15 s) Network + SSH (key burn@fd53) Offline: this stick's signed build (full stick only) Offline + SSH (key burn@fd53) (full stick only) iPXE shell (signed images only) (UEFI / PXE)
Boot media
| Medium | Firmware | What it does |
|---|---|---|
Micro-boot stick (slag-microboot.usb, 2 MB) | BIOS + UEFI | dd to a stick. Network only: it always boots the latest signed build. |
Full stick (slag-<build>.usb, about 90 MB) | BIOS + UEFI | dd to a stick. Network, and Offline from the build on the stick. On UEFI the stick's build is verified like a network one; on BIOS, syslinux boots it directly, unverified, so keep the stick write-protected or rebuild it. |
ipxe.efi | UEFI | On an EFI system partition as EFI/BOOT/BOOTX64.EFI. |
snponly.efi, undionly.kpxe | UEFI, BIOS | DHCP/TFTP network boot. Anything that network-boots there lands in slag, which still wipes nothing until a person selects disks and types DESTROY. |
Secure Boot must be off: the stubs are not shim-signed.
Download
The order page shows real output from each before you download. Both sticks are ready to write. Each is a whole-disk image: dd replaces everything on the stick.
| File | Size | What it is |
|---|---|---|
slag.sh | 187 KB | The script itself, the same copy the sticks carry. |
slag-20261105T1029Z-a0ff32d4.usb | 89 MB | The full stick: Network, and Offline from build 20261105T1029Z-a0ff32d4, the same build pxe.slag.sh serves. |
slag-microboot.usb | 2 MB | The micro-boot stick: Network only, always the latest signed build. |
slag-netboot-ca.crt | 2 KB | The slag netboot root that both sticks pin. |
SHA256SUMS, .sig | Hashes of the files above, signed by itys-burn-netboot-codesign. |
Check the signature, then the hash, then write the stick. Find the stick's name with lsblk first: /dev/sdX below is a placeholder, and the wrong name wipes the wrong disk.
$ curl -fO 'https://slag.sh/download/{SHA256SUMS,SHA256SUMS.sig,slag-netboot-ca.crt,slag-20261105T1029Z-a0ff32d4.usb}' $ openssl x509 -in slag-netboot-ca.crt -noout -fingerprint -sha256 sha256 Fingerprint=F6:6D:65:4D:83:CE:3C:D2:90:40:BE:E4:E0:85:00:FD:F7:51:74:6A:02:6D:A0:46:46:C5:44:AA:CC:5E:9D:8F $ openssl cms -verify -binary -inform DER -in SHA256SUMS.sig -content SHA256SUMS \ -CAfile slag-netboot-ca.crt -purpose any -out /dev/null CMS Verification successful $ sha256sum -c --ignore-missing SHA256SUMS slag-20261105T1029Z-a0ff32d4.usb: OK $ lsblk -o NAME,SIZE,MODEL,TRAN $ sudo dd if=slag-20261105T1029Z-a0ff32d4.usb of=/dev/sdX bs=4M conv=fsync status=progress
This page and the files come from the same server, so the fingerprint here proves only that they match each other. For more, compare it with a copy you got another way, for example from someone who has already checked it. On UEFI the stick checks its own build against that root at every boot anyway.
The slag menu
On tty1, slag lists the disks, you pick the ones to wipe, choose an optional overwrite pattern (most retirements want none), and type DESTROY. slag never offers its own stick (labels SLAG and SLAG-MB), mounted disks or read-only devices. A disk shows PASS only when slag.sh logged VERIFIED: /dev/X reads back all zero for it, and, with a pattern, also PATTERN VERIFIED. The receipt and the per-disk logs go to /var/log/slag.
SSH
The "+ SSH" entries put slag.ssh=1 on the kernel command line; nobody on the network can turn it on, and without it no interface comes up and nothing listens. With it, the appliance brings up the network (DHCP, or the static address slag-kexec.sh passed) and dropbear with key-only logins: no passwords, no port or agent forwarding, and the one key baked into the image at build time (burn@fd53). The host key fingerprint is printed on every console so you can check it before you trust the connection.
Headless
Over SSH or a serial console, slag runs without the menu, with the same protection rules, PASS rules and receipt:
# slag --list-candidates # what may be wiped, and why not # slag --wipe sda nvme0n1 --confirm DESTROY # chosen disks # slag --wipe-all --confirm DESTROY # every disk shown "ok" # slag --wipe-all --pattern harmon --confirm DESTROY # zeroize, verify, then a pattern # slag --wipe-all --confirm DESTROY --background # detached; prints the file to follow
A protected or unknown disk, an unknown pattern, or a missing --confirm DESTROY is refused before anything is touched. Otherwise the output ends with the receipt lines, then receipt: PATH and exit: N.
0: every disk PASS.1: a disk failed.2: refused, nothing touched.
Retire a VPS without its console
A machine you can reach as root over SSH can be switched into the slag appliance in place, with kexec. Nothing of its own OS keeps running, so every disk, the root disk included, is wiped and verified as on bare metal. No provider API or console is needed.
$ netboot/retire-vps.sh --dry-run root@HOST # fetch, verify, plan; nothing changes $ netboot/retire-vps.sh --yes-kexec NAME root@HOST # kexec, then list the disks $ netboot/retire-vps.sh --yes-kexec NAME --wipe --yes-wipe NAME root@HOST
NAME is the machine's hostname, typed out; the wipe needs both flags because it is the irreversible part. The kexec alone only stops the OS: a reboot from the provider's panel brings it back with its disks untouched.
Hand off
retire-vps.shmakes the appliance's SSH host key on your machine and pushes it, withslag-kexec.sh, to the machine. The host key is loaded ahead of the signed initramfs, so the first connection to the appliance is already pinned, and it cannot replace any file of the image.Fetch, verify, kexec
On the machine,
slag-kexec.shinstalls what it is missing (curl, openssl, ip, kexec-tools), fetches the buildboot.ipxenames over HTTPS pinned to the slag root, checks every signature and the signer, and kexecs into it with SSH on and the machine's current IPv4 and IPv6 addresses, gateways and MAC on the kernel command line, since many VPSes have no DHCP.Wait and list
It waits for the appliance (the slag key, the pinned host key) and lists its disks. Port 22 of a public VPS is brute-forced around the clock, which fills dropbear's slots for clients that have not logged in yet and locks you out. So the appliance's SSH answers only your address, as the old OS saw you (
--ssh-fromto change it), and nftables silently drops everyone else. It listens on the same port the old sshd did (--ssh-portto change it), because a provider firewall that let you in will let you in again; a random port would not get through one. Each machine also gets one SSH connection, opened with retries and reused for every step, so only one login ever has to get through.Wipe (--wipe --yes-wipe NAME)
slag's headless
--wipe-all --backgroundon every disk slag offers (read-only devices, such as config drives, are left out and named).--pattern NAMEpasses through. slag's output and every slag.shRECEIPT.txtare copied back to your machine with anACTIONS.log.
After the wipe the appliance keeps running in RAM: delete or power off the VPS at the provider.
Keys: retire-vps.sh checks every key it will use before it starts. A key held by your agent counts only if the agent signs with it right away. A desktop keyring such as GNOME's lists a locked key and then waits on its own dialog, which would stall the run unseen. Any key that fails is unlocked once on the terminal into a private agent for this run, which is stopped when the run ends.
By hand, on the machine
slag-kexec.sh is also published here, on a public certificate, so a fresh VPS can fetch it. It then pins the slag root itself for everything it downloads. Its .sig sits alongside, signed by the code-signing certificate.
# curl -fsSO https://slag.sh/slag-kexec.sh # bash slag-kexec.sh --dry-run # bash slag-kexec.sh --yes-kexec "$(hostname)" # then: ssh -i ~/.ssh/burn root@<same IP>
Limits
- The signature check runs inside the old OS, so a compromised OS could kexec something else. For those, use the stick or the provider's rescue system.
- Kernel lockdown (Secure Boot) refuses the slag kernel; the script detects it and says so.
- x86_64 only. About 1 GiB of RAM is enough.
How it is tested
The repository's lab/run-lab.sh runs each scenario in a throwaway KVM guest (Debian 12, kernel 6.1, QEMU 8.2.2) and records the operator's SSH session and the guest's serial console. After each wipe the VM is stopped and the host checks the target's disk image without trusting the script: converted to raw and compared with zeros, searched for a marker string written before the wipe, and the image file's own bytes searched for it as well.
| Scenario | What it shows |
|---|---|
| S1 | A second disk wiped from rescue-style: 519,707,681 non-zero bytes to 0. |
| S2 | Partition, untargeted disk, LVM, loop and duplicate targets refused; the disk's hash unchanged. |
| S3r | In place with --reclaim after an inventory: 729 files mapped (none off the targets), all re-read zero; inventory directory 700, tarball 600; overlay 11,083,776 bytes to 598,016. |
| S3, S3b | The running root disk wiped in place: 905,611,732 non-zero bytes to 0, and still zero after the kernel's late writes. |
| S3x | The probe that found the superblock rewrite: a read-only remount alone lets it through, a read-only device stops it. |
| S4 | Read errors injected on purpose: the run reports FAIL and exits 1, and the rest of the disk is still zeroed (0 non-zero bytes left on the host-side image). |
| S5 | An emulated NVMe drive: sanitize recorded as unsupported, format used, disk all zero. |
| S6 | Sampled verify on a 5 TiB disk reaches offsets past 4 TiB. |
| S7 | The tools copied into RAM work, and none of the copies overwrote another. |
| S8 | In place with --serve: slag.log fetched 15 times from the host while the wipe ran; afterwards RECEIPT.txt matched both its .sha256 file and the SHA-256 on the console, and said VERDICT: PASS. / and /<token>/ listed nothing, a wrong token got 404. A new SSH login after the wipe failed (Connection closed); the HTTP fetch still worked. |
| S9, S9d | --send-receipt with an Authorization header: the header arrived, the body was the receipt minus the appended send-receipt ok HTTP 200 line, and the token appeared nowhere in the console, log or receipt. With the receiver down: send-receipt failed after 4 tries, verdict still PASS. |
| S10 | An address not on the machine, bad ports, a missing port, an unresolvable name, and ftp://, file:// or credential URLs: all refused before the confirmation, exit 1, disk hash unchanged. |
| S13 | The strike. An ext4 disk with a live SSH key and an RSA key, a shadow line and an AWS key id written and deleted; a swap disk with a WireGuard key. --strike-only: exit 3, and from the host no key signature left on either disk while every marker survived. Then the full wipe: PASS, both disks zero. |
The appliance has its own lab, netboot/lab/netboot-lab.sh: QEMU guests boot every micro-boot medium (USB on BIOS and UEFI, PXE, the full stick offline), refuse a tampered boot script or initramfs, a wrong signer and a foreign CA, wipe a scratch disk (also as an NVMe drive) that the host then reads back as all zero, check SSH (host key on the console matches the server, no passwords, only the slag key), run the headless refusals and a pattern, and retire a Debian 12 VM with retire-vps.sh, root disk included. The production checks boot the live pxe.slag.sh build the same way.
Not covered by the lab: waiting for a real NVMe sanitize (QEMU's emulated drive has none), ATA and SAS secure erase, and the host protected area check. Those need real drives.
Questions
Why zeros and not random data?
Because zeros can be verified. A random pass leaves nothing to check the disk against, and on modern drives a single pass of anything is enough for the blocks you can address. The blocks you cannot address are the hardware erase's job.
Why not just delete the instance?
Deleting an instance tells the provider you are done with it. It does not say when or how the blocks are reused, and it does nothing about snapshots. Wipe first, then delete, then rotate.
My policy names DoD 5220.22-M (or Gutmann, or VSITR).
Use --pattern with that method's name: the zero pass and its verify run first, then the method, and the receipt records the SHA-256 the disk must hash to. See overwrite patterns.
Can I wipe a single partition?
No. Partition tables, RAID metadata, other volumes and another disk's EFI partition all leave data behind. Whole disks only.
Why do I have to type the device names?
Because "yes" is typed out of habit, and sda sdb is typed while looking at the serial numbers on the screen.