1. 先搞清楚这条命令到底在解决什么问题history | grep ssh这行东西估计每个跟 Linux 打过交道的人都在某个深夜敲过。它干的事情极其朴素把当前 shell 的命令历史全量倒出来交给grep过滤留下所有带ssh字样的行。就三秒钟你就能翻出上个月连过的那台跳板机地址、那个只连过一次的边缘节点、以及那条被自己改坏又忘了改回去的端口转发参数。关键词拆开看无非三个history提供数据源grep提供筛选ssh只是你要找的特征串。但真到了排障现场这三个词凑一起能救命。这篇东西写给谁看刚上手运维、还在被那台机器 IP 到底是多少折磨的同学能直接抄作业写过几百次这条命令但从没想过HISTSIZE是干嘛的老手也能在中间几节里找到点之前忽略的细节。我不会只丢一行命令就完事重点是把为什么有时候查不到怎么查得又快又准怎么让它长期可用这几件事讲透。毕竟这条命令的价值不在于语法而在于你对 shell 历史机制的理解深度。1.1 history、grep、ssh 三个角色各司其职history是 bash 的内建命令不是/usr/bin/history那个可执行文件你在文件系统里ls是找不到它的。它的输出格式固定为行号 两个空格 原始命令比如512 ssh ops192.0.2.10 -p 2222。这个行号很关键后面用!512重放命令、用history -d 512删除某条记录都靠它定位。grep在这里就是个纯文本过滤器默认按基本正则匹配大小写敏感。所以history | grep ssh抓不到SSH、抓不到Ssh也抓不到scp——虽然scp底层走的也是同一套远程传输协议。这一点新人经常栽跟头明明记得昨天传过文件grep ssh死活没结果其实人家叫scp。ssh在这个上下文里只是被搜索的字符串但它的特殊性在于一条 ssh 命令行里往往压缩了大量上下文信息——用户名、目标主机、端口、密钥路径、跳板配置、本地/远程端口转发规则。这就是为什么查 ssh 历史比查别的命令更有价值一行记录基本等价于一份浓缩的我当时在干什么的证词。1.2 半夜被叫起来的第一个动作说个真实场景细节做了虚构处理。某天凌晨两点监控告警说一台业务节点负载飙到 40值班的 A 同学登上去看发现有个进程在疯狂写日志但不知道是谁、什么时候、从哪台机器上去的操作。他做的第一件事就是history | grep ssh翻出这台机器最近被谁连过、连的时候带了什么参数——因为很多时候问题不是自动化脚本引起的而是白天某个临时操作留下的后遗症。还有更常见的版本接手一台前人维护的机器配置文件里写着AllowUsers但不知道还有谁在用或者要下线一台旧机器得先确认没有业务方还在往里连。这时候~/.bash_history就是最廉价、最直接的软审计日志不用装任何采集组件不用申请任何权限一条管道就出结果。这也是它真正的定位轻量级、即时性的现场取证工具。它不是正式的审计系统不具备防篡改、不可抵赖的特性但在 90% 的日常排障里够用了。1.3 先把边界划清楚它查不到什么这条命令最大的坑不是语法是预期错位。很多人以为敲完就能看到这台机器上发生过的所有 ssh 操作实际上你看到的东西受限于一堆条件只覆盖当前用户的历史别人的~/.bash_history你没权限读除非有 sudo。只覆盖bash如果对方用的是 zsh、fish、dash历史文件位置和格式都不一样zsh 默认在~/.zsh_history且带自己的扩展格式。只覆盖交互式 shell通过ssh host command这种非交互方式执行的一次性命令通常压根不进历史。只覆盖落盘了的部分还在内存缓冲里没写文件的命令可能在另一个终端里根本看不见。只覆盖没被清理的部分history -c、手动编辑文件、设置HISTIGNORE都能让它消失。把这五点先记住后面所有为什么查不到的问题八成都能归到其中一条。2. history 的存储机制为什么有时候 grep 出来是空的这是全文最值得花时间的一节。绝大多数命令明明敲过却查不到的疑问根源都在 bash 处理历史的两段式设计上。搞懂这一段你排查历史问题就不需要靠重启终端碰运气了。2.1 内存缓冲与文件落盘是两回事bash 的历史分两层一层是进程内存里的列表你按上方向键翻到的就是它另一层是磁盘上的~/.bash_history文件。这两层默认不是实时同步的。bash 的行为大致是这样启动时把~/.bash_history读进内存受HISTSIZE限制只读最后 N 条之后你敲的每条命令只往内存里追加只有在 shell 正常退出时才把内存内容覆盖写回文件默认是覆盖不是追加。这个覆盖动作是无数人踩坑的源头——你开了两个终端终端 A 敲了 50 条命令终端 B 敲了 3 条先退出 A写入 50 条再退出 B用 3 条覆盖掉全部A 那 50 条历史就蒸发了。解决办法是在~/.bashrc里加两行shopt -s histappend PROMPT_COMMANDhistory -a; $PROMPT_COMMAND第一行把退出时覆盖改成退出时追加第二行更狠让每次显示提示符之前都执行history -a也就是每条命令执行完立刻追加到文件。加了这两行之后多终端并存、意外断连、终端被强制关闭历史都不会丢。注意history -a只追加自上次写入以来新增的部分不会重复写所以这个操作是幂等的不用担心文件被写爆。2.2 四个环境变量决定你能查多远光有文件还不够能查多久取决于四个参数。这几个值在不同发行版上差异很大别信网上抄来的默认值自己echo一下最准。变量作用常见默认建议值HISTSIZE内存中保留多少条500 / 100010000HISTFILESIZE文件里保留多少行500 / 200050000HISTCONTROL去重 / 忽略空格开头未设置ignorebothHISTIGNORE忽略特定模式未设置ls:cd:pwd:exitHISTTIMEFORMAT是否带时间戳未设置%F %THISTSIZE和HISTFILESIZE的关系经常被搞混前者管内存你在当前会话能翻到多少后者管文件下次开 shell 能加载多少。如果只调大HISTSIZE不调HISTFILESIZE那你退出时写文件会被截断到HISTFILESIZE行等于白调。两个必须一起改。HISTCONTROLignoreboth是ignorespace和ignoredups的合体前者让以空格开头的命令不被记录后者让连续重复的命令只记一次。ignorespace这个特性非常实用——你在命令行里临时输入一个带敏感参数的命令前面加个空格就干净了。2.3 打开时间戳之后历史文件长什么样HISTTIMEFORMAT这个变量值得单独讲。不设它的时候history输出只有行号和命令你根本不知道那条ssh ops192.0.2.10是三小时前还是三个月前的。设了之后export HISTTIMEFORMAT%F %T 输出就变成512 2024-06-11 02:17:43 ssh ops192.0.2.10 -p 2222。排障时这个时间戳的价值极高因为你能把命令和日志时间线对齐——看看这个 ssh 是发生在告警之前还是之后是操作导致了故障还是故障发生后有人上去修。但有个细节必须知道开启这个选项后~/.bash_history文件的格式会变。每条命令前面会多插入一行#Unix时间戳比如#1718069863 ssh ops192.0.2.10 -p 2222这意味着两件事。第一文件体积会变大每条命令平均多出约 12 个字节估算容量时要算进去。第二如果你的历史文件是在没开时间戳的时期生成的现在开了那些老记录前面没有时间戳history显示它们时时间列会是错的或者是空的——这是正常现象不是数据损坏。还有个更隐蔽的问题history | grep ssh本身是一条命令它自己也会被记录进历史而且因为它在管道右侧启动某些情况下记录的是history | grep ssh完整形式导致你下次再 grep 的时候会多出一条自引用噪音。这个后面第 3 节会讲怎么排除。3. grep 的写法演进从能用到好用基础版history | grep ssh能解决 60% 的问题剩下 40% 需要稍微动点脑筋。这一节按实用度排序从最常用的变形讲起。3.1 基础变形大小写、词边界、模糊匹配先把最常见的三个开关记住history | grep -i ssh # 忽略大小写能抓到 SSH、Ssh history | grep -iw ssh # 整个单词匹配避免匹配到 sshd_config 之类 history | grep -iE ssh|scp|sftp # 扩展正则一次抓全远程传输类命令-w这个参数的价值在一个具体场景里体现得最明显。假设你曾经编辑过/etc/ssh/sshd_config历史里有vim /etc/ssh/sshd_config同时也连过几十台机器。只敲grep ssh的时候vim那行会混在结果里把真正有用的连接记录冲散。加上-w之后只匹配ssh作为一个完整单词出现的行sshd_config因为sshd后面跟着_不算词边界就被过滤掉了。反过来如果你就是想找所有跟 ssh 相关的东西包括配置文件编辑、密钥生成、服务重启那就不加-w甚至应该用grep -i ssh加上更宽的模式。-E加多关键词是我个人最推荐的写法。实际工作里你要找的往往不是 ssh 本身而是所有跟远程访问相关的操作这时候ssh|scp|sftp|rsync一起抓覆盖面比单抓 ssh 强太多因为很多人传文件用的是rsync -e ssh那行里 ssh 可能只是参数的一部分。3.2 排除噪音让结果只剩你要看的东西随着历史记录变多原始输出会变得很难读。三个降噪技巧排除自引用。grep -v grep能过滤掉你自己查历史的那条命令history | grep ssh | grep -v grep排除明显的本地操作。如果你只想看真正发起的连接可以反向排除一些常见误匹配history | grep ssh | grep -vE vim|cat|less|man|systemctl去重并按频次排序。想知道自己最常连哪台机器用这个history | grep -oE ssh [^ ] | sort | uniq -c | sort -rn | head -20这里的-o是关键它让 grep 只输出匹配到的部分而不是整行配合后面的sort | uniq -c就能统计出每个目标主机被连了多少次。我在一次梳理哪些旧机器可以下线的工作里就是用这个办法一分钟内列出了半年内所有被连过的目标按频次排序零访问的那批直接进下线清单。提示这个统计方式对ssh -p 2222 userhost这种带参数的写法会失真因为[^ ]只抓到-p。遇到这种情况模式改成ssh .*([^ ])再用awk取最后一段更准。3.3 多会话与多机器的历史怎么归并单机单会话是最理想的情况。现实是你有三台跳板机、五个终端、两台本地开发机历史散落在各处。归并思路分三层。第一层同一台机器上的多个终端。靠 2.1 节说的histappendhistory -a解决前提是配置到位。没有配置的话你可以在查之前先手动合并一次当前会话到文件history -a然后cat ~/.bash_history | grep ssh这样至少能看到文件里已有的全部内容。第二层同一台机器上的多个用户。需要 sudo 权限去读别人的历史文件sudo grep -h ssh /home/*/.bash_history | sort -u | tail -50-h参数的作用是不输出文件名前缀避免每行前面挂一串路径。sort -u去重tail -50看最近的。这个命令在这台机器上到底有几个人在操作的排查里非常有效因为不同用户的历史文件是分开的history命令本身看不到别人的。第三层跨机器的历史汇总。这个就没法靠一条命令解决了需要提前做采集把各机器的历史文件定期收集到一处。最朴素的做法是用rsync定时拉取稍微正规一点是配置集中式日志采集。第 4 节会讲具体的落地方案。4. 落地把它做成随手可用的工具一条命令敲三次以上就该封装。这一节讲三个层次的落地从个人用的别名到容量规划的估算再到团队级的做法。4.1 别名与函数封装最轻量的方案是在~/.bashrc里加别名alias hsshhistory | grep -iE ssh|scp|sftp | grep -v grep | tail -30 alias htimeHISTTIMEFORMAT%F %T history | tail -50tail -30是刻意的限制。不加的话一个用了两年的账号历史可能有上万条全刷出来眼睛都花了而且真正有用的永远是最近的。想全看的时候去掉 tail 用| less翻页。但别名有个局限不能带参数。想要查任意关键词的能力用函数更合适hfind() { if [ -z $1 ]; then echo 用法: hfind 关键词 return 1 fi history | grep -i -- $1 | grep -v hfind | tail -40 }这个函数比我见过的很多版本都管用原因是它加了--分隔符。如果你搜的关键词恰好以-开头比如查-p 2222不加--的话 grep 会把它当成自己的选项直接报错退出。这种细节平时遇不到遇到一次就够你懵半天。另外那个grep -v hfind也别省。函数名本身会出现在历史里不加过滤的话你每查一次就多一条噪音查十次能堆出十行无用记录。4.2 容量与轮转参数怎么算调大HISTSIZE之前先算一下成本。历史文件是纯文本体积很好估。假设一条命令平均 60 个字符含用户、主机、端口这些开启时间戳后每条约 75 字节。每日命令数单日体积保留 90 天保留 365 天200 条约 15 KB约 1.3 MB约 5.4 MB1000 条约 75 KB约 6.6 MB约 27 MB5000 条约 375 KB约 33 MB约 134 MB结论很明确对个人账号来说这个开销可以忽略不计HISTFILESIZE50000完全撑得住。真正的成本不在磁盘在检索效率——grep扫 5 万行是毫秒级的扫 50 万行也能接受所以不用太担心性能。配置推荐如下直接抄export HISTSIZE10000 export HISTFILESIZE50000 export HISTCONTROLignoreboth export HISTIGNOREls:cd:pwd:exit:clear:history export HISTTIMEFORMAT%F %T shopt -s histappend PROMPT_COMMANDhistory -a; $PROMPT_COMMAND顺序有讲究HISTIGNORE里加了history之后你敲的history命令本身就不再入档前面说的自引用噪音问题从源头上就消掉了一半。但注意HISTIGNORE匹配的是完整命令的精确模式用冒号分隔支持通配符所以history | grep ssh这种带管道的不会被history这一项匹配掉还是得靠grep -v兜底。改完记得source ~/.bashrc然后在新开的终端里验证——已存在的 shell 不会重新读取配置这个坑我见过太多次了。4.3 从个人历史到团队审计个人用history | grep ssh没问题。但如果要回答过去 30 天有哪些机器被谁连过这种团队级问题靠翻各人主目录就不现实了。这时候需要把历史变成结构化数据。一个不依赖任何商业组件的做法是在统一的 shell 配置里挂一个PROMPT_COMMAND钩子把每条命令连同用户、主机名、时间、TTY 一起写到本地的统一日志文件再由日志采集统一收走。log_cmd() { local cmd cmd$(history 1 | sed s/^ *[0-9]* *//) [ -z $cmd ] return printf %s|%s|%s|%s\n \ $(date %F %T) $USER $(hostname -s) $cmd \ /var/log/cmd-audit/commands.log }这段钩子的核心是history 1——它取的是历史列表里的最后一条也就是刚刚执行完的那条。加上时间、用户、主机三列之后你就有了一个可以用awk、grep随意切分的扁平文本。格式统一之后团队级的查询就变成一句话awk -F| $4 ~ /ssh/ {print $1, $2, $3, $4} /var/log/cmd-audit/commands.log | tail -100比起在几十台机器上分别history | grep ssh这种方式的价值在于时间线能对齐。故障发生时你把各机器的命令日志按时间排序谁先动的手一目了然。注意这类记录会包含命令行里的所有参数包括可能出现的临时凭据、内网地址、业务标识。落盘前务必评估脱敏方案至少要把明显的敏感字段用正则替换掉日志目录权限设成0700且只对特定组可读。这件事不能事后补救必须在设计阶段就定下来。5. 常见问题与排查速查表这一节把我在实际环境里见过的问题整理成表遇到情况直接对号入座不用从头推。5.1 现象与原因对照现象最可能的原因处理方式明明敲过grep完全无结果命令在另一个终端且未配置histappend退出时被覆盖加shopt -s histappend和history -a只能看到最近的几十条HISTSIZE太小或文件被HISTFILESIZE截断两个变量同时调大有记录但没有时间未设置HISTTIMEFORMAT就产生了这些历史无法追溯只能从现在开始设时间列显示得乱七八糟老记录无时间戳新记录有混合显示属正常现象history -c后重新积累或忽略搜索关键词以-开头时 grep 报错grep 把关键词当成了自己的选项加--分隔符一条命令被记了两次多终端各自写文件后合并属正常sort -u去重即可别人的操作查不到history只读当前用户用 sudo 读对方~/.bash_history刚执行的命令在文件里搜不到只grep了文件没grep内存先history -a落盘再查5.2 几个真踩过的坑坑一su之后历史断了。用su切换用户理论上会读新用户的历史文件但如果你是用su不带-参数环境变量大部分被继承下来了包括HISTFILE指向的还是原来的文件。表现就是切过去之后history看到的还是上一个用户的内容或者双方混在一起。解决方案是用su -带减号或者sudo -i让环境干净重载。坑二HISTIGNORE写太宽把关键记录吃掉了。有人为了清净把HISTIGNORE设成了*ssh*本意是忽略那些无聊的重复连接结果排障时发现所有 ssh 操作都不见了。这个变量是输入时过滤的被忽略的命令压根不会进历史事后完全无法找回比事后删除危险得多。要清净用HISTCONTROLignoredups就够了别动HISTIGNORE。坑三history -c清了之后发现清错终端了。history -c只清当前 shell 的内存列表不动文件。真正的彻底清空需要history -c history -w先清内存再写空回文件。这个动作基本不可逆敲之前想清楚是哪个终端。我用它的时候习惯先cp ~/.bash_history ~/.bash_history.bak留一手。坑四zsh 环境照抄 bash 配置无效。zsh 的历史配置文件是~/.zsh_history格式是: 时间戳:耗时;命令这种扩展形式HISTSIZE对应的是SAVEHIST和HISTSIZE两个不同变量HISTTIMEFORMAT压根不存在zsh 默认就记录时间。所以history | grep ssh在 zsh 里能跑但配置项要全部换一套。6. 历史命令里藏着的信息面聊到这儿该说点稍微严肃的东西。~/.bash_history这个文件在便利性和风险性上是完全对称的——它记录得越全泄露面就越大。一个简单的判断标准如果你不愿意让运维同事看到某条命令那它就不该以明文躺在历史文件里。6.1 哪些东西容易无意间留下痕迹最常见的几类直接写在命令行里的账号凭据mysql -u root -pXXXX这种写法密码会完整进历史带 token 的接口调用curl -H Authorization: Bearer xxx内网拓扑信息一堆ssh目标地址拼起来就是内网地图临时文件路径里带业务名称的能反推出业务结构。这类痕迹的危险在于它不像配置文件那样有明确的权限边界。配置文件你至少知道要chmod 600历史文件很多人压根没管过默认644同机器的其他用户cat一下就全看见了。处理方式分两头。能改习惯的改习惯——密码走交互式提示或密钥认证token 走环境变量或配置文件。改不了习惯的加一道保护chmod 600 ~/.bash_history更彻底一点在交互式命令前加个空格配合HISTCONTROLignorespace这条命令完全不进历史。这个小技巧我常用成本低到可以随手做。6.2 团队里怎么定这条线个人习惯之外团队需要一条明确的规则线不然每个人的标准都不一样。我见过的比较务实的做法是三条第一收集范围写清楚。明确告知哪些机器的命令历史会被集中采集、保留多久、谁能看。不写清楚就采集后面一旦出现争议很难解释。第二保留期和业务对齐。财务相关的机器保留期长一些临时测试机 7 天就够。一刀切定 365 天既浪费存储又放大风险。第三查询要留痕。谁在什么时候查了别人的历史本身就是一条该被记录的记录。这个要求听起来有点重但真正做起来就是一个包装脚本的事比事后追责容易得多。从history | grep ssh这么轻的东西一路聊到团队规范确实有点远。但实际干过几年就会发现工具本身从来不复杂复杂的是人和人之间的边界。我在这个命令上花的调试时间差不多可以换算成好几个通宵了。最开始是为了找自己三天前连过的一台机器后来变成帮同事定位操作记录再后来是给团队设计采集方案。走过的路里最值钱的经验就一条别在事后才想起history。等你真正需要它的时候能查到什么完全取决于你半年前怎么配置的那几行~/.bashrc。所以如果这台机器你还打算用一年以上现在就花五分钟把那几个HIST变量和histappend加上比什么都实在。