2.3 HTTPS 与证书:全站加密
场景:证书过期十分钟线上白屏
某天晚上八点,运营在群里发了一张截图:小程序打开后一片空白。我第一反应是后端挂了,上 CVM 一看进程好好的,接口用命令行也能通。点开详情才看到提示域名证书过期,一查临近,证书十七号到期,今天十九号。小程序只认 HTTPS,过期直接不给连。那次故障我们花了二十分钟重新申请证书、十分钟生效,但晚高峰的发布量掉了三成,用户感知强烈。事后算了一下损失,三成发布量按当天的活跃基数折算约两千条帖子,对一个小社区来说是很疼的一周。
说句题外话,故障时最难受的不是技术问题,是用户群里已经开始有人冷嘲热讽。我们后来在公告里学乖了,先认错再修,比闷头修复再发声少挨一半骂。
这次的教训不是证书多难弄,而是没有任何监控盯着它。证书是免费 DV 证书,一年期,到期前腾讯云有邮件提醒,但提醒混在几十封官方邮件里没人看,群聊里也无人转发,等于通知系统存在但等于不存在。事件之后我们做了两件事:全站改用自动续期方案,同时加上证书过期监控,十四五天的时候数字停在十四,大家都当没看见,直到三天档告警炸了群才动手。这个细节说明分档告警也要有升级机制,连续两档未处理应当自动电话值班人。
故障当晚我们还梳理了一遍证书相关的所有入口:Nginx 用的域名证书、小程序后台 request 合法域名配置、CDN 回源证书。三个入口分散在三处,任何一处遗漏都会造成部分用户白屏。梳理清单贴进了运维文档,之后每次证书轮换都按清单逐项打勾,再也没漏过。
顺带一提,白屏的锅差点扣到前端头上。前端同学自查了二十分钟才确认包没问题,这段插曲提示我们:多入口链路的故障定位要有清单,否则团队成员之间容易互相猜疑。
事后我们还算了一笔时间的账:全手工管理证书时,一年要花在申请、部署、核对上的时间大约十二个小时,而自动化脚本只用了半天写、之后零维护。对一个四人的小团队,十二个小时足够做完一个小需求,把运维时间花在刀刃上才是正确选择,而且即使全部自动化,人工巡检这一眼仍有必要留着,机器会坏人会懒,两套机制互相补位。
方案:免费证书加自动续期
方案分两层。接入层是 Nginx 做 HTTPS 终止,证书对小程序足够;内部链路上 CVM 与 MySQL、Redis 走内网,不再单独加密。证书统一在 SSL 证书控制台申请免费 DV 证书,绑域名后走自动续期,Nginx 配置里写好新证书路径,续期后平滑 reload:
关于性能我们也测过一轮。HTTPS 的握手开销在小程序端几乎无感,因为微信客户端本身做了连接复用,真正费的是首次建连。我们的优化手段是开启 http2 的连接复用、调整 ssl_session 缓存到十分钟,接口首包耗时从四百毫秒压到二百毫秒。TLS 版本上我们统计过线上实际分布,留下来做配置依据:
| 协议版本 | 占比 | 处置 |
|---|---|---|
| TLS 1.3 | 约六成 | 优先 |
| TLS 1.2 | 约四成 | 保留 |
| TLS 1.0 及以下 | 不足百分之一 | 直接关闭 |
| 项目 | 选择 | 理由 |
|---|---|---|
| 证书类型 | 免费单域名 DV | 小程序够用 |
| 证书类型补充 | 通配符证书 | 业务多时换 |
| 续期 | 自动续期 | 杜绝过期 |
| HTTPS 终止 | Nginx | 性能好 |
| 内网链路 | 明文内网 | 无需重复加密 |
| 端口 | 仅 443 | 80 跳转 |
| 监控 | 到期告警 | 三档提醒 |
证书轮换的完整流程也固化成了清单,每个环节都有回退预案。整套流程已经成为我们处理任何有到期时间的资源的模板:
| 步骤 | 动作 | 校验 | 回退 |
|---|---|---|---|
| 一 | 申请新证书 | 控制台状态 | 旧证书保留 |
| 二 | 部署到目录 | 文件存在 | 删除新文件 |
| 三 | nginx 配置检查 | nginx -t | 还原配置 |
| 四 | reload 生效 | 抓包验证 | reload 回退 |
| 五 | 更新清单日期 | 通知群广播 | 二次确认 |
配置与代码
Nginx 的关键配置是证书路径、协议版本和跳转。只开 TLS 1.2 和 1.3,老协议直接关掉,小程序本身支持得很好。八十端口全部跳转到 443,避免用户抓到明文链接:
server {
listen 443 ssl http2;
server_name api.xiangyi.example;
ssl_certificate /etc/nginx/ssl/api.crt;
ssl_certificate_key /etc/nginx/ssl/api.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 30s;
proxy_http_version 1.1;
}
}
自动续期的检测脚本每天跑一次,发现三十天内到期就走控制台接口申请并部署:
# check_cert.sh 每天定时执行
END=$(openssl x509 -in /etc/nginx/ssl/api.crt -noout -enddate | cut -d= -f2)
LEFT=$(( ($(date -d "$END" +%s) - $(date +%s)) / 86400 ))
echo "证书剩余 $LEFT 天"
if [ "$LEFT" -lt 30 ]; then
bash /opt/deploy_cert.sh && nginx -t && nginx -s reload
fi
证书健康度还做了一眼可见的巡检脚本,把域名、剩余天数、协议版本一并列出,每天发一次到运维群。人看着数字一天天变小,比任何邮件提醒都有压迫感:
# cert_report.sh 每日巡检
for d in api.xiangyi.example img.xiangyi.example; do
echo "| $d | $(echo | openssl s_client -servername $d -connect $d:443 2>/dev/null \
| openssl x509 -noout -enddate | cut -d= -f2) |"
done
踩坑
第一个坑就是上文的白屏事件,根因是无监控。第二个坑是证书路径权限,nginx 用户读不到 key 文件,reload 静默失败而进程还在用旧证书,直到完全过期才暴露,后来把权限收紧成 root 拥有、nginx 组只读。第三个坑是一个页面里嵌了第三方统计域名,那个域名一直是 HTTP,小程序整体校验不通过,部分页面报错,排查了两天,方案是统计域名也上 HTTPS 或者直接摘掉该统计,我们选了后者,顺便把页面体积也降了六十 KB。第四个坑是 http2,某个老版本安卓微信对 http2 的多路复用有兼容问题,偶发请求串音,关掉 http2 后消失,三个月后微信更新了我们才悄悄开回来,开之前还做了两周灰度观察。第五个坑是证书与域名备案的联动,换了域名主体后旧证书仍在用,但备案信息已变更,被管局要求限期整改。第六个坑是多域名通配符成本,业务多了之后单域名证书要申请十几张,运维表格乱成一团,规划是通配符证书加自动续期,管理成本骤降,证书台账从十几张缩到一张,巡检脚本的输出也从此只占一行。第七个坑是 CDN 回源证书,图片域名切到 CDN 后回源走 HTTPS,但源站证书没配,部分请求报错五十,配好回源协议后恢复。第八个坑是 openssl 时间解析的时区问题,脚本里对比的是零时区而告警按北京时间算,早八个小时报过期提醒,虽然是虚惊一场,但说明了任何脚本都要拿真实环境验证一次再上线。
小结
HTTPS 对小程序不是可选项,证书管理的关键词只有一个:自动化。申请、续期、部署、reload、监控,这五个动作全部脚本化之后,这块运维基本就消失了。配上 CLS 的访问日志还能看到协议分布与失败码分布,每周扫一眼就能确认链路健康。顺带一个建议:到期提醒不要只挂在邮件上,企业微信或者告警群里的消息才有人看,我们团队的三十天十四天三档提醒都走群通知。对建站全流程不熟的同学,推荐先看 轻量服务器建站实战,证书与 Nginx 那部分讲得比这里细。内部链路的加密取舍可以再对照 腾讯云综合实践 的内网拓扑章节,确认自家架构哪些段真的不需要重复加密。多花在自动化上的一天,会在某天晚上八点还给你。
