mcpField
A
aegis-tls-proxy

aegis-tls-proxy

github.com/as-foss安全16125 次安装9 周前更新
代码仓库

关于 aegis-tls-proxy

aegis-tls-proxy 是用于审计 Linux 主机或配置仓库中 TLS 终止反向代理层的只读技能。它检查代理监听与虚拟主机、TLS 和响应头、请求限制、访问控制、上游信任及证书续期。

主要功能

  1. 01检查监听端口与虚拟主机
  2. 02核查 TLS 配置和证书
  3. 03审查安全响应头及继承规则
  4. 04审查请求限制与超时
  5. 05检查管理路径与上游信任
  6. 06审查 ACME 续期配置

适用场景

  1. 01审计 Linux 反向代理配置
  2. 02排查 TLS 或证书链问题
  3. 03检查管理路径是否对外暴露
  4. 04审查代理到应用的传输与请求头

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/as-foss/aegis-skills --skill aegis-tls-proxy

安装后在 Claude Code 中输入/aegis-tls-proxy确认已加载。

附带文件references/background.mdreferences/further-reading.md

详细介绍

范围与目标

对 Linux 主机或其配置即代码仓库中的 TLS 终止反向代理层进行只读审计。确定:

  1. 哪个进程占用 80/443 端口,以及是否泄露版本信息。
  2. 完整的虚拟主机映射,以及遇到无法识别的 Host 标头时会如何响应。
  3. TLS 协议下限、密码套件/曲线选择、会话处理、OCSP 装订、证书链完整性、SAN 覆盖范围,以及证书到期自动化。
  4. 边缘响应标头(HSTS 及相关标头),以及 nginx 的 add_header 继承陷阱。
  5. 边缘的请求大小和超时限制,与应用实际接受的限制之间的差异。
  6. 通过 CIDR 允许列表、基本身份验证、mTLS 或单独的管理虚拟主机保护管理路径(/admin、/actuator、/metrics)。
  7. 代理到应用的链路:传输安全,以及转发标头(X-Forwarded-For、Host)的信任配置是否正确。
  8. ACME/certbot 运维规范:续期定时器、部署钩子、速率限制和质询路径方面的陷阱。

次要覆盖范围:Caddy 自动 HTTPS 模型、HAProxy 绑定行 TLS 默认值、Apache 的 SSLProtocol/SSLCipherSuite 对应项,以及作为 WAF 选项的 mod_security。

范围之外(相关模块):大流量拒绝服务防护和速率限制调优细节(DOS 模块)、应用授权逻辑(AUTHZ 模块)、主机标头投毒载荷等 HTTP 协议滥用(代码审计 HTTP 协议模块)。「检查内容 / 查看位置 / 利用」中的所有命令都只用于检查,不会修改系统;修改操作只会出现在「修复与验证」中。

前置条件与术语

零基础简介:本模块使用的术语,每项一行。每类术语的详细通俗解释都收录在仓库的 GUIDE.md 词汇表中。

  • 反向代理:接受访客连接并将其转发到应用的前端服务
  • TLS 终止:加密流量解密的位置(此代理)
  • 协议/密码套件下限:仍可接受的最旧加密版本和最低强度;旧版设置会增加降级压力
  • HSTS:告知浏览器此后仅通过 HTTPS 访问该域名的响应标头
  • default_server / catch-all:为未知主机名的请求提供响应的站点;意外的首个匹配项往往出现在这里
  • add_header 继承陷阱:在 location 中声明一个标头,会导致该 location 不再继承所有服务器级标头
  • ACME / certbot:自动签发和续期证书的机制,其质询路径必须保持可访问
  • 污点流 / source→sink:不可信数据从入口点流向危险函数的路径
  • 发现状态:已确认 > 可能 > 需要复核;证据规则见 templates/finding-report.md

心智模型

代理处有两个信任边界交汇:

  • 客户端到边缘:TLS 终止、HSTS、标头策略、大小/时间限制、路径门控。攻击者控制此边界上的一切。
  • 边缘到应用程序:通常使用明文 HTTP。仅当流量不会离开机器时(127.0.0.1、::1、Unix 套接字)才可接受。经过任何网络段传输时,都是明文传输(CWE-319)。

以下结构规则是此处大多数问题的根源:

  • 两次采用先匹配优先。若没有任何 server 块声明 default_server,nginx 会选择第一个匹配的 server 块,因此未命名的兜底配置会悄无声息地将未知 Host 标头路由到最先加载的虚拟主机。ngx_http_access_module 按从上到下的顺序评估 allow/deny,并在首次匹配时停止,因此把 deny all; 放在最前面会使其下方所有 allow 都成为死代码。
  • 继承只会重置一次。nginx add_header 指令只有在当前层级未声明任何 add_header 时,才会从外层级继承。只要在某个 location 内声明一个 add_header,该 location 响应中的所有服务器级标头都会被清除。对指令数组应假定其采用重置语义。
  • 实际发送的证书链不等于存储的证书链。客户端看到的内容,完全取决于代理在握手期间发送的内容,而不是证书目录中的文件。证书链缺失会导致非浏览器客户端报“未知颁发机构”错误,而浏览器却看似正常。
  • 续期与配置相互独立。certbot 会按自己的计时器续期;如果加固配置阻止了 ACME 验证路径,或缺少重载钩子,当下不会出错,但证书会在几周后失效。

检查时请牢记此模型:下方几乎所有问题都违反了这四条规则之一。

检查内容

1. 代理清单与归属

运行 sudo ss -tlnp | grep -E '(:80|:443)\s' 并记录每个监听器的 users:(("name",pid=X,fd=Y)) 所属进程,包括 [::]:443 IPv6 套接字。将每个监听器映射到其配置文件。标记本应限定到特定接口、却绑定到 0.0.0.0 的监听器。

2. 版本泄露(轻微)

运行 curl -sI https://HOST/ | grep -iE '^(server|x-powered-by|x-aspnet)'。Server: nginx/1.24.0 会泄露版本,属于轻微问题;nginx 的修复方式是 server_tokens off;,但响应中仍会保留裸露的 Server: nginx(产品可见,版本隐藏)。若要完全屏蔽,需使用第三方 headers-more 模块——在问题报告中不要声称其他方式可以做到。透传的横幅(如 X-Powered-By)表明缺少 proxy_hide_header。

3. 提取虚拟主机映射

在机器上以 root 身份运行:sudo nginx -T 2>&1 | less。这会转储包含所有 include 的有效配置。注意:需要 root 权限(因为要读取密钥路径和被包含的文件);反映的是磁盘状态,而不是待重载的运行时状态(应将配置文件的修改时间与 systemctl status nginx 的时间戳进行比较);容器化部署中的二进制文件/配置文件位置可能不同。配置即代码的替代方法:使用 grep -RInE '^\s*(server_name|listen|include)' /repo/path 手动重建映射。对于 Caddy,读取 /etc/caddy/Caddyfile;对于 HAProxy,搜索 bind/acl host/use_backend;对于 Apache,运行 grep -RIn '<VirtualHost\|ServerName' /etc/apache2/sites-enabled/。

4. 默认虚拟主机兜底行为

确定隐式默认配置:运行 sudo nginx -T | grep -n 'default_server',并确认哪个 443 server 块在未声明该项时排在最前面。从外部进行只读探测:curl -sk -o /dev/null -w '%{http_code}\n' --resolve probe.invalid:443:TARGET_IP https://probe.invalid/。若返回 200/301/302,意味着无法识别的 Host 标头能到达某个应用程序——这构成 Host 标头攻击面(参见代码审计 http-protocol 模块)。预期应关闭连接(return 444;)或提供明确的兜底桩配置。

5. TLS 协议版本下限、密码套件、曲线和会话设置

检查生效值:sudo nginx -T | grep -nE 'ssl_protocols|ssl_ciphers|ssl_ecdh_curve|ssl_session_tickets|ssl_prefer_server_ciphers'。必须严格设置为 ssl_protocols TLSv1.2 TLSv1.3;。若存在 TLSv1、TLSv1.1 或 SSLv3,则属于问题(如果提供带身份验证的端点,则为高危)。未设置 ssl_protocols 意味着使用依赖于库的默认值——较旧的 OpenSSL 构建会启用 TLSv1.0——因此在进行实时探测前应视为未验证(利用 R6)。密码套件不要手动编写字符串:应使用 Mozilla SSL Configuration Generator 针对已部署 nginx/OpenSSL 组合的中级配置文件所生成的内容;修复方案 F1 中提供了一个已知良好的基线字符串。曲线:预期为 ssl_ecdh_curve X25519:prime256v1:secp384r1;。会话:ssl_session_tickets off; 会牺牲恢复性能,以换取恢复会话的前向保密性——无论选择哪种设置,都要记录下来。

6. 证书链、SAN 覆盖范围、Stapling 和到期自动化

使用利用 R7/R8 中列出的命令进行验证:已提供的证书链长度 ≥2(叶证书 + 中间证书),SAN 覆盖每个已提供服务的 server_name,通过 -status 检查 Stapling 状态,并确认距离到期超过 30 天或存在活动计时器。检查自动化:systemctl list-timers --all | grep -iE 'certbot|acme|dehydrated',并在已安装 Certbot 的情况下运行 sudo certbot certificates。说明:包括 Let's Encrypt 在内的多家大型 CA 从 2025 年起停用了 OCSP 响应器,改用 CRL——如果 CA 已不再提供 OCSP,那么缺少 Stapling 属于低危/信息性问题。

7. HSTS 和边缘响应头

运行 curl -sI https://HOST/ | grep -i strict-transport-security。最低要求:max-age=15552000(180 天)。只有在确认每个子域名都提供有效 HTTPS 后,才添加 includeSubDomains——这实际上是一项承诺。关于预加载的讨论见修复方案/V4。统计出现次数:如果 HSTS 响应头出现多次,则表示边缘层和应用层重复设置,存在冲突;选择其中一层(建议使用边缘层以保持一致),并禁用另一层。在边缘层同时设置:X-Content-Type-Options nosniff、X-Frame-Options DENY(或 CSP frame-ancestors)以及 Referrer-Policy。检查 add_header 继承陷阱(心智模型):每个声明了任何 add_header 的 location 都必须重复所有服务器级响应头——使用 curl -sI 单独测试受影响的路径。

8. 请求限制和超时

将配置值与实际流量需求进行比较:

  • client_max_body_size(nginx 默认值为 1m)必须符合应用可接受的最大有效载荷类型:小型 JSON API 为 1–10m;上传端点只在其自身的 location 中设置明确的更大值,绝不能设为服务器级别。
  • 如果反向代理没有设置上限,却置于接受无上限上传的应用端点前,则属于中危 DoS 问题。
  • 将超时收紧至 client_body_timeout 10s; client_header_timeout 10s; keepalive_timeout 15s; send_timeout 10s; 附近(默认值为 60s/75s),以减少慢速连接耗尽资源的情况——此处仅作概述,完整说明见 DoS 模块。
  • 请求头缓冲区:client_header_buffer_size(默认值为 1k)和 large_client_header_buffers(默认值为 4 8k)会拒绝过大的请求头,并返回 400/414;将其降至 2 8k 可减少每个连接的最坏情况内存占用。
  • 关于 proxy_read_timeout 的说明:绝不要将其设置得低于端点真实的 p99 延迟(报表、导出、流式传输)。先测量最慢的真实端点,再留出余量——否则只是以功能故障(响应被截断、无端返回 504)换取降低 DoS 风险。
9. 管理路径防护

从配置和应用文档中枚举敏感路径:/admin、/actuator、/metrics、/debug、/phpmyadmin、/wp-login。从外部网络执行:for p in admin actuator metrics debug; do printf '%s ' "$p"; curl -s -o /dev/null -w '%{http_code}\n' "https://HOST/$p/"; done。如果互联网可以收到 200/302 响应,则说明存在未设门禁的问题。每条路径的必要最终状态:403(CIDR 拒绝)、401(基本身份验证)或 mTLS 拒绝。凡是存在 allow/deny 规则的地方,都要检查顺序:allow 行必须在 deny all; 之前。除非 optional_no_ca 与 $ssl_client_verify 强制检查配合使用,否则应将其视为装饰项(修复方案 F3)。

10. 代理到应用的跳转

从实际配置中检查:sudo nginx -T | grep -nE 'proxy_pass|grpc_pass|uwsgi_pass'。目标为环回地址(127.0.0.1、::1、unix:)时,明文传输是合理的。通过明文传输到 RFC1918 或可路由目标属于 CWE-319 问题——应通过上游 TLS、mTLS 或 WireGuard 网状网络修复。确认转发标头存在:proxy_set_header Host ...、proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;(或采用有意设计的重写方式——参见污点追踪)、proxy_set_header X-Forwarded-Proto $scheme;。理解 Host 的语义:Host $host 会将客户端提供的值转发到内部(只有在边缘端拒绝未知主机后才安全;参见检查 4);$proxy_host 则发送上游身份,从而隔离应用,但会破坏依赖虚拟主机的路由。使用 proxy_hide_header X-Powered-By; / proxy_hide_header Server; 隐藏后端横幅。

11. 次级代理
  • Caddy:确认存在全局 email 选项(用于接收过期通知);查找任何 tls internal 用法(私有 CA——确认其根证书存放位置以及信任方);自定义标头使用 header { ... } 块结构。
  • HAProxy:检查每一行 bind ... ssl crt ...;要求配置全局 ssl-default-bind-options ssl-min-ver TLSv1.2(HAProxy ≥ 2.2);crt PEM 必须包含拼接在一起的叶证书和中间证书。
  • Apache:要求配置 SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1、从 Mozilla 生成器的 Apache 配置中逐字复制的 SSLCipherSuite,以及 Header always set Strict-Transport-Security "..."。注意 mod_security(SecRuleEngine On)可用作 WAF 层。
12. ACME/certbot 安全规范

检查 systemctl list-timers --all | grep -i certbot 是否显示活动计时器;运行 sudo certbot certificates 检查证书有效期;部署钩子必须存在且可执行:ls -la /etc/letsencrypt/renewal-hooks/deploy/ 中应包含 nginx 重载脚本(修复方案 F2);通过 certbot renew --dry-run 验证功能(只会访问 LE 预发布环境)。检查加固规则中是否存在阻止 /.well-known/acme-challenge/ 的规则:身份验证门禁、deny 规则或会遮蔽挑战路径的 location 级重定向,都会导致临近到期时续期悄然失败。注意速率限制:生产环境 LE 配额按周计算(重复证书和域名上限);流水线必须使用 --staging。

查看位置

证据收集:tools/sweeps/sweep-tls-proxy.sh 会原样捕获 [TLS-nn] 部分;请依据本模块的评分标准进行评判,切勿只依据原始输出。

文件系统(主机):

  • /etc/nginx/nginx.conf — 主配置文件。Debian 会将配置拆分到 /etc/nginx/conf.d/*.conf,以及由 /etc/nginx/sites-enabled/* 符号链接指向的 /etc/nginx/sites-available/*;上游/RHEL 布局则保留在单个文件中,或仅使用 conf.d。
  • 有效配置:sudo nginx -T(磁盘状态;需注意待重载的情况)。
  • 证书:/etc/letsencrypt/live/<name>/fullchain.pem 和 privkey.pem;归档副本位于 ../archive/ 下;续期参数位于 /etc/letsencrypt/renewal/<name>.conf;钩子位于 /etc/letsencrypt/renewal-hooks/{deploy,renewal}/。
  • Caddy:/etc/caddy/Caddyfile;管理 API 默认监听 localhost:2019 — 请确认未对远程开放。
  • HAProxy:/etc/haproxy/haproxy.cfg;证书包位于各个 crt 参数指定的路径。
  • Apache:Debian 使用 /etc/apache2/{sites-enabled,mods-enabled,ports.conf};RHEL 使用 /etc/httpd/{conf,conf.d}/。
  • 定时器/服务:根据实际情况运行 systemctl list-timers --all、systemctl status nginx caddy haproxy apache2 httpd。
  • 软件包版本:运行 dpkg -l | grep -E 'nginx|caddy|haproxy|apache2' 或 rpm -qa | grep -E 'nginx|caddy|haproxy|httpd'。
  • 实时套接字/进程:ss -tlnp、ps aux | grep -E '[n]ginx|[c]addy|[h]aproxy|[h]ttpd|[a]pache'。
  • 访问日志(只读查看):/var/log/nginx/access.log 中 Host 值为外部域名的条目可确认是否存在兜底路由。

远程探测(无需凭据):ExplOitation & Reproduction 中列出的 openssl s_client 和 curl 调用。

仓库扫描(配置即代码):

  • grep -RInE 'ssl_protocols|ssl_ciphers|client_max_body_size|Strict-Transport-Security|set_real_ip_from|optional_no_ca|proxy_pass|reverse_proxy|bind .*ssl' .
  • 重建虚拟主机映射:grep -RInE 'server_name|ServerName|<VirtualHost|listen ' .

模式与特征

查找特征(nginx 指令):

  • ssl_protocols 列出 TLSv1、TLSv1.1 或 SSLv3 → 暴露了过时协议。
  • 任意位置都没有 ssl_protocols → 使用库默认的最低版本,尚未验证;评分前先探测线上服务。
  • ssl_ciphers 包含 RC4、3DES、仅含 CBC 的套件或含糊的别名(HIGH、MEDIUM)→ 使用 Mozilla 生成器重新生成。
  • allow 0.0.0.0/0; 位于 deny all; 之前或替代它 → 无效门禁。
  • deny all; 出现在 allow 规则之前 → allowlist 是死代码(可用性故障:所有人都被阻止)。
  • 使用 ssl_verify_client optional_no_ca;,但未通过 $ssl_client_verify 强制执行 → 身份验证只是表面功夫,并非访问控制。
  • set_real_ip_from 0.0.0.0/0; 或受信任范围覆盖公共互联网 → 客户端 IP 可被伪造。
  • 未通过检查 4 的边缘节点上使用 proxy_set_header Host $host; → 客户端控制的 Host 污染会传入内部;在支持虚拟主机的应用中使用 Host $proxy_host → 路由会无声中断。
  • HTTPS 边缘节点后缺少 proxy_set_header X-Forwarded-Proto $scheme; → 应用生成 http:// URL 或陷入重定向循环(回归特征)。
  • 某个 location 中有 add_header,但其同级 location 中没有 → 该路径上的服务器级响应头会消失(使用 curl -sI 逐路径验证)。
  • 单个响应中出现两个 Strict-Transport-Security 响应头 → 重复设置冲突。

合规特征:

  • ssl_protocols TLSv1.2 TLSv1.3; + 生成器提供的 intermediate 密码套件字符串 + ssl_session_tickets off;。
  • 默认兜底:server { listen 443 ssl default_server; ssl_reject_handshake on; }(nginx ≥ 1.19.4)或 return 444; 存根。
  • 门禁 location 内的顺序:先写 allow <cidr>;,然后写 deny all;,再写 auth_basic。
  • 上游目标为 http://127.0.0.1:<port> 或 unix: 套接字。
  • 仅在单层设置 HSTS,并带有 always 标志。

响应特征:

  • Server: nginx 与 Server: nginx/x.y.z 的区别(信息泄露)。
  • 对超大请求头探测返回 400/414 → 缓冲区限制已启用。
  • POST 请求体超过上限时返回 413 → client_max_body_size 已启用;超大上传没有返回 413,且应用接受了请求 = 发现问题的组合特征。

日志特征:access.log 中反复出现 $host 字段与任何已配置 server_name 都不匹配的行 → 已确认生产环境启用了默认兜底。

污点追踪指南

来源(边缘处由客户端控制):Host 请求头、请求行 / 绝对 URI 形式、X-Forwarded-For、X-Real-IP、Forwarded、Cookie、包含编码的原始路径。

XFF 规则:每个受信任的代理都会追加它所看到的对端地址。正确的推导方式是从右向左遍历列表,恰好跳过你所运维的基础设施代理数量,并在遇到第一个无法归属的值时停止。其左侧的所有内容都是攻击者写入的文本。错误配置风险排序:

  1. 应用使用整个链或最左侧条目 → 可轻易伪造(绕过 AUTHZ、规避速率限制、投毒日志)。参见 AUTHZ 模块。
  2. 边缘运行了过于宽泛的 real_ip 配置,其中 set_real_ip_from 范围过大 → 边缘自身会根据伪造输入重写 $remote_addr,污染 limit_conn/limit_req 键和审计日志。
  3. 正确配置:set_real_ip_from 仅列出你的 CDN/LB 网段;real_ip_header 指定由它们控制的请求头(例如,在 Cloudflare 后方使用 CF-Connecting-IP);设置 real_ip_recursive on;,并配置准确的受信任代理数量。

Host 污点:使用 proxy_set_header Host $host; 时,客户端发送的内容会进入应用路由、URL 生成、缓存键和密码重置链接。应先在边缘进行限制(明确的 server_name 列表,加上拒绝请求的 default_server),之后转发的值才会限定在你拥有的名称范围内。如果边缘无法拒绝未知 Host,则转发 $proxy_host,并让应用根据自身配置的基础 URL 生成对外可见的 URL。

边缘决策之后的下游接收点检查清单:会话 Cookie 的作用域属性、重定向 Location 构造器、Webhook/回调验证器、缓存键,以及使用 XFF 派生地址的管理员 IP 允许列表。

追踪步骤:选取一种入站请求形式(例如,携带伪造 XFF 和陌生 Host 的 POST /reset),从上到下检查配置——匹配了哪个 server、匹配了哪个 location、转发了哪些请求头、应用可能会据此推导出什么——并记录每一处污点未经检查便越过边界的位置。

漏洞利用与复现

所有演示均为只读网络观测。将 HOST 替换为受审计的名称;除非另有说明,否则从任何允许列表 CIDR 范围之外运行。

R1. 监听器归属 sudo ss -tlnp | grep -E '(:80|:443)\s' 解读:users:(("nginx",pid=X,fd=Y)) 可识别终止连接的服务;不同地址上的多个 443 监听器表明存在值得映射的分流入口。

R2. 版本披露 curl -sI https://HOST/ | grep -iE '^(server|x-powered-by|x-aspnet)' 解读:Server: nginx/1.24.0 → 信息披露(低)。仅显示 Server: nginx → 已关闭版本标记。透传的 X-Powered-By → 后端横幅信息泄露。

R3. 配置实际状态 Root:sudo nginx -T 2>&1 | grep -nE 'ssl_protocols|ssl_ciphers|ssl_ecdh_curve|ssl_session_tickets|server_tokens|client_max_body_size|client_body_timeout|keepalive_timeout|large_client_header_buffers|allow |deny |ssl_verify_client|set_real_ip_from|proxy_pass|proxy_set_header|add_header' 注意事项:需要 root 权限;显示的是磁盘状态——如果尚待重新加载,运行时状态会有所不同。 代码库替代方案:grep -RInE '^\s*(listen|server_name|ssl_|add_header|client_|allow|deny|proxy_|set_real_ip_from)' . 解读:根据输出建立 vhost × 指令矩阵;每个空白单元格都表示“未验证”,而非“安全”。

R4. 兜底行为 curl -sk -o /dev/null -w '%{http_code}\n' --resolve probe.invalid:443:TARGET_IP https://probe.invalid/ 解读:200/301/302 → 未知 Host 到达了某个应用(参见 code-audit http-protocol 模块)。curl 退出码为 52 且状态码为 000 → 连接已关闭(return 444; 形式)。通过普通 curl -sk https://probe.invalid/ 观察到任何应用响应正文,都说明路由范围过宽。 R5. 协商的 TLS 基线 echo | openssl s_client -connect HOST:443 -servername HOST -status 2>/dev/null | grep -E '^(New|Cipher|Server Temp Key|OCSP)' 查看字段:

  • New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384 — 协商的协议和密码套件;最低接受 TLSv1.2,任何更旧的版本都会提高严重性。
  • Server Temp Key: X25519, 253 bits — 使用的曲线。
  • OCSP response: no response sent — 未启用 OCSP 装订;OCSP Response Status: successful (0x0) — 已装订。 完整输出还会显示证书链深度,以及相对于本地信任存储的 Verify return code: 0 (ok)。

R6. 强制使用旧协议(尝试举证) curl 形式:curl -svo /dev/null --tlsv1.1 --tls-max 1.1 https://HOST/ ; echo exit=$? 已加固的主机:退出码为 35,消息包含 alert protocol version 或 unsupported protocol。存在漏洞的主机:退出码为 0,且有一行 SSL connection using TLSv1.1 / ...。 客户端注意事项:现代 OpenSSL 客户端构建版本会拒绝提供 TLS ≤1.1(提示 no protocols available)——这种结果无法说明服务端是否存在问题;请改用: echo | openssl s_client -connect HOST:443 -servername HOST -tls1_1 2>&1 | grep -E 'alert|Cipher is|Verify return' 已加固:sslv3 alert handshake failure 或 tlsv1 alert protocol version。存在漏洞:协商出 TLSv1.1 密码套件并返回证书链。使用 -tls1 对 TLSv1.0 重复测试。

R7. 证书链完整性 echo | openssl s_client -connect HOST:443 -servername HOST -showcerts 2>/dev/null | grep -c 'BEGIN CERTIFICATE' 本地交叉检查:grep -c 'BEGIN CERTIFICATE' /etc/letsencrypt/live/<name>/fullchain.pem 查看结果:完整证书链通常包含 2 个证书(叶证书 + 中间证书),偶尔包含 3 个(交叉签名的根证书)。如果服务端恰好只提供 1 个证书,而存储的文件包含 ≥2 个证书,则传输中的证书链不完整。症状:浏览器可以正常访问(使用缓存或通过 AIA 获取中间证书),而 curl/wget/Java/Go/移动客户端会因 unable to get local issuer certificate / certificate signed by unknown authority 而失败——这是经典的“我这里能用”故障。

R8. SAN 覆盖范围和距过期时间 SAN:echo | openssl s_client -connect HOST:443 -servername HOST 2>/dev/null | openssl x509 -noout -ext subjectAltName 过期时间:end=$(echo | openssl s_client -connect HOST:443 -servername HOST 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2); echo "days=$(( ($(date -d "$end" +%s) - $(date +%s)) / 86400 ))" 查看结果:每个提供服务的 server_name 都必须出现在 SAN 中;剩余天数 <30 → 自动化可能有问题(检查 R12)。

R9. HSTS 和响应头集合 curl -sI https://HOST/ | grep -ic strict-transport-security,然后执行 curl -sI https://HOST/ 查看结果:计数 >1 → 重复设置导致冲突。max-age 值低于 15552000 → 强度不足。在公开的 HTTPS 应用中完全缺失 → 中危锚点。确认配套响应头(nosniff、frame policy、referrer-policy)各出现一次。

R10. 管理路径可达性(外部视角) for p in admin actuator metrics debug; do printf '%s ' "$p"; curl -s -o /dev/null -w '%{http_code}\n' "https://HOST/$p/"; done 查看结果:200/302 → 可从互联网直接访问,无访问控制(需记录发现;使用 curl -s 单独捕获有代表性的响应正文,例如 /actuator/health)。401 → 存在身份验证。403 → 存在拒绝访问/CIDR 访问控制。

R11. 跳点传输(根据配置判断) 来源:R3 中的 proxy_pass 行。 查看结果:http://127.0.0.1:* / Unix 套接字可以接受;http://<RFC1918-or-public-ip>:* 在网络段间明文传输 = CWE-319 锚点(在严重性评估中使用 AV:A 向量评分)。

R12. 续期机制 systemctl list-timers --all | grep -iE 'certbot|acme';ls -la /etc/letsencrypt/renewal-hooks/deploy/ 2>/dev/null;sudo certbot certificates 2>/dev/null | grep -E 'Certificate Name|Expiry' 查看结果:证书存在但定时器缺失 = 手动续期机制。部署目录为空且使用 nginx = 续期后的证书会一直未被使用,直到有人手动重新加载(可能导致未来出现静默故障)。Expiry 列中的剩余时间 <30d 可佐证 R8。

修复

针对每项发现分别应用;使用 nginx -t && systemctl reload nginx 验证。配置或证书变更后执行 reload,绝不执行 restart——重启会断开现有连接,且仅用于二进制升级。标记为 # FIXED 的代码片段。

F1. 完整的加固版 nginx 边缘配置(# FIXED)
# FIXED - /etc/nginx/sites-available/example.com

# Unknown-host catch-alls: keep ACME alive, close everything else
server {
    listen 80 default_server;
    listen [::]:80 default_server;
    server_name _;
    location /.well-known/acme-challenge/ { root /var/www/html; }
    location / { return 444; }
}
server {
    listen 443 ssl default_server;      # nginx >= 1.19.4
    ssl_reject_handshake on;            # refuses unknown-SNI handshakes; needs no cert
}

# Named vhost: HTTP -> HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com;
    location /.well-known/acme-challenge/ { root /var/www/html; }
    location / { return 301 https://$host$request_uri; }
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;                           # nginx >= 1.25.1; older builds: append "http2" to listen
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    # Mozilla SSL Configuration Generator, INTERMEDIATE profile baseline.
    # Regenerate for your exact nginx/OpenSSL pair instead of editing blind:
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_ecdh_curve X25519:prime256v1:secp384r1;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # add_header GOTCHA: these apply only because THIS level declares them.
    # Any location below adding its own add_header must REPEAT every line here.
    add_header Strict-Transport-Security "max-age=300" always;   # stage upward, see V4
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    client_max_body_size 10m;           # right-size to the app; tighter caps per location
    client_body_timeout 10s;
    client_header_timeout 10s;
    keepalive_timeout 15s;
    send_timeout 10s;
    large_client_header_buffers 2 8k;
    server_tokens off;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;                    # safe BECAUSE catch-alls reject aliens (F1 block above)
        proxy_set_header X-Forwarded-For $remote_addr;  # edge is sole proxy: rewrite, don't extend
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_connect_timeout 5s;
        proxy_read_timeout 30s;                         # raise only to measured p99 + headroom
        proxy_hide_header X-Powered-By;
        proxy_hide_header Server;
    }
}

Debian 启用方式:ln -s ../sites-available/example.com /etc/nginx/sites-enabled/ && nginx -t && systemctl reload nginx。

F2. certbot 续期部署钩子(# FIXED)
# FIXED - /etc/letsencrypt/renewal-hooks/deploy/10-reload-nginx.sh
#!/bin/sh
set -eu
/usr/sbin/nginx -t
/bin/systemctl reload nginx
logger -t certbot-hook "reloaded nginx for ${RENEWED_LINEAGE:-unknown}"

安装并设为可执行:install -m 0755 <src> /etc/letsencrypt/renewal-hooks/deploy/10-reload-nginx.sh。 reload 与 restart 的区别:reload 会平滑轮换工作进程,不会断开连接,且完全足以处理已替换的证书/密钥——这是常规续期场景。只有在监听套接字、二进制文件或模块集发生变化时,才有理由执行 restart。使用 certbot renew --dry-run 验证整个流程(LE 测试环境;不会消耗生产配额)。

F3. 管理路径防护模式(# FIXED)

CIDR 允许列表 + 基本身份验证层(顺序很重要):

# FIXED
location ^~ /admin/ {
    allow 203.0.113.0/24;     # office/VPN egress
    allow 198.51.100.0/24;    # bastion subnet
    deny all;                 # MUST follow the allows: first match wins
    auth_basic "Administrators";
    auth_basic_user_file /etc/nginx/admin_htpasswd;
    proxy_pass http://127.0.0.1:8000;
}
location = /metrics {
    allow 127.0.0.1;
    deny all;
    proxy_pass http://127.0.0.1:8000;
}

创建 htpasswd:sudo htpasswd -cB /etc/nginx/admin_htpasswd alice(apache2-utils;在 libxcrypt/glibc-crypt 系统上,nginx 支持 -B bcrypt;如需可移植的 APR1,请去掉 -B:sudo htpasswd -c /etc/nginx/admin_htpasswd alice)。添加更多用户时不要使用 -c(它会截断文件)。

专用管理子域上的 mTLS 门控:

# FIXED - admin.example.com server block
server {
    listen 443 ssl;
    server_name admin.example.com;
    ssl_certificate     /etc/letsencrypt/live/admin.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
    ssl_client_certificate /etc/nginx/client-ca.crt;   # YOUR issuing CA only
    ssl_verify_client on;
    ssl_verify_depth 2;
    # ... TLS/header/limit directives as in F1 ...
}

注意 ssl_verify_client optional_no_ca;:该指令会请求客户端证书,并检查证书是否可解析且能自行验证,但会接受来自任意 CA 的证书链——仅适用于日志记录/遥测。绝不要将其用作门控;如果模板中已有此配置,请改为 on,或手动强制执行:if ($ssl_client_verify != SUCCESS) { return 403; }。

另一种方式是使用独立的管理平面:仅将管理虚拟主机绑定到内部地址(listen 10.0.0.5:443 ssl;),这样即使尚未配置防火墙,它也无法从互联网访问。

F4. Caddy 关键配置行
{
    email [email protected]
}
example.com {
    tls [email protected]
    header {
        Strict-Transport-Security "max-age=15552000"
        -Server
    }
    reverse_proxy 127.0.0.1:8000
}

自动 HTTPS 默认情况下大多是安全的(强制重定向、自动管理证书、合理的协议下限)。不过仍应审核残余风险:账户邮箱是否能正常收信、是否存在 tls internal 颁发者(找到并保护私有 CA),以及是否存在自定义请求头配置。

F5. HAProxy 关键配置行
global
    ssl-default-bind-options ssl-min-ver TLSv1.2
frontend https_in
    bind *:443 ssl alpn h2,http/1.1 crt /etc/haproxy/certs/example.com.pem
    default_backend app

crt 捆绑文件必须按先叶证书、再中间证书的顺序拼接,并附上私钥(与 R7 中检查的 fullchain 布局相对应)。

F6. Apache 关键配置行
<VirtualHost *:443>
    SSLEngine on
    SSLCertificateFile      /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile   /etc/letsencrypt/live/example.com/privkey.pem
    SSLProtocol             all -SSLv3 -TLSv1 -TLSv1.1
    SSLHonorCipherOrder     off
    Header always set Strict-Transport-Security "max-age=300"
</VirtualHost>

请直接从 Mozilla 生成器的 Apache 配置文件中逐字复制 SSLCipherSuite,不要自行组合。Debian 模块:a2enmod ssl headers http2。WAF 选项:使用 a2enmod security2 并设置 SecRuleEngine On(mod_security),可在请求到达应用前添加请求过滤。

F7. ACME 挑战路径豁免不变量(# FIXED)

变更任何全局 deny/auth/redirect 规则之前,请确认挑战路径不受影响:

location /.well-known/acme-challenge/ { root /var/www/html; }   # stays PUBLIC

现在屏蔽它不会导致任何故障——但临近到期时续期会悄然失败(心智模型)。优先使用 dns-01 挑战,或通过 F1 中展示的专用兜底配置路由 HTTP-01。

验证与确认

V1. 协议下限(F1/F5/F6 之后) echo | openssl s_client -connect HOST:443 -servername HOST -tls1_1 2>&1 | grep -i alert → 预期出现 handshake failure / protocol version alert;使用 -tls1 重复测试。然后确认现代协议路径正常:echo | openssl s_client -connect HOST:443 -servername HOST 2>/dev/null | grep '^New' → New, TLSv1.3, ...(或 TLSv1.2),并使用强密码套件。

V2. 标头(F1/F4/F6 之后) curl -sI https://HOST/ | grep -i strict-transport-security → 必须恰好有一个携带当前暂定 max-age 的标头。重复项回归检查:curl -sI https://HOST/ | grep -ic strict-transport-security 必须输出 1;大于 1 表示应用也设置了该标头——应在一层移除(建议在边缘层处理)。

V3. 负向/回归测试

  • 网站可正常加载:curl -sS -o /dev/null -w '%{http_code}\n' https://HOST/ → 200(或应用正常的 30x 跳转链,最终以 200 结束)。
  • 从允许的 CIDR 网段进行健康检查:从位于白名单范围内的跳板主机执行:ssh bastion 'curl -s -o /dev/null -w %{http_code} https://HOST/admin/' → 200/401(使用有效凭据的流程正常)。从允许范围内得到 403,说明 deny all; 排在允许规则之前——这是 F3 顺序错误。
  • 上传仍可正常进行:curl -sS -o /dev/null -w '%{http_code}\n' -F [email protected] https://HOST/upload → 不能是 413;然后执行 truncate -s 64M /tmp/opencode/big.bin 并重复测试 → 413 证明上限生效。
  • 未知主机仍被拒绝:重复 R4 → 仍然退出 52/000。

V4. 外部评分工具抽查(仅公共端点):SSL Labs 和 testssl.sh ./testssl.sh HOST:443——目标是 A/A+,且没有协议/密码套件/证书链警告;每次 TLS 变更后都要重新测试。第三方工具仅提供参考性评分:经过审计的配置仍是事实依据。

  • 配置无误:nginx -t → 语法正确,且没有意外警告(新旧 http2 语法的弃用提示无害,但要记录)。

V4. HSTS 分阶段部署(必须按顺序进行) 在确认 HTTPS 稳定之前就提交较长的 max-age,会导致自我锁定:浏览器会在整个有效期内固定该策略,因此覆盖的主机/子域名上任何 HTTPS 部署故障都可能导致无法访问,最长持续整个有效期。分阶段执行:max-age=300(观察 24–48h,监控正常)→ 86400 → 604800 → 15552000。只有在列出所有子域名并确认每个域名的 HTTPS 均有效之后,才添加 includeSubDomains。最后再提交预加载申请——移除流程需要数月。

V5. 续期流程(F2 之后) sudo certbot renew --dry-run → 每个证书链均成功;钩子已触发(检查 journalctl --since today | grep certbot-hook);已安装证书的日期未变(dry run 不会安装任何内容)。

V6. 仓库回归 grep 检查(修复后;命中项必须能解释或已消除)

  • 旧版协议:grep -RInE 'TLSv1(\.[01])?[";[:space:]]' --include='*.conf' . | grep -viE 'TLSv1\.2|TLSv1\.3'
  • 装饰性 mTLS:grep -RIn 'optional_no_ca' .
  • IP 欺骗 real_ip:grep -RInE 'set_real_ip_from[[:space:]]+0\.0\.0\.0/0' .
  • 未设置请求体上限的透传虚拟主机:grep -RL 'client_max_body_size' $(grep -RIl 'proxy_pass' --include='*.conf' .)
  • 目视检查每个访问控制点附近的 allow/deny 顺序:grep -RIn -B3 'deny all;' --include='*.conf' .

V7. 证据留存:将修复前后的 R1–R12 输出保存到审计工单中;差异即为变更证明。

严重性评估

将发现与 CVSS v3.1 向量关联:

HIGH — 在承载身份验证的端点上启用了 TLSv1.0/1.1 和弱加密套件(CBC/3DES/RC4):会使旧版客户端遭受降级与拦截类攻击。CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N(7.4 High)。

MEDIUM — 面向公众的 HTTPS 应用缺少 HSTS:为网络邻近攻击者留下 SSL 剥离窗口;利用此问题需要用户交互。CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:N(4.2 Medium)。

MEDIUM — 无界上传应用端点前未设置有效的请求体大小上限:攻击者可通过大型 POST 请求,以低成本耗尽存储或内存。CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:L(5.3 Medium)。

MEDIUM — 代理到应用的链路以明文跨越网络段(非环回 RFC1918/公网):邻近攻击者可嗅探传输中的凭据、令牌和会话 Cookie。CVSS:3.1/AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N(6.4 Medium)。

LOW — 泄露版本信息(Server: nginx/x.y.z、X-Powered-By):仅有助于攻击者实施攻击;还需要存在相匹配的 n-day 漏洞。CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N(3.6 Low)。

LOW — 缺少 OCSP 装订:浏览期间会泄露轻微的隐私信号;随着证书颁发机构逐步弃用 OCSP 响应器、改用 CRL,此问题的影响日渐有限。CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N(3.6 Low)。

调整因素:如果管理路径可从互联网访问且无需身份验证,则将应用本身相关发现的严重性提高一个等级(缺少访问门控会加剧其后所有问题)。证书过期或证书链损坏且无自动化处理时,除机密性影响外,还应计为可用性事件。

常见误报

  • CDN 在源站前终止 TLS(Cloudflare/Akamai/CloudFront/ALB):你的 s_client 看到的是边缘节点的证书和配置,而不是源站的。应将发现归属于实际负责的层。特殊情况 — Cloudflare 的 “Flexible” SSL 模式:访客到边缘节点使用 HTTPS,但边缘节点到源站使用明文 :80;如果源站在端口 80 上响应应用请求,那么源站暴露本身属于单独一类发现(要求使用 Full/Full-strict 模式,并让源站拒绝 :80)。可通过 DNS/基础设施文档解析源站 IP,并在 CDN 网段之外直接探测来检测。
  • 云 ALB 使用提供商托管的策略名称终止 TLS:这里并不存在 nginx 专用指令;应根据提供商文档审计负载均衡器的安全策略名称,而不要因为缺少 ssl_protocols 就判定主机不合规。
  • 仅供内部使用的代理(网格 Sidecar、VPC 内入口)采用较宽松的装订或日志记录设置:仍采用相同的协议最低要求,但如果影响范围仅限一个信任区,则应降低严重性权重;记录范围假设,而不是默默判定通过。
  • 有意开放的测试/预发布虚拟主机(种子数据、虚假登录):以书面形式确认所有权,附上审查/到期日期,并从评分中排除 — 切勿将其标记为“合规”。
  • 网站有意仅使用 HTTP(完全没有 TLS):不适用“HTTPS 缺少 HSTS”这一基准;TLS 缺失本身属于更广泛的密码安全状况(按相应流程处理)。
  • 签发证书的 CA 已停用其响应器(例如,Let's Encrypt 在 2025 年逐步停用 OCSP 响应器)时,“缺少” OCSP 装订仅供参考,无需采取措施。
  • 通过预签名 URL 直接上传到对象存储、绕过代理:缺少 client_max_body_size 与此流程无关;大小上限应在存储桶策略中检查(属于不同模块)。

参考资料

评论

支持 Markdown