资讯详情

资讯详情

Linux mount 命令执行前的身份验证与 Shell 钩子拦截

在 Linux 上敲下mount之前先被拦一道、要求输入密码这听起来像是把 sudo 又造了一遍。我第一次听到这个需求的时候也是这么想的直到有台多人共用的构建机出了事一个跑批脚本里的mount --bind把一个空目录盖到了构建产物目录上编译照常跑日志照常写等第二天有人发现产物全没了回头翻日志才发现是被挂载遮蔽了——数据没删只是看不见了而那个脚本的作者早就拿到了 sudo 的免密时间窗一路畅通无阻。这件事之后我开始认真琢磨能不能让mount以及umount、losetup这类同族命令在执行前强制激活一段脚本弹出身份验证验证通过才放行。这套机制在 Bash、Zsh、Fish 里都能做做法不止一种控制力也天差地别。下面我把这几条路径的实现细节、真实控制边界以及我在实测中踩到的坑一条条摊开讲。适合已经会用 sudo、写过.bashrc但还没系统研究过 shell 钩子机制的人看也适合正在做运维流程约束、想给危险命令加一道人工确认闸门的同行。1. 把需求拆开mount 的破坏力到底在哪sudo 又漏在哪先把为什么要多此一举这件事说清楚否则后面的技术选型没有依据。很多人第一反应是我有 sudo要密码不就有了这话只对了一半。mount的危险性不在于它需要 root而在于它的效果极其隐蔽。挂载一个文件系统到某个目录本质是把该目录原有的 inode 树从命名空间里遮住上层应用看到的是一个全新的空目录或另一个设备的内容。写进去的文件落在新设备上卸载之后又变回原来的目录——中间产生和消失的数据完全不留痕迹。如果被挂的是 tmpfs你写的日志全部躺在内存里机器一重启就蒸发如果被挂的是--bind一个空目录盖在/home或/var/lib上服务会以为数据被清空了进而触发一堆自动重建逻辑把真实数据所在的路径彻底搞乱。更麻烦的是mount -o remount,rw /或者mount -o remount,ro /这一族。前者把只读根分区改成可写后者反过来把根分区锁成只读一个执行到一半的系统会瞬间开始到处报 I/O 错误。这类操作在排障时是救命手段在自动化脚本里就是灾难。umount也不无辜对一个正在被写入的挂载点执行umount -llazy unmount表面上立刻成功实际上内核还挂着一个已经脱离命名空间的引用文件句柄没关干净的话空间一直不释放df看着正常du对不上账这种幽灵占用能查一整天。再说 sudo 的漏洞在哪。sudo默认的时间戳缓存timestamp_timeout常见配置是 5 到 15 分钟也就是说你输入一次密码接下来这段时间里同终端甚至同用户的任何sudo mount都是免密的。对于交互式操作这是体验优化对于我要保证每一次挂载都经过人确认这个目标它就是致命缺口。构建脚本、CI runner、定时任务只要在这段时间窗里跑起来就能把任意挂载操作执行掉。而且 sudo 的授权粒度是命令级的sudo mount一旦放行sudo mount -o bind /tmp/empty /etc这种也是同一个二进制策略上区分不开——除非你在 sudoers 里写非常细的参数匹配那维护成本高到没人愿意做。所以这个需求实际上包含两个层次必须掰开看。第一层是流程约束我要让操作者在敲下命令的那一刻明确意识到我要动挂载了并且留下一条可追溯的记录防止手滑和脚本误伤。第二层是权限边界我要保证没有经过验证的进程根本拿不到挂载能力。这两层的实现手段完全不同前者在 shell 层做就够了后者必须下沉到 sudo/PAM 甚至内核。绝大多数人一开始想要的其实是第一层但描述成了第二层。后面我会分别给方案也会明确标出每种的失效边界。2. 五种拦截位置的实测控制力对比在动手写代码之前先把可选的拦截点列清楚。不同位置的生效范围、能否真正阻断、被绕过的难易程度差异极大选错了会发现门禁形同虚设。下面这张表是我在几台测试机上逐项验证后的结论不是理论推演。拦截位置生效范围能否真正阻断执行典型绕过方式适合解决哪一层alias 别名仅当前交互式 shell否脚本、子 shell、\mount、command mount几乎不适用shell 函数覆盖当前 shell 及其export -f的子 bash是返回非零即不执行command mount、/bin/mount、env mount、非 bash 子进程流程约束PATH 前置包装脚本所有按 PATH 查找的命令调用是绝对路径调用、sudo 的独立 PATH流程约束覆盖面更广DEBUG trap extdebug当前 shell 解析执行的每条命令是需 extdebug同上且对子进程无效流程约束 检测告警sudoers 策略 PAM所有经 sudo 的调用是基本无法绕过除非有 root权限边界auditd / eBPF 探针全系统 execve否auditd能eBPF 可阻断无审计兜底 / 硬边界几个关键结论值得单独强调。第一alias 在这个场景里基本没用因为别名不会传递给非交互式 shell而你要防的恰恰是脚本而且别名展开发生在解析阶段\mount加个反斜杠就跳过了。第二shell 函数能真正阻断因为函数返回值就是命令的退出码返回非零后续的链就断了但函数只存在于当前 shell 的进程空间任何非 bash 子进程、任何用绝对路径的调用都不受影响。第三sudoers 是唯一称得上硬边界的方案但它的粒度粗、配置维护麻烦而且要求所有操作都必须走 sudo实际上很多人在 root shell 里直接敲压根不经过 sudo。我的实际选择是组合拳交互式 Shell 用函数覆盖做门禁PATH 层放一个包装脚本兜住非交互场景sudoers 收紧时间窗做最后一道防线auditd 或者 syslog 记日志做追溯。单一方案都有明显缺口叠起来才够看。下面逐层展开。2.1 一个容易被忽略的前提先确认你要防的是谁同样是执行 mount 要密码防内部人和防自己完全是两码事。如果你只是想给自己加一道你真的要挂载吗的确认防止手快那其实不需要密码一个read -p 确认(yes/no)的交互确认就够了成本极低也不会被 sudo 时间窗影响。但如果你的场景里有多个人共用同一个账号或者有自动化脚本以同一个身份跑那确认就没意义了必须换成验证身份因为确认框会被脚本里的yes |直接喂饱。这个区分直接决定了密码校验的实现方式确认式只需要读一行输入身份验证式必须走一次真正的凭据校验。我在第一版里图省事用了read -p 输入 yes 继续结果在一次压力测试里一个带while read循环的脚本恰好把yes喂了进来门禁被静默通过。从那以后我把所有确认式门禁都标上了仅防手滑的注释。3. Bash 的 DEBUG trap每条命令执行前的那一瞬要命令执行前自动激活脚本Bash 里最贴近这个语义的机制是DEBUGtrap。它在 shell 准备执行一条简单命令之前触发触发时$BASH_COMMAND变量里放着的就是即将执行的命令文本尚未做参数展开和通配符展开的原始文本。这个特性非常适合做拦截因为它比函数覆盖更靠前——函数覆盖只能拦你已经定义为函数的命令名而 DEBUG trap 能看到整行原文。但它的行为细节比文档描述的复杂得多我在实测中整理了下面这几条都直接影响代码正确性。第一DEBUG trap默认不继承到函数内部也就是说函数体里执行的每条命令不会逐个触发 trap只有进入函数那一刻触发一次如果你打开了set -Tfunctrace或者shopt -s extdebug继承就打开了函数里每条命令都会触发性能瞬间掉一截而且容易触发递归。第二管道里的每一段、和;分隔的每一条、命令替换$(...)里的内容都会各自触发一次 trap一条mount /dev/sdb1 /mnt | tee log会触发两次。第三Tab 补全时也会触发此时$COMP_LINE非空如果不排除你按 Tab 找路径就会弹密码框。第四PROMPT_COMMAND里定义的函数本身也会触发 trap如果PROMPT_COMMAND里有命令替换每次回车都会触发一轮。基于这些特性一个能用的骨架长这样。它做的事情很朴素判断即将执行的第一词是不是受保护命令是的话就走验证流程。# ~/.bashrc 片段基于 DEBUG trap 的门禁骨架实测 bash 4.4 / 5.x __gate_armed0 # 提取命令行里第一个真正的命令词跳过 VARvalue 前置赋值 __gate_first_word() { local line$1 tok local -a toks read -ra toks $line for tok in ${toks[]}; do [[ $tok ** ]] continue printf %s ${tok##*/} # 去掉 /bin/ 这类前缀 return done } __gate_is_protected() { case $( __gate_first_word $1 ) in mount|umount|losetup|swapon|swapoff|mkfs*) return 0 ;; *) return 1 ;; esac } __gate_debug_hook() { # 补全阶段和自身调用直接跳过 [ -n $COMP_LINE ] return [[ $BASH_COMMAND __gate_* ]] return # 同一条命令行只校验一次 [ $__gate_armed 1 ] return __gate_is_protected $BASH_COMMAND || return __gate_armed1 __gate_ask || { printf 已取消%s\n $BASH_COMMAND 2 # 没有 extdebug 时这里只能告警无法阻止执行 return 1 } } # 每次回到提示符前重置闸门保证下一条命令行能再次校验 __gate_prompt_hook() { __gate_armed0; } PROMPT_COMMAND__gate_prompt_hook${PROMPT_COMMAND:; $PROMPT_COMMAND} trap __gate_debug_hook DEBUG这里有个必须先讲明白的硬限制默认情况下DEBUG trap 返回非零并不会阻止命令执行。想要让它真正拥有否决权必须打开shopt -s extdebug此时 trap 返回非零值Bash 就会跳过这条命令。但这个开关的副作用不小——它会同时打开 trap 向函数内部的继承还会改变函数调用栈的调试行为。我在一台跑着大量 bash 脚本的机器上打开它之后交互式终端明显变卡因为每条命令都要过一次 trap 函数的完整调用。所以我的实际用法是extdebug只在少数几台需要强约束的跳板机上开普通开发机上只保留 trap 做检测和告警真正的阻断交给下一节的函数覆盖。还有个小技巧值得提。$BASH_COMMAND对于复合命令if、for、while会给出整个结构的文本对于time、!这类保留字前缀也会带上。所以判断第一个词的方式必须做归一化不能简单用case $BASH_COMMAND in mount*—— 那样mounted这种普通函数名也会误伤。我上面用read -ra分词后取第一个非赋值词就是为了绕开这个坑。4. 函数覆盖 mount最简单也最容易留下缺口如果只追求交互式敲 mount 时弹密码最省事的做法是定义一个同名 shell 函数把原命令包起来。它比 DEBUG trap 简单一个数量级返回值天然就是命令的退出码不需要 extdebug也不会有补全误触的问题。核心就三行mount() { __gate_ask || { printf 验证未通过已取消\n 2; return 1; } command mount $ } umount() { __gate_ask || { printf 验证未通过已取消\n 2; return 1; } command umount $ }command这个关键字的作用是让 Bash 跳过函数和别名的查找直接走内建或 PATH 上的可执行文件否则函数会无限调用自己。$的引号绝对不能省mount -o bind,ro ...这种带空格的参数如果不加引号会被重新分词行为完全跑偏。返回值也建议显式return $?虽然函数最后一条命令的退出码会自动成为函数退出码但一旦你在后面加了日志语句退出码就被覆盖了我吃过这个亏。这个方案的最大问题是覆盖面窄得可怜。command mount直接绕过\mount加反斜杠绕过/bin/mount绝对路径绕过env mount绕过find ... -exec mount绕过任何非 bash 的子进程比如 Python 的subprocess.run([mount, ...])完全看不到你的函数。至于export -f mount它能把函数导出给 bash 子进程但对方必须也是 bash 并且继承了环境变量而且有些系统对导出的函数有安全限制。所以我把这套方案定位成防手滑,而不是防人。还有一个隐蔽的坑别名优先级高于函数。如果你的.bashrc里早就有alias mountmount -v之类的定义那么敲mount时先用别名展开展开后变成mount -v此时再查找函数——好消息是函数会命中坏消息是参数里多了个-v你的验证函数可能把它当成要挂载的设备名。排查这类问题最快的方法是type -a mount它会按别名、函数、内建、PATH 的顺序把所有同名定义列出来一眼就能看出谁在前。4.1 把保护范围从 mount 扩到一整个命令族只保护mount其实意义有限真正造成破坏的往往是一组命令。我在实际配置里维护了一个受保护命令清单用关联数组存命中即拦截declare -A __GATE_PROTECTED( [mount]1 [umount]1 [losetup]1 [swapon]1 [swapoff]1 [mkfs.ext4]1 [mkfs.xfs]1 [wipefs]1 [blkdiscard]1 [fdisk]1 [parted]1 [dd]1 )dd要不要放进去是个需要权衡的决定。它太常用了dd if/dev/zero oftest.img bs1M count10这种普通测试也会被拦一天下来能烦死人。我的处理方式是给它单独一条更宽松的规则只有当输出目标是块设备of/dev/或者参数里带of/dev/时才要求验证。这种按参数细分的逻辑就只能写在函数体里trace 层做不动因为 trace 拿到的是未展开的原文判断参数得再写一套解析。顺带说一句清单里我特意没放chmod、chown、rm。这类命令的高频程度和误伤成本完全不成比例加了之后使用者会想尽办法绕过去反而养成坏习惯。门禁的密度必须控制在一个不会让人想要作弊的水平线上这是经验之谈。5. 密码校验环节不落盘、不进历史、不出现在命令行里到了这一步怎么验证密码就成了整个方案的核心。看起来是几行代码的事实际上坑最多我把它单独拎出来讲。先说结论不要自己写密码文件比对不要用su最省心的是借sudo -S -k -v做一次校验。先解释为什么不能用su。su从终端读密码用的是getpass()它直接打开/dev/tty不走 stdin。也就是说printf %s\n $pw | su - $user -c true这种写法在你没有控制终端的情况下会直接卡死等待输入在你能通过管道喂进去的版本上也可能因为 PAM 配置而行为不同。而且su会 fork 一个完整的登录 shell 会话耗时在百毫秒级还会往 PAM 的失败计数里记一笔如果系统配了失败锁定策略几次输错就能把账号锁掉——这是非常糟糕的副作用。自己比对影子文件也一样不靠谱。较新版本的 Python 已经把spwd模块移除想直接读/etc/shadow里的哈希得自己解析crypt格式还得处理$6$、$y$yescrypt等不同算法稍微新一点的发行版默认算法你未必支持。这条路走不通。sudo -S -k -v是可行的因为它从 stdin 读密码-S的作用-k强制忽略已缓存的时间戳-v只做验证不执行命令。printf %s\n $pw | sudo -S -k -v会走一遍完整的 PAM 认证栈支持系统上配置的任何认证后端。代价是它会顺带刷新 sudo 的时间戳——但换个角度看这反而是好事因为验证通过之后你紧接着要执行的正是sudo mount时间戳刚好用得上省了一次重复输入。完整的校验函数我写成这样__gate_ask() { local user${SUDO_USER:-$USER} pw try # 非交互环境直接拒绝避免脚本通过管道喂密码 if [ ! -t 0 ]; then printf 当前不是交互式终端拒绝执行受保护命令\n 2 logger -t cmdgate -p auth.warning \ deny(non-tty) user$user cmd$BASH_COMMAND return 1 fi for try in 1 2 3; do printf 需要身份验证才能继续第 %d/3 次 $try 2 IFS read -r -s -t 45 pw printf \n 2 [ -z $pw ] { printf 未输入内容已取消\n 2; return 1; } if printf %s\n $pw | sudo -S -k -v /dev/null 21; then unset pw logger -t cmdgate -p auth.notice \ allow user$user tty${SSH_TTY:-${TTY:-none}} cmd$BASH_COMMAND return 0 fi logger -t cmdgate -p auth.warning \ badpass user$user tty${SSH_TTY:-${TTY:-none}} cmd$BASH_COMMAND done unset pw printf 验证次数用尽操作已取消\n 2 return 1 }几个细节值得展开。read -s是 Bash 内建的无回显读取比stty -echo安全得多——后者如果中途被 CtrlC 打断而没有恢复到stty echo你的终端就瞎了接下来输什么都看不见新手遇到这种情况会以为终端坏了。read -s把回显控制封装在内部异常退出会自己恢复。-t 45是超时防止有人占着终端不给输入导致后续命令永远挂起。提示文字一律用printf输出到2stderr不要用echo输出到 stdout否则你在一管道里执行mount ... | tee时提示语会混进管道数据里把下游的解析搞乱。密码变量用完立刻unset这一步不能省。虽然 Bash 脚本退出后进程内存就回收了但门禁函数是在你长期驻留的交互式 shell 里跑的变量会一直留在当前 shell 的环境里declare -p pw随手就能看到。而且如果开了set -x未清空的变量会原样打进调试输出甚至在共享屏幕上直接显示。所以我在函数的开头会做一次set x的兜底这一点在后面第 8 节还会提到。最后是日志。用logger而不是直接 /var/log/xxx.log因为普通用户对/var/log没有写权限sudo tee又会让门禁变慢还可能引发新的认证递归。logger把消息抛给 syslog由系统统一落盘和轮转journalctl -t cmdgate就能按标签查。日志里我特意记录了SSH_TTY和TTY多人共用账号时这是唯一能区分到底是哪个会话干的的线索。6. 非交互场景脚本、定时任务、systemd 里的兜底前面的方案全都建立在有个终端可以弹提示这个前提上。一旦命令来自 cron、systemd、CI runner 或者别的脚本[ -t 0 ]为假我的代码会选择直接拒绝。这个策略是最安全的但会打断正常业务——比如有个备份脚本确实需要定期mount一个镜像。这时候需要另一层设计。最直接的做法是在 PATH 靠前的位置放一个包装脚本。假设真实二进制在/usr/bin/mount我们写一个/usr/local/bin/mount#!/bin/bash # /usr/local/bin/mount —— PATH 层门禁 set -u REAL/usr/bin/mount [ -x $REAL ] || REAL/bin/mount # 已是 root 或来自受信任的自动化上下文直接放行 if [ $(id -u) -eq 0 ] [ -n ${CMD_GATE_BYPASS:-} ]; then exec $REAL $ fi if [ -t 0 ]; then # 交互式走一次密码校验 /usr/local/libexec/cmdgate-ask || exit 1 else logger -t cmdgate -p auth.warning deny(non-tty wrapper) cmdmount args$* printf 非交互环境不允许直接挂载请联系管理员\n 2 exit 1 fi exec $REAL $exec这个词很关键它会让包装脚本的进程直接被真实二进制替换掉不会多留一层进程信号传递也更干净。如果不加exec脚本会 fork 一个子进程你在kill的时候容易只杀掉外壳真实命令还在跑。但这个方案有个反直觉的坑sudo有自己的 PATH。sudoers 里的secure_path设置会覆盖用户环境变量里的 PATH而不同发行版给secure_path的默认值不一样有的把/usr/local/bin放前面有的压根不含它。这意味着sudo mount可能走你的包装脚本也可能直接命中/usr/bin/mount行为不确定。查的方法很简单sudo -V会打印secure_path的当前取值或者直接sudo sh -c echo $PATH看一眼。如果不含/usr/local/bin你就得把包装放到/usr/sbin或者改用 sudoers 的Defaults显式追加但后者会影响系统上所有 sudo 调用需谨慎。第二个坑来自文件系统的辅助程序机制。util-linux 在执行mount -t nfs这类操作时并不是自己实现协议而是调用/sbin/mount.nfs这样的辅助程序。如果你的包装脚本放在 PATH 前面且名字就叫mount辅助程序的调用路径不受影响它用的是绝对路径但反过来如果有人把包装命名为/sbin/mount.nfs那就会破坏 util-linux 的调用约定。所以包装脚本的命名和放置位置必须谨慎我现在的原则是只包装主命令不碰任何带后缀的辅助程序。第三个坑是 systemd。如果系统上有.mount单元或者某个服务里有RequiresMountsFor这些挂载由 systemd 自己完成完全不经过 shell 和 PATH任何 shell 层和 PATH 层的门禁都无效。要在这一层管控只能靠 systemd 的策略比如限制 unit 权限或者干脆接受systemd 管理的挂载不在门禁范围内这个事实。我在文档里明确写了这条边界避免后来的人误以为门禁是全系统的。7. 硬边界把 sudo 的时间窗和 PAM 一起收紧前面所有方案都是在用户愿意配合的前提下成立的。真要防住一个知道原理的对手必须下沉到 sudo 层。第一步是收紧时间戳。在 sudoers 里给特定用户或特定命令设定timestamp_timeout0意思是每次都必须重新输入密码一次授权只管一次命令# /etc/sudoers.d/mount-gate —— 用 visudo -f 编辑不要直接改主文件 Defaults:shareduser timestamp_timeout0 Defaults:shareduser !timestamp_type Cmnd_Alias MOUNT_OPS /usr/bin/mount, /usr/bin/umount, /usr/bin/losetup shareduser ALL(root) MOUNT_OPS这两行的作用是shareduser每执行一次列出的挂载类命令必须重新认证其他命令则完全不允许。这样即使前面所有 shell 层门禁都被绕过最后这道闸还是拦得住。sudoers 文件是语法敏感型配置一定要用visudo -f编辑写错了会导致 sudo 完全不可用很多人在这一步把自己锁在门外的经典方式就是直接vim进去改错一个字符。再进一步是 PAM 层。/etc/pam.d/sudo里可以挂pam_exec.so在认证流程里调用外部脚本实现执行 sudo 时发通知在特定时间段禁止 sudo这类逻辑auth [successok defaultignore] pam_exec.so quiet /usr/local/libexec/sudo-notify.shquiet参数很重要没有它脚本的输出会直接打在终端上包括标准错误。pam_exec会通过环境变量把PAM_USER、PAM_RHOST、PAM_TTY、PAM_TYPE传给你的脚本用这些就能做会话级别的判断。这里有个非常容易踩的递归陷阱如果脚本里又调用了sudo或者su就会再次触发 PAM 认证无限递归下去终端直接卡死。我第一版脚本里用sudo去写日志文件就是这个下场。后来全部改成logger才绕开。还有一点想提醒不要用pam_exec去接收密码。它只能看到用户认证的结果成功或失败拿不到明文密码也不应该去拿。想在挂载时额外收集一个独立的动态口令正确做法是配一个叠加的 PAM 模块比如pam_google_authenticator这类而不是在pam_exec里自己搓一套。这个边界划清楚能省掉大量后续的安全问题。至于 eBPF 或者 fanotify 这类在内核层面监控 execve 的方案理论上能做到任何进程执行 mount 二进制都会被拦截或记录覆盖面最完整。但它的代价是要写内核可加载的程序、要处理内核版本的兼容性、要评估对系统性能的影响而且在很多托管环境里根本没有加载内核模块的权限。我评估过一轮之后放弃了因为对这个需求来说属于用大炮打蚊子。除非你要做的是一个全公司范围的安全审计平台否则 shell 层加 sudo 层的组合已经能覆盖 95% 以上的实际风险。8. 我在实测中撞到的七个坑这部分是我真正想写的。前面那些机制翻文档都能查到但下面这些细节文档里不会写只能自己撞。第一个坑Tab 补全触发密码框。这个我一开始完全没意识到因为测试时都是手打完整命令。真实使用中敲一个mou按 TabBash 的补全机制会执行各种探测命令触发 DEBUG trap而此时$COMP_LINE非空。修复就是在钩子函数第一行加[ -n $COMP_LINE ] return。同类问题还有PROMPT_COMMAND里的命令替换——如果你的提示符里塞了$(git branch 2/dev/null)每次回车都会触发一次如果你恰好把git也加进了保护清单那你每按一次回车都会被问一次密码。第二个坑sudo -v通过之后sudo mount免密了但用户执行的是不带 sudo 的mount。这个逻辑上的不一致会让人困惑明明刚才验证过了怎么又不让过解决办法是把函数覆盖方案设计成验证通过后由函数自己调用sudo mount让验证和授权成为同一个动作mount() { __gate_ask || return 1 sudo mount $ }这样验证留下的时间戳刚好被接下来的sudo mount用掉用户体感是输一次密码就完成了挂载,而不是输完密码还要再输一次。第三个坑extdebug打开之后交互式 shell 明显变卡。原理前面说过它会打开 trap 向函数内部的继承导致函数体里每条命令都过一遍钩子。如果你在.bashrc里定义了几十个函数每次调用都会触发成百上千次 trap。我的应对是给钩子加了一个极早返回的快速路径先用一个 shell 内建的字符串比较判断命令是否在保护清单里不在就立刻return 0把开销压到最低。实测下来打开 extdebug 后每条命令的额外延迟在亚毫秒级肉眼无感。第四个坑在容器里read -s卡死。用docker exec -it进去是有 tty 的但用docker exec不带-it就没有此时read -s从 stdin 读不到东西会一直阻塞直到超时。我在__gate_ask里做的[ ! -t 0 ]判断就是为了拦住这种情况。这个判断不能省否则一个失控的 CI job 卡在密码提示上能挂几个小时没人发现。第五个坑密码通过命令行参数泄漏。有一种常见的偷懒写法是把密码作为参数传给验证程序比如verify $pw这样在你ps aux或者top的时候密码会明晃晃地出现在进程列表里任何同机用户都能看到。绝对不要这么做。管道传递是相对安全的因为管道内容不在进程参数里。同时也要留意set -x调试模式下 Bash 会把每一行展开后的内容打到 stderr包括你printf %s $pw那一行。所以我在校验函数开头会临时关掉 trace{ local _x$-; set x; }函数结束再恢复。第六个坑日志里记录完整命令导致敏感信息外泄。mount -o passwordxxx //server/share /mnt这种带凭据的挂载命令并不罕见如果原样记进日志等于把凭据写进了 syslog。我现在记录之前会做一次脱敏把password、pass、cred后面的内容替换成星号。这个处理很粗糙但能挡住绝大部分无心之失。第七个坑root 用户自己执行时门禁完全失效。这一点特别值得说。sudo对 root 通常不要求重新认证因为 root 已经是最高权限所以在 root shell 里执行sudo -S -k -v会直接返回成功密码校验形同虚设。如果你的场景是防止 root 自己手滑那这套基于密码的方案根本用不上得换成一次明确的交互确认或者干脆做一个危险操作需要二次确认的提示接受它只能防手滑、防不了刻意的现实。我在一台单人的实验机上就是这么处理的mount前面加一个必须手动输入完整命令名才能继续的确认输入错了就取消。麻烦但确实让我少犯了几次错。最后分享一个小习惯。我在门禁脚本里给每个受保护命令都留了一条为什么的记录写在logger的 message 里比如reasoninteractive-mount。半年后回头看日志能一眼分清哪些是人工操作、哪些是脚本触发、哪些是被拒绝的尝试。门禁本身的价值只有一半另一半在于它留下的痕迹——出事之后能回溯到谁在什么时间、从哪个终端、试图做什么这才是流程约束真正的落点。至于阈值调多严我的经验是先从最宽松的版本开始跑两周看看日志里的误伤率再逐步收紧一上来就全拦的方案通常活不过三天。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →