服务器从一台变成三台之后,SSH 管理就开始失控:密钥散落在几个人的笔记本上、离职的人还记得密码、审计日志里只有 IP 分不清是谁、某个跳板机的 root 密码被贴在了聊天记录里。这些问题不是「安全意识」问题,而是缺一套集中认证和审计的结构。本文写的是小团队能落地的方案:从密钥集中下发,到跳板机强制转发,再到会话录制与命令审计,每一步都给可执行的配置。
一、先明确目标:不是「更安全的密码」,而是「可归因」
很多团队加固 SSH 的第一反应是把密码换成密钥,但密钥分散在多台机器上,效果只是「更难管理」。集中认证要解决的是三件事:
- 身份唯一:一个自然人对应一个可追踪的身份,不共享账号;
- 路径唯一:所有运维访问必须经过同一台入口,入口之外直接拒绝;
- 记录完整:每次登录、每条高权限命令都能对应到人。
做到这三点,即使某一台内网机器被攻破,也拿不到横向移动的便利,事后也能回答「是谁在什么时候改了 nginx 配置」。
二、第一步:内网机器只允许从入口登录
最容易被忽略也最有效的一步,是把业务机的 SSH 收窄到「只接受来自跳板机的连接」,而不是所有人可连。
# /etc/ssh/sshd_config (业务机)
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
AllowUsers deploy ops
Match Address 10.10.0.0/24
AllowUsers *
顺序上要注意:Match 块必须放在文件末尾,块内的指令会覆盖全局设置。更稳妥的做法是用防火墙收口,因为 sshd 配置写错会把自己锁在门外:
# 只允许跳板机访问 22 端口
iptables -A INPUT -p tcp --dport 22 -s 10.10.0.5 -j ACCEPT
iptables -A INPUT -p tcp --dport 22 -j DROP
改防火墙前务必先开一个已登录的会话留着,验证新连接能通再退出。这是所有运维里最容易造成「自己把自己关在门外」的操作。
三、第二步:密钥集中管理,而不是人手一份拷来拷去
小团队不必上完整的堡垒机产品,可以先用「一台跳板 + 一份带注释的 authorized_keys」过渡:
# /home/ops/.ssh/authorized_keys
restrict,command="/usr/local/bin/audit-log.sh ops-zhang" ssh-ed25519 AAAAC3...zhang@laptop
restrict,command="/usr/local/bin/audit-log.sh ops-li" ssh-ed25519 AAAAC3...li@workstation
restrict 是 OpenSSH 7.2 之后引入的选项,一次性关掉端口转发、agent 转发、pty、X11 等所有扩展功能,再按需用 permitopen 或 pty 放开。它比逐条写 no-* 更不容易漏项。
如果团队超过五个人,或者有人用多台设备,用 SSH 证书(CA)更省事:跳板机上放一份 CA 密钥,为每个人签发一张有效期 30 天的证书,业务机只信任这个 CA。这样加人、减人都不需要逐台改 authorized_keys:
# 在 CA 主机上签发
ssh-keygen -s /etc/ssh/ca_user -I "zhang@2026Q4" -n deploy,ops -V +30d id_zhang.pub
# 业务机 sshd_config
TrustedUserCAKeys /etc/ssh/ca_user.pub
证书方式的关键收益是「过期即失效」。离职或设备丢失时,不用去翻哪台机器上还有他的公钥,等证书过期即可,需要立刻生效则用 RevokedKeys 撤销。
四、第三步:跳板机强制转发,禁止直连
只有「业务机只信跳板」还不够,因为跳板机上的人可以自由登录业务机。更彻底的做法是在跳板机上把登录用户限制为只能转发,不能拿 shell:
# /etc/ssh/sshd_config (跳板机)
Match User ops
PermitTTY no
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding yes
ForceCommand /bin/echo "jump host: use -J or ProxyJump"
PermitTTY no 是关键:没有伪终端,用户就无法在跳板机上执行交互命令,但 ssh -J 的 TCP 转发依然可用。这样跳板机就退化成一个纯粹的网络通路,被攻破也拿不到 shell。
客户端侧统一配置,避免每个人自己拼命令:
# ~/.ssh/config
Host web-*
ProxyJump ops@10.10.0.5
User deploy
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 30
五、第四步:会话录制与命令审计
审计的目标不是「监视每个人」,而是出事时能复盘。三条成本递增的路线:
- 只记命令:配置
SyslogFacility AUTH+ auditd 的execve规则,记录每个进程的完整参数。开销极低,但对交互式编辑器里的操作无效(在 vim 里改了什么看不到)。 - 记录完整终端输出:用
script命令或tlog包装登录 shell,把所有输出落成文件。可读性最好,能还原「屏幕上到底显示了什么」。 - 强制录制:sshd 的
ForceCommand指向一个记录器,用户无法绕过。
最小可用的命令审计规则(auditd):
# /etc/audit/rules.d/ops.rules
-a always,exit -F arch=b64 -S execve -F auid>=1000 -F auid!=4294967295 -k ops_cmd
-w /etc/nginx/ -p wa -k nginx_conf
-w /etc/ssh/sshd_config -p wa -k ssh_conf
查询时用 ausearch -k ops_cmd -ts recent 拉出最近的命令。注意 auid!=4294967295 这一条,它排除了无法归属到具体用户的进程(比如某些系统服务),否则日志会被大量噪声淹没。
审计日志本身要防篡改:写入远端 syslog(rsyslog 的 @@ 转发),或者定期 auditctl -l 对比规则是否被改动。日志只留本机的审计,等于把证据交给被审计的人保管。
六、第五步:给运维操作加一道「变更留痕」
集中认证解决了「谁登录了」,但不能回答「改了什么、为什么改」。小团队的最低要求是把配置变更纳入版本管理:
# /etc/nginx 目录直接做成 git 仓库
cd /etc && git init nginx && cd nginx
printf '*.log\ntmp/\n' > .gitignore
git add -A && git commit -m "baseline: $(date +%F)"
之后每次改动前 git commit -m "原因",出问题可以 git diff 看改了什么、git checkout 秒级回滚。成本几乎为零,但比任何审计工具都实用 —— 因为复盘时最想知道的「改之前是什么样」,它直接给出答案。
七、落地顺序与验收
不要一次全改。推荐顺序:先做防火墙收口(业务机只允许跳板访问),再做密钥集中与证书(消除共享账号),然后跳板机禁 shell,最后上审计与录制。每一步都可独立回滚,且每一步都能立刻降低风险。
验收用四条命令就够了:
- 从办公网直连业务机,应超时(
ssh -o ConnectTimeout=5 web-01); - 经跳板机登录,应成功,且
who里显示的用户名对应真实姓名; - 在跳板机上执行
ls,应被拒绝(PermitTTY no生效); ausearch -k ops_cmd -ts today | tail能看到刚才执行的命令。
八、常见踩坑与排障顺序
这套结构上线后,最容易出问题的不是配置本身,而是「改了一半」造成的半通不通状态。整理六个实际踩过的坑:
- 把
Match块写在全局指令之前:sshd 对Match的处理是「从该行开始到下一个 Match 或文件末尾」,写在前面会让后面的全局项覆盖它,表现是「配置里明明有,就是不生效」。用sshd -T -C user=ops,addr=10.10.0.9输出「该用户在该来源下实际生效的配置」,比读文件可靠得多。 - 改了 sshd_config 没重启,误以为生效:
systemctl reload sshd会重新读取配置但保留现有连接;新配置写错时 reload 会失败并保留旧配置,而restart可能直接让服务起不来。永远先sshd -t测语法,再 reload。 - 防火墙规则写成「加了 DROP 但没加 ACCEPT 顺序」:iptables 是按顺序匹配的,DROP 写在 ACCEPT 前面会把自己也拦掉。用
iptables -L INPUT -n --line-numbers确认顺序,并优先用-I插入到顶部。 - 证书有效期设太长:一年期的证书等于把「离职即失效」的收益还回去了。30 天是常见折中,短到需要定期续签但不至于天天打扰。
- agent 转发开着:
AllowAgentForwarding yes时,跳板机上的 root 可以借用你的 agent 登录任何你被授权的机器。前面配置里的no不是可选项。 - auditd 规则没持久化:
auditctl -a加的规则重启后消失,必须写进/etc/audit/rules.d/并用augenrules --load加载,重启后auditctl -l复核。
排障顺序建议固定为「客户端能否连通 → 跳板机是否允许转发 → 业务机是否接受该来源 → 用户是否被授权 → 命令审计是否记录」。上一层不通就不要查下一层,这样五分钟内能定位到具体哪一环。
九、权限分层:把「能登录」和「能改配置」分开
集中认证之后,下一个问题往往是「所有人都能 sudo」。小团队不必上完整的 RBAC,用 sudoers 分三档就够:
# /etc/sudoers.d/ops
Cmnd_Alias READONLY = /bin/systemctl status *, /usr/bin/journalctl, /usr/bin/tail
Cmnd_Alias RELOAD = /bin/systemctl reload nginx, /usr/bin/nginx -t
Cmnd_Alias FULL = ALL
ops-zhang ALL=(root) NOPASSWD: READONLY, RELOAD
ops-li ALL=(root) NOPASSWD: READONLY
%wheel ALL=(root) FULL
注意 systemctl 的路径要写成绝对路径且不要用通配符放开 /bin/systemctl *,否则等于给了 root(可以 systemctl 启动任意单元)。改完用 sudo -l -U ops-li 复核实际权限,这一步能查出 90% 的配置错误。
更细一层可以用 sudo 的 NOEXEC 或 sudoedit 限制「只能编辑指定文件」,但对小团队来说,把「只读」「能 reload」「能全权」分清楚,已经能挡住绝大多数误操作。
十、小结
集中认证与审计的价值不在「更复杂的登录流程」,而在把「谁、从哪、做了什么」变成可查询的事实。对小团队来说,一台跳板机 + SSH 证书 + 防火墙收口 + auditd 命令审计 + 配置 git 化,总工作量不超过半天,却能把最容易出事的运维环节从「靠自觉」变成「有结构」。下一步想再提升,才需要考虑专门的堡垒机产品 —— 但在那之前,这五步已经覆盖了绝大多数真实风险。