What rm already refuses, and what it does not
GNU rm refuses rm -rf / by default. That protects one literal path. The common accidents look different:
rm -rf /*: the shell expands the star first, sormgets/bin,/etcand the rest, and none of them is/.rm -rf /var/app /*: one stray space adds the whole system to the list.- An empty variable in a script:
rm -rf "$DIR"/*becomesrm -rf /*. - The right command in the wrong terminal, on a server you thought was staging.
--preserve-root=all goes a step further and refuses any folder that sits on a different disk from its parent, such as a mounted volume:
rm: skipping '/dev/shm', since it's on a different device
rm: and --preserve-root=all is in effect
Habits like listing a glob with echo before deleting it are in how to avoid mistakes on production servers.
Make interactive rm ask once
Put these two lines in ~/.bashrc on the server, and in /root/.bashrc:
alias rm='rm -I --preserve-root=all'
alias sudo='sudo '
-I asks once before a recursive delete or before removing more than three files. Anything but y removes nothing. The trailing space in the sudo alias tells bash to expand the next word as an alias too, so sudo rm asks as well:
$ sudo rm /tmp/s/*
rm: remove 4 arguments? n
zsh asks by itself before rm * and rm path/*, unless RM_STAR_SILENT is set:
zsh: sure you want to delete all 2 files in /tmp/z [yn]? n
Aliases apply only to interactive shells. Scripts, cron jobs and ssh server 'rm ...' use the real rm, and \rm or command rm skip the alias on purpose. Avoid alias rm='rm -i': a question for every file trains people to type -f.
safe-rm: paths that rm will skip
safe-rm puts a wrapper in front of rm that skips any path on a protected list. On Debian and Ubuntu:
sudo apt install safe-rm
echo '/srv/app/shared' | sudo tee -a /etc/safe-rm.conf
The list already includes system folders such as /etc, /usr and /var. Our test:
$ rm -rf /srv/app/shared
safe-rm: Skipping /srv/app/shared.
Know its limits before you rely on it:
- It protects the listed path, not what is inside.
rm -rf /srv/app/shared/*still emptied the folder. A line such as/srv/data/*protects each entry directly under/srv/data. - It exits with 0 even when it skipped something, so a script does not notice.
- The package adds its folder to
PATHin/etc/profile.d, so it works in login shells and withsudo -i.sudo rmuses sudo's ownsecure_pathand ran the realrmin our test, and so doesssh server 'rm ...'.
Guard the variables in scripts
Most scripted disasters come from an empty or unset variable. Stop the script before it reaches rm:
#!/usr/bin/env bash
set -euo pipefail
rm -rf -- "${BUILD_DIR:?}/"
s.sh: line 3: BUILD_DIR: parameter null or not set
${BUILD_DIR:?} stops with an error when the variable is empty or missing, and set -u does the same for every unset variable in the script. -- ends the options, so a file named -rf is treated as a file.
Permissions and immutable files
The strongest guard is not being able to delete. Work as a normal user and use sudo only for the commands that need it, so a slip in your home folder cannot reach the system. See how to create an SSH user with sudo.
For files that must not change even for root, set the immutable flag, on ext4 and other filesystems that support it:
sudo chattr +i /etc/app/config.yml
lsattr /etc/app/config.yml
----i---------e------- /etc/app/config.yml
rm: cannot remove '/etc/app/config.yml': Operation not permitted
The same error stopped rm -rf on the folder around it. Remove the flag with sudo chattr -i before a planned change. Updates and editors cannot write to the file either, so keep the flag for the files that really need it.
A trash instead of rm
When you clean up by hand, move files to a trash instead. trash-cli uses the same trash as desktop Linux:
sudo apt install trash-cli
trash-put old-release/
trash-list
trash-restore
trash-empty 30
trash-restore asks which item to put back. trash-empty 30 removes what has been in the trash for more than 30 days, which suits a cron job. The files still use disk space until then. Do not alias rm to trash-put: the options differ, and the habit fails on servers without it.
Snapshots, backups, and if it already happened
Every layer above can be bypassed. A snapshot cannot be deleted with rm. Take one before risky work: zfs snapshot pool/data@before-cleanup on ZFS, btrfs subvolume snapshot -r on btrfs, lvcreate --snapshot on LVM, or a volume snapshot in your cloud console. Copy backups to another machine, for example with rsync over SSH, and test a restore now and then.
If files are already gone, stop writing to that disk: new data can overwrite what a recovery tool might still find. Then restore from a snapshot or backup. One case can be saved at once: a file still open in a running program. Find it and copy it back before that program stops:
lsof +L1
cp /proc/374/fd/3 /root/data.recovered
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NLINK NODE NAME
tail 374 root 3r REG 0,186 2 0 12873355 /srv/data.txt (deleted)
The number after /proc/ is the PID, and the one after fd/ is the number in the FD column without its letter.
Questions
Does rm -rf / still work on Linux?
Not with GNU rm: it refuses / unless you add --no-preserve-root. But rm -rf /* is expanded by the shell into /bin, /etc and the rest, and that is not refused.
Can I undo rm -rf?
Not with rm itself. Restore from a snapshot or backup. A file that a running program still has open can be copied back from /proc/PID/fd until that program closes it.
Is alias rm='rm -i' a good idea?
It asks for every file, so people learn to answer yes without reading or to add -f. rm -I asks once, for three or more files or for a recursive delete, which keeps the question meaningful.
Does safe-rm protect the files inside a folder?
No. It skips the paths listed in its configuration. rm -rf /srv/app/shared/* still empties a protected /srv/app/shared, unless the entries inside are listed too, for example with /srv/app/shared/*.
Sources
- GNU coreutils manual: rm invocation
- safe-rm project
- chattr(1), Linux manual
- trash-cli project
- zsh manual: RM_STAR_SILENT
- OpenZFS: zfs-snapshot(8)