HTTPS 加固:HSTS、协议与套件

HTTPS 只解决了「传输加密」,不解决「怎么保证浏览器以后都用 HTTPS」和「页面里还有没有 http 资源」。加固做的事就三件:用响应头约束浏览器行为、把协议与套件收紧到只留安全算法、把混合内容清干净。改动不大,但每一项都可能有回退成本,所以要按顺序上、每步都验证。

要解决的问题

  • 必须加的安全响应头有哪些,值该怎么写?
  • HSTS 的 preload 为什么说「上了就很难下来」?
  • 怎么禁掉 TLS1.0/1.1,ssl_ciphers 该写哪一份?
  • OCSP Stapling 是什么,怎么确认它生效了?
  • HTTP/2 该用哪种写法配置?
  • 页面里的混合内容怎么快速定位?
  • 加固改完出问题怎么回滚?

一份可以直接用的加固配置

server {
    listen 443 ssl;
    http2 on;                          # nginx 1.25.1+ 写法;旧版写 listen 443 ssl http2;

    ssl_certificate     /etc/nginx/certs/example.com.fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/example.com.key;

    # 协议:只留 1.2 与 1.3
    ssl_protocols             TLSv1.2 TLSv1.3;
    # 套件:ECDHE + AEAD 组合
    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;
    ssl_prefer_server_ciphers off;     # TLS1.3 下交给客户端选更快,只跑 1.2 的老客户端才考虑 on
    ssl_ecdh_curve            X25519:secp256r1;

    # 会话复用
    ssl_session_cache   shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP Stapling:让服务器替客户端去验吊销状态
    ssl_stapling        on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/certs/example.com.chain.pem;
    resolver 223.5.5.5 119.29.29.29 valid=300s;
    resolver_timeout 5s;

    # 必备响应头,always 保证错误响应也带上
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options    "nosniff" always;
    add_header X-Frame-Options           "SAMEORIGIN" always;
    add_header Referrer-Policy           "strict-origin-when-cross-origin" always;
    add_header Content-Security-Policy   "default-src 'self'; img-src 'self' data:; script-src 'self'; style-src 'self' 'unsafe-inline'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
}

# 80 端口统一跳到 443
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}

必备响应头逐条解释

响应头作用推荐值风险与注意
Strict-Transport-Security让浏览器在有效期内把该域名的所有 http 请求直接改写成 httpsmax-age=31536000; includeSubDomainspreload 前必须确保所有子域都有可用证书
X-Content-Type-Options禁止浏览器猜测 MIME 类型nosniff基本无风险,必加
X-Frame-Options防止页面被其他站用 iframe 嵌套(点击劫持)SAMEORIGIN需要被第三方嵌 iframe 时会被误伤
Referrer-Policy控制跳转时 Referer 泄露多少信息strict-origin-when-cross-origin无风险
Content-Security-Policy限制脚本与资源来源,是缓解 XSS 最有效的手段之一从最小集开始,逐步收紧一次写严会把页面脚本全挡住,必须先 Report-Only

HSTS 的 preload 为什么危险:提交到浏览器预加载列表后,HSTS 规则会内置于 Chrome/Edge/Firefox 等浏览器,而列表更新周期以月计,撤销申请也要等同样长的时间。这期间只要某个子域没有证书、或者你想临时退回 http,用户就完全打不开——回退方案是回退不回来的。所以 preload 只适合「全站 https 且所有子域都有证书、业务长期确定」的站点,普通站点用 max-age=31536000; includeSubDomains 就够了。真要回退,只能把 max-age 改成 0 并等所有用户至少访问过一次。

CSP 的正确上法分三步:

  1. 先发 Content-Security-Policy-Report-Only,加上 report-uri/report-to 收集违规。
  2. 观察一到两周,把违规来源(第三方统计、CDN 域名、内联脚本)逐个放进白名单或改成 nonce。
  3. 确认不再有违规报告后,再换成强制执行的 Content-Security-Policy

OCSP 与 HTTP/2

OCSP Stapling 的原理:客户端本来需要额外去 CA 查询「这张证书是否被吊销」,既慢又泄露访问记录;开启后由 Nginx 预先取回带签名的 OCSP 响应并附加在握手里,客户端一次请求就能拿到。

# 确认 OCSP Stapling 是否生效:响应里应出现 OCSP Response Status: successful
openssl s_client -connect example.com:443 -servername example.com -status </dev/null 2>/dev/null | grep -A3 'OCSP Response'

它有几个前提:ssl_trusted_certificate 要指向中间证书链、必须有可用的 resolver(否则 Nginx 无法解析 CA 的 OCSP 地址)、首次请求时可能返回 Staple not stapled(还没取回来,稍等即可)。

特性配置验证
HTTP/2Nginx 1.25.1+:listen 443 ssl; http2 on;;旧版:listen 443 ssl http2;curl -sI --http2 https://example.com/ 看响应行是否为 HTTP/2
HTTP/2 前提必须基于 TLS(或明文 h2c,不推荐)协议协商失败会自动回落到 HTTP/1.1,不报错
HTTP/3 (QUIC)需 1.25+ 与 --with-http_v3_modulelisten 443 quic reuseport; + Alt-Svc一般交给 CDN 做,源站不必开

HTTP/2 的语法差异是最新踩坑点:listen 443 ssl http2; 在 1.25.1 之后会打印废弃警告,虽然还能用,但新配置一律写 http2 on;

混合内容:最容易被忽略的降级

混合内容(Mixed Content)指 https 页面里加载了 http 资源。浏览器会拦截脚本、样式、iframe 等主动混合内容,只对图片做自动升级——表现就是「页面样式全乱、控制台一堆红色报错」。

# 从 HTML 里捞出所有 http:// 引用,快速定位来源
curl -s https://example.com/ | grep -o 'http://[^"'"'"' >]*' | sort -u | head -20

修法按优先级:把引用改成 https://(或协议相对 //)→ 找对应服务是否支持 https → 确实没有 https 的第三方资源就换掉。临时兜底可以用 CSP 的 upgrade-insecure-requests 让浏览器自动把 http 资源升级为 https(仅对同源和允许升级的资源有效),但它不能替代真正的修复。

评分工具与回滚

# SSL Labs 评分(浏览器):https://www.ssllabs.com/ssltest/  目标是 A 或 A+
# 安全响应头评分:https://securityheaders.com/               目标是 A
# 命令行自查(testssl.sh 可离线跑)
git clone --depth 1 https://github.com/drwetter/testssl.sh.git && ./testssl.sh/testssl.sh https://example.com

加固属于「改错就全站不可用」的变更,一定要先备份再改,出问题一键回滚:

sudo cp /etc/nginx/conf.d/example.com.conf{,.bak-$(date +%F)}   # 改前备份
sudo nginx -t && sudo systemctl reload nginx                     # 改后校验并重载
# 回滚
sudo cp /etc/nginx/conf.d/example.com.conf.bak-2025-01-01 /etc/nginx/conf.d/example.com.conf
sudo nginx -t && sudo systemctl reload nginx

常见坑

现象做法
CSP 一次写死页面白屏、脚本全被拦先 Report-Only 观察一到两周
HSTS 加了 preload某个子域没证书,用户完全打不开且无法快速回退只加 max-age + includeSubDomains
ssl_trusted_certificate 指向站点证书OCSP Stapling 一直不生效指向中间证书链文件
没配 resolverstapling 取不到响应,还可能在启动时报错resolverresolver_timeout
沿用旧的 listen ... http2日志刷废弃警告,新特性不一致1.25.1+ 用 http2 on;
只盯控制台不看真实页面混合内容导致样式丢失却以为没事grep -o 'http://' 扫一遍页面源码
改完不备份出问题只能靠记忆改回去改前 cp 一份 .bak,先 nginx -t 再 reload

小结:加固的顺序是「先加零风险头(nosniffReferrer-PolicyX-Frame-Options)→ 再收协议与套件(只留 TLSv1.2 TLSv1.3 + ECDHE/AEAD 套件)→ 再开 OCSP Stapling 与 HTTP/2 →最后才是 HSTS,且不加 preload;CSP 一律从 Report-Only 起步;混合内容用一条 grep 就能定位来源;每一项改完都要用 nginx -t + reload + openssl s_client/curl 实测,并留着上一版配置可以一键切回。

笔记加载中…