网络技术

TLS/SSL 证书管理全攻略:从 Let's Encrypt 到自动化续期的运维实战

✎ -- 字 🕐 -- 分钟
字号

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 别名所需环境变量
Cloudflaredns_cfCF_Token, CF_Account_ID
阿里云 DNSdns_aliAli_Key, Ali_Secret
腾讯云 DNSPoddns_dpDP_Id, DP_Key
GoDaddydns_gdGD_Key, GD_Secret
Namecheapdns_ncNC_Apikey, NC_Apiuser
Route53dns_awsAWS_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 refused80 端口未开放 / 防火墙拦截开放 80 端口或改用 DNS-01 验证
DNS problem: NXDOMAINDNS 记录未生效或已被删除检查 DNS 记录,确认验证用的 TXT 记录存在
Rate limit exceeded短时间内申请次数过多等待 1 小时后再试,或使用 staging 环境测试
Authorization not found验证文件被删除或无法访问检查 .well-known 目录权限和 Nginx 配置
Account not registeredacme.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 APISaaS全面的 SSL 评级,免费单次扫描、安全审计
CertSpotterSaaSCT 日志监控,免费发现未授权的证书签发
UptimeRobotSaaS支持 SSL 监控的免费 uptime 工具小型项目
ssl-cert-checkCLI轻量级 Bash 脚本批量检查多个域名
Prometheus Blackbox Exporter自建集成现有监控体系K8s 集群、大规模运维
cert-managerK8s 原生自动管理 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

上线前对照这个清单逐项检查:

  1. 使用 ECDSA 证书:ECDSA(ECC)证书比 RSA 证书体积更小、性能更好。acme.sh 默认使用 ECC 密钥(--keylength ec-256)。
  2. 禁用不安全的加密套件:不要使用 RC4、3DES、NULL 等已被破解的套件。
  3. 启用 OCSP Stapling:将 OCSP 响应缓存在服务器端,减少客户端验证延迟。
  4. 配置合理的 session cachessl_session_cache shared:SSL:10m 能显著减少后续连接的握手开销。
  5. 设置安全的 HTTP 头:HSTS、X-Content-Type-Options、X-Frame-Options 都应该配置。
  6. 定期扫描安全评级:用 SSL Labs Server Test 检测配置是否达到 A+ 评级。
  7. CT 日志监控:通过 CertSpotter 等工具监控证书透明日志,防止恶意签发。
  8. 做好降级预案:在 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 不应该是一件让人焦虑的事。把证书管理自动化、监控化之后,它就像呼吸一样自然——你几乎感觉不到它的存在,但它始终在保护你的站点安全。