Language:Chinese VersionEnglish Version

凌晨3点到期的证书:SSL/TLS自动化实用指南

SSL/TLS证书到期是最令人尴尬且最可预防的生产环境故障之一。领英、Spotify、Microsoft Teams以及无数小型组织都经历过与证书相关的故障——这并非因为证书管理在技术上有多困难,而是因为手动流程最终会失败。本指南涵盖了完整的证书生命周期:ACME和Let’s Encrypt的内部工作原理、如何自动化颁发和续订、如何跨多台服务器管理证书,以及如何设置监控,在用户发现问题之前就捕捉到问题。

ACME工作原理:Let’s Encrypt背后的协议

Let’s Encrypt使用ACME协议(自动证书管理环境,RFC 8555)颁发免费、公开信任的TLS证书。理解ACME的机制可以帮助您调试故障并为您的基础设施选择合适的挑战类型。

ACME流程有四个步骤:

  1. 账户注册:您的ACME客户端生成密钥对并向ACME服务器(Let’s Encrypt的Boulder CA)注册。公钥标识您的账户。
  2. 订单创建:您为一个或多个域名请求证书。ACME服务器创建一个订单并返回一组授权挑战——您必须完成这些挑战以证明对每个域名的控制权。
  3. 挑战完成:您完成提供的挑战类型之一(HTTP-01、DNS-01或TLS-ALPN-01)。ACME服务器验证您的完成情况并将授权标记为有效。
  4. 证书颁发:您提交包含您的域名和公钥的证书签名请求(CSR)。CA对其进行签名并返回证书链。

挑战类型及使用场景

HTTP-01:ACME服务器期望在http://yourdomain.com/.well-known/acme-challenge/{token}找到特定文件。您的服务器必须在端口80上可公开访问。这是对Web服务器最简单的挑战,但对于内部服务、通配符证书以及位于严格防火墙后面的服务器会失败。

DNS-01:您在_acme-challenge.yourdomain.com创建一个具有特定值的TXT记录。ACME服务器通过DNS查找验证它。此挑战适用于通配符证书和内部服务。权衡之处在于它需要访问您DNS提供商的API——这是一个重要的安全考虑,因为DNS凭证具有很大的影响范围。

TLS-ALPN-01:使用较少,此挑战完全通过端口443上的TLS工作。当您无法修改DNS记录且无法提供HTTP流量,但可以接受TLS连接时很有用。

Certbot:参考实现

Certbot 是电子前哨基金会(EFF)的 ACME 客户端,也是文档最全面的选择。对于独立的 Nginx 或 Apache 服务器,它可以处理完整的证书生命周期。

# 在 Ubuntu/Debian 上安装带 Nginx 插件的 certbot
apt install certbot python3-certbot-nginx

# 获取并安装证书(Nginx 插件会自动修改 nginx.conf)
certbot --nginx -d example.com -d www.example.com

# 试运行测试续订而不实际续订
certbot renew --dry-run

# Certbot 安装了 systemd 定时器用于自动续订
systemctl status certbot.timer
# certbot.timer 每天运行两次,续订将在 30 天内过期的证书

# 使用预/后钩手动续订(例如,用于重新加载服务)
certbot renew 
  --pre-hook "systemctl stop nginx" 
  --post-hook "systemctl start nginx" 
  --deploy-hook "systemctl reload nginx"

对于 Certbot 的 DNS-01 挑战,您需要一个与您的提供商匹配的 DNS 插件。Certbot 为大多数主要 DNS 提供商维护插件:

# 通过 Cloudflare 的 DNS-01 获取通配符证书
pip install certbot-dns-cloudflare

# 创建凭证文件(请仔细限制权限)
cat > /etc/letsencrypt/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = your_api_token_here
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini

# 获取通配符证书
certbot certonly 
  --dns-cloudflare 
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini 
  -d "*.example.com" 
  -d example.com 
  --agree-tos 
  --email admin@example.com

Caddy:无需配置的自动 HTTPS

Caddy 采用了不同的方法:HTTPS 默认是自动的。您的 Caddyfile 中的每个域名都会从 Let’s Encrypt(或 ZeroSSL)获取证书,无需任何明确的证书配置。Caddy 自动处理颁发、存储、续订和重新加载。

# Caddyfile — 所有这些网站的 HTTPS 都是自动的
example.com {
    reverse_proxy localhost:8080
}

api.example.com {
    reverse_proxy localhost:3000
    
    # 通过 Caddy 插件进行速率限制
    rate_limit {
        zone dynamic {
            key {remote_host}
            events 100
            window 1m
        }
    }
}

# 使用自签名证书的内部服务(用于非公共域名)
internal.example.com {
    tls internal
    reverse_proxy localhost:9090
}

Caddy 默认将证书存储在 /var/lib/caddy/.local/share/caddy 中,并在证书距离过期 30 天内时自动续订。对于多服务器部署,Caddy 通过 Redis 或共享文件系统支持分布式证书存储,防止每个服务器独立请求同一域名的证书。

cert-manager:Kubernetes 中的证书自动化

cert-manager 是 Kubernetes 中 TLS 证书管理的实际标准。它引入了 Issuer/ClusterIssuer 资源来表示证书颁发机构,以及 Certificate 资源来请求特定证书。

# 通过 Helm 安装 cert-manager
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager 
  --namespace cert-manager 
  --create-namespace 
  --set crds.enabled=true

# 使用 Let's Encrypt 生产环境和 DNS-01 挑战(Cloudflare)的 ClusterIssuer
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
    - dns01:
        cloudflare:
          apiTokenSecretRef:
            name: cloudflare-api-token
            key: api-token
      # 仅将此求解器应用于 example.com 及其子域名
      selector:
        dnsZones:
        - "example.com"

---
# 请求通配符证书
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: wildcard-example-com
  namespace: production
spec:
  secretName: wildcard-example-com-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - "*.example.com"
  - "example.com"
  # 在到期前30天续订
  renewBefore: 720h

cert-manager 还与 Ingress 和 Gateway API 资源集成。添加注解后,cert-manager 会处理其余工作:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - api.example.com
    secretName: api-example-com-tls
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: api-service
            port:
              number: 80

证书监控:在用户之前发现过期问题

自动续费处理理想情况。监控则处理其他一切:续费失败、自动化范围外手动添加的证书、您无法控制的第三方服务上的证书。

# Shell脚本:检查域名列表的证书过期时间
#!/bin/bash
DOMAINS=(
  "example.com"
  "api.example.com"
  "admin.example.com"
)
WARNING_DAYS=30
CRITICAL_DAYS=7

for domain in "${DOMAINS[@]}"; do
  expiry=$(echo | openssl s_client -servername "$domain" 
    -connect "$domain:443" 2>/dev/null | 
    openssl x509 -noout -enddate 2>/dev/null | 
    cut -d= -f2)
  
  if [ -z "$expiry" ]; then
    echo "CRITICAL: 无法连接到 $domain"
    continue
  fi
  
  expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || 
                 date -jf "%b %d %T %Y %Z" "$expiry" +%s)
  now_epoch=$(date +%s)
  days_remaining=$(( (expiry_epoch - now_epoch) / 86400 ))
  
  if [ "$days_remaining" -lt "$CRITICAL_DAYS" ]; then
    echo "CRITICAL: $domain 将在 $days_remaining 天后过期 ($expiry)"
  elif [ "$days_remaining" -lt "$WARNING_DAYS" ]; then
    echo "WARNING: $domain 将在 $days_remaining 天后过期 ($expiry)"
  else
    echo "OK: $domain 将在 $days_remaining 天后过期"
  fi
done

对于生产环境监控,请使用专用工具而非基于cron的脚本。ChecklyUptimeRobot都提供带有Slack/PagerDuty集成的证书过期监控。Prometheus配合blackbox_exporter可以将证书过期作为指标进行监控:

# prometheus/blackbox.yml — TLS探测配置
modules:
  https_cert_check:
    prober: http
    timeout: 10s
    http:
      valid_status_codes: [200]
      tls_config:
        insecure_skip_verify: false
      preferred_ip_protocol: ip4

# Grafana告警规则:当证书在14天内过期时触发
# 指标: probe_ssl_earliest_cert_expiry - time()
# 条件: < 14 * 24 * 60 * 60 (秒)

多服务器证书分发

当您在负载均衡器后运行多个Web服务器时,需要一种分发证书的策略。常见有三种方法:

在负载均衡器处终止TLS:最简洁的方法。AWS ACM、Cloudflare或专用负载均衡器处理证书。您的后端服务器接收纯HTTP流量。证书在一个地方管理。缺点是负载均衡器与后端之间的流量未加密——对于VPC内部流量可以接受,但对于对合规性敏感的环境存在问题。

共享文件系统挂载:让Certbot在一个节点上运行,将证书存储在共享的NFS/EFS挂载点,配置所有Web服务器从该路径读取。简单但会在NFS挂载点创建单点故障。

cert-manager 与 Kubernetes Secrets 复制: 如果您的服务器运行在 Kubernetes 中,cert-manager 会将证书写入 Secrets,而 external-secrets-operator 可以在命名空间或集群之间复制它们。

运营检查清单

  • 清点贵组织使用的每张证书,包括第三方服务、内部服务和客户端证书上的证书
  • 设置监控,在到期前 30 天、14 天和 7 天发出警报 — 三个独立的警报阈值,严重程度递增
  • 在生产环境依赖自动化之前,在暂存环境中测试续订 — 运行 certbot renew --dry-run 或 cert-manager 的测试颁发者
  • 将 ACME 账户私钥存储在密钥管理器中(Vault、AWS Secrets Manager),而不是文件系统上
  • 为每个自动化流程记录手动续订程序 — 自动化可能会失败,有人需要在凌晨 2 点知道该做什么
  • 使用证书透明度日志监控(crt.sh 或 Facebook 的 CT 监控器)来检测为您的域名颁发的未授权证书

关键要点

  • ACME 的 DNS-01 挑战是通配符证书和无法通过端口 80 访问的服务所必需的。使用具有 API 作用域的 DNS 提供商令牌,而不是您的根账户凭据。
  • Caddy 完全自动化 HTTPS,使其成为新部署的理想选择,在这些部署中,简单性比定制需求更重要。
  • cert-manager 是 Kubernetes 证书管理的标准。与 Ingress 注释集成以实现最低摩擦的工作流程。
  • 监控必须涵盖您自动化系统之外的证书。三年前在遗忘的子域上手动安装的证书不会自行续订。
  • 始终要有记录的手动续订程序。自动化减少了手动干预的频率,而不是理解如何操作的需求。

By Michael Sun

Founder and Editor-in-Chief of NovVista. Software engineer with hands-on experience in cloud infrastructure, full-stack development, and DevOps. Writes about AI tools, developer workflows, server architecture, and the practical side of technology. Based in China.

Leave a Reply

Your email address will not be published. Required fields are marked *

You missed