加固之前先清点。不知道当前暴露了什么,改配置就是在猜。
第 0 层:先量
ss -ltnup
systemctl list-unit-files --state=enabled --no-pager | tail -3
dpkg -l | grep -c '^ii'
awk -F: '$3==0 {print $1}' /etc/passwd
awk -F: '$7 !~ /(nologin|false|sync)$/ {print $1, $7}' /etc/passwd
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -print 2>/dev/null | sort六条命令分别回答:谁在监听、启用了多少单元、装了多少包、有几个 UID 0 账号、有几个账号能拿到 shell、有几个 SUID/SGID 二进制。
把这六条的输出存档并标注日期,之后每次加固都拿这份基线对比。没有基线的加固无法验证,也无法在半年后回答"当时改了什么"。
我在一台 Ubuntu 24.04 的装机上跑过,dpkg 包数 1400 多,启用的 unit 文件 67 个。那台带桌面环境,纯服务器装机会少一截——差多少取决于装机时选了什么 tasksel。
第 1 层:网络暴露
ss -ltnup | awk 'NR>1 {print $1, $5, $7}'看两件事:监听地址,和对应的进程。
0.0.0.0:PORT 和 *:PORT 意味着所有网卡都能连进来。绝大多数内部服务应该绑在 127.0.0.1 或具体内网地址上。数据库、缓存、消息队列绑在 0.0.0.0 是我见得最多的低级错误,而且通常不是有意为之,是默认值没人改。
原则:能在应用层绑地址就别靠防火墙兜底。防火墙规则会被后来的运维改动;绑定地址写在应用配置里,改动要重启服务,动静大,反而不容易被误改。
ss 最后一列是进程信息,非 root 跑只能看到自己的进程。清点必须用 root,否则会漏掉一半。
第 2 层:可登录身份
awk -F: '$3==0 {print $1}' /etc/passwd
awk -F: '$7 !~ /(nologin|false|sync)$/ {print $1, $7}' /etc/passwd
ls -l /root/.ssh/ /home/*/.ssh/ 2>/dev/nullUID 0 的账号只应该有 root 一个。多出来的每一个都是历史遗留,得有人能说清它是干什么的。
能拿到 shell 的账号数决定了你要审计多少个 authorized_keys。这些文件是权限放得最松的地方之一——离职的人留下的公钥可以躺在那里好几年。
SSH 服务端的四条基本项:
sshd -T 2>/dev/null | grep -Ei '^(passwordauthentication|permitrootlogin|pubkeyauthentication|allowusers|allowgroups|kbdinteractiveauthentication) 'sshd -T 输出的是实际生效的完整配置,Include、Match 块和命令行覆盖都算进去了。别直接读 /etc/ssh/sshd_config——发行版把配置拆成了好几个文件,读单个文件得到的结论可能是错的。
PermitRootLogin 我建议设 no 而不是 prohibit-password。后者允许密钥登录 root,看起来安全,但某台开发机一旦被拿下、私钥又在 root 的 authorized_keys 里,就直接是最高权限。no 强迫所有人先落到普通账号再 sudo,多一步,且这步进日志。
第 3 层:服务单元
systemctl list-unit-files --state=enabled --no-pager
systemctl list-units --type=service --state=running --no-pager两份清单的差集有意思:enabled 但没 running 的是开机才起来的东西;running 但没 enabled 的是有人手动起的临时服务,后者往往是排查时忘了关的。
原则:移除优于关闭,关闭优于加固。
包被卸掉,它的二进制、systemd 单元、socket 激活配置、cron 条目一起消失。systemctl disable 只断了开机启动;mask 更彻底一点,但也只是让单元无法启动,二进制还在磁盘上,本地提权漏洞照样能用。
不确定该不该卸的,先 mask 观察一段时间再卸。直接卸的风险是连带依赖:apt remove 一个看起来无关的包,可能把某个元包一起拆掉。卸之前先跑:
apt-get -s remove PACKAGENAME | grep -E '^(Remv|Inst)'-s 是模拟,不动系统。看清连带删什么再决定。
云厂商的 agent、监控探针、日志采集器算不算攻击面,分情况:它们确实扩大了攻击面(有网络出站、有 root 权限、有自动更新通道),但也是出事后唯一的取证来源。我倾向保留监控和日志采集,卸掉那些"顺便"装上的许可证管理客户端。
第 4 层:本地提权路径
SUID/SGID 清单最该定期看。
find / -xdev \( -perm -4000 -o -perm -2000 \) -type f -print 2>/dev/null | sort标准 Ubuntu 装机上会看到 passwd、su、sudo、mount、umount、newgrp、chfn、chsh、chage、expiry、pkexec、fusermount3、ssh-agent,加上 /usr/lib 下面的 unix_chkpwd、ssh-keysign、dbus-daemon-launch-helper、polkit-agent-helper-1。这些是已知的、有人维护的。
清单里混进别的才需要关注。装过桌面或浏览器的机器会看到 chrome-sandbox、msedge-sandbox;装过虚拟化客户机工具会看到 vmware-user-suid-wrapper。这些不一定是漏洞,但出现在服务器上说明这台机器的用途和它的加固基线不匹配。
pkexec 值得单独提一句,它有过多起本地提权漏洞。纯服务器上用不到图形化授权提示,我倾向卸掉 polkit 相关包,前提是先用 apt-get -s remove polkitd 确认连带。
sudo 规则的清点:
grep -rEv '^\s*(#|$)' /etc/sudoers /etc/sudoers.d/ 2>/dev/null要找的是 NOPASSWD: 和 ALL=(ALL) 的组合,以及指向脚本路径的规则。后者等于把那个脚本的可写性变成 root 权限——脚本在 /tmp 或全局可写目录里,规则就等于无条件提权。
第 5 层:软件来源
grep -rhEv '^\s*(#|$)' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null
ls /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ 2>/dev/null第三方 apt 源是供应链上最容易被忽略的一环。加了源之后 apt upgrade 会自动升级那个源里的包,而它的签名策略、发布节奏、维护者是谁,你都不掌握。
我的做法是:每加一个第三方源,同时记下理由和预期移除时间。装某个工具时临时加的源,装完就删;确实要长期跟随上游的才留着。
自动更新要不要开,分情况。对外提供服务、有严格变更窗口的机器,我倾向不开自动升级,改成定期人工过一遍 changelog;内部的、无状态的、可随时重建的机器,开 unattended-upgrades 只装安全更新,比拖着不升好。
一条贯穿的原则
上面每一层都在做同一件事:把"这台机器上有什么"变成一份可对比的清单。加固不是一次性动作,是持续维护清单和实际状态的差值。
差值出现的时机通常是:装新软件、开新端口、加新账号、接新监控。前三个都有人记得做,第四个几乎没人记得——监控 agent 装完就没人看过它的配置和权限。
我不建议把这一层交给扫描器。扫描器报告的是它认识的东西,而机器上真正危险的往往是你自己写的脚本、自己开的端口、自己加的那条 sudo 规则,这些它不认识。手工过一遍六层大概半小时,比读两百页扫描报告有用。