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 请求直接改写成 https | max-age=31536000; includeSubDomains | 加 preload 前必须确保所有子域都有可用证书 |
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 的正确上法分三步:
- 先发
Content-Security-Policy-Report-Only,加上report-uri/report-to收集违规。 - 观察一到两周,把违规来源(第三方统计、CDN 域名、内联脚本)逐个放进白名单或改成 nonce。
- 确认不再有违规报告后,再换成强制执行的
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/2 | Nginx 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_module,listen 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 一直不生效 | 指向中间证书链文件 |
没配 resolver | stapling 取不到响应,还可能在启动时报错 | 配 resolver 与 resolver_timeout |
沿用旧的 listen ... http2 | 日志刷废弃警告,新特性不一致 | 1.25.1+ 用 http2 on; |
| 只盯控制台不看真实页面 | 混合内容导致样式丢失却以为没事 | 用 grep -o 'http://' 扫一遍页面源码 |
| 改完不备份 | 出问题只能靠记忆改回去 | 改前 cp 一份 .bak,先 nginx -t 再 reload |
小结:加固的顺序是「先加零风险头(nosniff、Referrer-Policy、X-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 实测,并留着上一版配置可以一键切回。