服务器安全加固
设计原则:最小开放 —— 能不开就不开,能只给一台就不给一个网段,能只给公钥就不留口令。
合规基线:默认等保一级 —— 用户没指定等级时,一律按《GB/T 22239-2019 信息安全技术 网络安全等级保护基本要求》的
第一级安全要求(自主保护级) 做检查与留证(§2);用户明确说「二级 / 三级 / 四级」才按对应等级叠加增量控制点(§2.3)。
一句话:先查清谁要访问、从哪个地址来、到底哪些端口真能从公网连上 → 备份防火墙并挂自动回滚保险 → 只按 /32 精确放行 + 兜底 drop → 验证 → 固化并撤保险 → 按等保口径逐项留证据。
全端口收口更激进:除 SSH 与 nginx 的 80/443 外一律不对外,远程访问改走 SSH 隧道。
> 本技能管主机的网络暴露面(谁能连哪个端口)、登录方式(口令 vs 公钥)与等保通用要求的自查留证。
> 不管应用层鉴权、证书、WAF,也不替代测评机构的测评结论。
> 可直接跑的脚本见 §11;等保逐条检查项与命令见 references/mlps-l1-checklist.md。
约定:本文示例里的 x.x.x.x / y.y.y.y 代表任意真实地址,1234 代表任意端口,
<主机A> / <主机B> 代表任意目标机,<构建机> 代表发起运维的跳板机。
落地时替换成你自己的值即可 —— 技能本身不含任何具体环境信息。
0. 五条原则
| 原则 | 含义 | 反例 |
|---|---|---|
| 最小开放 | 白名单精确到 /32,能写单机就不写网段 |
为了「省事」放 10.0.0.0/8、x.x.x.x/25 |
| 先勘察再动手 | 不知道有谁在连,就别急着关 | 直接 --add-port / 直接删服务 |
| 可回滚 | 任何远程改防火墙或登录方式 的动作都要能自己退回去 | 只 --permanent 后 reload,错了只能进机房 |
| 人工确认后才执行 | 收口会断别人的访问,清单必须先给人看 | 脚本一把梭把 8080 关了,业务方第二天来问 |
| 合规可举证 | 每做一项都留下「做了什么 + 怎么验证 + 输出是什么」,按等保条目归档(§2) | 改完就完,被问「凭啥说符合等保」时只剩口头解释 |
1. 优化项清单
| # | 优化项 | 一句话做法 |
|---|---|---|
| 1 | SSH 端口 IP 白名单 | rich rule priority=-10 accept + priority=-5 drop,见 §4 |
| 2 | 端口分层管控(80/443 全开 · 其余走 IP 白名单 · 不该跨机的绑环回) |
先分档再动手,见 §5 |
| 3 | docker 发布端口绑环回 | -p 127.0.0.1:宿:容,见 §5.6 |
| 4 | 业务端口收进 nginx 反代 | proxy_pass 不带 URI + location = 301,见 §5.7 |
| 5 | 公钥免密登录 + 关闭口令登录 | 先备齐/实测公钥 → 带自动回滚守卫关口令,见 §6 |
| 6 | sshd 抗爆破参数 | MaxAuthTries 3、LoginGraceTime 20、MaxStartups 10:30:60 |
| 7 | fail2ban / sshguard | 白名单已挡绝大多数,仍需拦白名单内的异常高频尝试 |
| 8 | authorized_keys 审计 |
逐条对指纹与来源,删无效行(如前缀拼错的无效行) |
| 9 | 等保自查(默认一级) | 按十个层面逐项打 ✓/✗ 并留证据,见 §2 |
| 10 | 账户与口令基线 | UID 0 唯一、无空口令、默认账户锁定、口令策略落地,见 §2 与 references/mlps-l1-checklist.md |
| 11 | 入侵防范与恶意代码 | 最小服务、内核参数、防病毒/EDR 在位且病毒库在更新 |
| 12 | 备份与恢复演练 | 有定时备份任务 + 至少一次真实恢复验证(只有备份不算,能恢复才算) |
> 本表只是待办列表,不带「已做/未做」状态 —— 每台机器做到哪一步、什么时候做的,
> 写进你自己的运维报告,不要回填进本技能。
> 顺序建议:1(白名单)→ 5(免密+关口令)→ 6(抗爆破)→ 7/8 → 2/3/4(收口)→ 9(等保自查出报告)。
> 白名单先做——它把绝大多数扫描挡在门外,之后的爆破审计才有意义。
默认交付流程(用户只说「做安全加固」也按这个走):
| 步 | 动作 | 读哪节 |
|---|---|---|
| ① | 等保自查(只读):先跑 scripts/mlps-l1-check.sh 出基线报告,知道差距在哪 |
§2、§11.4 |
| ② | 勘察:谁在连、有没有内网、哪些是 docker 发布 | §3 |
| ③ | 收口:SSH 白名单 → 端口分档 → docker 绑环回 → 反代/隧道 | §4、§5 |
| ④ | 登录方式:公钥免密 → 带守卫关口令 | §6 |
| ⑤ | 复验 + 等保回归:逐条打勾,差距项写明「已整改/待整改」 | §7 |
| ⑥ | 出报告:加固项 + 等保条目 + 证据,归档 | §10 |
2. 等保合规基线(默认等保一级)
依据 GB/T 22239-2019《信息安全技术 网络安全等级保护基本要求》(业界俗称「等保 2.0」)。
第一级即「自主保护级」:系统受破坏后仅损害公民、法人和其他组织的合法权益,不损害社会秩序和公共利益、不损害国家安全。
三个必须先对齐的认知:
- 无需备案、无需测评 ≠ 不用管:安全建设仍要按标准落实,且公安机关将其纳入监督检查——查的就是「有没有做、有没有记录」。
- 技术只到「安全计算环境」:集中管控/集中审计所在的安全管理中心是二级及以上才出现的控制点,一级明确记为不适用。
- 管理五项齐全但要求是「基本」:有制度、有人负责、有培训、有建设与运维管理即可;
背景审查、外部人员访问权限清理属二级及以上要求,一级不因缺这两项判不符合。
2.1 等级怎么定:默认一级,除非用户指定
| 用户怎么说 | 怎么做 |
|---|---|
| 只说「做安全加固 / 做等保 / 过等保 / 等保合规 / 等保自查 / 等保整改 / 等级保护 / 按等保检查」 | 按一级跑完整自查(§2.2 + references/mlps-l1-checklist.md),报告首行注明「基线:等保一级(默认)」 |
| 明确说「一级」 | 按一级跑 |
| 明确说「二级 / 三级 / 四级」 | 先声明「内置表以一级为基线,二级及以上需叠加 §2.3 增量项,最终结论须由测评机构出具」→ 再按 §2.3 补查;二级直接照 references/mlps-l2-delta.md 逐条落,脚本用 --level 2 |
| 已定级备案、给出等级 | 以用户/备案材料的等级为准,不要自行判定 |
| 反问「我这算几级」 | 只给判定依据(受破坏后损害谁的利益),不替用户定级;定级是运营者责任 |
🔴 三条纪律:
- 默认就是一级,不要因为「看着像生产环境」自行升到二级/三级,也不要因为「才一级」就把检查做浅;
- 不许把没查的写成「符合」 —— 一级允许「部分符合 + 待整改」,如实记录比凑表更有用;
- 报告里分清楚「主机可验证」与「需人工确认」,后者不要替人签字。
2.2 一级检查总表(技术 5 层面 + 管理 5 层面)
技术层面——主机上基本都能查到,逐条命令见 references/mlps-l1-checklist.md:
| 层面 | 一级控制点要点 | 主机上的证据 | 对应小节 |
|---|---|---|---|
| 安全物理环境 | 机房选址能防震/防风/防雨;基本物理访问控制;防盗窃防破坏;防雷击;防火;防水防潮;温湿度控制;电力供应(UPS/双路) | 现场照片 + 机房巡检记录;云主机记「不适用(云服务商责任,见共享责任边界)」 | 本技能不覆盖,仅登记 |
| 安全通信网络 | 关键网络设备处理能力与带宽满足业务高峰;有与实际相符的网络拓扑图;通信传输用校验技术保证完整性;可信验证(一级为「可」,非强制) | 拓扑图版本与更新日期;nginx -T \| grep -E 'ssl_protocols\|ssl_ciphers';openssl s_client 看协议/套件;sha256sum 传输比对 |
§5.7、§6 |
| 安全区域边界 | 跨边界访问与数据流经边界设备受控接口通信;访问控制策略默认除允许外拒绝;删除多余/无效规则;规则要检查源/目的地址、端口、协议;可信验证(可) | firewall-cmd --list-all / nft list ruleset / ufw status verbose;默认策略 drop/reject;--list-rich-rules 无 /8、/25 这类宽放行;安全组截图 |
§4、§5、§11.2 |
| 安全计算环境 | 登录用户分配账户和权限;重命名或删除默认账户、修改默认口令;及时删除/停用多余、过期账户;避免共享账户;身份鉴别、访问控制、入侵防范、恶意代码防范、数据完整性/保密性/备份恢复 | 账户清单(UID 0 唯一、无空口令、无 test 类账户)、口令策略、文件权限、监听端口、AV/AIDE、备份任务与服务端日志 | §6、§11.4 + references/mlps-l1-checklist.md |
| 安全管理中心 | 一级不要求(集中管控、集中审计、集中恶意代码/补丁管理均为二级+) | 无需取证,报告写「不适用(一级无此控制点)」 | — |
管理层面——非主机可验证,靠文档 + 访谈,本技能只给清单与判定口径:
| 层面 | 一级控制点要点 | 要收的证据 |
|---|---|---|
| 安全管理制度 | 有基本网络安全管理制度与操作规程;明确安全策略、管理制度、制度制定与发布流程 | 制度文件(含版本/生效日期)、发布审批记录 |
| 安全管理机构 | 指定或授权专门部门/人员负责网络安全;落实安全保护责任;配备安全管理人员 | 任命或授权文件、岗位职责说明、人员名单 |
| 安全管理人员 | 对各类人员做安全意识教育与岗位技能培训;告知安全责任与惩戒措施;人员录用由专人负责 | 培训签到与考核记录、责任告知书、录用流程记录 |
| 安全建设管理 | 定级和备案、安全方案设计、产品采购、软件开发、工程实施、测试验收、系统交付 | 定级报告、安全方案、采购与验收记录、交付清单 |
| 安全运维管理 | 环境管理、资产管理、介质管理、设备维护、漏洞和风险管理、网络和系统安全管理、恶意代码防范管理、备份与恢复管理、安全事件处置 | 资产台账、巡检记录、漏洞与补丁记录、备份记录、事件处置工单 |
> 一级无需备案:安全建设管理里的「定级和备案」按「已完成自主定级、留存定级过程材料」把握即可,不要去催备案回执。
2.3 等级差异:什么时候需要升级(要点,非逐条对照)
用户提到更高等级时,按这张表把增量补上;二级的逐条落法见 references/mlps-l2-delta.md(编号 L2-01…L2-17,
与 --level 2 的脚本输出一一对应)。正式定级/测评以标准原文与测评机构意见为准。
| 控制点 | 一级(默认) | 二级增量 | 三级增量 |
|---|---|---|---|
| 备案 / 测评 | 无需备案、无需测评 | 需备案,按监管要求测评 | 需备案,每年测评 |
| 安全管理中心 | 不适用 | 要求:系统管理 + 审计管理(管理员分权、操作审计)。⚠️ 集中管控是三级,别在二级强推 | 增加安全管理 + 集中管控(统一策略、集中日志/事件/补丁/病毒管理) |
| 身份鉴别 | 账户唯一 + 基本口令强度 | 口令复杂度与定期更换、登录失败处理、远程管理防窃听、会话超时 | 两种以上组合鉴别(双因素)、鉴别信息加密存储 |
| 访问控制 | 分配账户与权限 | 最小权限、默认拒绝、授权粒度更细、由授权主体配置策略 | 更细粒度 + 重要主体/客体的安全标记与强制访问控制 |
| 安全审计 | 一级无独立审计控制点,按「关键操作留有记录」把握 | 要求:审计范围、记录内容、记录保护、留存 | 增加审计记录备份、防未授权删改、集中审计 |
| 可信验证 | 「可」(非强制) | 「可」 | 「应」:基于可信根验证引导/系统程序,动态度量、异常报警 |
| 数据备份恢复 | 基本备份措施 | 本地备份 + 恢复演练(要能证明可恢复) | 异地/热备、重要数据定期备份与恢复演练 |
| 恶意代码防范 | 基本措施 | 统一管理、及时更新病毒库 | 更严 + 集中管理 |
| 数据保密性 | 基本措施 | 重要数据传输/存储加密 | 密码技术保护,涉及商用密码合规 |
| 人员管理 | 培训 + 告知责任 | 增加背景审查、外部人员访问权限审批与清理、离岗权限回收 | 关键岗位定期审查、签署保密协议 |
| 运维管理 | 基本运维活动 | 增加漏洞扫描与补丁管理、变更管理、审计管理规范、日志留存 | 集中日志审计、漏洞与补丁闭环 |
2.4 判定口径与报告怎么出
| 结论 | 什么情况用 | 必须写清 |
|---|---|---|
| 符合 | 已落实,且证据能复现 | 命令/配置文件 + 输出摘要 |
| 部分符合 | 做了一部分,或只覆盖部分主机/账户 | 缺口是什么(哪台、哪个账户、哪条规则) |
| 不符合 | 未落实 | 整改建议 + 优先级 |
| 不适用 | 一级无此控制点 / 云上由云服务商负责 / 业务确实不涉及 | 理由(一句话) |
| 待人工确认 | 文档、访谈、现场照片类 | 交给谁确认、要什么材料 |
证据三件套:命令或文件 + 输出摘要(别贴几千行) + 主机标识与时间。
同一台机器重复排查时,用上次的证据做对比,才能证明「真的改了」。
2.5 加固动作 ↔ 等保控制点映射(做过的活别浪费)
| 等保控制点 | 本技能里对应的动作 |
|---|---|
| 安全区域边界 › 访问控制(默认拒绝、五元组、删冗余) | SSH 白名单(§4)、端口三档收口(§5)、端口段单独 drop(§5.6) |
| 安全区域边界 › 边界受控接口 | 只留 nginx 80/443 与 SSH,其余走隧道(§5.7、§5.8) |
| 安全计算环境 › 身份鉴别 | 公钥免密 + 关闭口令登录 + MaxAuthTries(§6) |
| 安全计算环境 › 访问控制 | 账户与权限梳理、文件权限、sudo 最小化(§2.2 + 检查表) |
| 安全计算环境 › 入侵防范 | 最小服务与端口、fail2ban/sshguard、内核参数(§5、§11.2) |
| 安全计算环境 › 恶意代码防范 | 防病毒/EDR 在位 + 病毒库更新 |
| 安全计算环境 › 数据完整性 | AIDE/rpm -Va、关键文件 sha256 基线 |
| 安全计算环境 › 数据备份恢复 | 定时备份任务 + 真实恢复演练 |
| 安全通信网络 › 通信传输完整性 | TLS 版本/套件、传输校验 |
| 安全运维管理 | §10 落地记录 + 巡检/漏洞/备份/事件台账 |
2.6 逐条检查项
- 完整检查表(每条:检查内容 / 命令 / 符合判据 / 证据 / 常见不符)
- 一级 →
references/mlps-l1-checklist.md - 二级增量 →
references/mlps-l2-delta.md(L2-01…L2-17,含「一级→二级 gap 清单」与二级报告模板) - 一键只读自查(输出
符合/部分符合/不符合/不适用/待人工确认+ 证据行 + 汇总)→
bash scripts/mlps-l1-check.sh # 只读,默认按一级
bash scripts/mlps-l1-check.sh --level 2 # 一级全量 + 二级增量,分别计分
bash scripts/mlps-l1-check.sh --strict # 有「不符合」时退出码 2(可进 CI)
> 脚本只读(不改任何配置、不重启服务),覆盖主机可验证项;文档/访谈/现场类会标成「待人工确认」,不会替你判符合。
> 三级/四级只会列出尚未覆盖的增量项,不会假装查过。
3. 第一步:勘察(跳过这步必踩坑)
3.1 谁在连这个端口?——从日志反查,不要问「应该是谁」
lastb -a | head -20
lastb -a | grep -Eo '([0-9]{1,3}\.){3}[0-9]{1,3}' | sort | uniq -c | sort -rn | head
lastb -a | awk 'NF>2{print $1}' | sort | uniq -c | sort -rn | head # 被爆破的用户名
journalctl -u sshd --no-pager | grep "Accepted" | tail -50 # 合法来源只有这里
journalctl -u sshd --no-pager | grep -c "Accepted password" # >0 = 口令登录还开着
lastb读/var/log/btmp,只保留当月;「最早一行」= 重装/轮转时间,不是攻击起点。- 取 IP 用
grep -Eo正则比awk $N稳(短行会报field -2)。 Accepted的来源与lastb的攻击 IP 交集为空,才说明「被爆破但没破」。grep -c "Accepted password"> 0 就是关闭口令登录的收益点(§6)。
3.2 这台机器到底有没有「内网地址」?
ip -4 -o addr show; ip route
ip -6 -o addr show scope global; ip -6 route | head -3
🔴 「公网地址直配网卡」是最常见的误判:IDC 把公网段直接配在 eno1 上(如 x.x.x.x/25),
两台同段二层互通(0.1ms),看着像内网,其实没有任何私有地址。此时「只开放内网 IP」做不到:
| 方案 | 做法 | 评价 |
|---|---|---|
| 精确 /32(推荐) | 只放对端公网 IP | 效果等同「只允许那一台」,无额外组件 |
| 隧道叠加 | GRE / WireGuard 建 10.99.0.0/30 |
隧道仍需对端公网放行某协议,暴露面没减少 |
| 跳板中转 | 只放跳板机,对端 ssh -L / ProxyJump |
真正不信任对端网络,代价是多一跳 |
IPv6 别想当然当替代通路:两端实测(ping -6 + /dev/tcp),常见结果是「有地址没路由」。
3.3 端口是「主机进程」还是「docker 发布」?
ss -tlnp | grep <端口> # process 是 docker-proxy 即为发布端口
docker ps --format '{{.Names}}\t{{.Ports}}'
🔴 docker -p 0.0.0.0:1234:1234 完全绕过 firewalld(§5.6 有实测证据)。这类端口只能靠
-p 127.0.0.1:1234:1234 收口,zone 里的白名单/drop 对它无效。
3.4 防火墙栈确认
systemctl is-active firewalld ufw 2>/dev/null
firewall-cmd --version # ≥0.6 才支持 rich rule 的 priority
grep -i FirewallBackend /etc/firewalld/firewalld.conf
firewall-cmd --list-all # ports / services / rich rules
4. SSH 端口 IP 白名单
4.1 规则结构(rich rule + 优先级,不要 source-based zone)
priority=-10 源在白名单 → 该端口 accept
priority=-5 其余所有源 → 该端口 drop
(zone 的 ports/services 隐含 priority 0,永远排在上面两条之后)
- 负优先级保证白名单判定先于 zone 自身
ports/services;同优先级按添加顺序。 - 🔴 绝对不要用 source-based zone:
--add-source会把该源全部流量切进新 zone,
连带80与其它已放行端口一起断。必须同 zone 内用「只针对该端口的 rich rule」。
4.2 实施
TS=$(date +%Y%m%d-%H%M%S); cp -a /etc/firewalld /root/firewalld.bak.$TS
systemd-run --on-active=600 --unit=sshwl-rollback firewall-cmd --reload # 回滚保险
for ip in <本机出口>/32 <构建机出口>/32 <对端IP>/32 <自身IP>/32; do
firewall-cmd --add-rich-rule="rule family=\"ipv4\" priority=-10 source address=\"$ip\" port port=\"<端口>\" protocol=\"tcp\" accept"
done
firewall-cmd --add-rich-rule="rule family=\"ipv4\" priority=-5 port port=\"<端口>\" protocol=\"tcp\" drop"
firewall-cmd --remove-port=<端口>/tcp # 摘掉「全开」声明,纵深防御
4.3 IPv6 必须同步
ss 显示 [::]:<端口> 或 *:<端口>(双栈)时,只写 family="ipv4" 的 drop,v6 侧仍全开:
firewall-cmd --add-rich-rule='rule family="ipv6" priority=-10 source address="<v6对端>/128" port port="<端口>" protocol="tcp" accept'
firewall-cmd --add-rich-rule='rule family="ipv6" priority=-5 port port="<端口>" protocol="tcp" drop'
4.4 固化
systemctl stop sshwl-rollback.timer sshwl-rollback.service
firewall-cmd --runtime-to-permanent
# ❌ 不要 --permanent 逐条重加(易漂移);❌ 不要 --reload(flush docker DNAT)
5. 端口分层管控(全开 / 白名单 / 仅环回)
目标态:三档并存,按需定档,不要一刀切全关。
| 档 | 典型端口 | 策略 | 谁能连 |
|---|---|---|---|
| ① 全开放 | 80、443(nginx) |
对全网开放,靠应用自身鉴权兜住 | 任何人 |
| ② IP 白名单 | SSH 端口,以及所有需要远程使用的业务端口(管理后台、API、监控口) | 只放白名单 /32,其余 drop —— 与 §4 完全同一套规则结构 |
白名单内的机器 |
| ③ 仅环回 | 数据库、缓存、监控采集端等「不该跨机直连」的 | 服务绑 127.0.0.1,公网地址上根本没人监听;远程要连就开 SSH 隧道 |
仅本机(+ 隧道) |
> ②是主力档:不是「关掉」,而是「只给固定几台机器开」——运维不用天天开隧道,外网扫描也进不来。
> 一刀切全禁只在「这个端口没人远程用」时才成立,否则第二天就有人要求重新放行。
5.1 先定「每个端口属于哪一档」(唯一需要人拍板的事)
| 档 | 判定依据 |
|---|---|
| ① 全开放 | 面向终端用户浏览器的入口,通常只有 80/443 |
| ② 白名单 | 内部系统间调用、管理后台、API —— 需要跨机,但不该对全网 |
| ③ 仅环回 | 单机自用(数据库、缓存、监控采集端),或「连白名单机器都不该直连」的 |
- 定档原则:能进③就别进②,能进②就别进①。同一台机三档并存是常态,但每个端口必须明确归档。
- 白名单地址通常复用 SSH 那几个
/32(本机出口、构建机、对端服务器、自身);新增对端时两处一起加。 - 定完档再动手,别边改边想——改错档位等于把该锁的打开、或把该开的锁死。
5.2 为什么「应用在 0.0.0.0 监听」≠「外面能连」——默认拒绝机制
firewalld 是默认拒绝模型:流量进入 filter_IN_public 链后,只有命中
ports / services / rich rule accept 才放行,其余一律被 zone 的兜底动作丢掉(实测表现为 Timeout,
说明包根本没被接受,不是被应用拒绝)。
所以只要不 --add-port,应用绑 0.0.0.0 也是外面连不上的。实测:
# ① 临时起一个 0.0.0.0:11111 的监听,且 firewalld 不放行
python3 -m http.server 11111 --bind 0.0.0.0 &
ss -tlnp | grep 11111 # LISTEN 0.0.0.0:11111 ← 确实全接口监听
firewall-cmd --list-ports # 里面没有 11111
# ② 从外部探测 → TimeoutError(防火墙挡住了,应用毫不知情)
# ③ firewall-cmd --add-port=11111/tcp --timeout=60 之后再探测 → OPEN
# ④ --remove-port=11111/tcp 后探测又回到 Timeout
这条结论的价值:收口的主力是「不放行」,不是「改应用」。应用绑不绑 0.0.0.0 只影响
「同机容器/进程能不能连」,不影响公网可达性。
🔴 但这个「默认拒绝」有 6 个失效例外,必须逐一排除:
| # | 例外 | 为什么会穿透 | 处置 |
|---|---|---|---|
| 1 | docker 发布 -p 0.0.0.0:宿:容 |
nat PREROUTING 先 DNAT,包直接进容器,filter_IN_public 轮不到 |
改 -p 127.0.0.1:,见 §5.6 |
| 2 | 有人 --add-port / --add-service / 另一 zone 放行 |
配置本身放开了 | 审计 --list-ports、--list-all-zones |
| 3 | source-based zone(--add-source) |
该源全部流量进新 zone,若新 zone 宽松就全开 | 禁用此法 |
| 4 | IPv6 侧漏配 | *:端口 是双栈,只 drop 了 v4 |
补 family="ipv6" |
| 5 | firewalld 未启动 / 被 stop / reload 后规则丢失 | 没有 netfilter 规则 = 全开 | systemctl is-enabled firewalld + 固化 |
| 6 | 手工 iptables/nft 规则、docker run --net=host |
host 网络不经 DNAT,但也受 filter 约束;手工规则可能 ACCEPT | 审计 nft list ruleset |
5.3 三层清单分析法(监听 × 放行 × 外部实测)
只看 ss 会误判,只看 firewall-cmd 也会误判(docker DNAT、v6、绑特定 IP 都藏在缝里):
# ① 监听层
ss -tlnp
# 0.0.0.0:X / *:X / [::]:X → 全接口(* 为双栈)
# x.x.x.x:X → 只绑公网单地址(同样对外)
# 127.0.0.1:X / [::1]:X → 仅本机(合规)
# ② 放行层
firewall-cmd --list-ports; firewall-cmd --list-services
docker ps --format '{{.Names}}\t{{.Ports}}' # 0.0.0.0 发布的容器端口
# ③ 实测层(从另一台机器,别看配置看连通性)
python -c "
import socket
h='<目标IP>'; ports=[<端口列表>]
for p in ports:
s=socket.socket(); s.settimeout(4)
try: s.connect((h,p)); print(p,'OPEN'); s.close()
except Exception as e: print(p,type(e).__name__)
"
结论分三类:
| 类 | 判据 | 处置 |
|---|---|---|
| A 公网可达 | 全接口监听 且(firewalld 放行 或 docker 0.0.0.0 发布)且 实测 OPEN |
收口(不在保留清单就关) |
| B 纵深待办 | 全接口监听,但未放行(实测 Timeout) | 不紧急但要补:现在靠一条 --add-port 挡着 |
| C 已合规 | 只绑 127.0.0.1/[::1] |
无需动 |
> ⚠️ B 类别漏:「今天没暴露」只说明防火墙恰好没开,不代表配置到位。
> 收口价值一半在 A(止血),一半在 B(消除「误放行即裸奔」)。
5.4 收口手段优先级
| 优先级 | 手段 | 适用 | 代价 |
|---|---|---|---|
| ① 绑环回(根治) | 服务只监听 127.0.0.1 |
能改配置的服务 / docker 发布 | 重启服务 |
| ② firewalld 收口(缓解) | 摘 --add-port + 加 priority=-5 drop |
第三方进程、改不了配置 | 无(即时) |
| ③ SSH 隧道(访问方式) | ssh -N -L 转发 |
收口后的远程访问 | 需 AllowTcpForwarding local |
各服务绑环回写法:
| 服务 | 做法 |
|---|---|
| ClickHouse | config.xml 的 <listen_host> 改 127.0.0.1(全局:http/tcp/其它端口一起变),systemctl restart clickhouse-server |
| Redis | bind 127.0.0.1 -::1 + protected-mode yes(见 install-redis 技能) |
| Spring Boot / TongWeb | server.address=127.0.0.1(或 SERVER_ADDRESS) |
| docker | -p 127.0.0.1:宿主:容器 |
| 无此配置项的第三方二进制 | 只能走 ② |
5.5 🔴 人工确认闸门(必须)
① 跑审计(只读)→ 输出 A/B/C 三层清单
② 把 A 类清单交给用户/业务方确认:哪些端口有人在用?保留哪些?
③ 用户明确回复「关哪些、留哪些」→ 才执行
④ 执行前挂回滚保险;执行后逐端口从外部复验
- 审计与执行分成两个动作,脚本也分两个模式(审计只读,
--close还需--confirm)。 - 确认时逐端口问「谁在用」,不要一次「全关」。典型踩坑:CI 从外网调数据库 HTTP 口灌数、
监控探针调采集口、同事浏览器直连调度后台。 - 关闭前查连接来源:
ss -tn state established '( sport = :<端口> )' | head
journalctl --no-pager --since -24h | grep -c ':<端口>'
5.6 docker 端口绕过 firewalld —— 现象、证据与四种应对
现象:容器用 -p 1234:1234 发布,firewalld 明明没放行,外面照样能连。
原因:包进入主机后先走 nat PREROUTING,docker 的规则把目的地址 DNAT 成容器 IP,
之后才进 filter。filter_IN_public 只对「目的是本机地址」的包生效,被 DNAT 的包早已不是本机目的地址,
于是 zone 的 accept/drop 全部轮不到。在 filter 层做白名单对 docker 发布端口天然无效。
实测(docker 25.0.3 / nftables 后端):
| 实验 | 发布方式 | firewalld 是否放行 | 外部探测结果 | 含义 |
|---|---|---|---|---|
| A | -p 0.0.0.0:11112:1234 |
否 | ConnectionRefusedError |
包穿过了防火墙进到容器(容器内无监听才回 RST);若被拦会是 Timeout |
| B | -p 127.0.0.1:11113:1234 |
否 | TimeoutError |
外部根本到不了 |
> 判读要点:Timeout = 被防火墙丢掉;Connection refused = 包已经到达了目标协议栈。
> 看到 refused 就说明「外层防护没生效」,这是识别 docker 绕过的关键信号。
四种应对(按推荐度):
| # | 方案 | 做法 | 评价 |
|---|---|---|---|
| 1 | 发布绑环回(首选) | -p 127.0.0.1:1234:1234;远程用 ssh -L 11234:127.0.0.1:1234 |
根治、零额外组件;实测外部 Timeout、宿主 127.0.0.1 正常 |
| 2 | host 网络 + 应用绑环回 | --net=host + 服务配置 127.0.0.1 |
适用于容器内服务能改配置;没有再套一层 DNAT |
| 3 | DOCKER-USER 链限制源 |
iptables -I DOCKER-USER -p tcp --dport 1234 ! -s <白名单> -j DROP |
在 FORWARD 最前,能拦住已 DNAT 的转发流;⚠️ docker 重启会重建链,需持久化(systemd unit 或 iptables-save) |
| 4 | raw 表在 DNAT 之前丢 |
iptables -t raw -I PREROUTING -p tcp --dport 1234 -s ! <白名单> -j DROP |
唯一在 conntrack 之前生效的位置;同样需持久化 |
| ✗ | 关掉 docker 的 iptables | daemon.json 设 "iptables": false |
不要:容器网络/出网/端口发布连带失效,排障成本极高 |
> 检查清单:docker ps --format '{{.Names}}\t{{.Ports}}' 里凡是以 0.0.0.0: 开头的一律视为
> 「firewalld 管不到」,必须按上面 1~4 处理,别指望 zone 规则。
5.7 收口前先把业务端口收进 nginx 反代
收口的正确姿势不是「关掉就不管」,而是先给它留正规入口(nginx 的 80/443),再关掉直连端口。
否则页面打不开、API 调不通,业务方第二天就来要求重新放行。
典型:调度中心类后台(监听某个业务端口),要能从浏览器访问,但不该暴露那个端口。
# nginx.conf -> server { listen 80; ... }
location = /admin { # 无尾斜杠必须 301,否则静态资源相对路径算错
return 301 /admin/;
}
location /admin/ {
proxy_pass http://127.0.0.1:1234; # ⚠️ 不带 URI 部分,保留 /admin 前缀
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s; # 后台常有长请求(触发任务、拉日志)
}
三条硬规矩:
| 规矩 | 原因 |
|---|---|
proxy_pass 不带 URI 部分(写 http://127.0.0.1:1234,不要 .../admin) |
带 URI 时 nginx 会用 URI 替换掉匹配到的 location 前缀;而 Spring 应用自身的 context-path 已是 /admin,会变成 /admin/admin/... |
单独写 location = /admin 做 301 |
少了尾斜杠时首页能开、但静态资源 404 |
| 后台里配的地址一律改成 nginx 入口 | 执行器地址 / 回调地址若还写 http://host:1234/...,收口后回调直接失败 |
验证(从外部、不经直连):
curl -sI http://<host>/admin # 302 → /admin/
curl -sI http://<host>/admin/ # 302 → 登录页
curl -s http://<host>/admin/login | head -3 # 应返回登录页 HTML
🔴 改 nginx 前必须 nginx -t,但它常常「本来就失败」。实测报
cannot load certificate "..."(有人在配 443,证书还没放上去)。处置顺序:
- 先修前提(补证书,或把
listen 443 ssl的 server 块注释掉)→ 再-t; -t通过后才-s reload;-t不通过绝不能 reload(reload 会加载失败,旧 worker 还在跑,
你无法确认新配置到底生效没有);- nginx 可能是手工启动(源码安装、无 systemd 单元)——
systemctl is-active nginx会说inactive,别被误导;reload 用<nginx前缀>/sbin/nginx -s reload。
反代确认可用后再收口直连端口。若该端口落在端口段放行内(如 8080-8090/tcp),段是摘不掉的,
用 priority=-5 的 drop 单独挡住它即可(drop 不受段放行影响,段内其他端口不受影响)。
5.8 远程访问改用 SSH 隧道
ssh -N -L 11234:127.0.0.1:1234 <目标主机> # 单端口
ssh -N -L 11234:127.0.0.1:1234 -L 15678:127.0.0.1:5678 <目标主机> # 多端口
ssh -N -J <跳板> -L 11234:127.0.0.1:1234 <user>@<目标> # 走跳板
# 之后本地访问 127.0.0.1:11234 即为远端 1234
前置:sshd AllowTcpForwarding local(重装系统默认常为 no,报 ChannelException: Administratively prohibited 就是它);
GatewayPorts no 保持默认,别把隧道对外开。
5.9 执行(按档位二选一)
② 白名单档(主力,等同 SSH 的规则结构):
firewall-cmd --list-ports # 记下原始状态
firewall-cmd --remove-port=<端口>/tcp # 先摘掉「对全网放行」的声明
for ip in <白名单1> <白名单2> <白名单3> <自身IP>; do
firewall-cmd --add-rich-rule="rule family=\"ipv4\" priority=-10 source address=\"$ip/32\" port port=\"<端口>\" protocol=\"tcp\" accept"
done
firewall-cmd --add-rich-rule="rule family=\"ipv4\" priority=-5 port port=\"<端口>\" protocol=\"tcp\" drop"
firewall-cmd --add-rich-rule="rule family=\"ipv6\" priority=-5 port port=\"<端口>\" protocol=\"tcp\" drop"
firewall-cmd --runtime-to-permanent # 验完再固化
③ 仅环回档:连 firewalld 都可以不加——把服务绑 127.0.0.1 才是根治(§5.4)。若要防「配置被改回去」,
再补一条 drop 兜底即可(此时白名单 accept 没有意义,因为进程不听公网地址)。
> 为什么摘了 --add-port 还要加 drop?前者只是「不再放行」,后者是不受其他放行影响的一道独立护栏
> (防手抖、防别的 zone/service/端口段再放开)。端口段放行(如 8080-8090/tcp)尤其只能靠 drop 单独挡。
> 但对 docker 发布端口这套无效,必须按 §5.6 处理。
5.10 ②+③ 叠加 = 双重保护(可选,取决于要不要让白名单机器直连)
同一端口可以同时吃两道防线:firewalld 只放白名单 + 服务只听 127.0.0.1。
叠加后的实测特征:
| 来源 | 结果 | 原因 |
|---|---|---|
| 白名单内 | Connection refused |
防火墙放行了,但服务在公网地址上没监听 → 内核回 RST |
| 白名单外 | Timeout |
被 firewalld drop |
本机 127.0.0.1 |
正常 | 走 lo,不受 zone 限制 |
怎么选:
| 场景 | 选档 |
|---|---|
| 白名单机器需要直连(灌数、BI 拉数、管理后台) | 只上 ②,服务听 0.0.0.0 |
| 谁都不许直连,只能通过隧道 | ②+③ 叠加(或直接只上 ③) |
> 判读口诀:白名单内 refused = 双重保护生效(服务没听公网),不是配置错了。
6. 公钥免密登录 + 关闭口令登录(带自动回滚守卫)
目标态:PasswordAuthentication no + KbdInteractiveAuthentication no + PermitRootLogin prohibit-password,
只允许公钥进来。爆破在口令层面直接失效(口令不再是一把可猜的钥匙)。
🔴 这是本技能里唯一「关错了就再也进不去」的动作 —— 防火墙关错了还能等 timer 回滚/走控制台,
口令关错了且公钥有问题,远端机器就只能进机房。所以必须同时满足两件事:
- 关之前:所有需要进来的人的公钥都已装好,且每条链路都用
BatchMode实测通过(§6.1 准入清单); - 关的时候:挂一个跑在服务器本地的回滚守卫 —— 窗口内没人成功登录就自动把口令登录打开(§6.3)。
> 顺序不可颠倒:先配免密实测通过 → 再关口令。反了就是自锁。
6.1 前置:免密公钥部署与准入清单
bash scripts/ssh-key-deploy.sh --host <主机A> --user root # 预演:看将做什么 + 现有指纹
bash scripts/ssh-key-deploy.sh --host <主机A> --user root --apply # 执行:备份 → 追加 → BatchMode 实测
为什么不用 ssh-copy-id(踩过):目标机常带 MaxAuthTries 3(§6.4 抗爆破),
而来源机往往有多把私钥(id_rsa + id_ed25519 + agent),ssh 握手会先试无效的钥匙,
3 次即被踢 → 报 Too many authentication failures。看着像密码错,其实是认证次数被卡住。
对策:直接从已通的链路 append 公钥,或强制 -o IdentitiesOnly=yes -i <指定私钥>。
关口令前的准入清单(逐条确认,缺一不可):
| # | 检查 | 命令 |
|---|---|---|
| ① | 目标机全部授权指纹都认识 | ssh <host> 'ssh-keygen -lf ~/.ssh/authorized_keys' |
| ② | 每条链路都 BatchMode 实测通过 | ssh -o BatchMode=yes <host> hostname(本机 / 跳板 / CI 各来一次) |
| ③ | 所有可登录账号都覆盖 | ssh <host> "awk -F: '\$7!~/(nologin\|false)/{print \$1}' /etc/passwd" |
| ④ | 防火墙白名单含你的出口 IP | firewall-cmd --list-rich-rules \| grep <SSH端口> |
- 🔴
authorized_keys里他人的钥匙别动(同事、备份机的)。逐条对指纹,删的只有拼错的无效行。 - ⚠️
authorized_keys是「每账号一份」:非 root 账号有自己的
/home/<user>/.ssh/authorized_keys,只顾 root 会让该账号直接失联。
但也要注意:那份钥匙的私钥可能就在目标机自己身上(自生成),外部拿不到 = 等于没配。 - ⚠️ 出口 IP 一变全体进不去:白名单只有
/32时,办公网换出口即全体失联,确认有控制台/VNC 兜底。
6.2 关闭动作
bash scripts/nopasswd-login.sh # 预演
bash scripts/nopasswd-login.sh --apply --window 300 # 执行:备份 → 先起守卫 → 写 drop-in → reload
写下的 drop-in(/etc/ssh/sshd_config.d/99-no-password.conf):
PasswordAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin prohibit-password
- drop-in 优先,不改主文件:现代发行版的
sshd_config多有Include /etc/ssh/sshd_config.d/*.conf,
且若无Match块 → 一条 drop-in 全局覆盖。先确认这两个前提(脚本会自动探测);
无Include的老机器脚本会自动降级为改主文件 + 注释原位yes。 PermitRootLogin生效值可能显示without-password—— 那是prohibit-password的等价写法,非笔误。- 只
reload不restart:不中断已建立的会话(当前会话是你最后的退路,别关)。 - 改完必看生效值,不看文件:
sshd -T | grep -iE '^passwordauthentication'。
只改文件不够 —— 主文件里残留的PasswordAuthentication yes会抢先命中。
6.3 回滚守卫(本技能的核心设计)
关之前 ──▶ systemd-run 起守卫(transient 单元,跑在服务器本地,脱离一切 SSH 会话)
│
└─ ① 备份 sshd 配置 ② 【先】起守卫 ③ 再写 drop-in 并 reload
│
N 分钟窗口后 ◀──────────────────────────────────┘
│
<marker> 在窗口内被创建?
├─ 是 → verdict=keep ,保持关闭(= 固化成功)
└─ 否 → verdict=rollback,自动删 drop-in + reload → 口令登录恢复
| 要点 | 说明 |
|---|---|
| 守卫必须跑在服务器本地 | systemd-run --unit=... --collect(/run/systemd/transient/)。发起方(agent/SSH)断开也照样判定 —— 否则「免密坏了」时发起方自己也连不上,守卫形同虚设 |
| 守卫先于关闭启动 | 顺序是「起守卫 → 再关」。反过来若关闭后守卫没起来,就是无守卫裸奔 |
| 成功判据只有一个 | <marker>(默认 /root/i_am_here)在窗口内被新建(mtime ≥ 起点) |
| 窗口内人工动作 | 从另一条链路免密登录并 touch /root/i_am_here(这就是「证明免密可用」的凭据) |
| 幂等 | 已在「关闭+守卫运行中」时重复执行直接短路(避免抹掉标记、重启窗口) |
🔴 绝对不要用「sshd 日志出现 Accepted publickey」当判据 —— 这是实测踩到的致命缺陷:
关闭动作本身就是经 ssh 触发的,那次登录必然落在窗口内,判据永远命中 → 守卫永远判 keep、永不回滚。
(实测:窗口内明明只有发起关闭的 1 次连接,计数 =1,守卫却判了 keep。)
> 通用教训:兜底判据若与「触发动作自身」耦合,必须剔除,否则守卫等于没装。
6.4 验证(关口令后必做)
| # | 验证 | 命令 / 期望 |
|---|---|---|
| ① | 生效值已关 | sshd -T \| grep -iE '^passwordauthentication' → no |
| ② | 免密仍可用 | ssh -o BatchMode=yes <host> hostname → 成功 |
| ③ | 口令确被拒 | ssh -o PreferredAuthentications=password -o PubkeyAuthentication=no <host> true → Permission denied,且拒绝列表里已无 password |
| ④ | 守卫判定 | cat /root/sshd-nopasswd/$(cat /root/sshd-nopasswd/latest)/verdict → verdict=keep |
| ⑤ | 无残留 | drop-in 仍在(keep)或已删(rollback);sshd -t OK |
手工回滚(任何时候):rm -f /etc/ssh/sshd_config.d/99-no-password.conf && systemctl reload sshd
6.5 定时执行(可选)
想「约定时间自动关」,别用外部调度器(agents 掉线就没人做窗口内登录):把关闭动作交给服务器本地 systemd timer。
# /etc/systemd/system/sshd-nopasswd-close.service
[Service]
Type=oneshot
ExecStart=/root/bin/nopasswd-login.sh --apply --window 300
RemainAfterExit=yes
# /etc/systemd/system/sshd-nopasswd-close.timer
[Timer]
OnCalendar=YYYY-MM-DD HH:MM:SS
Persistent=false
AccuracySec=1s
Unit=sshd-nopasswd-close.service
OnCalendar到点即触发,不依赖发起方在线;守卫同样跑在本地,闭环自洽。- ⚠️ 窗口内仍需有人登录建标记 —— timer 不会替你登录。若完全无人值守,
守卫会在窗口后回滚(这是设计的正确行为:没证据就认为免密不可靠)。 - 取消:
systemctl disable --now sshd-nopasswd-close.timer。
7. 验证(改完必验)
| # | 验证 | 命令 / 期望 |
|---|---|---|
| ① | 入口端口仍通 | 80/443 站点正常;SSH 从白名单主机新开连接可登录 |
| ② | 档位效果符合预期 | 白名单档:白名单内 OPEN、白名单外 Timeout;仅环回档:谁都连不上(白名单内为 refused)。出现「该断的却 OPEN」→ 查 docker 发布(§5.6)与端口段放行 |
| ③ | 规则顺序正确 | nft list ruleset \| grep -n <端口>:accept 行在 drop 行之前 |
| ④ | 无网段/全开残留 | --list-rich-rules \| grep /25 无输出;--list-ports 里不该再有该端口 |
| ⑤ | 隧道能干活 | ssh -N -L 后本地 curl 127.0.0.1:<本地端口> 与原来响应一致 |
| ⑥ | 登录方式 | 若做了 §6:口令被拒、免密可用、守卫 verdict=keep(§6.4) |
| ⑦ | 等保回归 | bash scripts/mlps-l1-check.sh 再跑一遍,差距项对比上一次报告是否收敛;报告首行写明「基线:等保一级(默认)」 |
> ①②③④ 证明「关掉了」,⑤ 证明「关掉之后还能干活」,⑦ 证明「改完确实更合规了」——
> 只做前四项,第二天业务就会来找你;漏掉 ⑦,检查时还是拿不出证据。
8. 锁死后自救
- 永远先挂自动回滚:
systemd-run --on-active=600 --unit=<名> firewall-cmd --reload(§6 的守卫同类思路)。 - 别用
--reload主动应用改动:它会 flush docker 的 nat 链(见 §5.6)。 - 固化后再撤 timer,否则 10 分钟后被打回原形。
- 被锁死的兜底顺序:控制台/VNC → 云商安全组/串口 → 同网段已放行的机器跳进来。
所以放行清单必须包含自己当前出口 IP,最好再加一台对端/跳板。 - 收口前先确认 nginx 真在跑并能反代——把
80关了才发现 nginx 没起,等于全站下线。 - 若锁死原因是关了口令登录(§6):等守卫窗口自动回滚;或从仍持有公钥且在白名单内的机器进来
执行 §6.4 的手工回滚命令。
9. 常见坑速查
| 现象 | 原因 / 处理 |
|---|---|
白名单外探测得到 Connection refused |
包已到达目标协议栈 = 外层没拦住:多半是 docker 0.0.0.0 发布(§5.6),或命中端口段放行 |
白名单内探测得到 Connection refused |
防火墙已放行,是服务自己没监听公网地址(绑了 127.0.0.1)→ 双重保护生效,属预期;要直连就开隧道,别把服务改回 0.0.0.0 |
| 加完白名单,docker 端口仍能连 | DNAT 绕过 filter;只能改 -p 127.0.0.1:(zone 规则无效) |
| 加完白名单,自己也连不上 | 白名单漏了当前出口 IP;等回滚 timer(§8),或走控制台 |
--add-source 后入口端口全断 |
source-based zone 把该源全部流量切走;改同 zone rich rule |
reload 之后数据库端口又对公网开放 |
reload flush 了 docker nat 链;改规则期间禁用 reload |
| 只 drop v4,扫描仍从 v6 进来 | *:端口 是双栈,补 family="ipv6" |
应用明明绑了 0.0.0.0,外部却连不上,怀疑防火墙没生效 |
这是正常的(默认拒绝);确认 --list-ports 里没有它即可,见 §5.2 |
改了 ClickHouse listen_host 没生效 |
listen_host 全局生效,多个端口共用,改完要重启 |
systemctl is-active nginx 说 inactive,站点却是通的 |
nginx 是手工启动(源码安装),没接 systemd;查 ps -ef \| grep nginx |
| 收口后调度后台打不开 | 应走 nginx 反代,别直连应用端口(§5.7) |
| 收口后执行器掉线 | 执行器主动注册(出向),入向收口不影响;查注册地址配置 |
rich rule 里 priority 报错 |
firewalld < 0.6 不支持 |
| 审计说「未放行」,外面却连得上 | 大概率是 端口段放行(如 8080-8090/tcp):只匹配精确端口会漏判。用 public-ports-guard.sh(已支持范围)或人工展开段内每个监听端口 |
| 收口报告成功,但外部仍 OPEN | 该端口在端口段放行里,--remove-port=1234/tcp 无效(段不是精确端口)→ 必须加 priority=-5 的 drop 单独挡 |
| nginx 反代后页面 404 / 资源加载失败 | proxy_pass 带了 URI 部分把前缀替换掉了;改成只写 http://127.0.0.1:<端口>,并补 location = /path 的 301 |
nginx -t 报 cannot load certificate |
证书没放上去(有人在配 443)→ 先补证书或注释掉 ssl server 块,-t 不过绝不 reload |
firewall-cmd --permanent --list-* 夹 --zone=docker 警告 |
只是提示(默认 zone 与活动 zone 不一致);显式加 --zone= 可静音 |
ssh-copy-id 报 Too many authentication failures |
目标机 MaxAuthTries 小 + 来源机私钥多,握手先试无效钥匙被踢 → 用 -o IdentitiesOnly=yes -i,或直接从已通链路 append(§6.1) |
| 关口令后仍能用口令登录 | 主文件里的 PasswordAuthentication yes 抢先命中(drop-in 没生效)→ sshd -T \| grep passwordauthentication 看生效值,别只看文件 |
| 改了 drop-in 却「什么都没变」 | sshd_config 没有 Include sshd_config.d/*.conf,或存在 Match 块把它挡在外面 → 先确认这两个前提(§6.2) |
| 守卫从不回滚(永远 keep) | 判据与「触发动作自身」耦合(用了 sshd 日志的 Accepted publickey)→ 唯一判据必须是标记文件(§6.3) |
| 窗口内忘了建标记,被回滚了 | 这是预期行为(无证据即认为免密不可靠)→ 重新执行并确保窗口内登录(§6.3) |
| 自查表全「符合」,一复现就露 | 把「没查」当「符合」写的 → 每条必须有命令输出;一级允许「部分符合 + 待整改」,别凑表(§2.4) |
| 拿「防火墙开了」当等保边界访问控制符合 | 默认拒绝 + 五元组 + 删冗余规则才是判据;--list-ports 里挂着宽放行就不算(§2.2) |
| 「有备份」但说不清能不能恢复 | 备份任务只证明「跑了」;要有一条恢复演练记录才算数据备份恢复符合 |
| 用户说「按等保检查」却自己升成三级 | 默认一级;用户没明确指定就不要加码,等级以用户/备案材料为准(§2.1) |
| 管理类条目被当成「不适用」跳过 | 制度/机构/人员/建设/运维是待人工确认,不是不适用;把清单交给负责人落实(§2.4) |
10. 落地记录(模板)
> ⚠️ 本技能不记录任何具体环境信息(真实 IP / 端口 / 主机名 / 时间 / 业务名)。
> 每台机器的实际执行状态,写进你自己的运维报告。下面这份表是报告里该怎么记的格式:
| 项 | 记什么 |
|---|---|
| 目标机 | 标识(主机名 / 编号 / 云实例 ID) |
| 网络 | 是否有私网地址;公网是否直配网卡;IPv6 是否可用 |
| 白名单 | 放行了哪些 /32、各代表什么角色(本机出口 / 跳板 / 对端 / 自身) |
| 备份 | 防火墙与 sshd 配置的备份路径与时间戳 |
| 审计结论 | A/B/C 三层清单(哪些公网可达、哪些待办、哪些已合规),及其逐项处置 |
| 登录方式 | 免密公钥是否全链路实测通过;守卫判定结果;最终 sshd 生效值 |
| 验证 | 逐条打勾:入口仍通 / 档位符合预期 / 规则顺序 / 无残留 / 隧道可用 / 口令被拒 |
| 等保基线 | 本次按哪一级(默认一级);用户是否指定了等级 |
| 等保结论 | 各层面 符合 / 部分符合 / 不符合 / 不适用 / 待人工确认 的条数,以及不符合项与整改建议 |
| 等保证据 | 每条结论对应的命令输出摘要(含主机标识与时间),mlps-l1-check.sh 的报告路径 |
11. 脚本用法
11.1 SSH 端口白名单
bash scripts/ssh-port-whitelist.sh --port 1234 \
--allow <ip,ip,...> # 预演
bash scripts/ssh-port-whitelist.sh --port 1234 --allow <ip,ip,...> --apply
firewall-cmd --runtime-to-permanent && systemctl stop sshwl-rollback.timer
11.2 暴露面审计 / 档位管控
# ① 审计(只读,任何时候都能跑):A/B/C 三层清单 + docker 告警 + 端口段放行告警
bash scripts/public-ports-guard.sh --keep 1234,80,443
# ② 白名单档(推荐):这些端口只放列出的源(缺 --confirm 会拒绝)
bash scripts/public-ports-guard.sh --keep 1234,80,443 \
--open 1235,1236,1237 \
--allow <ip,ip,...> --confirm
# ③ 全禁档(该端口确实无人远程用)
bash scripts/public-ports-guard.sh --keep 1234,80,443 --close 5678 --confirm
# ④ 验完固化
firewall-cmd --runtime-to-permanent && systemctl stop portguard-rollback.timer
脚本刻意不做:不改服务配置(绑环回要人手动改 + 重启)、不 --reload、不自动固化、
不在没有 --confirm 时改动任何端口。审计里会显式标出「docker 0.0.0.0 发布」和
「端口段放行」两类高危项——前者 firewalld 关不掉,后者人工审计最容易漏。
11.3 免密登录 / 关闭口令登录
# ① 部署公钥免密(先预演看现有指纹,再执行)
bash scripts/ssh-key-deploy.sh --host <主机A> --user root
bash scripts/ssh-key-deploy.sh --host <主机A> --user root --apply
# ② 关闭口令登录(带 N 秒自动回滚守卫;守卫跑在目标机本地)
bash scripts/nopasswd-login.sh
bash scripts/nopasswd-login.sh --apply --window 300
# ③ 窗口内:从另一条链路免密登录并建标记(守卫认这个凭据)
ssh <主机A> 'touch /root/i_am_here; ls -l /root/i_am_here'
# ④ 窗口后:看判定
cat /root/sshd-nopasswd/$(cat /root/sshd-nopasswd/latest)/verdict # keep / rollback
# ⑤ 手工回滚(任何时候)
rm -f /etc/ssh/sshd_config.d/99-no-password.conf && systemctl reload sshd
nopasswd-guard.sh 是守卫本体,由 nopasswd-login.sh 自动装到目标机 /root/bin/ 并由 systemd-run 拉起,
一般不需要手工调用(手工调用可用于测试回滚分支)。
11.4 等保自查(只读)
# ① 默认:按等保一级跑完全部主机可验证项,输出结论 + 证据 + 汇总
bash scripts/mlps-l1-check.sh
# ② 二级:一级全量 + 二级增量项(L2-01…L2-17),两套分数分开算
bash scripts/mlps-l1-check.sh --level 2
# ③ 除终端输出外,把报告另存(建议放运维档案,和 §10 的落地记录放一起)
bash scripts/mlps-l1-check.sh --level 2 --report /root/audit/mlps-l2-$(date +%F).txt
# ④ CI / 巡检用:出现「不符合」时退出码 2
bash scripts/mlps-l1-check.sh --strict
脚本行为约定:
| 项 | 说明 |
|---|---|
| 只读 | 不改配置、不改防火墙、不重启任何服务;只跑查询命令 |
| 分级 | 默认一级;--level 2 真正追加二级增量项(口令策略/失败锁定/会话超时/审计范围与留存/分权/备份演练/补丁与漏洞/变更管理)并单独计分;--level 3/4 只列出尚未覆盖的更高等级增量,不假装查过 |
| 非 root | 需要读 /etc/shadow 等文件的条目会标「待人工确认(需 root)」,不会报成不符合 |
| 不替人签字 | 文档/访谈/现场类条目一律 待人工确认,脚本不判符合 |
| 退出码 | 0 正常(含存在不符合);--strict 且有不符项(含二级增量项)时 2;参数/环境错误 1 |
> 报告首行会打印基线等级、主机标识与时间——这三样是等保证据的必备项,别删。
> 二级材料还要补 references/mlps-l2-delta.md §4 的「二级增量结论 / 备案与测评状态 / 差距与整改计划」三节。
> 该脚本只覆盖等保;网络暴露面与登录方式仍走 §11.1~§11.3。