SSL / 证书
本文范围
这篇文章只讲 HTTPS 证书从申请到生产运维的落地链路:证书类型、域名验证、HTTP-01 / DNS-01、云厂商证书、Let’s Encrypt、Certbot、acme.sh、Caddy 自动 HTTPS、Nginx 证书配置、内网自签 CA、证书链检查、续期演练、到期告警和常见证书故障。
本文不展开 TLS 密码学细节、CA 机构合规审计、浏览器根证书计划和企业级 PKI 平台建设。后面所有命令都围绕部署运维要解决的问题:证书能不能签、Nginx 能不能加载、客户端信不信、快过期时能不能提前发现。
读完以后,你应该能判断公网证书、内网证书、通配符证书和自签 CA 分别适合什么场景,并能把“证书错误”拆成 DNS、端口、验证、证书链、私钥、SNI、客户端信任库几段来排查。
前置条件
建议先准备好:
- 一个真实可控域名;如果只做内网验证,也要准备内部域名和内部 CA 方案。
- 一台能监听 80 / 443 的入口服务器,或一个已经部署好的 Nginx / Caddy。
- 能修改 DNS 记录,尤其是 DNS-01 需要添加
_acme-challengeTXT 记录或使用 DNS API。 - 服务器具备
openssl、curl、dig/nslookup,并能查看journalctl、Nginx 或 Caddy 日志。 - 证书、私钥和 DNS API token 都按最小权限保管;任何示例路径、域名、邮箱和 token 都必须替换成自己的安全值。
先别急着复制证书配置
很多人第一次配 HTTPS,都是从云厂商下载一个压缩包,里面有 .pem、.key、.crt,然后照着网上的 Nginx 配置贴上去。运气好,浏览器显示小锁;运气不好,就开始遇到“证书不可信”“域名不匹配”“证书链不完整”“续期后还是旧证书”。
这篇不把 SSL / TLS 写成密码学笔记。我们就按真实上线顺序来:
- 先知道证书到底管什么。
- 再判断你是公网、内网,还是离线环境。
- 然后申请证书。
- 接着放进 Nginx 或 Caddy。
- 最后做续期、告警和排障。
准备一台 Linux 服务器,最好已经有一个后端服务能访问:
curl -I http://127.0.0.1:8080如果返回 HTTP/1.1 200、HTTP/1.1 302 或者你业务自己的状态码,说明后端服务是活的。后面我们要做的事,就是把外部访问从:
http://example.com变成:
https://example.com这里要注意,HTTPS 不是只改一个端口。它至少涉及域名、DNS、80/443 端口、证书、私钥、证书链、Nginx/Caddy 配置和续期任务。
证书到底解决什么问题
用大白话说,HTTPS 做两件事。
第一件事,确认你访问的真的是这个网站。比如用户访问 https://example.com,浏览器要确认服务器拿出来的证书确实属于 example.com。
第二件事,协商加密通信。浏览器和服务器通过 TLS 握手商量出后续通信使用的密钥,后面的请求和响应就不会明文裸奔。
证书主要解决第一件事:身份可信。证书里一般会有这些信息:
域名
有效期
公钥
签发机构
证书用途
SAN,也就是 Subject Alternative Name
中间证书链服务端还会有一个私钥。私钥不发给浏览器,也不能泄露。证书可以公开,私钥不可以公开。
常见文件名大概是这样:
cert.pem 站点证书
chain.pem 中间证书链
fullchain.pem 站点证书 + 中间证书链
privkey.pem 私钥
ca.crt 根 CA 或内部 CA 证书Nginx 一般推荐用 fullchain.pem,因为只给站点证书、不带中间证书时,有些客户端会报证书链不完整。
P1 图解:证书链与 Nginx 加载链路
证书文件放到服务器上以后,还没有真正完成 HTTPS。客户端要验证证书链,Nginx 要能同时读取站点证书链和私钥,域名还要通过 SNI 命中正确的 server。这几个对象混在一起时,最容易出现“本地文件没问题,线上访问仍然报错”。
这张图拆开了三个边界。fullchain.pem 发给客户端,用来证明站点证书和中间证书链;privkey.pem 只在服务器本地读取,绝不能随证书一起分发;根 CA 信任库在客户端侧,不是 Nginx 配置里多写一行就能让所有客户端信任。
先验证本地文件:
openssl x509 -in fullchain.pem -noout -subject -issuer -dates -ext subjectAltName
openssl x509 -in fullchain.pem -pubkey -noout | openssl pkey -pubin -outform PEM | sha256sum
openssl pkey -in privkey.pem -pubout -outform PEM | sha256sum再验证 Nginx 配置和线上握手:
sudo nginx -t
sudo systemctl reload nginx
echo | openssl s_client -servername example.com -connect example.com:443 -showcerts 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates常见坑是:ssl_certificate 指到了 cert.pem 而不是 fullchain.pem;证书和私钥不是一对;Nginx reload 失败但没有发现;CDN、负载均衡和源站各自加载了不同证书;容器或 JVM 使用自己的信任库,系统信任了内部 CA 不代表应用也信任。
生产约束是:证书目录只给 Nginx 进程和管理员读取,私钥权限要收紧;多节点要有同步和回滚策略;CDN、负载均衡、源站、移动端 API 域名都要纳入同一张证书台账;涉及内部 CA 时,要同时规划根证书分发、吊销和过期替换。
先看一个本地证书文件:
openssl x509 -in fullchain.pem -noout -subject -issuer -dates这个命令解决什么问题?它告诉你证书签给谁、谁签发的、什么时候开始、什么时候过期。
正常能看到类似信息:
subject=CN = example.com
issuer=C = US, O = Let's Encrypt, CN = E6
notBefore=Jul 1 00:00:00 2026 GMT
notAfter=Sep 29 23:59:59 2026 GMT如果提示:
unable to load certificate一般是文件路径错了、文件不是证书格式、复制时内容截断了。
再看证书里到底包含哪些域名:
openssl x509 -in fullchain.pem -noout -ext subjectAltName这里要注意,现代浏览器主要看 SAN。只写 CN=example.com 已经不够了。你访问 www.example.com,SAN 里就必须有 www.example.com;你访问 IP,SAN 里就必须有这个 IP。
先判断你属于哪种环境
不要一上来就选工具。证书方案先看环境。
公网环境
公网环境一般有两种验证方式。
第一种是 HTTP-01。CA 会访问你的:
http://example.com/.well-known/acme-challenge/<token>所以它要求域名已经解析到服务器,80 端口公网可达。
第二种是 DNS-01。你在 DNS 里添加 TXT 记录:
_acme-challenge.example.com这种方式不要求服务器开放 80 端口,还能申请通配符证书,比如:
*.example.com这里要注意,通配符证书一般走 DNS-01。*.example.com 覆盖 api.example.com,但不覆盖裸域 example.com,所以经常需要同时申请:
example.com
*.example.com中国大陆公网
如果服务器在中国大陆,先别急着跑 Certbot。先做三件事。
1、确认 DNS 指向正确:
dig +short example.com应该看到你的公网 IP,或者看到指向负载均衡 / CDN 的 CNAME。
2、确认 80 和 443 端口放通:
sudo ss -lntp | grep -E ':80|:443'如果什么都没有,说明本机还没有服务监听 80/443。
RHEL 系服务器看 firewalld:
sudo firewall-cmd --list-ports
sudo firewall-cmd --permanent --add-port=80/tcp
sudo firewall-cmd --permanent --add-port=443/tcp
sudo firewall-cmd --reloadUbuntu 常见是 ufw:
sudo ufw status
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp3、确认云服务器安全组也放通。这个不在 Linux 里面看,要去云厂商控制台看。很多时候本机 Nginx 正常,公网就是访问不了,最后发现是安全组没开。
国内环境还要考虑备案、云厂商接入、DNS 生效时间、ACME 接口访问稳定性。不要把海外安装脚本、GitHub 原始地址、国外包源写成唯一方案。证书方案要保留云厂商控制台申请和手工替换的兜底路径。
企业内网
内网经常没有公网入口,或者域名是:
app.internal.example这种域名公网 CA 一般没法直接验证。常见做法有两个。
第一种,如果你能控制公网 DNS,可以用 DNS-01 申请公网可信证书。服务可以在内网,验证靠 DNS 完成。
第二种,使用内部 CA。安全团队或运维团队签一个内部根证书,再用它签业务系统证书。所有办公电脑、服务器、浏览器、Java truststore 都要信任这个内部根证书。
这里要注意,内网自签证书不是让用户每次点击“继续访问”。正确做法是把根 CA 分发到客户端信任区。
离线环境
离线环境不要指望 Let’s Encrypt,也不要临时在服务器上乱生成证书。一般需要提前准备:
内部 CA 根证书
内部 CA 私钥保管规则
服务器证书签发流程
客户端根证书分发方式
证书到期巡检
应急替换和回滚流程离线场景里,“能访问 HTTPS”只是第一步。真正麻烦的是三个月、半年、一年以后证书到期,谁知道、谁处理、怎么验证。
证书类型怎么选
普通网站和后台系统,用 DV 证书就够了。DV 只验证域名控制权,签发快,适合自动化。
OV 和 EV 会验证组织身份,流程更重。银行、金融、合规网站可能会用,普通个人技术站点和内部管理系统一般没必要一开始就上。
域名数量上,可以这样选:
只用 example.com 单域名证书
要用 www 和 api 多域名 SAN 证书
子域名很多 通配符证书
内网 IP 访问 内部 CA,SAN 里写 IP这里容易踩坑的是 www。很多人申请了 example.com,结果访问 www.example.com 报错。原因很简单,证书里没有这个域名。
检查方式:
openssl x509 -in fullchain.pem -noout -ext subjectAltName如果输出里没有 DNS:www.example.com,浏览器报域名不匹配就是正常的。
用云厂商申请证书
如果域名、DNS、CDN、负载均衡都在云厂商,最省事的方式往往是云厂商证书控制台。
一般步骤如下。
1、进入 SSL 证书控制台,申请 DV 证书。
2、填写域名,例如:
example.com
www.example.com3、选择验证方式。如果 DNS 也在同一家云厂商,优先用自动 DNS 验证。
4、等待签发。
5、下载 Nginx 格式证书包,或者直接部署到负载均衡、CDN、API 网关。
6、记录证书到期时间和负责人。
下载到服务器后,一般新建一个证书目录:
sudo mkdir -p /etc/nginx/certs/example.com把证书和私钥放进去:
sudo cp fullchain.pem /etc/nginx/certs/example.com/fullchain.pem
sudo cp privkey.pem /etc/nginx/certs/example.com/privkey.pem然后设置权限:
sudo chown -R root:root /etc/nginx/certs/example.com
sudo chmod 755 /etc/nginx/certs/example.com
sudo chmod 644 /etc/nginx/certs/example.com/fullchain.pem
sudo chmod 600 /etc/nginx/certs/example.com/privkey.pem这里要注意,私钥权限不要为了省事改成 777。如果 Nginx 是系统服务,通常 master 进程能用 root 读取私钥。容器里如果是非 root 用户运行,就按容器用户单独授权。
验证证书能被读取:
sudo nginx -t如果看到:
syntax is ok
test is successful说明配置语法和证书读取都没问题。接着 reload:
sudo systemctl reload nginx如果 Nginx 在容器里,不建议只 docker cp 到容器内部。容器重建后文件就没了。更稳的是挂载证书目录:
services:
nginx:
image: nginx:1.27
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro云厂商证书的生产建议是:证书不要只存在某个人电脑的下载目录里。要有证书台账,记录它部署到了哪些地方。很多事故不是 Nginx 没换证书,而是 CDN、负载均衡、老入口还有一份旧证书没人记得。
用 Certbot 申请 Let’s Encrypt 证书
Let’s Encrypt 适合公网域名自动签发和续期。Certbot 是最常见的客户端之一。
这里先讲最容易理解的 webroot 方式。它不会自动改你的 Nginx 配置,可控性更好。
第一步:准备验证目录
先新建目录:
sudo mkdir -p /var/www/acme/.well-known/acme-challenge这个目录是给 CA 放验证文件用的。后面 CA 会从浏览器外部访问这个路径。
Nginx 里加一个 80 端口 server:
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
return 301 https://$host$request_uri;
}
}改完先检查:
sudo nginx -t
sudo systemctl reload nginx再自己放一个测试文件:
echo ok | sudo tee /var/www/acme/.well-known/acme-challenge/test
curl http://example.com/.well-known/acme-challenge/test正常应该看到:
ok如果这里访问不到,先不要申请证书。HTTP-01 一定会失败。先查 DNS、安全组、防火墙、Nginx location。
第二步:申请证书
执行:
sudo certbot certonly --webroot \
-w /var/www/acme \
-d example.com \
-d www.example.com成功后查看证书:
sudo certbot certificates一般会看到类似路径:
/etc/letsencrypt/live/example.com/fullchain.pem
/etc/letsencrypt/live/example.com/privkey.pem这里要注意,不同安装方式路径可能不同。不要凭记忆写,始终以 certbot certificates 输出为准。
第三步:配置 Nginx
最小 HTTPS 配置如下:
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}如果你的 Nginx 版本不支持 http2 on;,可以用旧写法:
listen 443 ssl http2;改完以后检查:
sudo nginx -t
sudo systemctl reload nginx
curl -Iv https://example.com访问后应该看到服务端返回证书信息,没有 SSL certificate problem,浏览器地址栏应该显示 HTTPS。
第四步:续期演练
证书不是申请完就结束。先跑一次演练:
sudo certbot renew --dry-run如果这里失败,说明将来自动续期也会失败。常见原因是 80 端口不可达、challenge 路径被改、Nginx 配置被其他人调整。
续期后要 reload Nginx:
sudo certbot renew --deploy-hook "nginx -t && systemctl reload nginx"如果 Nginx 在容器里:
sudo certbot renew --deploy-hook "docker exec nginx nginx -t && docker exec nginx nginx -s reload"这里要注意,续期成功不代表线上已经使用新证书。必须 reload,并且用线上访问验证。
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates看 notAfter 是否已经变成新的到期时间。
用 acme.sh 申请证书
acme.sh 是纯 Shell 客户端,适合轻量服务器、DNS API、通配符证书。它的优势是简单,缺点是密钥、权限、reload 命令更需要自己管好。
如果想明确使用 Let’s Encrypt,可以先设置:
acme.sh --set-default-ca --server letsencryptHTTP-01 方式
仍然使用前面的 webroot:
acme.sh --issue \
--server letsencrypt \
-d example.com \
-d www.example.com \
-w /var/www/acme成功后不要直接让 Nginx 引用 ~/.acme.sh 内部目录。建议安装到 Nginx 标准目录:
sudo mkdir -p /etc/nginx/certs/example.com
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/certs/example.com/privkey.pem \
--fullchain-file /etc/nginx/certs/example.com/fullchain.pem \
--reloadcmd "sudo nginx -t && sudo systemctl reload nginx"这里要注意两个问题。
第一,acme.sh 如果用普通用户运行,它写 /etc/nginx/certs 可能没有权限。要么提前规划目录权限,要么用受控的 sudo 规则。
第二,--reloadcmd 必须真实可执行。不要写一个永远不会跑成功的 reload 命令,然后以为自动续期已经闭环。
验证:
sudo nginx -t
sudo systemctl reload nginx
curl -Iv https://example.comDNS-01 和通配符证书
通配符一般这样申请:
acme.sh --issue \
--server letsencrypt \
--dns dns_ali \
-d example.com \
-d '*.example.com'不同 DNS 服务商的参数不一样。生产环境里 AccessKey、Secret、Token 不要写进脚本仓库,不要写进文章,也不要暴露在命令历史里。一般放到受控环境变量、CI/CD 凭据或密钥管理系统里。
如果临时手工 DNS 验证:
acme.sh --issue \
--server letsencrypt \
--dns \
-d example.com \
-d '*.example.com'它会提示你添加 TXT 记录。这个方式能救急,但不适合长期自动续期。因为下次续期还要人手工加记录。
用 Caddy 自动管理 HTTPS
Caddy 的特点是:只要域名和端口条件满足,它默认会帮你自动申请证书、自动续期、自动把 HTTP 跳到 HTTPS。
如果你的场景只是一个域名反向代理到后端服务,Caddy 会很舒服。
新建 Caddyfile:
{
email admin@example.com
}
example.com {
reverse_proxy 127.0.0.1:8080
}检查配置:
caddy validate --config /etc/caddy/Caddyfile重载:
sudo systemctl reload caddy访问:
curl -Iv https://example.com如果成功,你应该能看到 HTTPS 握手正常,后端内容也能返回。
Caddy 自动 HTTPS 要满足几个条件:
域名解析到当前服务器
80 和 443 端口可访问
没有 Nginx 等其他服务占用端口
ACME 验证能走通如果 Caddy 日志里出现验证失败,先看日志:
journalctl -u caddy -n 200 --no-pager再看端口:
sudo ss -lntp | grep -E ':80|:443'如果 80/443 已经被 Nginx 占了,Caddy 就不能直接接管公网 HTTPS。要么停掉 Nginx,要么让 Caddy 换端口,要么仍然由 Nginx 做入口。
Caddy 内网证书
内网可以用:
app.internal.example {
tls internal
reverse_proxy 127.0.0.1:8080
}这表示用 Caddy 的内部 CA 签发证书。
这里要注意,Caddy 能签,不代表所有客户端都信。其他电脑、手机、服务器、Java 程序访问时,仍然要信任这个内部 CA。否则浏览器还是会提示不安全。
Caddy 手工证书
如果证书来自云厂商或企业内部 CA,也可以手工指定:
example.com {
tls /etc/caddy/certs/example.com/fullchain.pem /etc/caddy/certs/example.com/privkey.pem
reverse_proxy 127.0.0.1:8080
}这种方式适合企业统一签发证书,Caddy 只负责反向代理。
Caddy 不适合所有场景。已有大量 Nginx location、缓存、限流、OpenResty/Lua、企业网关规则时,不要为了自动 HTTPS 硬迁。Caddy 更适合配置简单、证书自动化优先的小型服务。
Nginx HTTPS 完整配置
现在回到最常见的 Nginx。假设证书放在:
/etc/nginx/certs/example.com/fullchain.pem
/etc/nginx/certs/example.com/privkey.pem完整配置可以这样写:
server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com;
ssl_certificate /etc/nginx/certs/example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_session_tickets off;
access_log /var/log/nginx/example.access.log;
error_log /var/log/nginx/example.error.log warn;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}配置写完以后,按这个顺序验证:
1、检查 Nginx 配置:
sudo nginx -t如果失败,先看报错行号。证书路径错、私钥权限不够、指令写错,都会在这里暴露。
2、平滑重载:
sudo systemctl reload nginx3、访问 HTTP,看是否跳 HTTPS:
curl -I http://example.com一般应该看到:
HTTP/1.1 301 Moved Permanently
Location: https://example.com/4、访问 HTTPS:
curl -Iv https://example.com如果这里报 SSL certificate problem,就进入证书链或客户端信任排查;如果返回 502,就说明证书层过了,后端代理有问题。
这里要注意,证书问题和后端问题要分开看。HTTPS 握手成功之后再出现 502,通常不是证书问题,而是 proxy_pass 后面的服务不可达。
内网自签 CA 实战
内网自签不要直接生成一个 server.crt 就结束。更好的做法是先生成内部 CA,再用内部 CA 给服务签证书。客户端只需要信任内部 CA,以后这条内部链路就能复用。
第一步:生成内部 CA
新建工作目录:
mkdir -p pki
cd pki生成 CA 私钥:
openssl genrsa -out ca.key 4096生成 CA 根证书:
openssl req -x509 -new -nodes \
-key ca.key \
-sha256 \
-days 3650 \
-out ca.crt \
-subj "/C=CN/O=Example Internal CA/CN=Example Internal Root CA"执行后应该看到两个文件:
ls -l ca.key ca.crt这里要注意,ca.key 是根 CA 私钥。它比某一个业务系统的私钥更敏感。泄露以后,不是一台机器换证书,而是整条内部信任链都要处理。
第二步:写 SAN 配置
新建 san.cnf:
[req]
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[dn]
C = CN
ST = Beijing
L = Beijing
O = Example
OU = IT
CN = app.internal.example
[req_ext]
subjectAltName = @alt_names
[alt_names]
DNS.1 = app.internal.example
DNS.2 = app
IP.1 = 10.0.0.10这里要注意,如果用户用域名访问,就写 DNS;如果用户用 IP 访问,就写 IP。访问什么,SAN 里就要有什么。
第三步:生成服务器证书
生成服务器私钥:
openssl genrsa -out server.key 2048生成 CSR:
openssl req -new -key server.key -out server.csr -config san.cnf用内部 CA 签发:
openssl x509 -req \
-in server.csr \
-CA ca.crt \
-CAkey ca.key \
-CAcreateserial \
-out server.crt \
-days 397 \
-sha256 \
-extfile san.cnf \
-extensions req_ext查看结果:
openssl x509 -in server.crt -noout -subject -issuer -dates -ext subjectAltName应该能看到 DNS:app.internal.example 和 IP Address:10.0.0.10。
第四步:让客户端信任内部 CA
Windows PowerShell 管理员执行:
Import-Certificate -FilePath .\ca.crt -CertStoreLocation Cert:\LocalMachine\RootUbuntu / Debian:
sudo cp ca.crt /usr/local/share/ca-certificates/example-internal-ca.crt
sudo update-ca-certificatesRHEL / Rocky / AlmaLinux:
sudo cp ca.crt /etc/pki/ca-trust/source/anchors/example-internal-ca.crt
sudo update-ca-trustJava 应用如果不走系统信任库,要单独导入 truststore:
keytool -importcert \
-alias example-internal-ca \
-file ca.crt \
-keystore truststore.jks \
-storepass changeit这里非常常见的坑是:系统 curl 已经正常,但 Java 仍然报:
PKIX path building failed原因通常是 JVM 使用的 truststore 没有导入内部 CA。
证书怎么检查
证书排障不要靠猜,先用命令看。
查看证书有效期
openssl x509 -in fullchain.pem -noout -dates看 notAfter。如果已经过期,浏览器报证书日期无效就很正常。
检查 30 天内是否会过期:
openssl x509 -checkend $((30 * 24 * 3600)) -noout -in fullchain.pem返回 0 表示 30 天内不会过期。返回非 0 表示 30 天内会过期,适合放进巡检脚本。
查看证书和私钥是否匹配
openssl x509 -in fullchain.pem -pubkey -noout \
| openssl pkey -pubin -outform pem \
| sha256sum
openssl pkey -in privkey.pem -pubout -outform pem \
| sha256sum两个摘要一致,说明证书和私钥是一对。不一致时,Nginx 常见报错:
key values mismatch查看线上证书
一定要带 -servername:
openssl s_client \
-connect example.com:443 \
-servername example.com \
-showcerts </dev/null为什么要带?因为多个域名可能共用同一个 IP。没有 SNI,服务器可能返回默认证书,你就会误判。
只看线上证书日期:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates如果你怀疑 CDN 和源站证书不一样,可以绕过 DNS 直连源站:
curl -Iv --resolve example.com:443:203.0.113.10 https://example.com把 203.0.113.10 换成你的源站 IP。这个命令非常适合排查“我源站换了证书,用户看到的还是旧证书”。
续期和告警
证书过期属于非常典型的低级生产事故。它不是技术难,而是流程没闭环。
最小巡检脚本可以这样写:
#!/usr/bin/env bash
set -euo pipefail
cert_file="/etc/nginx/certs/example.com/fullchain.pem"
seconds=$((30 * 24 * 3600))
if openssl x509 -checkend "$seconds" -noout -in "$cert_file" >/dev/null; then
echo "OK: certificate is valid for more than 30 days"
else
echo "WARN: certificate will expire within 30 days"
exit 1
fi这个脚本解决的是本地证书文件是否快过期。但生产里还要检查线上真实证书:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates因为本地文件更新了,不代表 Nginx 已经 reload,也不代表 CDN 边缘节点已经更新。
如果用了 Prometheus,可以用 Blackbox Exporter 探测 HTTPS 证书过期时间。告警规则示例:
groups:
- name: ssl-certificate
rules:
- alert: SSLCertificateExpiresSoon
expr: (probe_ssl_earliest_cert_expiry - time()) / 86400 < 30
for: 10m
labels:
severity: warning
annotations:
summary: "TLS certificate will expire soon"
description: "{{ $labels.instance }} certificate expires in less than 30 days."一般建议两级告警:
30 天:提醒负责人检查自动续期
7 天:严重告警,进入应急处理P1 图解:续期与告警时间线
续期不是等证书快过期才运行一条命令,而是一条从签发当天就开始的时间线。不同 CA 的证书有效期可能变化,所以监控不能只依赖人工日历,要读取线上真实证书的 notAfter,再按剩余天数触发告警和应急流程。
这条时间线要同时覆盖“本地文件”和“线上入口”。openssl x509 -checkend 只能说明某个文件是否快过期;openssl s_client 才能看到用户真实访问到的证书。CDN、负载均衡、多台 Nginx、容器网关都可能让这两个结果不一致。
续期链路建议至少验证这些命令:
openssl x509 -checkend $((30 * 24 * 3600)) -noout -in /etc/nginx/certs/example.com/fullchain.pem
sudo certbot renew --dry-run
sudo nginx -t
sudo systemctl reload nginx
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates常见坑是:自动续期成功了,但 deploy hook 没有 reload Nginx;DNS-01 使用的 API token 过期;HTTP-01 被强制跳转、鉴权或 CDN 缓存影响;只监控源站证书,漏掉 CDN 或负载均衡证书;测试环境域名过期没人管,最后影响联调和回归。
生产约束是:告警阈值要按证书实际有效期计算;续期任务必须有日志和失败告警;续期后必须复核线上证书而不是只看本地文件;多人共享的证书变更要有台账、负责人和回滚路径。证书生命周期越来越短时,自动化续期和可观测性比“记得手动更新”可靠得多。
证书台账至少要记录:
域名
证书类型
签发 CA
申请方式
部署位置
是否自动续期
续期命令或控制台入口
负责人
到期时间
告警链路
回滚方式没有台账时,最容易漏的是 CDN 证书、负载均衡证书、移动端 API 域名、测试域名、老系统域名。
常见故障排查
下面按现象来查,不要一上来就重装 Nginx。
浏览器提示证书过期
现象:
NET::ERR_CERT_DATE_INVALID先看线上证书:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates -subject -issuer再看本地证书:
openssl x509 -in /etc/nginx/certs/example.com/fullchain.pem -noout -dates如果本地和线上都过期,重新申请或续期。
如果本地没过期,线上过期,重点查:
Nginx 是否 reload
配置引用路径是否正确
负载均衡其他节点是否同步
CDN 边缘证书是否更新修复后验证:
sudo nginx -t
sudo systemctl reload nginx
curl -Iv https://example.com浏览器提示域名不匹配
现象:
NET::ERR_CERT_COMMON_NAME_INVALID
SSL_ERROR_BAD_CERT_DOMAIN查 SAN:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -subject -ext subjectAltName常见原因:
访问 www.example.com,但证书只有 example.com
访问 IP,但证书只有域名
通配符 *.example.com 不覆盖 example.com
Nginx 默认 server 返回了错误证书修复方式就是重新申请包含正确 SAN 的证书,或者调整 Nginx server_name 和证书配置。
curl 提示 unable to get local issuer certificate
现象:
curl: (60) SSL certificate problem: unable to get local issuer certificate先看服务端到底发了什么证书链:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null常见原因:
Nginx 只配置了站点证书,没有配置 fullchain
客户端 CA 证书包太旧
内网自签根证书没有导入客户端
容器镜像里没有 ca-certificatesDebian / Ubuntu 容器里可以补:
apt-get update
apt-get install -y ca-certificates
update-ca-certificatesAlpine 容器里:
apk add --no-cache ca-certificates
update-ca-certificatesNginx 提示无法读取私钥
现象:
cannot load certificate key
BIO_new_file() failed
Permission denied先看文件:
sudo ls -l /etc/nginx/certs/example.com
sudo nginx -t常见原因:
路径写错
私钥文件不存在
权限不允许 Nginx 读取
容器挂载路径和配置路径不一致修复:
sudo chown root:root /etc/nginx/certs/example.com/privkey.pem
sudo chmod 600 /etc/nginx/certs/example.com/privkey.pem
sudo nginx -t
sudo systemctl reload nginx如果是非 root 容器,不要简单照抄 root:root,要按容器内运行用户调整权限。
Nginx 提示 key values mismatch
现象:
key values mismatch检查证书和私钥:
openssl x509 -in fullchain.pem -pubkey -noout \
| openssl pkey -pubin -outform pem \
| sha256sum
openssl pkey -in privkey.pem -pubout -outform pem \
| sha256sum两个值不一样,就是证书和私钥不是一对。常见于下载了多个证书包,替换时只替换了证书,没替换私钥。
HTTP-01 验证失败
现象:
Invalid response from http://example.com/.well-known/acme-challenge/...
Timeout during connect
Connection refused按顺序查:
dig +short example.com
curl -I http://example.com/.well-known/acme-challenge/test
sudo ss -lntp | grep ':80'
sudo nginx -T | grep -n "acme-challenge" -A 5 -B 5如果测试文件都访问不到,ACME 肯定也访问不到。多节点负载均衡时,验证文件可能只放到了一台机器上,这时要么共享 webroot,要么改用 DNS-01。
HTTPS 页面仍然不安全
浏览器控制台出现:
Mixed Content说明 HTTPS 页面里加载了 HTTP 资源:
<script src="http://cdn.example.com/app.js"></script>
<img src="http://example.com/logo.png">修复方式:
静态资源全部改 HTTPS
同域资源尽量用相对路径
CDN 也配置 HTTPS
后端生成的图片、下载、回调地址改成 HTTPS后端生成的地址还是 HTTP
用户访问的是 HTTPS,但后端生成跳转地址还是 HTTP,通常是反向代理头没传好。
Nginx 加上:
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;Spring Boot 常见配置:
server.forward-headers-strategy=framework这里要注意,不要在业务代码里到处硬编码 https://。先让网关和框架正确识别原始协议。
Caddy 自动申请失败
先看日志:
journalctl -u caddy -n 200 --no-pager再看 DNS 和端口:
dig +short example.com
sudo ss -lntp | grep -E ':80|:443'
curl -I http://example.com常见原因:
DNS 没解析到当前机器
80/443 被 Nginx 占用
安全组没放通
内网域名却想申请公网证书
验证失败太多触发限频内网就用 tls internal 或内部 CA,不要硬走公网 ACME。
落地工程深水区
证书最容易出事故的地方不是第一次签发,而是三个月、半年、一年以后没人记得它会过期。生产证书治理要按资产管理,不要按“谁当时申请的谁记得”。
| 深水问题 | 现场表现 | 落地约束 |
|---|---|---|
| 续期无人负责 | 证书过期当天才发现,全站 HTTPS 不可用 | 建立证书资产表:域名、签发方式、到期时间、部署位置、负责人、续期命令和告警 |
| DNS API token 权限过大 | 自动续期脚本泄露后,攻击者能修改整片域名解析 | DNS-01 token 最小权限,只允许操作指定 zone 或 _acme-challenge,并定期轮换 |
| reload 没验证 | 证书文件更新了,但线上仍然是旧证书 | 续期后必须执行服务 reload,并用 openssl s_client 从公网入口验证线上证书 |
| 证书链不完整 | 新手机、老系统、Java 客户端报不可信,浏览器可能正常 | Nginx 使用 fullchain;发布前用不同客户端和 openssl -showcerts 验证链路 |
| 内网 CA 分发混乱 | 办公网正常,服务器、容器、移动端不信任内部证书 | 内部根 CA 要有分发、撤销、轮换和客户端信任清单,不能只手工导入到几台机器 |
| 私钥权限不当 | Nginx 读不到私钥,或者私钥被业务用户读取 | 私钥只给入口服务运行用户或 root 可读,备份加密保存,禁止放进 Git、镜像和聊天记录 |
| 多入口不一致 | 负载均衡、CDN、Nginx、Caddy 各拿一套证书,过期时间不同 | 域名入口拓扑要写清证书部署层级,续期和回滚同步覆盖每一层 |
| 频繁重签触发限频 | 脚本循环失败,Let’s Encrypt 达到限额 | 先用 staging 或测试域名演练,失败时停止自动重试,保留错误日志再处理 |
生产建议是把证书续期当成发布流水线的一部分:签发、安装、reload、线上验证、告警静默恢复、资产表更新,一步都不能省。
发布前检查清单
上线前照着检查一遍:
域名解析正确
安全组放通 80 和 443
系统防火墙放通 80 和 443
Nginx / Caddy 监听正常
证书 SAN 包含所有访问域名
证书和私钥匹配
Nginx 使用 fullchain
HTTP 能跳转 HTTPS
后端能识别 X-Forwarded-Proto
前端没有混合内容
续期演练通过
到期告警已配置
旧证书和配置已备份上线后验证:
curl -I http://example.com
curl -Iv https://example.com
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName证书变更出现问题时,按这个顺序查:
DNS
安全组
系统防火墙
进程监听
Nginx / Caddy 配置
证书文件
证书链
CDN / 负载均衡
客户端信任库常用命令速查
查看证书有效期:
openssl x509 -in fullchain.pem -noout -dates查看 SAN:
openssl x509 -in fullchain.pem -noout -ext subjectAltName检查 30 天内是否过期:
openssl x509 -checkend $((30 * 24 * 3600)) -noout -in fullchain.pem检查线上证书:
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates查看完整证书链:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null检查证书和私钥是否匹配:
openssl x509 -in fullchain.pem -pubkey -noout \
| openssl pkey -pubin -outform pem \
| sha256sum
openssl pkey -in privkey.pem -pubout -outform pem \
| sha256sum检查 Nginx:
sudo nginx -t
sudo systemctl reload nginxCertbot 续期演练:
sudo certbot renew --dry-runacme.sh 安装证书:
acme.sh --install-cert -d example.com \
--key-file /etc/nginx/certs/example.com/privkey.pem \
--fullchain-file /etc/nginx/certs/example.com/fullchain.pem \
--reloadcmd "sudo nginx -t && sudo systemctl reload nginx"Caddy 配置检查:
caddy validate --config /etc/caddy/Caddyfile
systemctl reload caddy官方文档入口
- Let’s Encrypt FAQ:https://letsencrypt.org/docs/faq/
- Let’s Encrypt Challenge Types:https://letsencrypt.org/docs/challenge-types/
- Certbot User Guide:https://eff-certbot.readthedocs.io/en/stable/using.html
- acme.sh:https://github.com/acmesh-official/acme.sh
- Nginx HTTPS servers:https://nginx.org/en/docs/http/configuring_https_servers.html
- Nginx SSL module:https://nginx.org/en/docs/http/ngx_http_ssl_module.html
- Caddy Automatic HTTPS:https://caddyserver.com/docs/automatic-https
- Caddy reverse_proxy:https://caddyserver.com/docs/caddyfile/directives/reverse_proxy
- Caddy tls directive:https://caddyserver.com/docs/caddyfile/directives/tls
- OpenSSL x509:https://docs.openssl.org/3.2/man1/openssl-x509/
- OpenSSL s_client:https://docs.openssl.org/3.0/man1/openssl-s_client/
- Mozilla SSL Configuration Generator:https://ssl-config.mozilla.org/
