我凌晨一点多在群里看到一条消息,某公司一个小程序后台接口集体报错,用户端页面全打不开。值班的人第一反应是服务器挂了,结果端口通、进程在、日志干干净净,最后用一条命令定位了问题——证书已经过期两天了。这种事故我这些年见过太多次,而且绝大多数和“技术不会修”没关系,就是单纯忘了SSL证书有有效期。今天借这个标题,把证书更新的完整链路、原理、格式坑和自动续期方案一次讲清楚,也当给自己留一份备忘。
1. 一张过期的证书,到底能坑多少人
1.1 客户端报错里其实写着答案
证书过期这件事,最直观的表现是浏览器或者客户端直接拒连。拿Chrome举例,地址栏不会变成红色,而是直接给你一整页错误提示,错误码是NET::ERR_CERT_DATE_INVALID。很多人一看“DATE”就懵了,以为服务器时间不对,其实这个报错的意思是“证书的有效期已经过了”。
在命令行里测试更直接。用 curl 访问一个证书过期的站点,结果大概长这样:
$ curl -vI https://expired.example.com ... * SSL certificate problem: certificate has expired * closing connection 0 curl: (60) SSL certificate problem: certificate has expired注意这个退出码 60,它是 curl 对“证书验证失败”的统一返回码。再用 openssl 手工做一次 TLS 握手,输出里会有一行:
Verify return code: 10 (certificate has expired)这个 return code 10 就是证书过期的标准标记。所以判断证书是不是过期,不需要等浏览器渲染页面,openssl 一次握手就能得到明确结论。
1.2 影响范围远比你想的广
网页打不开只是最小的影响面。真正麻烦的是那些“没有浏览器界面”的东西:手机 App 内置的接口、小程序服务端、企业内网的老系统、CI/CD 部署脚本、IoT 设备上报链路、VSFTPD 这类 FTP over TLS 服务、还有各种自研客户端。它们内部用的 TLS 库验证方式更严格,证书一过期,直接抛异常,而且很多客户端异常信息写得很含糊,比如“handshake failed”“connection reset”“SSL error”,根本不会告诉你“证书过期了”。
我甚至见过因为一张证书过期,整个公司的自动化报表链路停了两天的情况。报表脚本每天凌晨跑一次,某天开始全部失败,运维查了数据库、查了网络、查了权限,最后才发现是调用外部接口的 HTTPS 证书过期了。这种场景最要命的地方在于:服务端看起来一切正常,日志也没有明显报错,你根本不会第一时间往证书方向去想。
1.3 别把排查方向带偏
证书相关报错还有几个容易混淆的变体,排查时需要区分清楚:
| 现象 | 可能原因 |
|---|---|
| 浏览器提示“证书过期” | 证书真的过期,或本地系统时间超前 |
| 浏览器提示“证书尚未生效” | 服务器时间比真实时间慢,或本地时间落后 |
| curl 报证书验证失败,但日期看着正常 | 中间证书没配全,证书链不完整 |
| 个别老设备/老客户端打不开 | 证书签名算法或 TLS 版本不兼容 |
| 服务器时间飘了,openssl 校验不过 | NTP 没配好,系统时钟偏差超过有效期边界 |
这里最有迷惑性的就是时间同步。如果你的服务器时间比真实时间慢了几天,而证书恰好还剩两三天才到期,客户端会认为证书“尚未生效”(valid from 还没到),同样报出 DATE 相关错误。我曾经排过一个故障,最后发现是服务器 CMOS 电池没电,每次重启时间都回退,证书永远“还没到生效时间”。所以排查证书问题时,第一件事除了看证书日期,还要顺手看一眼服务器系统时间和 NTP 同步状态。
2. 读懂有效期机制,才知道为什么现在“更新频繁”
2.1 证书里到底存了什么
很多人以为 SSL 证书就是个“证明我网站安全的文件”,这理解有点偏差。实际上一张 X.509 证书里存的是一组结构化信息:绑定的域名、公钥、签发机构(CA)、有效期起止时间,以及 CA 对整段内容做的数字签名。
浏览器或客户端在验证证书时,做的事可以简化成两步:第一步,用 CA 的公钥验证证书上的数字签名是否有效,确认这张证书确实是那个 CA 签发的;第二步,把系统当前时间和证书里的 notBefore、notAfter 两个时间戳做比较,确认当前时间落在有效期内。两步都通过,才算建立信任。
所以证书的有效期不是“一个建议”,而是信任验证的一个硬性输入。你把一张 2023 年过期的证书放到 2025 年,无论签名多完美、CA 多权威,客户端都会直接拒掉。这有点像小区门禁卡,卡是真的,门禁系统也认这个发卡机构,但卡本身的授权期限过了,刷上去就是进不了门。
2.2 有效期为什么越缩越短
前些年 SSL 证书有效期还很长,三年、五年都不稀奇。后来整个 CA/B 论坛(CA/Browser Forum,全球 CA 机构和浏览器厂商的联合组织)一直在收缩有效期上限,我记得比较清楚的时间线:
- 早期证书最长可达 8 年甚至更长;
- 后来缩减到 5 年;
- 2018 年前后缩到 825 天(约 27 个月);
- 2020 年 9 月之后签发的证书,有效期上限是 398 天(约 13 个月)。
现在 Chrome、Apple 等还在推动把 TLS 证书有效期进一步压到 90 天。背后的逻辑不复杂:证书有效期内,绑定的私钥一直在生产环境里跑,暴露时间越长,被泄露、被暴力破解的风险窗口越大。万一某个 CA 被攻破,或者私钥丢失、算法被证明不安全(比如当年的 SHA-1、1024 位 RSA),有效期越短,能自动淘汰旧问题证书的速度就越快。
2.3 免费证书为什么大多是 90 天
现在大家常用的免费证书,像 Let's Encrypt、阿里云免费版、腾讯云免费版,有效期基本都是 90 天上下。
Let's Encrypt 从第一天起就是 90 天,目的很简单:用短期证书倒逼自动化。因为只有证书足够短,你才必须建立自动续期机制;而自动化签发、自动化部署本身就是提升安全水平的路径。国内云厂商的免费证书策略也跟随这个方向,阿里云的免费版 SSL 证书目前也是 3 个月有效期,到期后在控制台重新申请并部署。
90 天意味着什么?意味着如果你纯靠记忆力去续期,一年至少要记得四次,而且每次都要留出操作余量。这不是“尽量别忘”的问题,而是“必须建立机制”的问题。后面我详细讲怎么把这件事自动化,先把各种证书格式和常规申请流程理清楚。
3. 免费证书的申请、续期与自动化部署
3.1 阿里云免费证书:续期其实是重新签发一张
阿里云 SSL 证书的免费续期,实操过的朋友应该都知道:控制台里点“续期”,本质上不是把旧证书的有效期延长,而是重新签发一张新证书,然后你手动下载、替换到服务器上。
具体流程大概是:
- 登录阿里云控制台,搜索“数字证书管理服务”(也叫 SSL 证书);
- 找到你之前申请的免费证书,如果临近到期,控制台会有提醒,也有“续期”入口;
- 提交续期申请后,需要做域名验证,验证方式一般是 DNS 解析验证或者文件验证;
- 验证通过后,等证书签发(免费证书一般几分钟到几小时);
- 在证书列表里下载,注意选择服务器类型,Nginx 下下来的是 PEM 格式证书和私钥文件,Tomcat 下下来的是 PFX 或 JKS,IIS 下的是 PFX;
- 把新证书替换到服务器对应目录,重启或 reload Web 服务,验证生效。
这里有几个我自己趟过的坑:
- 别在快过期时才去申请。免费证书的验证流程虽然不复杂,但如果用的是文件验证,你得能往网站根目录写文件;DNS 验证则需要你有域名解析控制权。遇到节假日、域名不在自己手里、DNS 服务商反应慢,都可能卡住。提前两周提交是最低要求。
- 替换证书后一定要 reload 服务。Nginx 是
nginx -t && nginx -s reload,Tomcat 是重启或 reload connector。有人替换了文件但忘记 reload,页面仍然加载旧证书,检查半天以为是没替换成功。 - 下载时选对类型。同一张证书在不同服务器类型下打包格式完全不同,最稳妥的做法是下载 Nginx 版(PEM + KEY),然后再按自己需要转成 PFX 或 JKS。关于格式转换,下一节专门展开讲。
3.2 用 certbot 让自建服务器实现自动续期
如果是自己管理的服务器和域名,我强烈建议直接上 Let's Encrypt + certbot,把 90 天续期做成全自动。这套方案适合大多数自建 Nginx、Apache 之类的场景,而且完全免费。
安装 certbot 之后,假设你的 Nginx 配置已经指向了域名example.com,第一次签发可以这样:
# 用 nginx 插件,自动签发并修改 nginx 配置 $ sudo certbot --nginx -d example.com -d www.example.com # 或者用 webroot 模式,不改 nginx 配置,只负责签发 $ sudo certbot certonly --webroot -w /var/www/html -d example.com签发成功后,证书文件在/etc/letsencrypt/live/example.com/目录下。Nginx 配置里指向它:
server { listen 443 ssl; server_name example.com; ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; }自动续期的关键在于 cron 或 systemd timer。我习惯用 cron,简单直接:
# 每天凌晨3点检查一次,到期前30天内才续,续完自动 reload nginx 0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "/bin/systemctl reload nginx"certbot renew本身是幂等的,没到续期时间它不会做任何操作,天天跑也没压力。--deploy-hook里的systemctl reload nginx是续期成功后重新加载证书的关键,没有这一步,Nginx 还会继续用内存里的旧证书。
配置完之后,先手动验证一遍:
$ sudo certbot renew --dry-run看到Congratulations, all simulated renewals succeeded就代表自动化链路通了。
3.3 续期时最容易漏掉的证书链拼接
无论是阿里云免费证书还是 Let's Encrypt,证书文件通常不是一个,而是一组:叶证书(你的域名证书)、中间证书(Intermediate CA)、可能还有根证书。Nginx 配置里的ssl_certificate必须指向一个包含完整证书链的文件,一般叫fullchain.pem。
只知道填第一个证书文件、没链中间证书的配置,浏览器端会报ERR_CERT_COMMON_NAME_INVALID或者unable to get local issuer certificate,在 curl 和 openssl 里则表现为 verify error。所以:
- 用 Let's Encrypt 就直接填
fullchain.pem,别去填cert.pem; - 用阿里云下载的 Nginx 包,解压后会有
full_chain.pem和privkey.pem两个文件,对应填到ssl_certificate和ssl_certificate_key; - 用在线检测工具(比如 SSL Labs,后面会提到)可以非常直观地看到证书链是否完整。
4. cer、pem、pfx 这些格式别被绕晕:一次讲清转换与部署
4.1 这些格式到底谁是谁
做证书部署绕不开格式转换。很多人第一次看到.cer、.crt、.pem、.pfx、.jks直接被绕晕,其实它们之间的关系没那么复杂。
| 扩展名 | 本质 | 通常包含内容 | 常见场景 |
|---|---|---|---|
| .cer / .crt | 证书,编码可能是 DER 或 PEM | 只有证书,没有私钥 | Windows、各类平台下载 |
| .pem | Base64 文本,内容可以多样 | 证书、私钥、证书链都可能 | Nginx、Apache、多数 Linux 服务 |
| .key | 私钥文件,编码可能是 PEM 或 DER | 只有私钥 | 各类服务端配置 |
| .pfx / .p12 | PKCS#12 打包容器 | 证书 + 私钥(可能带中间证书) | IIS、Tomcat、Windows 导入 |
| .jks | Java KeyStore | 证书 + 私钥,Java 专有格式 | 老版本 Tomcat、Java 应用 |
最关键的认知是:PEM 其实是“文本编码方式”,PFX 是“打包容器”,JKS 是“Java 专用仓库”。同一份证书材料,可以在这几种形态之间来回转换,理论上不丢信息,实际操作里大多数人卡在命令记不住和编码格式识别上。
4.2 只有cer没有私钥,转不出能用的pfx
很多人在 Tomcat 部署时拿到一个.cer文件,然后尝试转换成.pfx,结果各种报错。这里有一个必须说清楚的规则:只有证书(cer)没有配套私钥(key),永远转不出能用于服务器部署的 pfx。
因为 pfx 里的核心是私钥,公钥证书本身是公开的,谁都可以下载。服务器端做 TLS 握手必须持有私钥,只有证书没有私钥等于只有指纹没有本人。所以从平台下载证书时,除非平台明确给了私钥文件,否则那个 cer 只能用于“查看证书信息”或“导出给别人验证”,不能用于服务器部署。
正常转换的前提是:你手里同时有证书文件和私钥文件。比如你在阿里云下载证书时选了 Nginx 类型,拿到full_chain.pem和privkey.pem,这时想转成 Tomcat 用的 pfx,命令如下:
# 先确认证书和私钥是 PEM 编码(文本文件,第一行是 BEGIN CERTIFICATE / BEGIN PRIVATE KEY) $ file cert.pem key.pem # 转换成 pfx,过程中需要设置导出密码,后面配置 Tomcat 要填 $ openssl pkcs12 -export \ -out server.pfx \ -inkey privkey.pem \ -in full_chain.pem \ -name tomcat如果你拿到的 cer 是 DER 二进制编码,浏览器或 Windows 可以直接打开,但 openssl 处理前需要先转成 PEM:
# DER -> PEM $ openssl x509 -inform DER -in cert.cer -out cert.pem转换完成后,可以用这条命令检查 pfx 里的内容,确认证书和私钥是否齐全、匹配:
$ openssl pkcs12 -in server.pfx -info -noout这里会提示输入密码,输入后能看到bag attributes、subject、issuer等信息。重点确认里面有friendlyName: tomcat对应的私钥条目,同时证书的域名和你要部署的域名一致。
4.3 Tomcat 装载 PFX 的配置与验证
拿到 server.pfx 之后,把它放到 Tomcat 的配置目录,比如${CATALINA_HOME}/conf/下,然后修改conf/server.xml。
对于 Tomcat 8.5 以上的版本,推荐直接使用 Java 标准 SSL 连接器,配置如下:
<Connector port="8443" protocol="HTTP/1.1" SSLEnabled="true" maxThreads="150" scheme="https" secure="true" keystoreFile="conf/server.pfx" keystorePass="你的密码" keystoreType="PKCS12" clientAuth="false" sslProtocol="TLS" />关键属性就三个:keystoreFile指向 pfx 文件路径,keystorePass填转换时设置的密码,keystoreType必须写PKCS12。如果这两个地方错了,最常见的报错是:
java.io.IOException: keystore password was incorrect java.security.KeyStoreException: Unrecognized Java KeyStore format前者是密码不对,后者通常是文件类型和keystoreType不匹配。修改完成后重启 Tomcat,然后用 curl 或 openssl 验证:
# 检查8443端口 HTTPS 握手是否能拿到证书 $ echo | openssl s_client -connect localhost:8443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates能看到subject=CN = example.com,并且notAfter日期还是未来,就说明 pfx 部署成功了。
5. vsftpd 配置 SSL 证书的隐藏要求与翻车现场
5.1 vsftpd 需要什么样的证书文件
VSFTPD 是老牌 FTP 服务器了,很多人还用它做文件传输,配合 TLS 加密后就是 FTPS。它加载证书用的是 OpenSSL 库,所以只要 PEM 格式的文件就能用,不需要 pfx,也不需要 jks。但有几个隐藏要求,网上很多教程没说清楚:
- 证书和私钥可以分开两个文件,分别用
rsa_cert_file和rsa_private_key_file指定; - 也可以把证书和私钥拼在同一个 PEM 文件里,两个配置项指向同一个文件;
- 私钥文件权限必须严格控制。VSFTPD 是 root 启动、后续降权运行,但它读私钥是在启动阶段用 root 读的,所以私钥权限设成
600、属主 root 最稳妥。我见过有人把私钥权限改成644后 VSFTPD 直接拒绝加载,OpenSSL 层面对私钥文件权限有安全检查,过不了就会报错。
证书过期后,VSFTPD 还能正常启动,但所有要求 TLS 的客户端会在握手阶段失败,日志里出现一行类似SSL_accept: 14094415 ...或者certificate has expired的错误。这种故障隐蔽性很强,因为 FTP 服务本身是“通”的,普通连接也能建立,但一启用隐式 TLS 或者显式 TLS 就废。
5.2 一份可以照抄的 vsftpd.conf SSL 配置
我整理一份当前环境下比较稳妥的 VSFTPD SSL 配置:
ssl_enable=YES rsa_cert_file=/etc/vsftpd/ssl/vsftpd-cert.pem rsa_private_key_file=/etc/vsftpd/ssl/vsftpd-key.pem # 匿名用户不允许走加密,也不允许传数据 allow_anon_ssl=NO force_local_data_ssl=YES force_local_logins_ssl=YES # 关闭空密码用户的SSL要求,避免某些客户端被卡住 require_ssl_reuse=NO # 严格限定 TLS 版本,禁用古老的 SSLv2/SSLv3 ssl_tlsv1=YES ssl_sslv2=NO ssl_sslv3=NO重点解释几个配置项:
force_local_logins_ssl=YES:本地用户登录时必须走 TLS 加密,防止密码在网络上裸奔;force_local_data_ssl=YES:数据传输也必须加密;require_ssl_reuse=NO:这个必须解释一下。VSFTPD 默认要求客户端复用 SSL 会话,但 FileZilla 等主流客户端对这个特性支持得不好,不关掉的话会出现“可以登录但列目录或传输文件时断开”的诡异问题。这个配置一开始直接害我排查了大半天,后来查到是 session reuse 的坑,关掉后一切正常。
5.3 三个高频 SSL 报错与定位
| 报错/现象 | 常见原因 | 处理方式 |
|---|---|---|
| 500 OOPS: SSL: cannot load RSA private key | 私钥路径错、权限不对、证书和私钥不匹配 | 检查rsa_private_key_file路径;用openssl rsa -in key.pem -check校验私钥;用openssl x509 -in cert.pem -noout -modulus和私钥 modulus 对比 |
| 客户端能连但登录/传输瞬间断开 | require_ssl_reuse=YES导致客户端会话复用不兼容 | 配置里加require_ssl_reuse=NO |
握手失败,日志有certificate has expired | 证书过期 | 更新证书并重启 vsftpd,同时检查客户端有没有缓存旧证书 |
这里额外提醒一句:更新 VSFTPD 证书之后,务必重启服务,不是 reload 一下就行。VSFTPD 很多版本的配置和证书都是在启动时加载的,reload 不一定生效。我一般是这样做的:
$ sudo systemctl restart vsftpd $ sudo tail -f /var/log/vsftpd.log然后拿着 FileZilla 连接测试,用openssl s_client -connect your-server:21 -starttls ftp也能确认证书有没有换成功。
6. 一套最简单的检查与巡检方法,把“忘记续期”变成小概率事件
6.1 随手可查证书有效期
检查远程站点的证书:
$ echo | openssl s_client \ -servername example.com \ -connect example.com:443 2>/dev/null \ | openssl x509 -noout -dates输出两行:
notBefore=Jan 1 00:00:00 2025 GMT notAfter=Apr 1 00:00:00 2025 GMT检查本地证书文件:
$ openssl x509 -in /path/to/cert.pem -noout -dates如果是 pfx 或 jks,稍微多一步:
# pfx 需要先导出证书,或者直接看 pkcs12 信息 $ openssl pkcs12 -in server.pfx -clcerts -nokeys -out /tmp/cert.pem $ openssl x509 -in /tmp/cert.pem -noout -dates这套命令跑完,你就能算出还剩多少天到期。顺手看一眼服务器系统时间:
$ date $ timedatectl如果发现 NTP 没开启,先把这个隐患解决掉,否则证书时间校验会被系统时钟拖下水。
6.2 写一个批量巡检脚本
服务器多的话,我建议写个几行的 bash 脚本,放进 crontab 每天跑。下面这个脚本会检查一批域名,输出证书的过期时间和服务状态:
#!/bin/bash check_domain() { local d="$1" local port="${2:-443}" local enddate local exp_epoch local now_epoch local days_left enddate=$(echo | openssl s_client \ -servername "$d" \ -connect "$d:$port" 2>/dev/null \ | openssl x509 -noout -enddate 2>/dev/null \ | cut -d= -f2) if [ -z "$enddate" ]; then echo "$d:$port 获取证书失败" return fi exp_epoch=$(date -d "$enddate" +%s) now_epoch=$(date +%s) days_left=$(( (exp_epoch - now_epoch) / 86400 )) echo "$d:$port 剩余 ${days_left} 天 ($enddate)" } for domain in example.com api.example.com ftp.example.com; do check_domain "$domain" done把这个脚本放到/usr/local/bin/check_cert.sh,然后加一个 crontab:
0 9 * * * /bin/bash /usr/local/bin/check_cert.sh >> /var/log/cert_check.log 2>&1每天看一眼日志,或者把输出重定向到监控告警里,到期前 30 天开始提醒。如果你用的云平台有云监控、事件服务,也可以直接把检查结果接进去,做成更自动化的告警。
6.3 我的个人习惯
讲了这么多理论和命令,最后说几个我自己的实际习惯,供参考:
- 申请证书当天就在日历里设提醒,提前 30 天和提前 7 天各一次。免费的 90 天证书,一年至少四个周期,日历提醒是最低成本的保底方案。
- 别把所有站点部署在同一张证书的到期日上。如果都是 Let's Encrypt 自动续期还好,如果是手动续期,尽量错开时间,避免某个月突然要换五六张证书。
- acme.sh 配合 DNS API 是更省心的替代方案。如果你不想装 certbot 那一套,acme.sh 支持通过 DNS API 自动完成域名验证,签发后还能定义 install-cert 钩子自动重载 Nginx、Tomcat。对阿里云、Cloudflare 等 DNS 服务商都有现成脚本,设置一次之后也是全自动。
- 每个季度抽十分钟用 SSL Labs 跑一次全站 HTTPS 评分,能一次性看到证书链、TLS 协议版本、弱密码套件等细节。这个习惯能帮你把“证书有效期”之外的问题也提前暴露出来。
我在实际维护中发现,SSL 证书管理最危险的时间点不是“忘了续期”的那一天,而是第一次手动续期成功之后的松懈。第一次坑爬出来之后,大多数人会想“我都知道怎么换了,下次提前换就行”,结果第二次还是会在某个凌晨被用户提醒。所以我的原则是:任何能用脚本、定时任务、日历提醒自动化的事,绝不依赖自己的记性。花十分钟把检查机制搭好,远比每年临到头来折腾几次从容得多。