证书过期导致线上事故?配置不当被安全扫描标红?这篇文章从原理到实战,覆盖证书申请、部署、自动化、监控和排障的全生命周期管理。
一、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 证书类型对比
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.timer2.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>&13.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 格式说明
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.key5.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.cnf7.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
fi8.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 ssl9.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 安全规范
私钥保护:权限设为 600,仅 root 可读
定期轮换:即使证书未过期,也建议每年轮换一次
最小权限:证书操作使用专用的非特权用户
备份私钥:加密存储,异地备份
# 私钥权限设置
chmod 600 /etc/nginx/ssl/privkey.pem
chown root:root /etc/nginx/ssl/privkey.pem
# 私钥加密备份
openssl rsa -aes256 -in privkey.pem -out privkey-encrypted.pem10.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
;;
esac10.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 监控 + 自动化脚本,形成闭环。