What it is
A manual? At two in the morning? With TABLES?
decom.sh is a single bash script for taking a Linux server out of service. 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. decom.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.
Get it
It is one bash script. The source, the docs and the VM lab are at git.itys.net/mjh/decom.
# curl -fsSLo decom.sh https://git.itys.net/mjh/decom/raw/branch/main/decom.sh # chmod +x decom.sh # ./decom.sh selftest # no root, touches no disk
Or clone the whole repository: git clone https://git.itys.net/mjh/decom.git. Read the script before you run it as root. It is meant to destroy data.
Quick start
Boot the server into the provider's rescue system if it has one (see rescue mode or in place). As root:
# decom.sh inventory --out /root/decom.out # decom.sh checklist --out /root/decom.out # scp -r /root/decom.out /root/decom.out.tar.gz you@elsewhere: # decom.sh wipe --target /dev/sda --target /dev/sdb --out /root/decom.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
decom-wipeand your terminal follows its log. If the SSH session drops, the wipe continues; reconnect and runjournalctl -fu decom-wipeortail -f /run/decom.*/decom.log. - When it finishes, copy the receipt (
/run/decom.*/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
decom.sh inventory [--out DIR]. Default output: /tmp/decom.<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
decom.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
decom.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, normalize zeroes a test file. If anything fails, the wipe stops before touching a disk.
Detach
The wipe runs as the systemd unit
decom-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 unitdecom-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.Hardware erase, then wait
Every target is hardware-erased before any of them is written. 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.Normalize to zero
Every block is read and zeros are written only where a block is not already zero. See normalize.
Fence (in place only)
When the target is the running root disk, the whole disk is set read-only right after it is normalized, 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.
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.
Normalize to zero
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!
Normalize 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.
- Normalize 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: normalize 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 normalize 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 normalize 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 normalize 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 setDECOM_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 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:
===================== DECOM WIPE RECEIPT =====================
tool: decom.sh 1.1.0
host: decomlab
started: 2026-10-30T17:57:56Z
finished: 2026-10-30T17:58:26Z
run: in-place (re-exec from tmpfs /run/decom.20261030T175756Z)
normalize: 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=decomroot 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
normalize 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
normalized to zero: 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:
# decom.sh wipe --target /dev/vda --serve 10.8.0.5:8099 [decom] 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:decom.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
decom-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.
# decom.sh wipe --target /dev/sda --send-receipt https://ntfy.example.net/decom \
--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/decom.<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. |
--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, 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 decom-wipeand the receipt.
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
decom.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.
decom.sh wipe --target ... --dry-run runs the target checks and prints every command the wipe would run, without the confirmation and without writing anything.
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: decom.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. |
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.
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.