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 那部分讲得比这里细。内部链路的加密取舍可以再对照 腾讯云综合实践 的内网拓扑章节,确认自家架构哪些段真的不需要重复加密。多花在自动化上的一天,会在某天晚上八点还给你。

延伸阅读