证书过期导致线上事故?配置不当被安全扫描标红?这篇文章从原理到实战,覆盖证书申请、部署、自动化、监控和排障的全生命周期管理。


一、SSL/TLS 基础

1.1 TLS 握手过程

TLS 1.3 将握手从两个 RTT 优化到了一个 RTT,核心流程如下:

Client                                    Server
  |                                          |
  |--- ClientHello (支持的密码套件) -------->|
  |                                          |
  |<-- ServerHello + Certificate + Finished -|
  |                                          |
  |--- Finished + HTTP Request ------------->|
  |                                          |
  |<-- HTTP Response ------------------------|

TLS 1.2 则需要两个 RTT(ClientHello → ServerHello → Certificate → ServerKeyExchange → ...),在高延迟网络下差异明显。建议生产环境统一使用 TLS 1.2+,优先 TLS 1.3。

1.2 证书类型对比

类型

验证级别

签发时间

价格

适用场景

DV(域名验证)

仅验证域名所有权

分钟级

免费~低价

个人站点、API 服务

OV(组织验证)

验证域名+组织真实性

1-3 天

中等

企业官网、SaaS

EV(扩展验证)

严格验证组织身份

1-2 周

较高

金融、电商

1.3 证书链

证书链是从你的服务器证书到根证书的信任路径:

根证书 (Root CA)          ← 内置于操作系统/浏览器
  └── 中间证书 (Intermediate CA)  ← CA 签发,需要部署到服务器
      └── 服务器证书 (Leaf)       ← 你的域名证书

关键:部署时必须包含完整的证书链(服务器证书 + 中间证书),否则部分客户端会报 unable to verify the first certificate 错误。

验证证书链完整性:

# 检查远程服务器证书链
openssl s_client -connect example.com:443 -showcerts </dev/null 2>/dev/null | grep -E "s:|i:"

# 检查本地证书文件的链
openssl verify -CAfile ca-bundle.crt server.crt

二、Let's Encrypt 免费证书

2.1 Certbot 安装与使用

# Ubuntu/Debian
sudo apt update && sudo apt install -y certbot python3-certbot-nginx

# CentOS/RHEL
sudo dnf install -y certbot python3-certbot-nginx

# 申请证书(Nginx 插件会自动配置)
sudo certbot --nginx -d example.com -d www.example.com

# 仅申请证书,不修改 Nginx 配置
sudo certbot certonly --webroot -w /var/www/html -d example.com

# 使用 standalone 模式(需临时占用 80 端口)
sudo certbot certonly --standalone -d example.com

证书默认存储路径:/etc/letsencrypt/live/example.com/

  • fullchain.pem — 完整证书链(服务器证书 + 中间证书)

  • privkey.pem — 私钥

  • cert.pem — 仅服务器证书

  • chain.pem — 中间证书

2.2 自动续期

Let's Encrypt 证书有效期 90 天,必须自动续期:

# 测试续期流程(不会真正续期)
sudo certbot renew --dry-run

# 设置 cron 自动续期(每天凌晨 2:30 检查)
echo "30 2 * * * root certbot renew --quiet --deploy-hook 'systemctl reload nginx'" | sudo tee /etc/cron.d/certbot-renew

# 或者使用 systemd timer(推荐)
sudo systemctl enable --now certbot.timer

2.3 通配符证书

通配符证书需要 DNS-01 验证,适合管理多个子域名:

# 使用 Cloudflare DNS 插件
sudo apt install python3-certbot-dns-cloudflare

# 先创建 API Token 文件
sudo tee /etc/letsencrypt/cloudflare.ini > /dev/null <<'EOF'
dns_cloudflare_api_token = YOUR_API_TOKEN
EOF
sudo chmod 600 /etc/letsencrypt/cloudflare.ini

# 申请通配符证书
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d "*.example.com" \
  -d "example.com"

支持的 DNS 插件:Cloudflare、Aliyun、DNSPod、Route53、Google Cloud DNS 等。


三、证书自动化

3.1 acme.sh — 更灵活的证书工具

acme.sh 是纯 Shell 实现的 ACME 客户端,比 certbot 更轻量:

# 安装
curl https://get.acme.sh | sh -s email=admin@example.com

# 使用 DNS API 自动验证(以阿里云为例)
export Ali_Key="YOUR_ACCESS_KEY"
export Ali_Secret="YOUR_ACCESS_SECRET"

# 签发通配符证书
acme.sh --issue --dns dns_ali -d example.com -d "*.example.com"

# 安装证书到指定目录
acme.sh --install-cert -d example.com \
  --key-file       /etc/nginx/ssl/example.com.key \
  --fullchain-file /etc/nginx/ssl/example.com.crt \
  --reloadcmd      "systemctl reload nginx"

3.2 Cron 自动续期

acme.sh 安装时已自动添加 cron 任务:

# 查看 acme.sh 自带的 cron
crontab -l | grep acme

# 自定义 cron:每天凌晨 3 点检查续期
0 3 * * * "/root/.acme.sh/acme.sh" --cron --home "/root/.acme.sh" > /var/log/acme-renew.log 2>&1

3.3 Nginx 自动重载

证书更新后需要让 Nginx 重新加载新证书,几种方案:

# 方案 1:在 acme.sh 的 reloadcmd 中直接 reload
acme.sh --install-cert -d example.com \
  --reloadcmd "systemctl reload nginx"

# 方案 2:使用 inotifywait 监听证书文件变化
sudo apt install inotify-tools

cat > /usr/local/bin/cert-watcher.sh <<'SCRIPT'
#!/bin/bash
CERT_DIR="/etc/nginx/ssl"
inotifywait -m -e modify,create "$CERT_DIR" |
while read dir event file; do
  if [[ "$file" == *.crt || "$file" == *.pem ]]; then
    echo "[$(date)] Certificate changed: $file, reloading nginx..."
    nginx -t && systemctl reload nginx
  fi
done
SCRIPT
chmod +x /usr/local/bin/cert-watcher.sh

# 方案 3:使用 systemd path unit
cat > /etc/systemd/system/cert-watcher.path <<'EOF'
[Unit]
Description=Watch SSL certificate changes

[Path]
PathModified=/etc/nginx/ssl/
Unit=nginx-reload.service

[Install]
WantedBy=multi-user.target
EOF

cat > /etc/systemd/system/nginx-reload.service <<'EOF'
[Unit]
Description=Reload Nginx after cert update

[Service]
Type=oneshot
ExecStart=/usr/sbin/nginx -t
ExecStart=/usr/bin/systemctl reload nginx
EOF

sudo systemctl enable --now cert-watcher.path

四、Nginx SSL 配置

4.1 现代 TLS 配置

server {
    listen 443 ssl http2;
    server_name example.com;

    # 证书路径
    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    # 协议版本:仅允许 TLS 1.2 和 1.3
    ssl_protocols TLSv1.2 TLSv1.3;

    # 密码套件(TLS 1.2 使用)
    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;

    # 优先使用服务器密码套件
    ssl_prefer_server_ciphers off;

    # TLS 1.3 的密码套件由 OpenSSL 自动管理,无需手动配置

    # 会话缓存
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # DH 参数(TLS 1.3 不需要,TLS 1.2 可选增强)
    # openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
    # ssl_dhparam /etc/nginx/ssl/dhparam.pem;
}

4.2 OCSP Stapling

OCSP Stapling 让服务器主动缓存 OCSP 响应,避免客户端每次连接都去 CA 查询:

server {
    # ... 其他 SSL 配置 ...

    # 启用 OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    # 信任链(用于验证 OCSP 响应)
    ssl_trusted_certificate /etc/nginx/ssl/chain.pem;

    # OCSP 查询的 DNS 解析器
    resolver 8.8.8.8 223.5.5.5 valid=300s;
    resolver_timeout 5s;
}

验证 OCSP Stapling 是否生效:

openssl s_client -connect example.com:443 -status </dev/null 2>/dev/null | grep -A 2 "OCSP response"

4.3 HSTS(HTTP 严格传输安全)

server {
    # ... SSL 配置 ...

    # HSTS:告知浏览器只通过 HTTPS 访问
    # max-age=31536000 = 1 年
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
}

# HTTP 永久重定向到 HTTPS
server {
    listen 80;
    server_name example.com;
    return 301 https://$server_name$request_uri;
}

注意:HSTS 一旦启用,即使证书出问题,浏览器也会强制拒绝 HTTP。建议先用较短的 max-age 测试,确认无误后再设为长期值。

4.4 完整的生产配置模板

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

    # --- SSL 证书 ---
    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    # --- TLS 协议与密码套件 ---
    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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
    ssl_prefer_server_ciphers off;

    # --- 性能优化 ---
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;
    ssl_buffer_size 4k;

    # --- OCSP ---
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
    resolver 8.8.8.8 223.5.5.5 valid=300s;
    resolver_timeout 5s;

    # --- 安全头 ---
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "DENY" always;

    # --- 应用 ---
    root /var/www/example.com;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

五、证书格式转换

不同场景需要不同的证书格式,以下是常见转换命令:

5.1 格式说明

格式

扩展名

编码

用途

PEM

.pem, .crt, .key

Base64 文本

Linux/Nginx/Apache

PKCS#12

.pfx, .p12

二进制

Windows/IIS/Java KeyStore

DER

.der, .cer

二进制

某些嵌入式设备

JKS

.jks

二进制

Java 应用(旧)

5.2 转换命令

# PEM → PFX(用于 Windows IIS)
openssl pkcs12 -export \
  -out certificate.pfx \
  -inkey privkey.pem \
  -in fullchain.pem \
  -certfile chain.pem \
  -password pass:YourPassword

# PFX → PEM
openssl pkcs12 -in certificate.pfx \
  -out certificate.pem \
  -nodes \
  -password pass:YourPassword

# PEM → DER
openssl x509 -in cert.pem -outform DER -out cert.der

# DER → PEM
openssl x509 -in cert.der -inform DER -out cert.pem -outform PEM

# PEM → JKS(Java KeyStore)
keytool -importkeystore \
  -srckeystore certificate.pfx \
  -srcstoretype PKCS12 \
  -srcstorepass YourPassword \
  -destkeystore keystore.jks \
  -deststoretype JKS \
  -deststorepass YourPassword

# 合并证书链为单个 PEM 文件
cat server.crt intermediate.crt root.crt > fullchain.pem

# 从 PFX 中提取私钥
openssl pkcs12 -in certificate.pfx -nocerts -nodes -out private.key

5.3 格式转换速查图

                    ┌─────────┐
                    │  PEM    │ (Nginx, Apache, Linux)
                    └────┬────┘
                         │
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
    ┌─────────┐    ┌─────────┐    ┌─────────┐
    │  PFX    │    │  DER    │    │  JKS    │
    │  .p12   │    │  .der   │    │  .jks   │
    └─────────┘    └─────────┘    └─────────┘
    (IIS, Win)     (嵌入式设备)    (Java 应用)

六、多域名证书

6.1 SAN 证书(一张证书覆盖多个域名)

# 使用 certbot 申请多域名证书
sudo certbot certonly --nginx \
  -d example.com \
  -d www.example.com \
  -d api.example.com \
  -d admin.example.com

# 使用 acme.sh
acme.sh --issue --dns dns_ali \
  -d example.com \
  -d www.example.com \
  -d api.example.com \
  -d "*.shop.example.com"

6.2 Nginx 多域名配置

# 方案 1:一张证书覆盖所有子域名
server {
    listen 443 ssl http2;
    server_name example.com *.example.com;
    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;
    # ...
}

# 方案 2:不同域名使用不同证书(SNI)
server {
    listen 443 ssl http2;
    server_name api.example.com;
    ssl_certificate     /etc/nginx/ssl/api/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/api/privkey.pem;
}

server {
    listen 443 ssl http2;
    server_name admin.example.com;
    ssl_certificate     /etc/nginx/ssl/admin/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/admin/privkey.pem;
}

七、内网证书(私有 CA)

在内网环境中无法使用公共 CA,需要自建私有 CA。

7.1 创建私有 CA

# 创建 CA 目录结构
mkdir -p ~/ca/{certs,crl,newcerts,private}
chmod 700 ~/ca/private
touch ~/ca/index.txt
echo 1000 > ~/ca/serial

# 生成 CA 私钥
openssl genrsa -aes256 -out ~/ca/private/ca.key.pem 4096
chmod 400 ~/ca/private/ca.key.pem

# 生成 CA 根证书(有效期 10 年)
openssl req -config /etc/ssl/openssl.cnf \
  -key ~/ca/private/ca.key.pem \
  -new -x509 -days 3650 -sha256 \
  -extensions v3_ca \
  -out ~/ca/certs/ca.cert.pem \
  -subj "/C=CN/ST=Beijing/L=Beijing/O=MyCompany/OU=DevOps/CN=MyCompany Root CA"

7.2 签发服务器证书

# 生成服务器私钥
openssl genrsa -out ~/ca/private/server.key.pem 2048

# 创建证书签名请求(CSR)配置
cat > ~/ca/server.cnf <<'EOF'
[req]
default_bits = 2048
prompt = no
distinguished_name = dn
req_extensions = v3_req

[dn]
C = CN
ST = Beijing
L = Beijing
O = MyCompany
CN = internal.example.com

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = internal.example.com
DNS.2 = *.internal.example.com
DNS.3 = localhost
IP.1 = 192.168.1.100
IP.2 = 127.0.0.1
EOF

# 生成 CSR
openssl req -new -key ~/ca/private/server.key.pem \
  -out ~/ca/server.csr \
  -config ~/ca/server.cnf

# 用 CA 签发证书(有效期 2 年)
openssl ca -config /etc/ssl/openssl.cnf \
  -extensions v3_req \
  -days 730 -notext -md sha256 \
  -in ~/ca/server.csr \
  -out ~/ca/certs/server.cert.pem \
  -extfile ~/ca/server.cnf

7.3 分发 CA 根证书到客户端

# Ubuntu/Debian
sudo cp ~/ca/certs/ca.cert.pem /usr/local/share/ca-certificates/mycompany-ca.crt
sudo update-ca-certificates

# CentOS/RHEL
sudo cp ~/ca/certs/ca.cert.pem /etc/pki/ca-trust/source/anchors/mycompany-ca.crt
sudo update-ca-trust

# macOS
sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain ~/ca/certs/ca.cert.pem

# Docker 容器
COPY mycompany-ca.crt /usr/local/share/ca-certificates/
RUN update-ca-certificates

八、证书监控

8.1 过期监控脚本

#!/bin/bash
# cert-check.sh — 检查域名证书到期天数
# 用法: ./cert-check.sh example.com [warn_days] [crit_days]

DOMAIN=${1:-example.com}
WARN_DAYS=${2:-30}
CRIT_DAYS=${3:-7}

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 的证书信息"
  exit 2
fi

EXPIRY_EPOCH=$(date -d "$EXPIRY" +%s 2>/dev/null || date -jf "%b %d %T %Y %Z" "$EXPIRY" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (EXPIRY_EPOCH - NOW_EPOCH) / 86400 ))

if [ "$DAYS_LEFT" -le "$CRIT_DAYS" ]; then
  echo "CRITICAL: $DOMAIN 证书将在 ${DAYS_LEFT} 天后过期 ($EXPIRY)"
  exit 2
elif [ "$DAYS_LEFT" -le "$WARN_DAYS" ]; then
  echo "WARNING: $DOMAIN 证书将在 ${DAYS_LEFT} 天后过期 ($EXPIRY)"
  exit 1
else
  echo "OK: $DOMAIN 证书还有 ${DAYS_LEFT} 天到期 ($EXPIRY)"
  exit 0
fi

8.2 Prometheus 黑盒监控

使用 blackbox_exporter 监控证书到期时间:

# blackbox_exporter 配置
modules:
  http_2xx:
    prober: http
    timeout: 10s
    http:
      valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
      valid_status_codes: [200]
      preferred_ip_protocol: ip4
      tls_config:
        insecure_skip_verify: false
# Prometheus 抓取配置
scrape_configs:
  - job_name: 'ssl-certificates'
    metrics_path: /probe
    params:
      module: [http_2xx]
    static_configs:
      - targets:
          - https://example.com
          - https://api.example.com
        labels:
          env: production
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: localhost:9115
# Prometheus 告警规则
groups:
  - name: ssl-certificate
    rules:
      - alert: SSLCertExpiringSoon
        expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 30
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "SSL 证书即将过期"
          description: "{{ $labels.instance }} 的证书将在 {{ $value | humanizeDuration }} 后过期"

      - alert: SSLCertExpiringCritical
        expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 7
        for: 10m
        labels:
          severity: critical
        annotations:
          summary: "SSL 证书紧急:即将过期"
          description: "{{ $labels.instance }} 的证书将在 {{ $value | humanizeDuration }} 后过期,请立即处理"

      - alert: SSLProbeFailed
        expr: probe_success{job="ssl-certificates"} == 0
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "SSL 探测失败"
          description: "{{ $labels.instance }} 的 HTTPS 探测连续失败超过 5 分钟"

8.3 Grafana 可视化

在 Grafana 中创建证书监控面板:

# PromQL 查询:所有域名的剩余天数
(probe_ssl_earliest_cert_expiry - time()) / 86400

# PromQL 查询:按环境分组的证书到期时间
(probe_ssl_earliest_cert_expiry - time()) / 86400 by (instance, env)

九、常见问题排查

9.1 证书验证失败

# 问题:ERR_CERT_AUTHORITY_INVALID
# 原因:缺少中间证书
# 解决:确保使用 fullchain.pem 而非 cert.pem
ssl_certificate /etc/nginx/ssl/fullchain.pem;  # ✅ 正确
# ssl_certificate /etc/nginx/ssl/cert.pem;     # ❌ 可能缺少中间证书

# 验证证书链是否完整
openssl s_client -connect example.com:443 -showcerts </dev/null 2>/dev/null
# 应该看到 depth >= 2(叶子 → 中间 → 根)

9.2 混合内容警告

# 问题:页面加载了 HTTP 资源,浏览器显示"不安全"
# 排查:查找页面中的 HTTP 链接
curl -s https://example.com | grep -Eo 'http://[^"'"'"' >]+'

# 解决:在 Nginx 中添加 Content-Security-Policy
add_header Content-Security-Policy "upgrade-insecure-requests" always;

9.3 TLS 握手失败

# 问题:SSL handshake failure
# 排查步骤:

# 1. 检查端口连通性
nc -zv example.com 443

# 2. 查看支持的协议和密码套件
nmap --script ssl-enum-ciphers -p 443 example.com

# 3. 用指定 TLS 版本测试
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3

# 4. 查看 Nginx 错误日志
tail -f /var/log/nginx/error.log | grep -i ssl

9.4 证书域名不匹配

# 问题:ERR_CERT_COMMON_NAME_INVALID
# 排查:查看证书包含的域名
openssl s_client -connect example.com:443 </dev/null 2>/dev/null \
  | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"

# 解决:重新签发包含正确 SAN 的证书

9.5 续期失败

# 常见原因:80 端口被占用
sudo lsof -i :80

# 常见原因:DNS 验证失败
dig TXT _acme-challenge.example.com

# 常见原因:防火墙阻止
sudo ufw status
sudo iptables -L -n | grep -E "80|443"

# 调试 certbot 续期
sudo certbot renew --dry-run --debug-challenges -v

十、证书管理最佳实践

10.1 安全规范

  1. 私钥保护:权限设为 600,仅 root 可读

  2. 定期轮换:即使证书未过期,也建议每年轮换一次

  3. 最小权限:证书操作使用专用的非特权用户

  4. 备份私钥:加密存储,异地备份

# 私钥权限设置
chmod 600 /etc/nginx/ssl/privkey.pem
chown root:root /etc/nginx/ssl/privkey.pem

# 私钥加密备份
openssl rsa -aes256 -in privkey.pem -out privkey-encrypted.pem

10.2 自动化脚本

一个完整的证书管理脚本,包含申请、部署、监控和告警:

#!/bin/bash
# cert-manager.sh — 证书全生命周期管理
set -euo pipefail

DOMAIN="${1:?用法: cert-manager.sh <domain> [action]}"
ACTION="${2:-check}"
CERT_DIR="/etc/nginx/ssl/${DOMAIN}"
WARN_DAYS=30
ALERT_WEBHOOK="https://hooks.example.com/ssl-alert"

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }

# 获取证书到期天数
get_expiry_days() {
    local expiry
    expiry=$(echo | openssl s_client -servername "$DOMAIN" -connect "$DOMAIN":443 2>/dev/null \
        | openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
    [ -z "$expiry" ] && { echo "-1"; return; }
    local exp_epoch now_epoch
    exp_epoch=$(date -d "$expiry" +%s)
    now_epoch=$(date +%s)
    echo $(( (exp_epoch - now_epoch) / 86400 ))
}

# 申请证书
issue_cert() {
    log "申请证书: $DOMAIN"
    acme.sh --issue --dns dns_ali -d "$DOMAIN" -d "*.${DOMAIN}" \
        --keylength ec-256 --force
    acme.sh --install-cert -d "$DOMAIN" --ecc \
        --key-file       "${CERT_DIR}/key.pem" \
        --fullchain-file "${CERT_DIR}/fullchain.pem" \
        --reloadcmd      "nginx -t && systemctl reload nginx"
    log "证书申请完成"
}

# 部署证书
deploy_cert() {
    log "部署证书: $DOMAIN"
    mkdir -p "$CERT_DIR"
    acme.sh --install-cert -d "$DOMAIN" --ecc \
        --key-file       "${CERT_DIR}/key.pem" \
        --fullchain-file "${CERT_DIR}/fullchain.pem" \
        --reloadcmd      "nginx -t && systemctl reload nginx"
    chmod 600 "${CERT_DIR}/key.pem"
    log "证书部署完成,Nginx 已重载"
}

# 检查并告警
check_and_alert() {
    local days
    days=$(get_expiry_days)
    if [ "$days" -lt 0 ]; then
        log "ERROR: 无法获取 $DOMAIN 的证书信息"
        send_alert "无法获取证书信息" "critical"
        return 1
    elif [ "$days" -le 7 ]; then
        log "CRITICAL: $DOMAIN 证书 ${days} 天后过期"
        send_alert "证书将在 ${days} 天后过期" "critical"
        return 2
    elif [ "$days" -le "$WARN_DAYS" ]; then
        log "WARNING: $DOMAIN 证书 ${days} 天后过期"
        send_alert "证书将在 ${days} 天后过期" "warning"
        return 1
    else
        log "OK: $DOMAIN 证书还有 ${days} 天到期"
        return 0
    fi
}

# 发送告警
send_alert() {
    local message="$1" severity="$2"
    curl -s -X POST "$ALERT_WEBHOOK" \
        -H "Content-Type: application/json" \
        -d "{\"domain\":\"${DOMAIN}\",\"message\":\"${message}\",\"severity\":\"${severity}\"}" \
        || log "告警发送失败"
}

# 主流程
case "$ACTION" in
    issue)   issue_cert ;;
    deploy)  deploy_cert ;;
    check)   check_and_alert ;;
    renew)
        log "尝试续期: $DOMAIN"
        acme.sh --renew -d "$DOMAIN" --ecc --force || true
        deploy_cert
        ;;
    *)
        echo "用法: cert-manager.sh <domain> [issue|deploy|check|renew]"
        exit 1
        ;;
esac

10.3 证书管理 Checklist

  • 证书链完整(包含中间证书)

  • 私钥权限 600

  • OCSP Stapling 已启用

  • HSTS 已配置(确认后再启用 preload)

  • TLS 1.0/1.1 已禁用

  • 自动续期 cron 已配置且测试通过

  • 证书到期监控已部署

  • 证书过期告警已配置(30 天预警,7 天紧急)

  • 通配符证书覆盖所有子域名

  • 内网证书的 CA 根证书已分发到所有客户端

  • 私钥已加密备份

  • SSL Labs 评分达到 A+

10.4 SSL 评分检查

# 使用 SSL Labs API 检查评分(无需注册)
curl -s "https://api.ssllabs.com/api/v3/analyze?host=example.com&publish=off&all=done" \
  | jq '.endpoints[0].grade'

# 本地快速检查
nmap --script ssl-enum-ciphers -p 443 example.com | grep "least strength"

十一、附录:常用命令速查

# 查看证书信息
openssl x509 -in cert.pem -text -noout

# 查看证书有效期
openssl x509 -in cert.pem -noout -dates

# 查看证书的 SAN(主题备用名称)
openssl x509 -in cert.pem -noout -ext subjectAltName

# 查看证书的指纹
openssl x509 -in cert.pem -noout -fingerprint -sha256

# 检查私钥与证书是否匹配
diff <(openssl rsa -in privkey.pem -modulus -noout) \
     <(openssl x509 -in cert.pem -modulus -noout)

# 检查 CSR 信息
openssl req -in server.csr -text -noout

# 测试 TLS 连接
openssl s_client -connect example.com:443 -servername example.com

# 查看证书 OCSP 信息
openssl s_client -connect example.com:443 -status </dev/null 2>/dev/null | grep -A 20 "OCSP"

# 批量检查多域名证书到期
for domain in example.com api.example.com admin.example.com; do
    echo -n "$domain: "
    echo | openssl s_client -servername "$domain" -connect "$domain":443 2>/dev/null \
        | openssl x509 -noout -enddate | cut -d= -f2
done

总结:证书管理的核心是自动化。从申请、部署、续期到监控告警,全流程自动化才能真正避免证书过期事故。推荐组合:Let's Encrypt/acme.sh + cron 自动续期 + Prometheus 监控 + 自动化脚本,形成闭环。