Skip to content

安全加固 ​

安全加固是通过配置和优化系统来减少攻击面的过程。如果说防火墙是"门口的保安"、入侵检测是"监控摄像头",加固就是把房子本身的门窗数量降下来、把钥匙管理收紧、把贵重物品锁进保险柜——它不产生新功能,却决定前面两层防线的压力有多大。本篇给出一份可复查的基线:最小化安装、冗余服务清理、补丁节奏、SSH 加固、内核参数与三系强制访问控制(MAC)差异。

内容参考自 CIS Benchmarks、Arch Wiki、鸟哥的私房菜与各发行版安全文档,见文末参考资料。

导语 ​

一台"默认安装 + 没打补丁 + SSH 口令登录 + 全端口开放"的服务器,在公网上的平均存活时间是以分钟计的。多数严重安全事故不需要攻击者有多么精巧的手法,只需要三件事凑齐:暴露面大(端口/服务多)、已知漏洞没补(补丁滞后)、认证太弱(弱口令/root 直登)。加固要做的就是逐一拆掉这三根柱子。这三根柱子也对应本篇前几章的边界:防火墙只部分缓解"暴露面",入侵检测在三者都成立之后才谈得上"发现得早",所以加固排在检测之前讲——先把可被利用的面收掉,再谈在剩下的面上布传感器。三根柱子也各自有"看似已解决、其实没解决"的假象:装了防火墙但 zone 选错、打了补丁但没重启、配了密钥但 PasswordAuthentication 仍是 yes——加固篇后半部分的大量篇幅,都是在拆这些假象。

它同时也是本篇前面几章的收束点:防火墙策略、SSH 密钥、检测工具配置得再好,如果没人定期复查、没有统一的基线文档,都会随时间漂移——新同事开的端口、临时调试留下的账号、半年没重启的内核,都是"配置过但失效了"的典型。加固不是一次性项目,而是一份每次变更后都要回来对表的清单。 把"漂移"当成默认结局,后面所有制度设计(盘点、SLA、归档、演练)才有必要性可言;若假设配置会自己保持正确,整篇都可以删掉。

"漂移"值得多解释一句,因为它解释了为什么业界会把加固做成 CIS Benchmarks 这类可重复审计的清单:安全配置在建成那一刻是准的,之后每一次紧急上线、每一位新人的临时放行、每一个"先这样跑着"的例外,都在让它偏离基线。没有回访机制的团队,实际安全水位是时间的减函数。本篇各节末尾的盘点命令,目的就是给这个减函数装一个定期重置的按钮——月度巡检跑一遍,比每年大搞一次"安全专项行动"更接近真实有效的加固节奏。减函数的说法还可以再推一步:漂移不仅降低安全水位,还提高理解成本——半年后没人说得清某条规则的来历,于是既不敢改也不敢删,例外越积越多,这就是"制度空转"在文件系统上的形态。

加固与防火墙、入侵检测的分工可以再钉死一次,避免读的过程中边界变糊:防火墙决定哪些流量能到进程,入侵检测回答到进程之后发生了什么异常,加固则决定进程与系统本身还剩多少可被利用的面。三者任何一层单独做到头都堵不死另外两层的洞——这就是为什么安全篇按这个顺序编排,而第 9 节的实战清单会把三层的动作串成同一条执行链。若要在一句话里记住三者:防火墙管"进不进得来",加固管"进来之后好不好用",检测管"用了之后知不知道"。

读法上建议把本篇当两遍材料:第一遍通读建立框架,重点是每节开头的"为什么"和表;第二遍只读第 9 节清单,对着虚拟机执行,卡住时再跳回对应节查命令。加固类内容最容易犯的错误是"读完感觉都会了,上线时还是漏步"——执行路径必须短到能背下来,这就是为什么第 9 节刻意写成可打印的 checklist 形态,而不是又一篇叙事散文。第二遍执行时请顺手把清单改成本机的定制版:删掉不适用的项、补上你盘点出来的特有服务——能改的清单才会被用,不能改的清单只会被供着。

学习目标 ​

  • 理解最小化安装与攻击面的关系,能在三系上识别并移除冗余软件与服务
  • 建立可执行的补丁节奏,知道三系各自的自动更新机制与风险
  • 完成 SSH 加固闭环:密钥登录、禁 root、语法检查、逃生通道
  • 掌握常用内核参数与文件权限基线,理解每条参数防的是什么
  • 分清 SELinux / AppArmor / 无 MAC 三系默认差异,知道排障入口在哪
  • 会用 lynis 等工具做基线自查,并把结果变成可跟踪的整改项

学完的操作性检验:拿到一台新装的三系虚拟机,你应该能按第 9 节清单独立走完九步,过程中不在任何一步把自己锁在外面,并能对同事讲清"这台机器为什么只开这几个端口、跑这几个服务"。讲不清理由的配置项,说明盘点还没做完。

六条目标与章节的对应关系是:目标 1 → 第 1–2 节,目标 2 → 第 3 节,目标 3 → 第 4 节,目标 4 → 第 5 节,目标 5 → 第 6 节,目标 6 → 第 7 节;第 8、9 节分别是反例清单与串讲。建议按序通读一次,之后把第 9 节打印出来当上线检查单——加固篇的正确用法是"读一遍建立理解,之后只查表",而不是每次上线都重读全文。六条目标背后还有一条贯穿线:凡能回读的(sshd -T、sysctl --system、getenforce)都要求回读,凡不可逆的(关 SSH、开防火墙、luks 式的格式化)都要求先留逃生通道——第 9 节九步的每一步验收,本质都是在这两句话里各挑一半。

检验标准里"讲清理由"比"跑完步骤"权重更高,因为加固的失效模式往往是配置还在、意图已丢:半年后没人知道 AllowUsers ops deploy 是业务要求还是某次事故的遗留,于是没人敢改、也没人敢删,例外越积越多。第 9 步的归档文档就是给每个配置项配一份"出生证明"——它是本篇把"操作"升格为"制度"的最后一环,也是六条目标里最容易被跳过、却最决定长期效果的一条。若把六条目标写成团队 OKR,最后一条(Lynis 自查 + 整改跟踪)应当是唯一带周期的:前五条是项目,第六条是运营——项目会结束,运营才有基线可言。

1. 最小化安装:攻击面从装机开始收 ​

加固的第一步不在配置文件里,而在装机镜像与包选择上:后面所有参数收紧,都是在给已经装进来的东西打补丁;装机阶段决定的"根本不装什么",则让对应漏洞连进入清单的机会都没有。每多装一个包,就多一份需要跟着打补丁的代码;每多开一个服务,就多一个可能被攻破的入口。 最小化不是"越少越好"的洁癖,而是让"这台机器上有什么"始终处于可枚举、可解释的状态。可以对照理解一个反例:一台装了二十年前教程全套组件的"全能服务器",没人能完整回答"关掉这个服务会不会影响业务"——于是所有可疑服务都留着,攻击面等于默认安装全集,补丁窗口也因为怕碰业务而一再推迟。可枚举性本身就是安全属性。把这条属性写进验收标准也成立:盘点问题应当能在不登录机器的前提下,仅凭仓库里的清单回答——需要登机现场翻的答案,说明清单还没成为事实源。

三系在"最小"的起点上并不平等,理解差异才能定合理预期:Debian 的 --no-install-recommends 与 dnf groupinfo "Minimal Install" 给了官方的最小集合入口;Arch 的 base 组本身就很克制,真正的膨胀往往来自后续 pacman -S 的顺手安装;Ubuntu Server 与 Debian Server 的默认项也不完全一致,同一份加固清单在这两家上盘点结果会有肉眼可见的差。定基线时先把"这台角色的官方最小集"钉死,再谈加法,否则每次盘点都会把发行版默认项当成业务需要。反过来,也别把"最小"误解成"越少越安全到不惜业务代价"——把日志轮转、时间同步这类安全依赖一起精简掉,等于用运维故障换了个假的安全分。最小化的判据始终是业务说明书上的反向排除,不是包数量排行榜。

bash
sudo apt install --no-install-recommends nginx   # Debian/Ubuntu:少连带安装
pacman -Q --explicit | sort > /tmp/explicit.txt  # Arch:显式安装清单
sudo dnf groupinfo "Minimal Install"             # RHEL 系:Minimal 组

Arch 的 pacman -Q --explicit 列出手动安装的包(依赖不算),是三系里最现成的"这台机器为什么装了这些"答案;Debian 侧可用 apt-mark showmanual,RHEL 侧是 dnf repoquery --userinstalled。三系对应关系记牢,跨发行版复刻基线才不会漏。盘点清单最好进配置仓库:每次装新包时 diff 一下"多出来的是谁、为什么",装机决策就有了审计痕迹——没有清单的服务器,三个月后没人说得清哪些包是业务需要、哪些是某次排障留下的。清单文件本身建议按"包名 + 安装理由 + 日期"三列维护,而不是裸的包名列表——裸列表只能回答"有什么",带理由的列表才能回答第 1 节开头的三连问。

盘点后的动作是三连:移除不需要的、禁用不运行的、记录仍保留的及理由。典型清理对象是桌面/办公套件(打印、蓝牙、多媒体)与历史遗留协议(telnet、rsh——明文传输且等价能力早已被 SSH 覆盖)。三连里"记录仍保留的及理由"最容易被跳过,却最决定半年后的可维护性:删掉的东西下次还能从仓库装回来,留下的东西若没有理由,下次盘点时会重新陷入"这到底要不要删"的原地循环。

bash
sudo apt purge telnet rsh-client rsh-server      # Debian/Ubuntu
sudo pacman -Rns bluetooth-printer               # Arch:连配置一起删
pacman -Qdtq | sudo pacman -Rns -                # 清孤儿依赖(先看清单!)
sudo dnf remove telnet rsh-server                # RHEL 系

清理时的"记录仍保留的及理由"常被当成官僚步骤跳过,它其实是给未来排障留的接口:半年后有人问"为什么这台机器还有 chrony",答案是"NTP 是时间戳与证书校验的依赖,禁了会连锁出问题"——有这条记录,问题三十秒关掉;没有,就要重新做一遍依赖分析。理由栏写一行即可(依赖谁、合规要求、暂未迁移的兼容项),放进配置仓库与包清单同文件维护,盘点 diff 时自然会带着理由一起 review。

一个实用的反向自检:ss -tulnp 看监听端口,每个 LISTEN 都应该能回答"为什么它在这台机器上"。答不上来的,要么关服务,要么至少用防火墙挡住(见防火墙)。最小化的收益不止少打几个补丁:包少了,pacman -Syu/apt upgrade 的变更面就小,深夜自动升级炸掉业务的概率随之下降——攻击面和变更风险是同一批代码的两副面孔,收窄它一举两得。

还有一条容易被跳过的纪律是安装来源的收敛:默认只用官方仓库,第三方 PPA、AUR 外的脚本安装、curl | bash 式的"一键部署"都要单独评审。每引入一个仓库,就等于把该仓库的签名密钥与构建链信任进了系统——出问题时你卸载的不只是软件,还有对信任链的追认。Arch 用户对 AUR 尤其要有这层自觉:AUR 是社区配方不是官方包,pacman -Q --explicit 盘点时要把 AUR 包单独标注,升级路径与官方包分开评估。最小化的终点不是"包数最少",而是"每个留下来的包都能说出仓库、版本与用途"。

2. 服务关闭与最小权限 ​

第 1 节收的是"盘上装了什么",本节收的是"跑起来之后谁有权做什么"——服务、账号、sudo 三个入口,任何一个过宽都会让第 1 节的努力打折:包删得再干净,一个 NOPASSWD:ALL 的共享账号也能五分钟内装回全套。三个入口也对应三种不同的审计对象:服务看 ss -tulnp 就能对账,账号要读 /etc/shadow 与组关系,sudo 则要翻提权日志——盘点命令越可脚本化,这项基线越不容易随人员变动失效,这也是本节命令都选一行可 grep 输出的原因。若把第 1 节比作"仓库盘点",本节就是"门禁与钥匙盘点":仓库再干净,人人持万能钥匙也等于没有门禁。两个节合起来其实只回答两个问题:代码还在吗、谁能碰它——问不全这两个问题的安全评审,往往只能给出"已加固"三个字。

服务与账号的收紧还共享一条纪律:先观察再删除,先留痕再变更。停服务前记下 systemctl show 的关键字段,删账号前 usermod -L 锁定观察一个周期,sudo 规则调整前备份 sudoers.d 目录——这些动作不增加任何安全强度,却把"改错了"的回滚成本从"翻文档重装"降到"还原一个文件"。加固阶段的高危操作密度高,回滚预案应当和配置命令成对出现,而不是等出事再想。观察期的长度没有标准答案,但要长到覆盖一个业务周期(至少一次发布、一次日志轮转),否则"没观察到问题"可能只是"还没轮到出问题的场景"。观察期结束时的处置同样要留痕:"继续保留并写理由"与"到期删除"都是合法结论,"忘了"不是——把观察期做成带到期日的工单,才能避免它悄悄变成永久白名单。

三个子节(服务、账号、sudo)还可以按攻击者视角排序阅读:能 SSH 进来的攻击者先看你有哪些账号可撞(2.2 的口令与锁定),撞进来后看sudo 能提多高(2.2 的白名单),最后才看机器上还有什么服务可以横向打(2.1 的 LISTEN 清单)。按这个顺序做加固,等于按攻击成本从低到高逐项加价——先抬高最便宜那条路的门槛,性价比总是最高。 若只能优先做一件事,多数公网机的答案是 2.2 的口令与 sudo:它同时抬高了爆破与提权两条最常见的路,而 2.1 的服务清理收益要等攻击者已经到了内网才完全兑现。

2.1 冗余服务 ​

服务清单是攻击面在运行时的投影:盘上没装的二进制不会被 exploit,装了但没跑的最多算补丁负担,正在 LISTEN 的才真正接敌。所以清理顺序永远是"先停在跑的,再禁开机的,最后决定卸不卸"——三步对应三个不同的暴露面维度。这个顺序也和排障直觉一致:先让流量停(立刻减面),再让重启不复活(防止维护窗口结束又回来),最后才动磁盘(最慢、也最不可逆)。

停用服务本身还会带来一个容易忽略的副作用:依赖它的日志与监控点会一起消失。关掉 cups 之后打印相关告警不再触发是正常的,但若误关了 auditd 或日志转发 agent,等于把检测体系静默拆掉——所以清理清单要区分"业务服务"与"安全服务",后者默认不在关闭候选里,除非能说出替代方案。

systemctl 的"在跑"和"开机自启"是两个维度,清理时两个都要看:

bash
systemctl list-units --type=service --state=running --no-pager
sudo systemctl disable --now cups avahi-daemon bluetooth

三系默认差异会影响你的"起点":RHEL 系开箱服务较少但 firewalld/auditd 在跑(这两个是安全资产,别关);Ubuntu 桌面版会带一串打印/蓝牙/桌面服务,服务器版少很多;Arch 基线最干净,你手动 pacman -S 装进来的每一个服务都要自己负责清理。判断标准统一:这台机器的业务说明书里没有的服务,先禁用观察,而不是"留着以防万一"。

清理服务时还要区分"禁用"与"卸载"两种动作的适用场景:disable --now 适合"业务可能还需要、先停掉减面"的观察期,出问题能立刻拉回来;purge/-Rns 则适合确认不再使用的组件,连配置文件一起从盘上抹掉,避免"服务停了但配置里的弱默认值还在、哪天误启动就中招"。推荐节奏是先 disable 观察一到两个变更周期,确认无业务依赖再 purge——直接 purge 的风险是依赖倒挂(某个你没意识到的组件把这个包当库用),直接永久留着的风险则是下一次全量升级时把一堆用不到的攻击面一起更新。

systemctl 的输出里还有一类常被忽略的状态:masked(被链到 /dev/null 的强制禁用)比 disabled 更硬,适合给"这台机器永远不该跑"的服务上锁——比如服务器上的 bluetooth.service、cups.service。与 chattr +i 锁敏感文件的思路一致,对高风险冗余服务用 masked,对可逆服务用 disabled,两级强度按"误启动的代价"分配。

2.2 账号、sudo 与密码策略 ​

最小权限从账号开始:清理陈旧账号、收紧 sudo、设置密码质量与过期策略。账号层的问题在事故复盘里出现频率极高——它不依赖任何漏洞,只需要一个"当时图方便"的例外:共享 root、给外包开永久 sudo、演示环境留着默认口令。把账号当资产管理,比多装一个 IDS 更能降低真实入侵的成功率。账号与防火墙在控制论上是同构的:都是白名单 + 默认拒绝,区别只在于防火墙挡的是网络五元组,账号挡的是"谁能在登录后以什么身份存在"——把第 2 节的清理动作想象成账号层的 default drop,直觉会顺很多。账号盘点还可以加一条时间维度:每个账号都要有"出生日期"和"下次复核日期"——没有复核日期的白名单会和防火墙里的旧规则一样,只增不减。

bash
sudo awk -F: '($2==""){print $1}' /etc/shadow   # 正常应无输出
# /etc/login.defs:PASS_MAX_DAYS 90 / PASS_MIN_DAYS 7 / PASS_WARN_AGE 14

密码质量由 PAM 的 pwquality 模块负责,三系包名不同,这是跨发行版配置最常见的卡点之一:

bash
sudo apt install libpam-pwquality    # Debian/Ubuntu
sudo pacman -S libpwquality          # Arch
sudo dnf install libpwquality        # RHEL 系(通常已装)
# /etc/security/pwquality.conf: minlen = 12, 各类 credit = -1

sudo 侧推荐"命令白名单 + 审计日志"而不是人人 ALL=(ALL) NOPASSWD:ALL。必须用 visudo -f 编辑(示例见下)——/etc/sudoers 语法错误会让所有 sudo 失效,而你正需要 sudo 去修它,这是加固阶段破坏性最强的单点失误之一;visudo 在保存前做语法检查,是唯一安全的编辑入口。账户锁定策略三系不完全一致:RHEL 系默认可用 pam_faillock + faillock --user 查看/解锁;Debian/Ubuntu、Arch 默认不启用系统级锁定,通常交给 Fail2Ban 按 IP 封禁(见入侵检测)。不确定本机是否启用了锁定模块时,读 /etc/pam.d/ 对应文件确认,不要照抄跨发行版片段。

命令白名单的粒度也有讲究:给运维组的 systemctl restart/status nginx 比 systemctl * 安全一个量级,但比"必须找 root"高效一个量级——sudoers 的设计目标不是消灭提权,而是把提权路径收窄到可审计的少数几条。配套的 Defaults logfile 把每次提权写进 /var/log/sudo.log,事后与 auditd 的 identity 事件对表,就能回答"昨晚是谁重启了 nginx"。白名单条目本身要定期 review:业务下线后残留的 sudo 规则,和防火墙里的僵尸端口是同一类负债。

账号盘点还有三个常被漏掉的角落,值得在清理时各扫一眼。其一,空口令行:awk -F: '($2=="")' 的输出在正常系统上必须为空,任何一行命中都是最高级告警。其二,uid 0 的非 root 账号:awk -F: '$3==0' 应该只有 root 一行——攻击者提权后最爱加的就是第二个 uid 0。其三,无过期策略的特权账号:chage -l ops 看 Password expires/Account expires,运维共享账号若设成"永不过期",等于把离职交接问题无限期推迟。三项都可以写进月度巡检脚本,输出非预期即告警——账号异常是少数"事前一行命令能查、事后翻盘极难"的类别。

bash
# /etc/sudoers.d/appops —— visudo -f 编辑
appops ALL=(root) /usr/bin/systemctl restart nginx, /usr/bin/systemctl status nginx
Defaults logfile="/var/log/sudo.log"

3. 补丁管理:把"更新"变成制度 ​

防火墙挡的是"还没进门的",补丁堵的是"已经公开、随时能进门的"——CVE 公开到 PoC 满天飞的间隔越来越短,补丁节奏因此成为加固清单里唯一直接和时间赛跑的项。未打补丁的已知漏洞是最廉价的攻击入口——厂商已公开修复方案,攻击者只需 searchsploit。补丁管理的核心不是某条命令,而是节奏:谁负责、多久一次、更新前备份什么、出问题怎么回退。把节奏拆开看,其实是一组可审计的问题:上次全量升级是什么时候(时间戳)、高危 CVE 的处理时限写在哪(SLA 文档)、升级失败的回退步骤演练过吗(变更记录)——答不出其中任何一问,说明"我们有在更新"只是一种感觉,不是制度。 三系的更新命令本身差异不大(pacman -Syu/apt upgrade/dnf upgrade),差异全在"谁按按钮、多久按一次、按完谁签字"——制度层的三系差异,永远大于命令层。 把"制度可审计"再落细一步,变更单上至少要有三个时间字段:公告日期、变更日期、重启生效日期——三者之间的间隔就是真实的暴露窗口,也是复盘时唯一有说服力的数字。

补丁制度还要和第 1 节的盘点、第 7 节的自查接起来:盘点决定"哪些包需要跟版本",补丁决定"什么时候跟",Lynis/CIS 决定"跟完是否达标"。三者断档时会出现经典病灶——软件是新的、基线是旧的,Lynis 每次都报同样几条没人认领的建议。把三节当成一条流水线读,比当成三个独立主题更容易记住各自的交付物:清单、日历、整改项。

把补丁比作"漏洞清零"很贴切:CVE 公开的那一刻,全世界的攻击脚本都站在同一起跑线上,你的窗口期就是"从公告发布到你装上补丁"之间的空档。空档的长度不取决于你有没有自动化工具,而取决于制度是否规定了截止日期与责任人——没有 SLA 的"尽快更新",在业务高峰期会无限顺延成"下季度再说"。常见的落地节奏是:安全公告触发的高危漏洞按 24–72 小时处理,一般更新按月度变更窗口批量执行;两档速度并存,才既不会被高危拖着天天重启,也不会把普通更新积压成半年一次的大爆炸。

分档之后还要明确"谁有权按下加速键":紧急通道若人人可走,就等于没有分档;若只有一人可走,则要有备份责任人与超时升级机制。可落地的写法是把 SLA 进变更系统——高危工单自动带 48 小时倒计时,到期未处理升级到更高级别——用工具固化制度,比发邮件重申纪律有效。同时给"紧急更新"定义最小验证集(哪些服务必须冒烟测试、谁签字),避免以安全之名跳过全部验证,把补丁变更变成新的故障源。

变更窗口本身也要设计:固定周几、避开业务高峰、预留回滚时间、提前通知下游——窗口不可预期的团队会自然形成"能拖就拖"的氛围。窗口日历一旦排定就尽量不挪,临时插入的紧急补丁走前面说的加速通道,两条节奏并行而不互相污染,是补丁制度能长期运转的结构条件。窗口前后还各有一个固定动作:更新前记版本与关键服务状态,更新后回读内核/关键包版本并重建第 5、7 节相关的基线——没有这两头记录,一次成功更新在下次排障时等于"发生过但说不清"。

三系的更新语义对照(注意 Arch 的特殊纪律):

动作Debian/UbuntuArchRHEL/CentOS/Rocky
刷新索引sudo apt updatesudo pacman -Sy(通常并入 -Syu)sudo dnf makecache
全量升级sudo apt upgradesudo pacman -Syusudo dnf upgrade
安全公告入口ubuntu.com/security、debian.org/securityarch-security mailing listdnf updateinfo list security
自动更新unattended-upgrades无官方自动升级(需自建 timer,慎用)dnf-automatic

Arch 的两条纪律值得单独强调:永远整系统 pacman -Syu,不要只升单包(部分升级会导致依赖不匹配);默认没有"官方自动升级",若用 systemd timer 跑 pacman -Syu,必须接受"可能升级内核后需要人工介入"的运维模式。Debian/Ubuntu 的 unattended-upgrades 与 RHEL 的 dnf-automatic 都支持仅装安全更新,服务器上建议开启:

bash
sudo apt install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades   # Debian/Ubuntu
# RHEL: /etc/dnf/automatic.conf → apply_updates = yes, upgrade_type = security
sudo systemctl enable --now dnf-automatic-install.timer

自动更新不等于"无人值守即可高枕无忧":它只解决安装,不解决重启生效与回归验证。把自动更新和监控绑定——升级后内核版本变了要告警、服务异常重启要能回滚——才敢在生产开启。三系的自动更新还各有边界:unattended-upgrades 默认只认安全源,普通功能更新仍留给变更窗口;dnf-automatic 的 upgrade_type 同理;Arch 没有对应物,若必须自动,用 timer + 日志采集 + 失败告警的组合自己搭,并把"升级失败停在半路"纳入演练场景。自动化的边界写清楚,比自动化本身更重要——哪些更新会自己发生、哪些必须人批,运维与开发需要同一份答案。

更新后的动作同样重要:读日志(/var/log/apt/history.log、dnf history、/var/log/pacman.log)、重启使内核/系统库生效(needs-restarting 或检查 /var/run/reboot-required)、再跑一次基线自查(AIDE 要重建基线,见入侵检测)。"打了补丁但三个月没重启"在很多 CVE 面前等于没打,把重启纳入变更窗口是加固制度的一部分。

补丁流程里还有一个常被忽略的角色分工问题:谁订阅公告、谁执行升级、谁验证业务、谁记录回滚点——四个角色在小团队里可以是同一个人,但必须在流程里显式存在,否则"等通知"会变成无人认领。常见可落地的分工是:安全公告由固定邮箱/频道订阅并分派,升级在变更窗口由值班执行,业务验证交给服务负责人,回滚点(快照、配置备份、旧内核保留)由执行人当场确认。把这四格写进变更单模板,比再买一个漏洞扫描器更能缩短真实窗口期——扫描器发现的是"有没有漏洞",制度决定的是"多久补上"。

回滚本身也要预演:升级前确认快照可回退、旧版本包还在缓存/仓库里、数据库有对应备份,只做了"升级成功"路径的团队,在第一次升级失败时会花数小时现场发明回退方案。Arch 的滚动升级尤其需要这条:pacman -Syu 前记下关键包版本,必要时用 pacman 缓存里的旧包退回,比在断网环境里现查 Wiki 稳妥。

4. SSH 加固:最高优先级的单点 ​

SSH 在多数服务器上是唯一预授权的公网入口,因此它的配置错误没有"降级可用"的中间态——要么还能进,要么整机失联。把本节流程走熟,后面防火墙、sudoers 的变更都会更从容,因为你知道逃生通道是经过验证的。SSH 也是本篇唯一一处配置文件即业务连续性的项:sysctl 写错大不了参数不生效,sshd 写错则当场切断管理通道——所以本节的每条纪律都围绕"如何让一次错误不至于成为最后一次连接"展开。把这条纪律再抽象一层,适用于所有远程管理服务(RDP、VNC、K8s API server):凡是"改错就断连"的配置面,都必须先有一个不依赖该配置面的验证/恢复通道——SSH 靠控制台,K8s 靠静态 kubeconfig,两者是同一条原则的两个实例。

SSH 是几乎所有服务器的常驻入口,也是爆破的第一现场。加固目标明确:只允许密钥、只允许授权账号、只允许必要的认证次数,并保证改错配置时进得去。三个"只"对应三类不同的攻击面:口令认证挡的是字典爆破,账号白名单挡的是"凭证泄露后还有哪些身份能用",认证次数挡的是单连接内的穷举速度——三层各自独立,砍掉任何一层,另外两层的负担都会变重,这也是为什么基线里它们总是成组出现。三个"只"还可以对应到失效测试:关掉密钥登录口令是否仍能进(不该能)、不在名单的账号是否被拒(该拒)、连续失败是否在第 3 次被掐(该掐)——写得出三个测试的加固,才算配完了。

把 SSH 放在加固清单里优先级最高的位置,理由有三:它是唯一暴露在公网且接受交互登录的服务(业务端口通常只跑只读协议或带应用层认证);它是攻击者自动化扫描的固定目标(22 端口的口令尝试从不间断);它还是你所有远程修复动作的通道——SSH 配错的代价不是"这个服务挂了",是"整台机器失去管理"。三者叠加,使 SSH 配置成为全篇唯一一处"改错就锁死"的高危变更,也是唯一值得为它设计完整回滚流程的变更。三条理由也解释了为什么本篇把 SSH 排在防火墙之前讲:先验证管理通道可用,再去收紧网络面,两步的失败模式才不会叠加在同一分钟里爆发。

本节的配置项刻意保持精简:只列直接影响暴露面与认证强度的六项,而不是把 man page 里几百个指令都搬进来。SSH 加固的常见失败模式恰恰是"抄了一份过长的 hardening 列表"——里面混着与业务冲突的转发限制、把 CI 打断的超时参数,改完大面积异常后团队整体回滚,连带 PasswordAuthentication no 这类关键项一起被撤。每加一项都要能说出防的是什么攻击、可能影响哪类客户端,说不出就不加;基线的可信度来自"少而都对",不来自"多而像安全"。清单短还有一个隐性收益:评审时同事能逐条读完并真正提出异议——三十行的 sshd_config 没人会细看,六行的会。

六项之外还有一条组织层面的纪律值得写进流程:SSH 变更必须"改配置 + 开新会话验证 + 旧会话保留到验证通过"三步并行,且验证动作本身(用新会话执行一次 whoami、确认 authorized_keys 生效)要写在变更单上。很多锁死事故不是不知道 sshd -t,而是改完直接关窗走了,把唯一验证机会留给了下一次连接——把"新会话验证"当成 SSH 变更的 Definition of Done,和代码合并前跑测试是同一性质的门槛。

bash
# /etc/ssh/sshd_config(关键项)
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
MaxAuthTries 3
AllowUsers ops deploy

改完配置的标准流程(顺序错了会锁死自己):

bash
$ sudo sshd -t && echo $?
0
$ ssh -o PreferredAuthentications=publickey ops@server "echo ok"  # 另开终端
ok
$ sudo systemctl restart sshd    # Debian 单元名是 ssh.service

sshd -t 返回非 0 时绝对不要 restart:配置有语法错误,sshd 会拒绝启动,所有远程会话断开后无人能再连——这与防火墙"先 enable 后 allow"同级,是加固作业的两大自锁源。密钥分发与 ed25519 生成见加密技术,此处不重复。

sshd_config 里还有几项值得按注释逐条对照默认值,它们决定的不是"能不能登",而是"登进来之后还有多少次犯错机会":MaxAuthTries 3 把单连接的认证尝试压到爆破脚本不划算的水平;AllowUsers/AllowGroups 在账号层做白名单,即使密码策略被绕过,不在名单里的账号也无权建立会话;LoginGraceTime 缩短握手窗口,让未完成认证的半开连接更快释放资源;X11Forwarding no、AllowTcpForwarding no(按需)则砍掉"登进来再横向"的隧道能力。这些开关改起来都很快,但每一项都建议配合 sshd -T | grep -i 回读确认生效值——配置文件里写了不等于运行时采纳,drop-in 目录与 Match 块都可能让主文件的行被覆盖。

改端口号(Port 2222)常被当作"安全措施",实际只是减少日志噪音的辅助手段,不能替代密钥登录与 Fail2Ban;改了端口记得同步防火墙放行(ufw/firewalld/nft 三系写法见防火墙),否则把自己挡在门外。

还有一类"加固反而降低可用性"的反模式值得点名:把 MaxStartups、ClientAliveInterval 调得过激,导致正常运维的多路复用连接或 CI 长会话被无谓掐断,团队于是开始抱怨安全流程,最终整体回滚。SSH 基线的每一项"收紧"都应配一个"验证仍可正常工作"的动作(新会话、从另一台机器连、CI 跑一次),收紧与验证成对出现,加固才不会变成和运维效率的拉锯战。

5. 内核与文件系统基线 ​

配置文件权限与内核参数是加固里"改动最小、覆盖面最广"的一层:几行 sysctl、几条 find 结果核对,防的是跨了十年仍然有效的协议层与文件系统层老攻击。本节刻意只收"通用基线",角色相关的(容器、数据库)分支在参数旁注明,避免把网关配置套到应用机上。也可以说,前几节收的是"你主动开了什么",本节收的是内核与文件系统默认就给了什么——不少暴露面不需要你配置错误,出厂默认就够宽。这也解释了为什么本节排在补丁与 SSH 之后:先把主动开的面收干净,再回头收默认给的面,两次收敛合起来才是"声明过的暴露面"。

sysctl 与权限基线还有一个共同特征值得先说:它们几乎不会被业务功能"需要",因此改动的回归风险极低——这与 SSH、MAC 那类"改错就疼"的项形成对比。正因为风险低,这两项最适合放进自动化配置管理(Ansible/Chef 一条 task 搞定),用代码保证每台机器一致;反过来,SSH 加固则更适合按机器手工走验证流程。按变更风险分配自动化程度,是加固工具选型时隐含但重要的一条原则。低风险项适合自动化的另一个原因是它们回读简单:一条 sysctl、一条 find 就能证明生效与否,脚本化后可以每天跑;高风险项的"回读"往往需要开新会话、换账号验证,天然难以塞进无人值守流水线。两种回读方式也对应两种巡检频率:低风险项进日报,高风险项进变更单——频率与回读成本成反比,而不是与风险高低成正比(高风险项频率低,是因为每次验证成本高,不是因为不重要)。

5.1 网络相关 sysctl ​

内核参数防的是一类协议层的老攻击,改动小、收益稳定。它们和第 2 节的服务清理共享同一哲学:关掉没人用的协议特性,就少一条被利用的路径——差别只在于这里关的是内核代码路径,不是用户态进程;也因此 sysctl 的风险主要不在"业务依赖",而在"角色搞错"(比如给容器宿主关了转发)。

bash
# /etc/sysctl.d/99-hardening.conf(关键项)
net.ipv4.ip_forward = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.rp_filter = 1
net.ipv4.conf.all.log_martians = 1
$ sudo sysctl --system

逐条说防的是什么:ip_forward=0 关掉"被当路由器"的默认行为;accept_redirects=0 与 accept_source_route=0 堵住 ICMP 重定向/源路由这类经典中间人手法(攻击者诱导你把流量绕道经过他);tcp_syncookies=1 是 SYN flood 的标准缓解;rp_filter 做反向路径校验,丢掉"源地址不可能从这个接口进来"的包;log_martians 让上述可疑包留痕。容器宿主或做 NAT 网关的机器必须保持 ip_forward=1——先确认角色再套基线,否则"加固"会直接打断业务。改完用 sysctl net.ipv4.ip_forward 回读确认,配置文件写对不等于运行时生效。回读这一步还顺带暴露"谁覆盖了谁":若文件写的是 1、回读是 0,多半还有更晚加载的 drop-in 或运行时残留——这正是下一小节要讲的文件序问题。

参数分文件管理也值得说明:/etc/sysctl.d/ 下的 .conf 按字母序加载,99-hardening.conf 这类靠前缀排到后面的文件可以覆盖发行版自带的 50-default.conf;反过来若你的基线文件名排序在默认文件之前,覆盖会静默失效——命名序号本身就是配置语义的一部分。sysctl 的优先级还受运行时临时改动干扰(sysctl -w 只改内存),巡检时既要看文件也要 sysctl --system 后回读实际值,两层一致才算落地。团队内还可以再进一步:把"角色相关的例外"(容器宿主要开的项)拆成独立文件并在文件头注释适用范围,而不是在通用基线里就地改 0/1——例外写在注释里会被复制走,例外写在独立文件里才可整体启用/停用。

5.2 文件权限与不可变位 ​

sysctl 管的是"内核怎么处理包",权限基线管的是"谁能碰到文件"——两者合起来才构成主机层的默认姿态。本小节的三条检查(shadow 属性、SUID 清单、不可变位)都可以在两分钟内跑完,适合作为巡检脚本的固定段落。权限检查还有一个读输出时的窍门:先找"不该出现的",再解释"应该出现的"——SUID 清单里 passwd、sudo 都是预期项,真正值得停下来的是任何你不认识的名字;反过来从预期项往下核对,会在熟悉的名字上花掉大部分注意力。

bash
$ ls -l /etc/shadow /etc/passwd
-------- 1 root root 1034 ... /etc/shadow
$ sudo find / -xdev -type f \( -perm -4000 -o -perm -2000 \) -ls 2>/dev/null | head

SUID 清单里出现不认识的二进制值得深挖(getcap -r / 2>/dev/null 查 capabilities 也一并看)。对极高敏感文件可用 chattr +i 锁定(连 root 也不能随意改,需先 -i 解锁),适合 /etc/passwd、/etc/shadow 这类"平时绝不动"的文件——但先确认没有自动化工具需要写它们(useradd、chpasswd 都会写 shadow),否则会把正常运维搞挂。不可变位与第 6 节的 MAC、本节的 600 权限共享同一条设计哲学:给"高价值 + 低变更频率"的对象加一层违反直觉的摩擦,让误操作和低级入侵都在最后一厘米停下来。

文件权限基线里还有三类"默认太松、盘点时必看"的对象。其一,world-writable 目录:find / -xdev -type d -perm -0002 的输出里,除了 /tmp、/var/tmp、/dev/shm 这类设计如此的粘滞位目录,其余都应逐个解释;web 根目录若出现在清单里,基本等于"任何人可写"。其二,敏感文件的属组过宽:/etc/ssh/sshd_config、应用的 .env/密钥文件应该是 600 或 640 且属主为专用账号,备份副本同样要收权限——很多团队只收紧了源文件,却把 id_rsa.bak 留成 644。其三,备份与 core dump 路径:/var/backups、core pattern 指向的目录若权限失控,等于把加固收紧的凭据又从侧面导出。三类都可以用一条 find 串起来做成周检脚本,输出进告警通道,和 AIDE 的变更检测形成"状态 vs 变化"的互补——AIDE 发现"变了",权限周检发现"本来就松",一个管时间轴,一个管横截面。

6. 三系默认差异:SELinux、AppArmor 与"都没有" ​

前五节的配置在三系上大体同构(包名与路径不同,概念一致),MAC 是少数默认形态完全不同的一节:一边是 Enforcing 开箱即用,一边是 AppArmor 挡着,一边则两样都没有。把"默认不同"记成一张表,比在每台机器上现场猜命令可靠。这一节的读法也和前几节不同:前几节追求"三系都做出同一结果",本节首先承认结果本来就不同,目标从"统一配置"降级为"统一理解各自默认,再决定是否对齐"——跨发行版照抄清单时,MAC 几乎总是出事的那一栏。若你只维护单一发行版,本节仍建议读完:知道隔壁发行版默认开着什么,才知道你的"没报拒绝"是策略严密还是根本没在拦。 检查动作本身也值得固化成一句话脚本:getenforce || aa-status || echo "no MAC"——三选一的输出贴进变更单,比口头说"MAC 开着"可对账得多。

强制访问控制(MAC)是 DAC(rwx)之上的第二道闸:即使进程被攻破拿到高权限,策略没允许就碰不到敏感对象。三系默认完全不同,排障入口也完全不同——这是跨发行版操作时最容易"用错命令还以为自己错了"的地方。本节不展开策略编写(那在基础篇 · 安全基础),只解决加固场景里最实际的三个问题:我这台机器有没有 MAC、拒绝了去哪查、什么时候该放行而不是关闭。三个问题都能在登录后一分钟内回答,这也是把 MAC 写进上线清单而不是"有空再研究"的原因。一分钟的检查动作本身也有两种失败形态要区分:命令不存在(没装/没启用,看表)与命令存在但拒绝为零(可能从未产生过 denial,也可能是策略太宽)——后者需要再用一次已知会触发拒绝的小实验验证策略真的在拦。

理解 MAC 为什么值得这单独一节,要先接受一个不舒服的事实:DAC(rwx/属主)假设"拿到高权限的人可以做任何事",而现实中最多的入侵恰恰是先拿到高权限再做事(web 进程被 RCE、sudo 配置被滥用)。MAC 打破的正是这个假设——它问的不是"你有没有 root",而是"这个进程有没有被允许碰这个文件"。nginx worker 即使被攻破变成 root,在 SELinux Enforcing 下也碰不到 /etc/shadow,因为它的类型标签里根本没这条许可。这是防火墙与最小权限之外的第三条独立防线,也是本篇清单里唯一一条"防已失陷"而不是"防被攻陷"的控制。

把 MAC 的拒绝日志当成"放行向导"而不是"错误提示",是本节最想传递的操作态度:每一条 denial 都是一次精确的权限申请单——它告诉你哪个域、哪个类型、哪种操作被挡了。逐条读、逐条放行,策略会随着业务运行自然长成贴合实际的形状;反过来把日志级别调没、或整体切 Permissive,等于把申请单扔了,几个月后第一次 Enforcing 时会积压几百条不知从何下手的历史拒绝。安全策略的可维护性来自"小步放行",不来自"一次配对"。

对比项Debian/UbuntuArchRHEL/CentOS/Rocky
默认 MACAppArmor(启用)默认不启用 SELinux 也不启用 AppArmorSELinux(Enforcing)
查状态aa-status若自行启用 SELinux:getenforcegetenforce / sestatus
拒绝日志/var/log/kern.log、journal 中 apparmor 记录视所启方案而定/var/log/audit/audit.log,用 ausearch
排障主命令aa-status、dmesg | grep apparmor—ausearch -m avc、sealert

在 Ubuntu 上执行 getenforce 得到空输出或 command not found,不是你操作错了,是这台机器根本不用 SELinux。三系在这一层的共同建议是:不要因为"挡手"就关闭 MAC——RHEL 系关掉 SELinux 会连带失去大量服务的类型标签假设;Ubuntu 关 AppArmor 等于拆掉第二道闸。正确姿势是读拒绝日志做最小放行,概念与命令详解见基础篇 · 安全基础,SELinux 的诊断闭环见SELinux 实战,AppArmor 的 profile 写法与 aa-logprof 修正轨迹见AppArmor 实战,本篇只负责把"三系默认不同"钉进基线清单。

"最小放行"在操作上意味着一条固定路径:复现拒绝 → 从日志抄下完整的 avc/denial → 用 audit2allow(SELinux)或 aa-genprof(AppArmor)生成候选规则 → 人工删掉与本次业务无关的宽泛项 → 应用并复测。最后一步的"人工删减"不可省略:直接采纳工具输出,常见结果是给进程开了一整类权限,防线等于从"按需"退化成"按工具默认"。放行规则要像防火墙规则一样进配置仓库、可 diff、可回滚——MAC 策略和 iptables 一样,是会被同事与自动化反复改写的活配置。

Arch 用户的现实是:DAC + 防火墙 + 最小权限就是出厂基线,需要 MAC 属于高级自定义,没有"照抄 RHEL 文档"的捷径。知道这个前提,你就能理解为什么很多 RHEL 向的加固清单在 Arch 上会"缺一半"。

三系之外还有一层实际约束:容器与编排平台正在承担部分 MAC 职责(seccomp、AppArmor profile、Kubernetes SecurityContext),宿主机不启用 SELinux 的发行版在容器场景下未必等于"完全没有 MAC"。但这不构成跳过宿主 MAC 的通用理由——宿主防线失守时,容器内逃逸后的横向移动仍靠宿主层策略兜底。评估时按部署形态分开看:裸机/虚机看本节三系表,容器看平台安全上下文,两者叠加才是完整视图。

7. 基线自查与合规工具 ​

前面六节的每一项都可以手工执行,问题是人会累、会漏、会在第三次巡检时抄上次的结果。工具的真正价值不是"发现新问题",而是用同一套标准可重复地暴露退化——这正是"制度"与"运动式检查"的分水岭。也可以说,手工清单保证深度(懂这台机器的业务),工具扫描保证广度与节奏(同一天、同一版本、同一套期望值再跑一遍),两者缺一会分别退化成"凭经验的个人英雄"或"没人认领的扫描报告"。本节的落脚点因此不是"选哪个扫描器",而是如何让一次扫描的产出变成下一次扫描的基线——没有沉淀的扫描,只是换了个格式的焦虑。扫描产出的沉淀方式可以很朴素:把每次的 suggestion 清单导出成 CSV、带上日期存进仓库,三个月后按编号 diff 一次——能 diff 的安全产出才会驱动整改,不能 diff 的只是截图。

人工清单逐项执行最可靠,但依赖执行者记得住全部条目;工具扫描胜在覆盖广、可重复,劣势是不理解你的业务上下文。两者的关系不是二选一,而是工具出题、人判卷——本节把这条协作方式说清楚,并给出三系通用的起点工具。本节也刻意只引入一个起点工具:Lynis 覆盖"通用硬化的盲区发现"已经足够,再堆 CIS/STIG/自制脚本只会制造三套互相矛盾的"权威分数"——自查体系的可信度来自长期可比,不来自单次扫描项数。单一工具还有一个组织上的好处:团队只需约定一套术语(hardening index、suggestion 编号)就能开整改会,多工具并存时连"高风险"的定义都要先对齐半小时。起点工具选型的第三条标准是三系同名:Lynis 在 apt/pacman/dnf 里都能装、输出结构一致,跨发行版对比才不会引入工具差异这个混淆变量。

人工清单难免遗漏,用工具跑一遍能补上盲区。自查工具与前面几节的人工流程是互补关系,不是替代关系:人工清单保证"这台机器的业务相关的项都过了",工具保证"你没想到的通用项也扫了"。Lynis 是三系通用的开源安全审计工具,输出按"hardening 索引"给建议,适合当基线自查的起点:

bash
sudo apt install lynis                # Debian/Ubuntu
sudo pacman -S lynis                  # Arch
sudo dnf install epel-release && sudo dnf install lynis   # RHEL 系
$ sudo lynis audit system --quiet

Lynis 的 suggestions 会包含大量与业务无关的建议,不要全盘照抄:把它当成"检查项生成器",逐条判断是否适用于这台机器的威胁模型,适用的纳入基线文档,不适用的记录"已评估,不适用及原因"。CIS Benchmarks 是同一思路的标准化版本(按发行版/角色给出更严格的项),适合有合规要求的环境;个人与小团队用 Lynis + 本篇清单即可起步,不必一上来就追求 CIS 全量通过——能持续维护的 80% 基线,胜过无法执行的 100% 清单。

工具结果的"处置闭环"比打分本身更重要:Lynis 的 hardening index 适合看趋势(这次比上次高了吗),不适合跨机器比绝对值——不同角色的机器分数本来就没法对齐。正确用法是把每条 suggestion 走一遍"采纳 / 评估后不适用 / 暂缓并记原因"三态流转,输出物是一份带状态的整改清单,而不是一张分数截图。有合规诉求时再引入 CIS 的自动化审计(ansible-lockdown、发行版对应的 STIG 工具链),把本篇清单当作"理解每一条为什么存在"的导读——工具告诉你不合规范哪一条,本篇帮你判断这一条在你的威胁模型里值不值得付性能与运维代价。

8. 常见坑 ​

下面十条按"会造成自锁 / 会造成基线失效 / 会造成制度空转"的顺序展开——前几条能让你当场进不去机器,中间几条让配置看似正确实则无效,最后几条则在几个月尺度上让整套加固慢慢失守。每条都对应前文某一节的纪律,读到陌生的编号时回跳那一节即可——坑清单不是附加内容,是正文的反向索引。把十条按"发现时机"再切一刀也有用:自锁类在变更当场就会现形,失效类要等下一次回读或扫描才暴露,空转类则要等季度复盘——越靠后的类别,越需要把检查写进固定节奏,而不是指望现场直觉。 三类的根因也不同:自锁源于缺验证步骤,失效源于缺回读,空转源于缺责任人——对应补上"新会话验证、生效值回读、整改项指派"三个动作,基本能覆盖十条的预防面。把三个动作直接写进变更单模板,比在 wiki 上再发一次"安全注意事项"有效——模板强制执行,通知只提高概率。

  1. 改 sshd_config 不做 sshd -t 就 restart。语法错误导致 sshd 起不来,全部会话断开后无法再连。流程永远是:语法检查 → 保持旧会话 → 新会话验证 → 再重启。
  2. 先关 PasswordAuthentication 再分发公钥。顺序反了直接锁死。先密钥验证成功,再关口令;云主机保留控制台逃生。
  3. visudo 之外手改 /etc/sudoers。一个拼写错误让 sudo 全员失效,连修复工具都用不了。一律 visudo -f /etc/sudoers.d/文件。
  4. pacman -Qdtq | pacman -Rns - 不看清单直接清孤儿。可能连带删掉手动装过但暂时无依赖的工具(如监控 agent)。先单独跑 pacman -Qdtq 过目,确认无误再管道删除。
  5. 把 ip_forward=0 套到容器/路由宿主上。Docker、KVM NAT 宿主必须转发,一刀切关闭会中断业务。基线要按角色分支。
  6. 更新后不重启、不重建 AIDE 基线。内核与 glibc 更新不重启不生效;AIDE 基线不更新则下次扫描全是噪音。两件事都放进变更收尾清单。
  7. 三系 MAC 概念混用。在 Ubuntu 查 SELinux、在 RHEL 查 AppArmor,都会一无所获。先看三系对照表选工具。
  8. chattr +i 锁了会被自动化修改的文件。useradd/chpasswd 需要写 /etc/shadow,锁死后账号运维全部失败。加锁前先确认写入方。
  9. lynis 建议全盘执行。不区分角色的"全采纳"会制造大量无效配置与性能开销。逐条评估、记录取舍,才是基线管理。
  10. 加固完成后不做回访。端口、账号、服务会随业务变化重新膨胀。把本篇第 1、2、4 节的盘点命令做成月度巡检脚本,加固才是"制度"而不是"一次性运动"。

按性质归类有助于系统性自查:第 1–3 条是会把自己锁在外面的单点失误(流程纪律问题,靠固定操作顺序根治);第 4–6 条是基线套错角色或更新链断裂(要按机器角色分支、把收尾动作写进变更单);第 7–9 条是工具使用方式错误(三系概念、chattr、lynis 都是"会用不等于用对");第 10 条则是制度缺失,前九条防的是某次事故,它防的是"一切慢慢坏掉"。巡检脚本不必复杂:把 ss -tulnp、systemctl、包清单、sshd -T 关键项的输出存成带日期的快照,diff 上一次结果,异常项自然浮出水面。

9. 实战:新服务器上线加固清单 ​

前面八节按主题展开,实战节则按时间顺序把它们串成一次可执行的上线流程——这也是全篇唯一一处"必须按序号做"的地方,因为九步之间有真实的依赖,不是排版上的偏好。若只允许保留本篇一页纸,那一页就是本节:前面八节负责"为什么",本节负责"按什么顺序做、每步怎么验收"。把九步打印出来贴在工位的人会发现,真正需要动脑的只有第 1 步的判断(什么算冗余)和第 9 步的归纳(五栏怎么写),其余七步都是有唯一正确顺序的机械动作——机械部分交给 checklist,判断部分留给人,是所有安全 runbook 的共同结构。九步还刻意不追求"一次全做完":第一遍允许只完成 60% 并把剩余项记成整改单,比追求满分却卡在第 1 步盘点三天更有价值——基线是迭代出来的,不是憋出来的。迭代视角下,九步的"完成"定义也要放宽:不是"全部变绿",而是"没有一项处在无主状态"——做过的有记录,没做的有工单,这比临时冲刺到全绿更能抵抗下一次漂移。

按顺序执行,每步都能独立验证(三系包安装分别用 apt/pacman/dnf,下列 Arch 示例用 pacman)。九步不是九个可选项,而是一条依赖链:盘点产出范围 → 补丁决定基线版本 → SSH 收入口 → 防火墙收网络面 → 检测装眼睛 → 内核权限收根权限 → MAC/自查做验证 → 归档留下证据。跳步执行的常见后果是"后面几步建立在前面未定义的假设上"——比如没盘点就先装 AIDE,基线里会混进一堆事后要清的临时包。依赖链还有一条反向的验收顺序:验收应当从第 9 步的五栏文档倒着查——每栏都能指回某一步的回读输出,说明链条完整;某一栏空着,缺的不是那一栏,是对应那一步没做或没留痕。倒查还有一个心理上的好处:从交付物出发问"证据在哪",比从步骤出发问"我做完了吗"更难自我欺骗——前者要贴输出,后者只需要点头。

清单执行前还有三个准备动作值得写进 runbook 头部。其一,确认控制台/救援通道可用(云主机测一次 VNC,物理机确认 IPMI/iLO),这是敢做第 3、4 步的前提。其二,准备一个"变更中"的时间盒:九步串行做完通常要数小时,中途断在 SSH 配置那一步最危险,所以第 3 步前后要留出不被打断的窗口。其三,开一张变更单,每步完成打勾并贴回读输出——清单的价值一半在执行,一半在留痕;没有留痕的加固,下一次换人时只能整套重做。三个准备动作共享同一个失败模式假设:默认"执行者会中途被打断、会记错、会关窗"——runbook 的冗余不是为熟手准备的,是为三个月后第一次照单执行的新人准备的。新旧执行者之间的交接质量,最终就落在变更单上:能被下一个人续着做的清单,才算真正执行完。

九步之间的"暗线"前面说过补丁→基线→自查,还有一条更隐蔽的证据线要贯穿始终:第 1 步的三份清单是证据,第 3、4 步的回读输出是证据,第 7、8 步的扫描报告是证据,第 9 步把它们钉进同一页文档。安全审计问的从来不是"你们做过加固吗",而是"请出示上一次加固的记录"——把证据线当成交付物的一部分,清单执行完的产物就自然满足了内审与外审的共同格式,不需要事后补材料。

清单还隐含一条回滚预算:九步里真正高危的是第 3 步(SSH)与第 4 步(防火墙),它们共享同一种失败模式——远程通道断掉且控制台未就绪。所以时间盒与救援通道不是仪式感,而是给这两步买的保险;第 1、2、5–8 步即使做错,也基本能在本地或带外通道里修回来。按"回滚难度"给九步标色(红/黄/绿),打印时会比线性列表更好用——变更单的优先级应当由回滚成本决定,不是由步骤编号决定。

  1. 盘点:ss -tulnp 看监听、systemctl list-units --type=service --state=running 看服务、pacman -Q --explicit(或 apt-mark showmanual/dnf repoquery --userinstalled)看软件包,删/禁与业务无关项。产出物是三份清单,直接贴进第 9 步的归档文档。
  2. 补丁:全量升级到当前版本(Arch 用 sudo pacman -Syu),确认重启需求并安排重启窗口。补丁这一步放在盘点之后、SSH 之前,是因为 SSH 加固最好建立在"当前版本的 sshd"上——先关口令再发现旧版本有个认证 bug 要回滚,流程会拧巴;先把版本基线抬平,后面的配置变更才站在稳定地面上。
  3. SSH:分发 ed25519 密钥 → sshd -t → 新会话验证 → 改 sshd_config(禁 root、禁口令)→ 重启 → 保持旧会话再验一次。
  4. 防火墙:默认拒绝入站,先放行 SSH 再启用(顺序见防火墙),同步云安全组。把防火墙排在 SSH 之后而不是之前,是因为 SSH 加固完成后再收网络面,可以立刻用"密钥 + 新策略"的组合验证通道仍然通畅——两步顺序对调,就会在口令已关、防火墙未就绪的窗口里裸奔一截。
  5. 检测:装 AIDE + Fail2Ban + auditd,初始化基线并接告警(见入侵检测)。检测放在配置基本定型之后 init 基线,能显著减少"基线刚建就被后续变更冲掉"的重跑次数;若顺序反了(先 init 再做第 3、4 步的大量配置),第 5 步完成时基线已经过期,等于白做。
  6. 内核与权限:写入 sysctl.d 基线并 sysctl --system 回读;抽查 SUID 清单;收紧 sudo 白名单。
  7. 确认 MAC 状态:getenforce/aa-status 按发行版检查,记录当前状态与是否符合预期。
  8. 基线自查:lynis audit system 过一遍建议,适用项纳入清单,不适用项记录原因。
  9. 归档:把"这台机器开了什么端口、跑了什么服务、谁有 sudo、密钥指纹"写成一页文档入库——没有文档的加固,三个月后没人说得清哪些是有意为之。

九步走完,这台机器的攻击面从"默认安装的全集"收敛到"业务声明的子集",且每一条配置都有据可查。注意 2、5、8 三步构成一条容易被省略的暗线:补丁改变了文件 → 检测基线必须重建 → 自查结果随之刷新——只做加固不做检测,配置会漂移而无人察觉;只做检测不做加固,则每次扫描都在为已知问题产生重复告警。清单的完整性比单步的完成质量更重要。暗线还可以再延长一环:自查产生的整改项若不回到第 1 步的盘点与第 3 节的变更窗口,就会变成"每次 Lynis 都报同样几条"的常驻噪音——九步实际是一个闭环,第 8 步的输出喂给下一轮第 1 步,而不是终点。

清单还应该配一张"逃生卡"贴在变更单首页:SSH 进不去时云控制台 VNC 怎么进、sudoers 坏了用 live ISO 怎么改、误删关键包从哪个缓存/仓库回装。九步里任何一步锁死自己,决定恢复时长的不是你对配置多熟,而是逃生通道事先有没有验证过——没测过的 VNC 在真正需要的那天经常是灰的。把"验证逃生通道可用"作为第 0 步写进流程,成本是五分钟,收益是后面八步都敢动手。逃生卡还应包含一句本地信息:机器在哪个账号、IAM 角色叫什么、工单走哪个队列——没有这些,就算 VNC 是亮的,新人也找不到入口。

归档的那一页文档建议固定五栏:监听端口与来源限制、运行中服务及开关理由、sudo/特权账号名单、SSH 与防火墙关键生效值(sshd -T、ufw status 摘要)、密钥与证书指纹/到期日。五栏对应本篇第 1、2、4、5 节的产出物,月度巡检时逐栏 diff——文档不是写给审计员看的装饰,是下次你或同事做变更前的对照基线。

安全加固的本质就是这句话:让系统回到你主动选择的状态,并且一直保持下去。 "主动选择"四个字是本篇全部九步的共同主语——选择意味着可解释、可回读、可回滚;"一直保持下去"则意味着第 7、8 节的周期性自查不是附加项,而是这句话的后半句本身。若只能留下一句给团队贴在墙上,选后半句更实用:大多数人不缺第一次加固的意识,缺的是第三个月还记得回来对表。

参考资料 ​

加固类资料汗牛充栋,质量差异也大,这里按用途分三档:CIS Benchmarks 是有合规诉求时的权威清单(按发行版与服务器角色分册,项多但可裁剪);Arch Wiki 与各发行版官方安全文档是理解"这系默认为什么这样"的一手来源;鸟哥与成熟博客适合建立直觉,但具体参数要以当前发行版手册核对——加固条目随版本迭代很快,三年前的"最佳实践"可能已在新版本中失效或反转。建议以本篇清单为骨架,用 CIS 对照补盲区,用发行版官方文档核对每一项的三系差异。三档资料的失效速度也不同:CIS 随大版本改版,发行版文档随包管理器与服务名走,博客则可能在下个 Ubuntu LTS 就过期——核对时以"离你机器最近的那份"为准(本机 man、本机发行版文档),外部资料只负责提出问题。

阅读顺序上的实用建议:先按本篇第 9 节的九步顺序过一遍 CIS 对应章节,把"我们不采纳及原因"写进备注;再按发行版官方文档核对表里的三系差异;最后才用 Lynis 做周期性扫描兜底。反过来从 CIS 全文开始读,最容易死在前 50 条与业务无关的细节上,既没建立框架也没形成可执行清单——清单类文档的读法是从场景出发做减法,不是从头到尾做填空。"不采纳及原因"这一栏的价值会随时间上升:两年后有人问"为什么我们没开这条",你能指着备注说"评估过,因为容器里跑的是 …",而不是重新开一次安全评审——减法的记录和加法的配置同等重要。

三份清单(本篇第 9 节、防火墙第 8 节、入侵检测第 8 节)建议收进同一个配置仓库的 security/baseline/ 目录,与 sysctl 片段、sshd_config、nftables.conf 同目录版本管理。这样每次 diff 仓库,改的就不只是配置,还有"我们声称的安全姿态"——代码评审流程顺便成了安全评审,是把加固制度塞进现有工程习惯的最低成本路径。仓库结构还可以再放一层"逃生卡"与归档模板(第 9 步的五栏表),让新机器上线时是复制模板改差异,而不是从空白文档写起:模板化是制度从"写在 wiki 上"变成"跑在 git 里"的分界线。 仓库与三份清单的关系可以再收一句:清单是接口,仓库是实现——接口稳定(五栏、九步不变),实现可以随工具链演进,这样即使明年换掉 Lynis,读者按本篇学到的框架仍然成立。

相关页面 ​

基于 Apache License 2.0 许可发布