博客 / 詳情

返回

SSL證書踩坑與自動續期:半夜被叫醒的教訓

凌晨2點,告警電話響了。

"網站打不開,顯示證書過期。"

一看日曆,證書有效期90天,剛好今天到期。忘續了。

從那以後,我把所有證書都做了自動續期。整理一下踩過的坑。

常見的坑

坑1:證書過期

這是最常見的問題。證書有有效期,過期了瀏覽器就報錯。

檢查方法:

# 查看證書過期時間
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates

# 輸出
notBefore=Dec 23 00:00:00 2024 GMT
notAfter=Mar 22 23:59:59 2025 GMT

或者用這個一行命令:

echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -enddate

坑2:證書鏈不完整

現象:PC瀏覽器正常,手機瀏覽器報錯。

# 檢查證書鏈
openssl s_client -connect example.com:443 -servername example.com

# 正常應該顯示完整的證書鏈
Certificate chain
 0 s:/CN=example.com
   i:/C=US/O=Let's Encrypt/CN=R3
 1 s:/C=US/O=Let's Encrypt/CN=R3
   i:/C=US/O=Internet Security Research Group/CN=ISRG Root X1

如果只有0沒有1,説明中間證書沒配。

解決:

# 下載中間證書,拼到一起
cat example.com.crt intermediate.crt > fullchain.crt

Nginx配置:

ssl_certificate     /etc/nginx/ssl/fullchain.crt;
ssl_certificate_key /etc/nginx/ssl/example.com.key;

坑3:證書和域名不匹配

# 檢查證書包含的域名
openssl x509 -in cert.crt -noout -text | grep -A1 "Subject Alternative Name"

# 輸出
X509v3 Subject Alternative Name:
    DNS:example.com, DNS:www.example.com

如果訪問api.example.com,但證書只包含example.com,就會報錯。

解決:申請泛域名證書*.example.com

坑4:私鑰和證書不匹配

# 檢查私鑰和證書是否匹配
openssl x509 -noout -modulus -in cert.crt | md5sum
openssl rsa -noout -modulus -in private.key | md5sum

# 兩個md5值應該一樣

如果不一樣,説明證書和私鑰不是一對,需要重新申請。

坑5:多域名證書配置錯誤

一個證書包含多個域名,但Nginx配置錯了。

# 錯誤:每個server block用不同證書
server {
    server_name example.com;
    ssl_certificate /etc/nginx/ssl/example.crt;
}
server {
    server_name api.example.com;
    ssl_certificate /etc/nginx/ssl/api.crt;  # 應該用同一個
}

# 正確:多域名證書只需要配一次
server {
    server_name example.com www.example.com api.example.com;
    ssl_certificate /etc/nginx/ssl/fullchain.crt;  # 包含所有域名的證書
}

Let's Encrypt自動續期

Let's Encrypt的證書90天過期,必須做自動續期。

安裝certbot

# Ubuntu/Debian
apt install certbot python3-certbot-nginx

# CentOS
yum install certbot python3-certbot-nginx

申請證書

# 自動配置Nginx
certbot --nginx -d example.com -d www.example.com

# 或者只申請證書,自己配置
certbot certonly --nginx -d example.com

手動續期

# 測試續期(不會真的續期)
certbot renew --dry-run

# 真正續期
certbot renew

自動續期

certbot安裝後會自動創建定時任務,但建議檢查一下:

# 查看定時任務
systemctl list-timers | grep certbot

# 或者查看cron
cat /etc/cron.d/certbot

如果沒有,手動添加:

# 每天凌晨2點檢查續期
0 2 * * * root certbot renew --quiet --post-hook "systemctl reload nginx"

--post-hook是續期成功後執行的命令,用來重載Nginx。

續期失敗排查

# 查看日誌
tail -100 /var/log/letsencrypt/letsencrypt.log

# 常見原因
# 1. 80端口被佔用,驗證失敗
# 2. DNS解析不對
# 3. 防火牆攔截了驗證請求

acme.sh自動續期

比certbot更輕量,純shell實現。

安裝

curl https://get.acme.sh | sh
source ~/.bashrc

申請證書

# 使用DNS驗證(推薦,不需要80端口)
export Ali_Key="your_key"
export Ali_Secret="your_secret"
acme.sh --issue --dns dns_ali -d example.com -d "*.example.com"

# 使用HTTP驗證
acme.sh --issue -d example.com -w /var/www/html

安裝證書

acme.sh --install-cert -d example.com \
    --key-file       /etc/nginx/ssl/example.key \
    --fullchain-file /etc/nginx/ssl/fullchain.crt \
    --reloadcmd     "systemctl reload nginx"

acme.sh會自動設置定時任務續期。

證書監控

續期做好了,還要有監控兜底。

腳本監控

#!/bin/bash
# check_ssl.sh

DOMAINS="example.com api.example.com"
ALERT_DAYS=7
WEBHOOK="https://oapi.dingtalk.com/robot/send?access_token=xxx"

for domain in $DOMAINS; do
    expire_date=$(echo | openssl s_client -connect ${domain}:443 -servername ${domain} 2>/dev/null | openssl x509 -noout -enddate | cut -d= -f2)
    expire_ts=$(date -d "${expire_date}" +%s)
    now_ts=$(date +%s)
    days_left=$(( ($expire_ts - $now_ts) / 86400 ))
    
    if [ $days_left -lt $ALERT_DAYS ]; then
        curl -s -H "Content-Type: application/json" \
            -d "{\"msgtype\":\"text\",\"text\":{\"content\":\"[證書告警] ${domain} 還有${days_left}天過期\"}}" \
            $WEBHOOK
    fi
    
    echo "${domain}: 剩餘${days_left}天"
done

加到crontab每天跑一次:

0 9 * * * /opt/scripts/check_ssl.sh

Prometheus監控

用blackbox_exporter:

# blackbox.yml
modules:
  https_2xx:
    prober: http
    http:
      valid_http_versions: ["HTTP/1.1", "HTTP/2"]
      valid_status_codes: [200]
      tls_config:
        insecure_skip_verify: false
# prometheus.yml
- job_name: 'ssl_expiry'
  metrics_path: /probe
  params:
    module: [https_2xx]
  static_configs:
    - targets:
      - https://example.com
      - https://api.example.com
  relabel_configs:
    - source_labels: [__address__]
      target_label: __param_target
    - source_labels: [__param_target]
      target_label: instance
    - target_label: __address__
      replacement: blackbox_exporter:9115

Grafana面板告警:

# 證書剩餘天數
probe_ssl_earliest_cert_expiry - time() < 86400 * 7

多服務器證書同步

如果有多台服務器用同一個證書,續期後需要同步。

方案一:rsync同步

#!/bin/bash
# sync_ssl.sh

SERVERS="10.0.0.2 10.0.0.3 10.0.0.4"
CERT_PATH="/etc/nginx/ssl"

for server in $SERVERS; do
    rsync -avz ${CERT_PATH}/ root@${server}:${CERT_PATH}/
    ssh root@${server} "systemctl reload nginx"
done

方案二:共享存儲

證書放在NFS/對象存儲上,所有服務器掛載同一個目錄。

方案三:配置中心

把證書存在配置中心(Consul、Nacos),服務啓動時拉取。

運維小技巧

我們有幾台Web服務器在不同城市,證書統一管理比較麻煩。

用星空組網把所有服務器組到一起後,用Ansible一個命令就能把證書同步到所有節點:

ansible webservers -m copy -a "src=/etc/nginx/ssl/ dest=/etc/nginx/ssl/"
ansible webservers -m shell -a "systemctl reload nginx"

HTTPS最佳配置

順便提一下Nginx的HTTPS優化配置:

# SSL配置
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;
ssl_prefer_server_ciphers off;

# Session複用
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 8.8.4.4 valid=300s;
resolver_timeout 5s;

# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;

總結

問題 原因 解決方案
證書過期 忘了續期 自動續期+監控告警
證書鏈不完整 缺少中間證書 fullchain配置
域名不匹配 證書不包含該域名 泛域名證書
私鑰不匹配 證書和私鑰不是一對 重新申請
手機報錯PC正常 證書鏈問題 檢查中間證書

證書管理核心:

  1. 自動續期是必須的
  2. 監控告警是兜底
  3. 多服務器要有同步機制
  4. 定期檢查證書狀態

別等凌晨被叫醒才想起來。


有SSL相關經驗歡迎交流~

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.