跳至正文

我他妈差点删了根目录,还好有这个Linux小技巧

事情是这样的。上周五,我手贱想清理一下服务器上某个临时目录,大概就是/home/deploy/temp_files这个路径。当时SSH窗口开了七八个,脑子全是并发请求、日志切割、数据库索引这些破事,然后我打了个cd /home/deploy/temp_fi,习惯性按了Tab补全。结果那天不知道哪个脑子抽风的家伙,把temp_files目录删了重建过,Tab补全到的是/home/deploy/temp_files_old。我根本没注意,直接敲了rm -rf *。

手指按下回车的那一秒,我整个人像被雷劈了一样清醒。那个目录里存的不是临时文件,是另一套微服务的缓存索引文件,大概有40G。系统瞬间开始疯狂报找不到xxxx缓存文件,然后服务一个接一个挂掉。我当时的心态就是——完了,这个月的绩效没了。

但是等等,这个故事不是用来讲我有多蠢的。我要说的是,Linux运维里有一个我用了快十年才真正觉得“我操这玩意儿真香”的小操作——别名+参数校验组合拳。

我平时的习惯是直接在.bashrc里加几个alias,比如alias rm=’rm -i’,这样每次删东西都会问一句。但说实话,生产环境上你开十个窗口,手速一快,那个“y/n”你根本不会看,肌肉记忆直接就按y了。所以这种方案就是自欺欺人。

真正让我觉得有点用的,是写一个函数包裹rm命令,加上一个简单的白名单校验。我把它叫做safe_rm,放在~/.bashrc里:

“`bash
safe_rm() {
local target=”$1″
local safe_paths=(“/home/deploy/temp” “/var/log/nginx/old” “/tmp/cleanup”)
local is_safe=0

for path in “${safe_paths[@]}”; do
if [[ “$target” == “$path”* ]]; then
is_safe=1
break
fi
done

if [[ $is_safe -eq 0 ]]; then
echo “错误: 目标路径 [$target] 不在白名单中,拒绝执行”
echo “白名单: ${safe_paths[*]}”
return 1
fi

if [[ -z “$target” ]]; then
echo “错误: 必须指定要删除的路径”
return 1
fi

/bin/rm -rf “$target”
}
“`

然后我用alias rm=’safe_rm’覆盖掉原来的rm。这个函数做的事情很简单:你执行rm /home/deploy/temp_files_old,它会检查这个路径是不是以白名单里的某个路径开头。不是?直接拒绝,连问都不问。是?那就正常执行/bin/rm。

这个方案有个关键点——白名单路径必须写绝对路径,而且写法是前缀匹配。比如你写了/home/deploy/temp,那/home/deploy/temp_files_old也会匹配到,因为它是/home/deploy/temp开头。但如果你只写了/home/deploy/temp/,那就必须带斜杠结尾才匹配。我自己习惯写不带斜杠,这样匹配范围宽一点,但风险也大一点。你可以根据自己的场景调整。

那有人会说了,我要是想删一个不在白名单里的路径怎么办?简单,我留了一个后门:alias rms=’command rm -rf’。rms这个别名直接调用原生的rm,跳过所有校验。但命名上故意搞成rms,跟rm不一样,手快按错了也执行不了。我只有在明确确认路径无误的情况下,才会手动敲rms。

这招救了我好几次。上周四有个同事跑过来跟我说,他在测试服务器上执行rm -rf /var/log/*,结果不小心在路径末尾多了个空格,变成rm -rf /var/log/ *,差点把当前目录所有文件都删了。还好他那个测试机上我提前配了这个safe_rm,白名单里没有/var/log,直接报错拒绝。他跑过来问我啥情况,我一看,背后凉飕飕的。

当然,这个方案不是万能的。比如说,你如果手速太快,直接把白名单路径本身敲错了,那就不会匹配。还有就是,如果你用的不是bash而是zsh,或者某些发行版的sh,函数定义语法可能不一样,得自己调一下。我在Ubuntu 20.04和CentOS 7上测试过,bash 4.4和5.0都没问题。zsh的话,函数定义差不多,但数组声明方式不一样,得用typeset -a或者local -a。

还有个坑:如果你用sudo rm,这个alias不会生效,因为sudo会以root身份执行一个新的shell,那个shell不加载你的.bashrc。解决方案有两个,一个是直接sudo -i然后source ~/.bashrc,但太麻烦了。我自己的做法是,在/etc/bash.bashrc里也加一份同样的函数定义,或者写一个独立的脚本放在/usr/local/bin/safe_rm.sh,然后用alias sudo=’sudo ‘,注意alias后面有个空格,这会让sudo识别后续的alias。但这个做法有安全隐患,你自己掂量。

踩了这么多坑,我总结一下经验:

第一,别太信自己的肌肉记忆。我见过太多老运维,干了十年,手一抖照样删根目录。白名单校验这种机制,比你自己脑子的判断靠谱。

第二,别把alias搞得过于复杂。我见过有人写了一个三百行的rm安全脚本,又是日志记录又是邮件通知又是二次确认,结果自己都记不住怎么用,最后干脆不用,回到裸rm。简单粗暴才是最持久的。

第三,生产环境上,所有涉及删除的操作,尽量先走一遍ls确认。比如你想删/home/deploy/temp下的东西,先ls /home/deploy/temp,看清楚里面有什么。但说实话,我承认我自己也经常跳过这个步骤,所以才需要safe_rm这种机制兜底。

第四,如果你跟我一样在团队里混,把这个函数写到团队的公用的shell配置里,比如/etc/profile.d/下,让所有人都继承。别问我为什么,因为总有一天,你那个刚入职三个月的实习生,会在生产服务器上执行rm -rf .,然后告诉你“我只是想删当前目录下的一个文件夹”。

至于我那次手贱删了缓存索引的事故,最后怎么解决的?我赶紧停了所有相关服务,从备份里恢复了那40G数据,花了两个小时。还好那天是周五下午,没什么流量高峰。但那份后怕,让我老老实实写下了这个safe_rm函数,并且至今还在用它。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注