关于 aegis-tls-proxy
aegis-tls-proxy 是用于审计 Linux 主机或配置仓库中 TLS 终止反向代理层的只读技能。它检查代理监听与虚拟主机、TLS 和响应头、请求限制、访问控制、上游信任及证书续期。
主要功能
- 01检查监听端口与虚拟主机
- 02核查 TLS 配置和证书
- 03审查安全响应头及继承规则
- 04审查请求限制与超时
- 05检查管理路径与上游信任
- 06审查 ACME 续期配置
适用场景
- 01审计 Linux 反向代理配置
- 02排查 TLS 或证书链问题
- 03检查管理路径是否对外暴露
- 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 终止反向代理层进行只读审计。确定:
- 哪个进程占用 80/443 端口,以及是否泄露版本信息。
- 完整的虚拟主机映射,以及遇到无法识别的 Host 标头时会如何响应。
- TLS 协议下限、密码套件/曲线选择、会话处理、OCSP 装订、证书链完整性、SAN 覆盖范围,以及证书到期自动化。
- 边缘响应标头(HSTS 及相关标头),以及 nginx 的
add_header继承陷阱。 - 边缘的请求大小和超时限制,与应用实际接受的限制之间的差异。
- 通过 CIDR 允许列表、基本身份验证、mTLS 或单独的管理虚拟主机保护管理路径(
/admin、/actuator、/metrics)。 - 代理到应用的链路:传输安全,以及转发标头(
X-Forwarded-For、Host)的信任配置是否正确。 - 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 规则:每个受信任的代理都会追加它所看到的对端地址。正确的推导方式是从右向左遍历列表,恰好跳过你所运维的基础设施代理数量,并在遇到第一个无法归属的值时停止。其左侧的所有内容都是攻击者写入的文本。错误配置风险排序:
- 应用使用整个链或最左侧条目 → 可轻易伪造(绕过 AUTHZ、规避速率限制、投毒日志)。参见 AUTHZ 模块。
- 边缘运行了过于宽泛的
real_ip配置,其中set_real_ip_from范围过大 → 边缘自身会根据伪造输入重写$remote_addr,污染limit_conn/limit_req键和审计日志。 - 正确配置:
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与此流程无关;大小上限应在存储桶策略中检查(属于不同模块)。
参考资料
- Mozilla SSL 配置生成器 — 各服务器及版本对应的标准协议/密码套件配置:https://ssl-config.mozilla.org/(镜像:https://mozilla.github.io/server-side-tls/ssl-config-generator/)
- nginx 文档:https://nginx.org/en/docs/
- RFC 6797 — HTTP 严格传输安全(HSTS)
- RFC 8446 — 传输层安全(TLS)协议 1.3 版
- CWE-16:配置 — https://cwe.mitre.org/data/definitions/16.html
- CWE-319:敏感信息明文传输 — https://cwe.mitre.org/data/definitions/319.html
- OWASP Top 10:2021 A02 – 加密失效 — https://owasp.org/Top10/A02_2021-Cryptographic_Failures/
- Let's Encrypt 速率限制(规划测试环境与生产环境时参考):https://letsencrypt.org/docs/rate-limits/
- Man 手册页:ss(8)、openssl-s_client(1ssl)、htpasswd(1)、curl(1)、certbot(1)(若已打包);许多发行版中的 nginx 附带 nginx(8),否则请参阅上文的 nginx 在线文档。