TLS 憑證自動化輪換機制

TLS Certificate Automated Rotation Mechanism

憑證輪換概述

TLS 憑證是保護網路通訊安全的重要元件,但憑證具有有效期限,過期的憑證會導致服務中斷。憑證輪換(Certificate Rotation)是指在憑證到期前,以新憑證取代舊憑證的過程。

傳統手動輪換憑證的方式不僅耗時費力,還容易因人為疏忽導致服務中斷。因此,建立自動化的憑證輪換機制是現代基礎架構管理的重要課題。

為什麼需要自動化輪換

自動化憑證輪換帶來以下優勢:

  1. 降低人為錯誤:消除手動操作可能產生的疏漏
  2. 提升安全性:縮短憑證有效期,降低私鑰外洩風險
  3. 確保服務持續性:避免因憑證過期導致的服務中斷
  4. 符合合規要求:滿足企業安全政策對憑證管理的規範
  5. 減少維運負擔:釋放維運人員處理重複性工作的時間

輪換策略設計

設計憑證輪換策略時,需考慮以下要點:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# 憑證輪換策略配置範例
rotation_policy:
  # 在到期前多久進行輪換
  renewal_threshold: 30d

  # 輪換失敗時的重試間隔
  retry_interval: 6h

  # 最大重試次數
  max_retries: 5

  # 輪換完成後的驗證等待時間
  validation_delay: 60s

  # 是否保留舊憑證作為備份
  keep_old_cert: true
  backup_retention: 7d

Let’s Encrypt 自動續約

Let’s Encrypt 提供免費的 TLS 憑證,搭配 Certbot 可實現自動續約。

安裝 Certbot

1
2
3
4
5
6
# Ubuntu/Debian
sudo apt update
sudo apt install certbot python3-certbot-nginx

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

設定自動續約

1
2
3
4
5
# 測試續約流程
sudo certbot renew --dry-run

# 設定 cron job 自動執行續約
sudo crontab -e
1
2
# 每天凌晨 2:30 執行續約檢查
30 2 * * * /usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx"

續約腳本範例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
#!/bin/bash
# /usr/local/bin/cert-renewal.sh

LOG_FILE="/var/log/certbot-renewal.log"
DOMAIN="example.com"

echo "[$(date)] Starting certificate renewal check" >> $LOG_FILE

certbot renew --cert-name $DOMAIN \
  --deploy-hook "/usr/local/bin/post-renewal.sh" \
  >> $LOG_FILE 2>&1

if [ $? -eq 0 ]; then
    echo "[$(date)] Renewal check completed successfully" >> $LOG_FILE
else
    echo "[$(date)] Renewal check failed" >> $LOG_FILE
    # 發送告警通知
    /usr/local/bin/send-alert.sh "Certificate renewal failed for $DOMAIN"
fi

Nginx 憑證熱重載

Nginx 支援不停機重載設定,可在更新憑證後立即生效。

1
2
3
4
5
6
7
8
# 驗證 Nginx 設定
sudo nginx -t

# 熱重載 Nginx(不中斷現有連線)
sudo nginx -s reload

# 或使用 systemctl
sudo systemctl reload nginx

自動化熱重載腳本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#!/bin/bash
# /usr/local/bin/post-renewal.sh

# 驗證新憑證
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates

# 測試 Nginx 設定
nginx -t
if [ $? -ne 0 ]; then
    echo "Nginx configuration test failed"
    exit 1
fi

# 執行熱重載
nginx -s reload
echo "Nginx reloaded with new certificate"

Kubernetes cert-manager

cert-manager 是 Kubernetes 叢集中管理憑證的標準解決方案。

安裝 cert-manager

1
2
3
4
5
6
7
8
# 使用 Helm 安裝
helm repo add jetstack https://charts.jetstack.io
helm repo update

helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set installCRDs=true

ClusterIssuer 設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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-key
    solvers:
    - http01:
        ingress:
          class: nginx

Certificate 資源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: example-com-tls
  namespace: default
spec:
  secretName: example-com-tls-secret
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  commonName: example.com
  dnsNames:
  - example.com
  - www.example.com
  # 憑證輪換設定
  renewBefore: 720h  # 30 天前開始續約
  duration: 2160h    # 90 天有效期

HashiCorp Vault PKI

Vault PKI 引擎提供企業級的憑證管理能力。

啟用 PKI 引擎

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 啟用 PKI secrets engine
vault secrets enable pki

# 設定最大 TTL
vault secrets tune -max-lease-ttl=87600h pki

# 產生根憑證
vault write pki/root/generate/internal \
  common_name="Example Root CA" \
  ttl=87600h

設定憑證角色

1
2
3
4
5
6
7
vault write pki/roles/example-dot-com \
  allowed_domains="example.com" \
  allow_subdomains=true \
  max_ttl="720h" \
  ttl="72h" \
  key_type="rsa" \
  key_bits=2048

自動輪換設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Vault Agent 設定
template {
  source      = "/etc/vault-agent/templates/cert.tpl"
  destination = "/etc/ssl/certs/example.crt"
  perms       = 0644
  command     = "systemctl reload nginx"
}

template {
  source      = "/etc/vault-agent/templates/key.tpl"
  destination = "/etc/ssl/private/example.key"
  perms       = 0600
}

監控與告警

建立完善的監控機制,確保及時發現憑證問題。

Prometheus 監控指標

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# Prometheus 告警規則
groups:
- name: certificate-alerts
  rules:
  - alert: CertificateExpiringSoon
    expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 14
    for: 1h
    labels:
      severity: warning
    annotations:
      summary: "憑證即將在 14 天內過期"
      description: "{{ $labels.instance }} 的憑證將在 {{ $value | humanizeDuration }} 後過期"

  - alert: CertificateExpired
    expr: probe_ssl_earliest_cert_expiry - time() < 0
    for: 0m
    labels:
      severity: critical
    annotations:
      summary: "憑證已過期"
      description: "{{ $labels.instance }} 的憑證已經過期"

憑證到期檢查腳本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/bin/bash
# /usr/local/bin/check-cert-expiry.sh

DOMAINS=("example.com" "api.example.com" "app.example.com")
WARN_DAYS=30
CRIT_DAYS=7

for domain in "${DOMAINS[@]}"; do
    expiry_date=$(echo | openssl s_client -servername $domain -connect $domain:443 2>/dev/null | \
                  openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)

    expiry_epoch=$(date -d "$expiry_date" +%s)
    current_epoch=$(date +%s)
    days_left=$(( ($expiry_epoch - $current_epoch) / 86400 ))

    if [ $days_left -lt $CRIT_DAYS ]; then
        echo "CRITICAL: $domain certificate expires in $days_left days"
    elif [ $days_left -lt $WARN_DAYS ]; then
        echo "WARNING: $domain certificate expires in $days_left days"
    else
        echo "OK: $domain certificate expires in $days_left days"
    fi
done

最佳實踐

  1. 縮短憑證有效期:建議使用 90 天或更短的有效期,降低私鑰洩漏風險
  2. 提前輪換:在到期前 30 天開始嘗試輪換,預留足夠的錯誤處理時間
  3. 多重驗證:輪換後執行憑證鏈驗證、連線測試等多項檢查
  4. 保留備份:保留舊憑證一段時間,以便必要時回滾
  5. 集中管理:使用 cert-manager 或 Vault 集中管理所有憑證
  6. 完善監控:建立多層次的監控告警機制
  7. 文件記錄:維護憑證清單,記錄各憑證的用途與負責人
  8. 定期演練:定期測試輪換流程,確保自動化機制正常運作

參考資料

comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy