TLS/SSL 证书管理全攻略:从 Let's Encrypt 到自动化续期的运维实战
HTTPS 已经成为互联网的标配。Chrome 会给 HTTP 站点打上"不安全"标签,搜索引擎也更青睐 HTTPS 站点。但对于很多开发者来说,证书管理仍然是一件让人头疼的事——证书过期导致线上故障、配置不当引发安全警告、续期脚本莫名失效……这些问题我都踩过坑。
这篇文章会把 TLS/SSL 证书从原理到实践给你讲透,涵盖 Let's Encrypt 免费证书、acme.sh 自动化续期、Nginx 最佳配置、自签证书与内部 CA 搭建、证书监控告警等场景,读完就能上手。
一、TLS/SSL 证书到底是怎么回事?
在动手之前,先搞清楚几个核心概念,这样后面配置时就不会懵了。
1.1 证书的作用:身份认证 + 加密通道
TLS 证书解决两个问题:
- 身份认证:浏览器通过证书验证"你真的是 alois.bond",而不是中间人在冒充。
- 加密通信:建立加密通道,防止数据在传输过程中被窃听或篡改。
整个握手过程大致分为四步:客户端发起请求 → 服务器返回证书 → 客户端验证证书 → 协商密钥开始加密通信。这一套流程背后是 PKI(公钥基础设施)在支撑,其中 CA(证书颁发机构)扮演着关键角色。
1.2 证书类型对比
市面上常见的证书分三种验证级别:
| 类型 | 验证方式 | 颁发速度 | 浏览器显示 | 典型价格 | 适用场景 |
|---|---|---|---|---|---|
| DV(域名验证) | 验证域名所有权 | 秒级~分钟级 | 🔒 锁图标 | 免费 | 个人博客、小型站点 |
| OV(组织验证) | 验证域名+组织信息 | 小时级~天级 | 🔒 锁图标+组织名 | 数百~数千元/年 | 企业官网、电商 |
| EV(扩展验证) | 严格验证组织资质 | 天级~周级 | 🔒 锁图标+公司名(绿条) | 数千元/年 | 金融、支付、银行 |
对于绝大多数开发者来说,DV 证书完全够用。Let's Encrypt 提供的免费 DV 证书正是这个级别的,它已经成为全球最大的证书颁发机构之一。
二、Let's Encrypt 与 ACME 协议
Let's Encrypt 是一个由非营利组织 ISRG 运营的免费 CA。它通过 ACME(Automated Certificate Management Environment)协议实现自动化的证书申请和续期,彻底改变了证书管理的方式。
2.1 ACME 挑战类型
Let's Encrypt 支持三种验证域名所有权的方式:
| 挑战类型 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| HTTP-01 | 在网站 .well-known/acme-challenge/ 目录下放置验证文件 | 简单直接,无需 DNS 权限 | 需要 80 端口开放,不支持泛域名 |
| DNS-01 | 在 DNS 记录中添加 TXT 记录 | 支持泛域名证书,无需开放端口 | 需要 DNS API 权限 |
| TLS-ALPN-01 | 在 443 端口完成 TLS 握手验证 | 适合 80 端口被封锁的场景 | 客户端支持有限 |
生产环境最常用的是 DNS-01,因为它支持泛域名证书且不需要对外开放端口。如果你的服务器在国内,80 端口可能需要备案,DNS-01 是最佳选择。
三、acme.sh —— 最灵活的 ACME 客户端
市面上有几个主流的 ACME 客户端:Certbot(官方推荐)、acme.sh(社区最火)、lego(Go 实现)。我个人推荐 acme.sh,原因很简单——它零依赖(纯 Shell 脚本)、支持几乎所有 DNS 服务商 API、证书管理灵活。
3.1 安装 acme.sh
# 一键安装
curl https://get.acme.sh | sh
# 或者用邮箱注册(推荐)
curl https://get.acme.sh | sh -s email=your-email@example.com
# 安装后重新加载 shell 配置
source ~/.bashrc
安装完成后,acme.sh 会在你的 home 目录下创建 ~/.acme.sh/ 目录,所有证书都会存放在这里。默认已经注册了一个 ZeroSSL 账号(acme.sh 同时支持 ZeroSSL 和 Let's Encrypt),你也可以指定使用 Let's Encrypt:
# 将默认 CA 切换为 Let's Encrypt
acme.sh --set-default-ca --server letsencrypt
# 查看当前默认 CA
acme.sh --info | grep CA
3.2 申请单域名证书(HTTP-01 方式)
# 前提:Nginx 已配置好 80 端口并能正常访问
# acme.sh 会自动在 webroot 下放置验证文件
acme.sh --issue \
-d alois.bond \
-w /www/wwwroot/alois.bond
# 申请成功后证书位置
# ~/.acme.sh/alois.bond_ecc/
# ├── alois.bond.cer # 域名证书
# ├── alois.bond.key # 私钥(务必保护好!)
# ├── fullchain.cer # 完整证书链
# └── ca.cer # 中间 CA 证书
3.3 申请泛域名证书(DNS-01 方式)
泛域名证书可以覆盖 *.alois.bond 下的所有子域名,包括 alois.bond 本身。这是目前最推荐的方案:
# 方式一:使用 DNS API(以 Cloudflare 为例)
export CF_Token="你的Cloudflare_API_Token"
export CF_Account_ID="你的Account_ID"
acme.sh --issue \
--dns dns_cf \
-d alois.bond \
-d '*.alois.bond'
# 方式二:手动添加 DNS TXT 记录
acme.sh --issue \
--dns \
-d alois.bond \
-d '*.alois.bond' \
--yes-I-know-dns-manual-mode-enough-go-ahead-please
# 执行后会提示你添加 TXT 记录
# 添加完成后等 DNS 生效(dig TXT _acme-challenge.alois.bond)
acme.sh --renew \
-d alois.bond \
-d '*.alois.bond' \
--yes-I-know-dns-manual-mode-enough-go-ahead-please
常用 DNS 服务商 API 参考:
| DNS 服务商 | acme.sh DNS 别名 | 所需环境变量 |
|---|---|---|
| Cloudflare | dns_cf | CF_Token, CF_Account_ID |
| 阿里云 DNS | dns_ali | Ali_Key, Ali_Secret |
| 腾讯云 DNSPod | dns_dp | DP_Id, DP_Key |
| GoDaddy | dns_gd | GD_Key, GD_Secret |
| Namecheap | dns_nc | NC_Apikey, NC_Apiuser |
| Route53 | dns_aws | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY |
3.4 安装证书到 Nginx
证书申请下来后,需要安装到 Nginx 的证书目录:
# 安装证书(复制到指定目录并自动配置 Nginx reload)
acme.sh --install-cert \
-d alois.bond \
--key-file /etc/nginx/ssl/alois.bond.key \
--fullchain-file /etc/nginx/ssl/alois.bond.crt \
--reloadcmd "systemctl reload nginx"
这一步很关键:--reloadcmd 指定了证书更新后要执行的命令,acme.sh 会在自动续期成功后调用它,确保证书更新后 Nginx 立即加载新证书。
四、Nginx HTTPS 最佳配置
4.1 基础 HTTPS 配置
server {
listen 443 ssl http2;
server_name alois.bond;
# 证书路径
ssl_certificate /etc/nginx/ssl/alois.bond.crt;
ssl_certificate_key /etc/nginx/ssl/alois.bond.key;
# TLS 协议版本(只启用安全版本)
ssl_protocols TLSv1.2 TLSv1.3;
# 加密套件(Mozilla Intermediate 配置)
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
# 优先使用服务端加密套件
ssl_prefer_server_ciphers off;
# 会话缓存(提升性能)
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 1.1.1.1 valid=300s;
resolver_timeout 5s;
# HSTS(强制 HTTPS,建议确认无误后再启用)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# 安全头
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
root /www/wwwroot/alois.bond;
index index.html index.php;
# ...其余配置
}
4.2 HTTP 强制跳转 HTTPS
server {
listen 80;
server_name alois.bond;
# 对所有请求返回 301 永久重定向
return 301 https://$host$request_uri;
}
4.3 使用 Mozilla SSL Configuration Generator
安全配置会随时间变化,推荐使用 Mozilla 的 SSL Configuration Generator 生成最新的安全配置。它提供三个级别:
| 级别 | 兼容性 | 安全性 | 适用场景 |
|---|---|---|---|
| Modern | 最新浏览器 | 最高 | 内部系统、API |
| Intermediate | 主流浏览器(推荐) | 高 | 通用网站 |
| Old | 老旧浏览器 | 一般 | 需要兼容 Windows XP IE8 |
五、自动化续期配置
Let's Encrypt 证书有效期只有 90 天,所以续期是整个方案中最关键的一环。好消息是 acme.sh 安装时会自动添加 cron 任务:
# 查看 acme.sh 自动添加的 crontab
crontab -l | grep acme
# 典型输出(每天凌晨 0 点检查并续期)
# 56 0 * * * "/root/.acme.sh"/acme.sh --cron --home "/root/.acme.sh" > /dev/null
自动续期的工作流程是这样的:acme.sh 每天自动检查证书剩余有效期,如果少于 30 天,就自动执行续期并使用 --reloadcmd 指定的命令重载 Nginx。
5.1 手动验证续期是否正常
# 强制续期(测试用,不管是否快过期)
acme.sh --renew -d alois.bond --force
# 查看证书信息(有效期、域名等)
acme.sh --info -d alois.bond
# 查看证书过期时间
openssl x509 -in /etc/nginx/ssl/alois.bond.crt -noout -enddate
# 批量查看所有证书状态
acme.sh --list
5.2 续期失败排查
续期失败最常见的原因有几种:
| 错误现象 | 可能原因 | 解决方案 |
|---|---|---|
| Verify error: Connection refused | 80 端口未开放 / 防火墙拦截 | 开放 80 端口或改用 DNS-01 验证 |
| DNS problem: NXDOMAIN | DNS 记录未生效或已被删除 | 检查 DNS 记录,确认验证用的 TXT 记录存在 |
| Rate limit exceeded | 短时间内申请次数过多 | 等待 1 小时后再试,或使用 staging 环境测试 |
| Authorization not found | 验证文件被删除或无法访问 | 检查 .well-known 目录权限和 Nginx 配置 |
| Account not registered | acme.sh 账号过期或损坏 | 重新注册:acme.sh --register-account -m email@example.com |
六、自签证书与内部 CA 搭建
在内网环境、开发测试环境或者微服务间通信(mTLS)场景下,使用自签证书比申请公网证书更灵活。你可以搭建一个内部 CA,统一签发和管理内部证书。
6.1 创建内部根 CA
# 生成根 CA 私钥
openssl genrsa -out ca.key 4096
# 生成根 CA 证书(有效期 10 年)
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 \
-out ca.crt \
-subj "/C=CN/ST=Guangdong/L=Shenzhen/O=MyOrg/CN=MyOrg Root CA"
# 验证根证书
openssl x509 -in ca.crt -text -noout
6.2 用内部 CA 签发服务证书
# 生成服务私钥
openssl genrsa -out server.key 2048
# 生成 CSR(证书签名请求)
openssl req -new -key server.key -out server.csr \
-subj "/C=CN/ST=Guangdong/L=Shenzhen/O=MyOrg/CN=internal.example.com"
# 创建 SAN 扩展文件(支持多域名)
cat > san.ext << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage=digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName=@alt_names
[alt_names]
DNS.1 = internal.example.com
DNS.2 = *.internal.example.com
DNS.3 = localhost
IP.1 = 127.0.0.1
IP.2 = 10.0.0.100
EOF
# 用根 CA 签发服务证书(有效期 1 年)
openssl x509 -req -in server.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out server.crt -days 365 -sha256 \
-extfile san.ext
# 验证证书链
openssl verify -CAfile ca.crt server.crt
6.3 客户端信任内部 CA
签发的证书要在客户端被信任,需要将根 CA 证书添加到信任存储:
# Linux (Debian/Ubuntu)
sudo cp ca.crt /usr/local/share/ca-certificates/myorg-root.crt
sudo update-ca-certificates
# Linux (CentOS/RHEL)
sudo cp ca.crt /etc/pki/ca-trust/source/anchors/
sudo update-ca-trust
# macOS
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain ca.crt
对于 Node.js 应用,可以设置 NODE_EXTRA_CA_CERTS 环境变量来指定额外的 CA 证书:
export NODE_EXTRA_CA_CERTS=/path/to/ca.crt
node server.js
Python 应用则需要将 CA 证书路径传给 requests 库或设置环境变量:
import requests
import os
# 方式一:通过环境变量
os.environ["REQUESTS_CA_BUNDLE"] = "/path/to/ca.crt"
response = requests.get("https://internal.example.com/api")
# 方式二:每次请求指定
response = requests.get(
"https://internal.example.com/api",
verify="/path/to/ca.crt"
)
七、证书监控与告警
即使配置了自动续期,也应该有独立的监控机制来确保万无一失。毕竟自动续期脚本也可能因为各种原因失效。
7.1 简单监控脚本
#!/bin/bash
# cert-monitor.sh —— 证书过期监控脚本
DOMAIN="alois.bond"
THRESHOLD=30 # 提前 30 天告警
WEBHOOK="https://hooks.slack.com/services/xxx" # Slack/企业微信 Webhook
END_DATE=$(echo | openssl s_client -servername "$DOMAIN" \
-connect "$DOMAIN:443" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$END_DATE" ]; then
echo "[ERROR] 无法获取 $DOMAIN 证书信息"
exit 1
fi
END_SECONDS=$(date -d "$END_DATE" +%s)
NOW_SECONDS=$(date +%s)
DAYS_LEFT=$(( ($END_SECONDS - $NOW_SECONDS) / 86400 ))
echo "$DOMAIN 证书剩余 $DAYS_LEFT 天"
if [ "$DAYS_LEFT" -lt "$THRESHOLD" ]; then
# 发送告警(这里以 Slack 为例)
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"⚠️ $DOMAIN SSL证书将在 $DAYS_LEFT 天后过期!\"}" \
"$WEBHOOK"
fi
7.2 专业的证书监控工具
| 工具 | 类型 | 特点 | 适合场景 |
|---|---|---|---|
| SSL Labs API | SaaS | 全面的 SSL 评级,免费 | 单次扫描、安全审计 |
| CertSpotter | SaaS | CT 日志监控,免费 | 发现未授权的证书签发 |
| UptimeRobot | SaaS | 支持 SSL 监控的免费 uptime 工具 | 小型项目 |
| ssl-cert-check | CLI | 轻量级 Bash 脚本 | 批量检查多个域名 |
| Prometheus Blackbox Exporter | 自建 | 集成现有监控体系 | K8s 集群、大规模运维 |
| cert-manager | K8s 原生 | 自动管理 K8s Ingress 证书 | Kubernetes 集群 |
对于只有几个域名的个人站点,cron + shell 脚本就足够了。运维多台服务器或 K8s 集群的话,建议接入 Prometheus + Blackbox Exporter 统一监控。
八、常见陷阱与最佳实践
8.1 五大常见错误
1. 证书过期忘记续期——这是最常见的线上事故。解决方法是双重保障:acme.sh 自动续期 + 独立监控脚本。
2. 私钥泄露——私钥(.key 文件)一旦泄露,任何人都可以冒充你的站点。务必设置 600 权限:
chmod 600 /etc/nginx/ssl/*.key
chown root:root /etc/nginx/ssl/*.key
3. 使用了过时的 TLS 版本——TLS 1.0 和 1.1 已被主流浏览器弃用。在 Nginx 配置中只启用 TLS 1.2 和 1.3:
ssl_protocols TLSv1.2 TLSv1.3;
4. 证书链不完整——缺少中间证书会导致部分客户端(尤其是 Android 旧版本)无法验证。务必使用 fullchain.cer 而非单独的站点证书:
# 正确:使用完整证书链
ssl_certificate /etc/nginx/ssl/alois.bond.crt; # 内容来自 fullchain.cer
# 错误:只使用站点证书(缺少中间 CA)
ssl_certificate /etc/nginx/ssl/alois.bond.crt; # 内容来自 alois.bond.cer
5. HSTS 配置过于激进——max-age=63072000 意味着两年内浏览器都会强制 HTTPS。一旦证书出问题,用户完全无法访问。建议先在测试环境验证,或者先用较短的 max-age 值(如 max-age=86400)测试一段时间。
8.2 最佳实践 Checklist
上线前对照这个清单逐项检查:
- 使用 ECDSA 证书:ECDSA(ECC)证书比 RSA 证书体积更小、性能更好。acme.sh 默认使用 ECC 密钥(
--keylength ec-256)。 - 禁用不安全的加密套件:不要使用 RC4、3DES、NULL 等已被破解的套件。
- 启用 OCSP Stapling:将 OCSP 响应缓存在服务器端,减少客户端验证延迟。
- 配置合理的 session cache:
ssl_session_cache shared:SSL:10m能显著减少后续连接的握手开销。 - 设置安全的 HTTP 头:HSTS、X-Content-Type-Options、X-Frame-Options 都应该配置。
- 定期扫描安全评级:用 SSL Labs Server Test 检测配置是否达到 A+ 评级。
- CT 日志监控:通过 CertSpotter 等工具监控证书透明日志,防止恶意签发。
- 做好降级预案:在 CDN 层面配置证书(如果使用 CDN),作为源站证书问题的备用方案。
九、总结
回顾一下整篇文章的核心要点:
- Let's Encrypt + acme.sh 是目前最成熟的免费证书方案,推荐使用 DNS-01 方式申请泛域名证书。
- Nginx 配置 参照 Mozilla Intermediate 级别,只启用 TLS 1.2+,使用强加密套件。
- 自动化续期 依赖 acme.sh 的 cron 任务 +
--reloadcmd钩子,但务必配置独立的监控告警作为安全网。 - 内部 CA 适用于内网服务和微服务间 mTLS 通信,减少对外部 CA 的依赖。
- 证书监控 用最简单的 shell 脚本就能实现,关键是提前 30 天告警,给修复留足时间。
HTTPS 不应该是一件让人焦虑的事。把证书管理自动化、监控化之后,它就像呼吸一样自然——你几乎感觉不到它的存在,但它始终在保护你的站点安全。