凌晨2点,告警电话响了。
"网站打不开,显示证书过期。"
一看日历,证书有效期90天,刚好今天到期。忘续了。
从那以后,我把所有证书都做了自动续期。整理一下踩过的坑。
常见的坑
坑1:证书过期
这是最常见的问题。证书有有效期,过期了浏览器就报错。
检查方法:
# 查看证书过期时间
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
# 输出
notBefore=Dec 23 00:00:00 2024 GMT
notAfter=Mar 22 23:59:59 2025 GMT
或者用这个一行命令:
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate
坑2:证书链不完整
现象:PC浏览器正常,手机浏览器报错。
# 检查证书链
openssl s_client -connect example.com:443 -servername example.com
# 正常应该显示完整的证书链
Certificate chain
0 s:/CN=example.com
i:/C=US/O=Let's Encrypt/CN=R3
1 s:/C=US/O=Let's Encrypt/CN=R3
i:/C=US/O=Internet Security Research Group/CN=ISRG Root X1
如果只有0没有1,说明中间证书没配。
解决:
# 下载中间证书,拼到一起
cat example.com.crt intermediate.crt > fullchain.crt
Nginx配置:
ssl_certificate /etc/nginx/ssl/fullchain.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;
坑3:证书和域名不匹配
# 检查证书包含的域名
openssl x509 -in cert.crt -noout -text | grep -A1 "Subject Alternative Name"
# 输出
X509v3 Subject Alternative Name:
DNS:example.com, DNS:www.example.com
如果访问api.example.com,但证书只包含example.com,就会报错。
解决:申请泛域名证书*.example.com。
坑4:私钥和证书不匹配
# 检查私钥和证书是否匹配
openssl x509 -noout -modulus -in cert.crt | md5sum
openssl rsa -noout -modulus -in private.key | md5sum
# 两个md5值应该一样
如果不一样,说明证书和私钥不是一对,需要重新申请。
坑5:多域名证书配置错误
一个证书包含多个域名,但Nginx配置错了。
# 错误:每个server block用不同证书
server {
server_name example.com;
ssl_certificate /etc/nginx/ssl/example.crt;
}
server {
server_name api.example.com;
ssl_certificate /etc/nginx/ssl/api.crt; # 应该用同一个
}
# 正确:多域名证书只需要配一次
server {
server_name example.com www.example.com api.example.com;
ssl_certificate /etc/nginx/ssl/fullchain.crt; # 包含所有域名的证书
}
Let's Encrypt自动续期
Let's Encrypt的证书90天过期,必须做自动续期。
安装certbot
# Ubuntu/Debian
apt install certbot python3-certbot-nginx
# CentOS
yum install certbot python3-certbot-nginx
申请证书
# 自动配置Nginx
certbot --nginx -d example.com -d www.example.com
# 或者只申请证书,自己配置
certbot certonly --nginx -d example.com
手动续期
# 测试续期(不会真的续期)
certbot renew --dry-run
# 真正续期
certbot renew
自动续期
certbot安装后会自动创建定时任务,但建议检查一下:
# 查看定时任务
systemctl list-timers | grep certbot
# 或者查看cron
cat /etc/cron.d/certbot
如果没有,手动添加:
# 每天凌晨2点检查续期
0 2 * * * root certbot renew --quiet --post-hook "systemctl reload nginx"
--post-hook是续期成功后执行的命令,用来重载Nginx。
续期失败排查
# 查看日志
tail -100 /var/log/letsencrypt/letsencrypt.log
# 常见原因
# 1. 80端口被占用,验证失败
# 2. DNS解析不对
# 3. 防火墙拦截了验证请求
acme.sh自动续期
比certbot更轻量,纯shell实现。
安装
curl https://get.acme.sh | sh
source ~/.bashrc
申请证书
# 使用DNS验证(推荐,不需要80端口)
export Ali_Key="your_key"
export Ali_Secret="your_secret"
acme.sh --issue --dns dns_ali -d example.com -d "*.example.com"
# 使用HTTP验证
acme.sh --issue -d example.com -w /var/www/html
安装证书
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/ssl/example.key \
--fullchain-file /etc/nginx/ssl/fullchain.crt \
--reloadcmd "systemctl reload nginx"
acme.sh会自动设置定时任务续期。
证书监控
续期做好了,还要有监控兜底。
脚本监控
#!/bin/bash
# check_ssl.sh
DOMAINS="example.com api.example.com"
ALERT_DAYS=7
WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx"
for domain in $DOMAINS; do
expire_date=$(echo | openssl s_client -connect ${domain}:443 -servername ${domain} 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
expire_ts=$(date -d "${expire_date}" +%s)
now_ts=$(date +%s)
days_left=$(( ($expire_ts - $now_ts) / 86400 ))
if [ $days_left -lt $ALERT_DAYS ]; then
curl -s -H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[证书告警] ${domain} 还有${days_left}天过期\"}}" \
$WEBHOOK
fi
echo "${domain}: 剩余${days_left}天"
done
加到crontab每天跑一次:
0 9 * * * /opt/scripts/check_ssl.sh
Prometheus监控
用blackbox_exporter:
# blackbox.yml
modules:
https_2xx:
prober: http
http:
valid_http_versions: ["HTTP/1.1", "HTTP/2"]
valid_status_codes: [200]
tls_config:
insecure_skip_verify: false
# prometheus.yml
- job_name: 'ssl_expiry'
metrics_path: /probe
params:
module: [https_2xx]
static_configs:
- targets:
- https://example.com
- https://api.example.com
relabel_configs:
- source_labels: [__address__]
target_label: __param_target
- source_labels: [__param_target]
target_label: instance
- target_label: __address__
replacement: blackbox_exporter:9115
Grafana面板告警:
# 证书剩余天数
probe_ssl_earliest_cert_expiry - time() < 86400 * 7
多服务器证书同步
如果有多台服务器用同一个证书,续期后需要同步。
方案一:rsync同步
#!/bin/bash
# sync_ssl.sh
SERVERS="10.0.0.2 10.0.0.3 10.0.0.4"
CERT_PATH="/etc/nginx/ssl"
for server in $SERVERS; do
rsync -avz ${CERT_PATH}/ root@${server}:${CERT_PATH}/
ssh root@${server} "systemctl reload nginx"
done
方案二:共享存储
证书放在NFS/对象存储上,所有服务器挂载同一个目录。
方案三:配置中心
把证书存在配置中心(Consul、Nacos),服务启动时拉取。
运维小技巧
我们有几台Web服务器在不同城市,证书统一管理比较麻烦。
用星空组网把所有服务器组到一起后,用Ansible一个命令就能把证书同步到所有节点:
ansible webservers -m copy -a "src=/etc/nginx/ssl/ dest=/etc/nginx/ssl/"
ansible webservers -m shell -a "systemctl reload nginx"
HTTPS最佳配置
顺便提一下Nginx的HTTPS优化配置:
# SSL配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# Session复用
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;
总结
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 证书过期 | 忘了续期 | 自动续期+监控告警 |
| 证书链不完整 | 缺少中间证书 | fullchain配置 |
| 域名不匹配 | 证书不包含该域名 | 泛域名证书 |
| 私钥不匹配 | 证书和私钥不是一对 | 重新申请 |
| 手机报错PC正常 | 证书链问题 | 检查中间证书 |
证书管理核心:
- 自动续期是必须的
- 监控告警是兜底
- 多服务器要有同步机制
- 定期检查证书状态
别等凌晨被叫醒才想起来。
有SSL相关经验欢迎交流~