企業憑證生命週期管理

Enterprise Certificate Lifecycle Management

在現代企業環境中,數位憑證(Digital Certificate)扮演著確保通訊安全、身份驗證與資料完整性的核心角色。隨著雲端服務、微服務架構與 DevOps 實踐的普及,企業所管理的憑證數量呈指數級增長。本文將深入探討企業憑證生命週期管理(Certificate Lifecycle Management, CLM)的各個階段,並提供實務上的自動化解決方案。

一、憑證生命週期階段概述

憑證生命週期涵蓋從憑證的產生到廢棄的完整過程,主要分為以下幾個階段:

1
2
3
4
5
6
7
8
9
┌─────────────┐    ┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   發現盤點   │───▶│   申請核發   │───▶│   部署安裝   │───▶│   監控管理   │
└─────────────┘    └─────────────┘    └─────────────┘    └─────────────┘
       ┌───────────────────────────────────────────────────────┘
┌─────────────┐    ┌─────────────┐    ┌─────────────┐
│   更新輪換   │───▶│   撤銷廢棄   │───▶│   治理合規   │
└─────────────┘    └─────────────┘    └─────────────┘

每個階段都需要明確的流程、適當的工具支援,以及完善的文件記錄。以下將逐一說明各階段的關鍵要點與實作方式。


二、憑證發現與盤點

2.1 為何需要憑證發現

在大型企業環境中,憑證可能分散於:

  • 網頁伺服器(Apache、Nginx、IIS)
  • 負載平衡器(F5 BIG-IP、AWS ALB、Azure Application Gateway)
  • Kubernetes 叢集
  • IoT 設備
  • 內部應用程式
  • 雲端服務

若缺乏完整的憑證清單,將導致憑證過期、安全漏洞或合規問題。

2.2 網路掃描發現

使用 nmap 搭配 SSL 腳本進行網路掃描:

1
2
3
4
5
# 掃描特定子網路的 HTTPS 服務
nmap -sV --script ssl-cert -p 443 192.168.1.0/24 -oX ssl_scan_results.xml

# 掃描多個常見 SSL/TLS 埠
nmap -sV --script ssl-cert -p 443,8443,636,993,995 10.0.0.0/16 -oX enterprise_ssl_scan.xml

2.3 使用 OpenSSL 取得憑證資訊

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash
# cert_discovery.sh - 憑證發現腳本

HOSTS=(
    "www.example.com:443"
    "api.example.com:443"
    "mail.example.com:993"
)

for host in "${HOSTS[@]}"; do
    echo "=== Scanning: $host ==="
    echo | openssl s_client -connect "$host" -servername "${host%%:*}" 2>/dev/null | \
    openssl x509 -noout -subject -issuer -dates -serial
    echo ""
done

2.4 Kubernetes 環境憑證盤點

1
2
3
4
5
6
7
8
# 列出所有 namespace 中的 TLS Secret
kubectl get secrets --all-namespaces -o json | \
jq -r '.items[] | select(.type=="kubernetes.io/tls") |
    "\(.metadata.namespace)/\(.metadata.name)"'

# 檢視特定 Secret 的憑證詳情
kubectl get secret my-tls-secret -n production -o jsonpath='{.data.tls\.crt}' | \
base64 -d | openssl x509 -noout -text

2.5 建立憑證資產清單

建議使用結構化格式記錄憑證資產:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# certificate_inventory.yaml
certificates:
  - common_name: "www.example.com"
    san:
      - "www.example.com"
      - "example.com"
    issuer: "DigiCert Global G2 TLS RSA SHA256 2020 CA1"
    serial: "0A:1B:2C:3D:4E:5F:6A:7B"
    not_before: "2024-01-15"
    not_after: "2025-01-15"
    key_algorithm: "RSA 2048"
    location:
      - type: "load_balancer"
        name: "prod-lb-01"
        ip: "10.0.1.100"
      - type: "kubernetes"
        namespace: "production"
        secret: "www-tls-secret"
    owner: "platform-team@example.com"
    criticality: "high"

三、申請與核發流程

3.1 憑證申請流程設計

標準化的憑證申請流程應包含:

  1. 需求提交:申請者填寫憑證需求表單
  2. 審核批准:資安團隊審核域名擁有權與業務需求
  3. CSR 產生:依據規範產生金鑰對與 CSR
  4. CA 簽發:向內部 CA 或公共 CA 提交 CSR
  5. 憑證下載:取得簽發的憑證與中繼憑證鏈

3.2 產生 CSR(Certificate Signing Request)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 產生私鑰與 CSR(RSA 2048-bit)
openssl req -new -newkey rsa:2048 -nodes \
    -keyout www.example.com.key \
    -out www.example.com.csr \
    -subj "/C=TW/ST=Taiwan/L=Taipei/O=Example Corp/OU=IT/CN=www.example.com"

# 產生包含 SAN 的 CSR
cat > san.cnf << EOF
[req]
distinguished_name = req_distinguished_name
req_extensions = v3_req
prompt = no

[req_distinguished_name]
C = TW
ST = Taiwan
L = Taipei
O = Example Corp
OU = IT
CN = www.example.com

[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = www.example.com
DNS.2 = example.com
DNS.3 = api.example.com
EOF

openssl req -new -newkey rsa:2048 -nodes \
    -keyout www.example.com.key \
    -out www.example.com.csr \
    -config san.cnf

3.3 使用 ACME 協定自動化申請(Let’s Encrypt)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 使用 Certbot 自動申請憑證
certbot certonly --webroot \
    -w /var/www/html \
    -d www.example.com \
    -d example.com \
    --email admin@example.com \
    --agree-tos \
    --non-interactive

# 使用 DNS-01 驗證(適用於 Wildcard 憑證)
certbot certonly --manual \
    --preferred-challenges dns \
    -d "*.example.com" \
    --email admin@example.com

3.4 使用 cert-manager 在 Kubernetes 中自動申請

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# ClusterIssuer 設定(Let's Encrypt Production)
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:
      - http01:
          ingress:
            class: nginx
      - dns01:
          route53:
            region: ap-northeast-1
            accessKeyID: AKIAXXXXXXXXXXXXXXXX
            secretAccessKeySecretRef:
              name: aws-route53-credentials
              key: secret-access-key

---
# Certificate 資源定義
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: www-example-com
  namespace: production
spec:
  secretName: www-example-com-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  commonName: www.example.com
  dnsNames:
    - www.example.com
    - api.example.com
  duration: 2160h    # 90 days
  renewBefore: 360h  # 15 days before expiry

四、部署與安裝自動化

4.1 Nginx 憑證部署

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#!/bin/bash
# deploy_cert_nginx.sh

DOMAIN="www.example.com"
CERT_PATH="/etc/letsencrypt/live/$DOMAIN"
NGINX_CONF="/etc/nginx/sites-available/$DOMAIN"

# 驗證憑證有效性
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
    "$CERT_PATH/fullchain.pem"

if [ $? -eq 0 ]; then
    echo "Certificate verification passed"

    # 測試 Nginx 設定
    nginx -t

    if [ $? -eq 0 ]; then
        # 重新載入 Nginx
        systemctl reload nginx
        echo "Nginx reloaded successfully"
    else
        echo "Nginx configuration test failed"
        exit 1
    fi
else
    echo "Certificate verification failed"
    exit 1
fi

4.2 F5 BIG-IP 憑證部署(使用 REST API)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
#!/usr/bin/env python3
"""
F5 BIG-IP Certificate Deployment Script
"""

import requests
import json
import base64
from requests.auth import HTTPBasicAuth

class F5CertDeployer:
    def __init__(self, host, username, password):
        self.base_url = f"https://{host}/mgmt/tm"
        self.auth = HTTPBasicAuth(username, password)
        self.headers = {"Content-Type": "application/json"}
        self.session = requests.Session()
        self.session.verify = False  # 生產環境應啟用驗證

    def upload_certificate(self, cert_name, cert_content, key_content):
        """上傳憑證與私鑰"""

        # 上傳憑證
        cert_payload = {
            "command": "install",
            "name": cert_name,
            "from-local-file": f"/var/tmp/{cert_name}.crt"
        }

        # 先將檔案上傳至 F5
        upload_url = f"https://{self.host}/mgmt/shared/file-transfer/uploads/{cert_name}.crt"
        self.session.post(
            upload_url,
            data=cert_content,
            auth=self.auth,
            headers={"Content-Type": "application/octet-stream"}
        )

        # 安裝憑證
        response = self.session.post(
            f"{self.base_url}/sys/crypto/cert",
            json=cert_payload,
            auth=self.auth,
            headers=self.headers
        )

        return response.json()

    def update_ssl_profile(self, profile_name, cert_name, key_name):
        """更新 SSL Profile"""

        payload = {
            "cert": f"/Common/{cert_name}",
            "key": f"/Common/{key_name}"
        }

        response = self.session.patch(
            f"{self.base_url}/ltm/profile/client-ssl/{profile_name}",
            json=payload,
            auth=self.auth,
            headers=self.headers
        )

        return response.json()

# 使用範例
if __name__ == "__main__":
    deployer = F5CertDeployer(
        host="f5-bigip.example.com",
        username="admin",
        password="secure_password"
    )

    with open("www.example.com.crt", "r") as f:
        cert_content = f.read()
    with open("www.example.com.key", "r") as f:
        key_content = f.read()

    deployer.upload_certificate(
        "www_example_com_2024",
        cert_content,
        key_content
    )

    deployer.update_ssl_profile(
        "clientssl_www_example_com",
        "www_example_com_2024",
        "www_example_com_2024"
    )

4.3 AWS Certificate Manager 整合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 匯入憑證至 ACM
aws acm import-certificate \
    --certificate fileb://certificate.pem \
    --private-key fileb://private-key.pem \
    --certificate-chain fileb://certificate-chain.pem \
    --region ap-northeast-1

# 列出所有憑證
aws acm list-certificates --region ap-northeast-1

# 更新 ALB Listener 使用新憑證
aws elbv2 modify-listener \
    --listener-arn arn:aws:elasticloadbalancing:ap-northeast-1:123456789012:listener/app/my-alb/abc123/def456 \
    --certificates CertificateArn=arn:aws:acm:ap-northeast-1:123456789012:certificate/12345678-1234-1234-1234-123456789012

五、監控與到期提醒

5.1 憑證到期監控腳本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash
# check_cert_expiry.sh - 憑證到期檢查腳本

WARNING_DAYS=30
CRITICAL_DAYS=7

ENDPOINTS=(
    "www.example.com:443"
    "api.example.com:443"
    "mail.example.com:993"
)

check_certificate() {
    local endpoint=$1
    local host="${endpoint%%:*}"
    local port="${endpoint##*:}"

    # 取得憑證到期日
    expiry_date=$(echo | openssl s_client -connect "$endpoint" \
        -servername "$host" 2>/dev/null | \
        openssl x509 -noout -enddate 2>/dev/null | \
        cut -d= -f2)

    if [ -z "$expiry_date" ]; then
        echo "ERROR: Cannot retrieve certificate for $endpoint"
        return 2
    fi

    # 計算剩餘天數
    expiry_epoch=$(date -d "$expiry_date" +%s)
    current_epoch=$(date +%s)
    days_remaining=$(( (expiry_epoch - current_epoch) / 86400 ))

    if [ $days_remaining -lt $CRITICAL_DAYS ]; then
        echo "CRITICAL: $endpoint expires in $days_remaining days ($expiry_date)"
        return 2
    elif [ $days_remaining -lt $WARNING_DAYS ]; then
        echo "WARNING: $endpoint expires in $days_remaining days ($expiry_date)"
        return 1
    else
        echo "OK: $endpoint expires in $days_remaining days ($expiry_date)"
        return 0
    fi
}

# 主程式
exit_code=0
for endpoint in "${ENDPOINTS[@]}"; do
    check_certificate "$endpoint"
    result=$?
    if [ $result -gt $exit_code ]; then
        exit_code=$result
    fi
done

exit $exit_code

5.2 Prometheus 監控整合

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# prometheus-blackbox-exporter 設定
# blackbox.yml
modules:
  https_2xx:
    prober: http
    timeout: 5s
    http:
      valid_http_versions: ["HTTP/1.1", "HTTP/2.0"]
      valid_status_codes: []
      method: GET
      tls_config:
        insecure_skip_verify: false

  ssl_expiry:
    prober: tcp
    timeout: 5s
    tcp:
      tls: true
      tls_config:
        insecure_skip_verify: false
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Prometheus scrape 設定
# prometheus.yml
scrape_configs:
  - job_name: 'ssl-expiry'
    metrics_path: /probe
    params:
      module: [ssl_expiry]
    static_configs:
      - targets:
          - www.example.com:443
          - api.example.com:443
    relabel_configs:
      - source_labels: [__address__]
        target_label: __param_target
      - source_labels: [__param_target]
        target_label: instance
      - target_label: __address__
        replacement: blackbox-exporter:9115
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Alertmanager 告警規則
# ssl_alerts.yml
groups:
  - name: ssl-certificates
    rules:
      - alert: SSLCertificateExpiringSoon
        expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 30
        for: 1h
        labels:
          severity: warning
        annotations:
          summary: "SSL Certificate expiring soon"
          description: "SSL certificate for {{ $labels.instance }} expires in less than 30 days"

      - alert: SSLCertificateExpiryCritical
        expr: probe_ssl_earliest_cert_expiry - time() < 86400 * 7
        for: 1h
        labels:
          severity: critical
        annotations:
          summary: "SSL Certificate expiring critically soon"
          description: "SSL certificate for {{ $labels.instance }} expires in less than 7 days"

5.3 自動化通知整合

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
#!/usr/bin/env python3
"""
Certificate Expiry Notification Service
"""

import ssl
import socket
import smtplib
from datetime import datetime, timedelta
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
import json

class CertExpiryNotifier:
    def __init__(self, smtp_config, warning_days=30, critical_days=7):
        self.smtp_config = smtp_config
        self.warning_days = warning_days
        self.critical_days = critical_days

    def get_cert_expiry(self, hostname, port=443):
        """取得憑證到期時間"""
        context = ssl.create_default_context()

        with socket.create_connection((hostname, port), timeout=10) as sock:
            with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                cert = ssock.getpeercert()
                expiry_str = cert['notAfter']
                expiry_date = datetime.strptime(expiry_str, '%b %d %H:%M:%S %Y %Z')
                return expiry_date

    def check_endpoints(self, endpoints):
        """檢查所有端點的憑證狀態"""
        results = []

        for endpoint in endpoints:
            hostname, port = endpoint.split(':')
            port = int(port)

            try:
                expiry_date = self.get_cert_expiry(hostname, port)
                days_remaining = (expiry_date - datetime.utcnow()).days

                status = 'ok'
                if days_remaining < self.critical_days:
                    status = 'critical'
                elif days_remaining < self.warning_days:
                    status = 'warning'

                results.append({
                    'endpoint': endpoint,
                    'expiry_date': expiry_date.isoformat(),
                    'days_remaining': days_remaining,
                    'status': status
                })
            except Exception as e:
                results.append({
                    'endpoint': endpoint,
                    'error': str(e),
                    'status': 'error'
                })

        return results

    def send_notification(self, results):
        """發送通知郵件"""
        alerts = [r for r in results if r['status'] in ('warning', 'critical', 'error')]

        if not alerts:
            return

        msg = MIMEMultipart('alternative')
        msg['Subject'] = f"[Certificate Alert] {len(alerts)} certificates require attention"
        msg['From'] = self.smtp_config['from']
        msg['To'] = self.smtp_config['to']

        html_content = self._generate_html_report(alerts)
        msg.attach(MIMEText(html_content, 'html'))

        with smtplib.SMTP(self.smtp_config['host'], self.smtp_config['port']) as server:
            server.starttls()
            server.login(self.smtp_config['username'], self.smtp_config['password'])
            server.send_message(msg)

    def _generate_html_report(self, alerts):
        """產生 HTML 報告"""
        rows = ""
        for alert in alerts:
            color = {'critical': '#dc3545', 'warning': '#ffc107', 'error': '#6c757d'}
            status_color = color.get(alert['status'], '#28a745')

            rows += f"""
            <tr>
                <td>{alert['endpoint']}</td>
                <td style="color: {status_color}; font-weight: bold;">
                    {alert['status'].upper()}
                </td>
                <td>{alert.get('days_remaining', 'N/A')}</td>
                <td>{alert.get('expiry_date', alert.get('error', 'N/A'))}</td>
            </tr>
            """

        return f"""
        <html>
        <body>
            <h2>Certificate Expiry Alert Report</h2>
            <p>The following certificates require your attention:</p>
            <table border="1" cellpadding="10" cellspacing="0">
                <tr style="background-color: #f2f2f2;">
                    <th>Endpoint</th>
                    <th>Status</th>
                    <th>Days Remaining</th>
                    <th>Expiry Date</th>
                </tr>
                {rows}
            </table>
            <p>Please take appropriate action to renew or replace these certificates.</p>
        </body>
        </html>
        """

# 使用範例
if __name__ == "__main__":
    smtp_config = {
        'host': 'smtp.example.com',
        'port': 587,
        'username': 'alerts@example.com',
        'password': 'secure_password',
        'from': 'alerts@example.com',
        'to': 'security-team@example.com'
    }

    endpoints = [
        'www.example.com:443',
        'api.example.com:443',
        'mail.example.com:993'
    ]

    notifier = CertExpiryNotifier(smtp_config)
    results = notifier.check_endpoints(endpoints)
    notifier.send_notification(results)

六、更新與輪換策略

6.1 憑證輪換最佳實踐

  1. 提前規劃:在憑證到期前 30-60 天開始更新流程
  2. 無停機更新:使用藍綠部署或滾動更新策略
  3. 自動化優先:盡可能使用自動化工具完成更新
  4. 測試驗證:更新後進行完整的功能與安全測試

6.2 Kubernetes Secret 輪換

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# 使用 cert-manager 自動輪換
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: auto-rotating-cert
  namespace: production
spec:
  secretName: auto-rotating-cert-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
    - www.example.com
  # 關鍵設定:自動輪換
  duration: 2160h      # 90 天有效期
  renewBefore: 720h    # 到期前 30 天自動更新
  privateKey:
    rotationPolicy: Always  # 每次更新時輪換私鑰

6.3 自動化輪換腳本

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#!/bin/bash
# rotate_certificate.sh - 憑證輪換腳本

set -e

DOMAIN="www.example.com"
CERT_DIR="/etc/letsencrypt/live/$DOMAIN"
BACKUP_DIR="/var/backup/certs/$(date +%Y%m%d_%H%M%S)"
SERVICES=("nginx" "haproxy")

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

# 1. 備份現有憑證
log "Backing up current certificates..."
mkdir -p "$BACKUP_DIR"
cp -r "$CERT_DIR" "$BACKUP_DIR/"

# 2. 更新憑證
log "Renewing certificate..."
certbot renew --cert-name "$DOMAIN" --force-renewal

# 3. 驗證新憑證
log "Validating new certificate..."
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
    "$CERT_DIR/fullchain.pem"

# 4. 檢查私鑰與憑證匹配
CERT_MODULUS=$(openssl x509 -noout -modulus -in "$CERT_DIR/cert.pem" | md5sum)
KEY_MODULUS=$(openssl rsa -noout -modulus -in "$CERT_DIR/privkey.pem" | md5sum)

if [ "$CERT_MODULUS" != "$KEY_MODULUS" ]; then
    log "ERROR: Certificate and key do not match!"
    exit 1
fi

# 5. 重新載入服務
for service in "${SERVICES[@]}"; do
    log "Reloading $service..."
    if systemctl is-active --quiet "$service"; then
        systemctl reload "$service"
    fi
done

# 6. 驗證服務狀態
log "Verifying service connectivity..."
sleep 5
if curl -sI "https://$DOMAIN" | head -1 | grep -q "200\|301\|302"; then
    log "SUCCESS: Certificate rotation completed successfully"
else
    log "WARNING: Service may not be responding correctly"
    exit 1
fi

log "Certificate rotation completed"

七、撤銷與廢棄處理

7.1 憑證撤銷時機

憑證應在以下情況立即撤銷:

  • 私鑰洩露或遭受入侵
  • 員工離職或權限變更
  • 憑證資訊有誤
  • 不再使用的服務或域名

7.2 撤銷憑證

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 使用 OpenSSL 向 CA 提交撤銷請求
# (需要 CA 的撤銷端點和授權)
openssl ca -revoke /path/to/certificate.pem \
    -keyfile /path/to/ca-key.pem \
    -cert /path/to/ca-cert.pem

# 使用 Certbot 撤銷 Let's Encrypt 憑證
certbot revoke --cert-path /etc/letsencrypt/live/www.example.com/cert.pem

# 同時刪除本地憑證檔案
certbot revoke --cert-path /etc/letsencrypt/live/www.example.com/cert.pem --delete-after-revoke

7.3 管理 CRL(Certificate Revocation List)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# 產生 CRL
openssl ca -gencrl \
    -keyfile /path/to/ca-key.pem \
    -cert /path/to/ca-cert.pem \
    -out /path/to/crl.pem \
    -crldays 30

# 檢視 CRL 內容
openssl crl -in /path/to/crl.pem -noout -text

# 驗證憑證是否在 CRL 中
openssl verify -crl_check \
    -CAfile /path/to/ca-cert.pem \
    -CRLfile /path/to/crl.pem \
    /path/to/certificate.pem

7.4 OCSP(Online Certificate Status Protocol)驗證

1
2
3
4
5
6
7
8
9
# 取得 OCSP Responder URL
openssl x509 -in certificate.pem -noout -ocsp_uri

# 驗證憑證狀態
openssl ocsp \
    -issuer issuer.pem \
    -cert certificate.pem \
    -url http://ocsp.example.com \
    -resp_text

八、治理與合規報告

8.1 憑證治理框架

有效的憑證治理應涵蓋:

面向內容
政策憑證申請審核流程、金鑰長度標準、有效期限規範
人員明確的權責分配、培訓計畫
流程標準化作業程序(SOP)、變更管理
技術自動化工具、監控告警系統
稽核定期審查、合規報告

8.2 合規報告產生

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#!/usr/bin/env python3
"""
Certificate Compliance Report Generator
"""

import json
import ssl
import socket
from datetime import datetime
from jinja2 import Template

class ComplianceReporter:
    def __init__(self):
        self.compliance_rules = {
            'min_key_size': 2048,
            'max_validity_days': 398,  # CA/B Forum 要求
            'required_key_usage': ['Digital Signature', 'Key Encipherment'],
            'weak_algorithms': ['MD5', 'SHA1', 'RC4', 'DES']
        }

    def analyze_certificate(self, hostname, port=443):
        """分析憑證合規狀態"""
        context = ssl.create_default_context()

        with socket.create_connection((hostname, port), timeout=10) as sock:
            with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                cert = ssock.getpeercert()
                cert_bin = ssock.getpeercert(binary_form=True)

        # 解析憑證詳情(簡化版)
        findings = []

        # 檢查有效期
        not_after = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')
        not_before = datetime.strptime(cert['notBefore'], '%b %d %H:%M:%S %Y %Z')
        validity_days = (not_after - not_before).days

        if validity_days > self.compliance_rules['max_validity_days']:
            findings.append({
                'severity': 'warning',
                'rule': 'max_validity_days',
                'message': f'Certificate validity ({validity_days} days) exceeds recommended maximum'
            })

        return {
            'hostname': hostname,
            'subject': dict(x[0] for x in cert['subject']),
            'issuer': dict(x[0] for x in cert['issuer']),
            'not_before': not_before.isoformat(),
            'not_after': not_after.isoformat(),
            'validity_days': validity_days,
            'serial_number': cert.get('serialNumber', 'N/A'),
            'findings': findings,
            'compliant': len([f for f in findings if f['severity'] == 'critical']) == 0
        }

    def generate_report(self, endpoints):
        """產生合規報告"""
        results = []
        for endpoint in endpoints:
            hostname, port = endpoint.split(':')
            try:
                result = self.analyze_certificate(hostname, int(port))
                results.append(result)
            except Exception as e:
                results.append({
                    'hostname': hostname,
                    'error': str(e),
                    'compliant': False
                })

        return self._render_report(results)

    def _render_report(self, results):
        """渲染報告為 HTML"""
        template = Template('''
<!DOCTYPE html>
<html>
<head>
    <title>Certificate Compliance Report</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 40px; }
        table { border-collapse: collapse; width: 100%; }
        th, td { border: 1px solid #ddd; padding: 12px; text-align: left; }
        th { background-color: #4CAF50; color: white; }
        .compliant { color: green; }
        .non-compliant { color: red; }
        .warning { color: orange; }
    </style>
</head>
<body>
    <h1>Certificate Compliance Report</h1>
    <p>Generated: {{ timestamp }}</p>

    <h2>Summary</h2>
    <ul>
        <li>Total Certificates Analyzed: {{ total }}</li>
        <li>Compliant: {{ compliant }}</li>
        <li>Non-Compliant: {{ non_compliant }}</li>
    </ul>

    <h2>Details</h2>
    <table>
        <tr>
            <th>Hostname</th>
            <th>Subject</th>
            <th>Issuer</th>
            <th>Expiry</th>
            <th>Status</th>
        </tr>
        {% for result in results %}
        <tr>
            <td>{{ result.hostname }}</td>
            <td>{{ result.subject.get('commonName', 'N/A') if result.subject else 'Error' }}</td>
            <td>{{ result.issuer.get('commonName', 'N/A') if result.issuer else 'Error' }}</td>
            <td>{{ result.not_after if result.not_after else 'N/A' }}</td>
            <td class="{{ 'compliant' if result.compliant else 'non-compliant' }}">
                {{ 'COMPLIANT' if result.compliant else 'NON-COMPLIANT' }}
            </td>
        </tr>
        {% endfor %}
    </table>

    <h2>Findings</h2>
    {% for result in results %}
    {% if result.findings %}
    <h3>{{ result.hostname }}</h3>
    <ul>
    {% for finding in result.findings %}
        <li class="{{ finding.severity }}">
            [{{ finding.severity | upper }}] {{ finding.message }}
        </li>
    {% endfor %}
    </ul>
    {% endif %}
    {% endfor %}
</body>
</html>
        ''')

        compliant_count = sum(1 for r in results if r.get('compliant', False))

        return template.render(
            results=results,
            timestamp=datetime.now().isoformat(),
            total=len(results),
            compliant=compliant_count,
            non_compliant=len(results) - compliant_count
        )

# 使用範例
if __name__ == "__main__":
    reporter = ComplianceReporter()

    endpoints = [
        'www.example.com:443',
        'api.example.com:443',
        'mail.example.com:993'
    ]

    report = reporter.generate_report(endpoints)

    with open('compliance_report.html', 'w') as f:
        f.write(report)

    print("Report generated: compliance_report.html")

8.3 定期稽核清單

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
## 憑證管理月度稽核清單

### 資產盤點
- [ ] 執行全網憑證掃描
- [ ] 更新憑證資產清單
- [ ] 確認所有憑證擁有者資訊正確

### 到期管理
- [ ] 檢視未來 90 天到期的憑證
- [ ] 確認更新計畫已排定
- [ ] 追蹤過期憑證處理狀態

### 安全合規
- [ ] 檢查弱加密演算法使用情況
- [ ] 驗證憑證鏈完整性
- [ ] 確認 CRL/OCSP 服務正常運作

### 流程改善
- [ ] 審查自動化覆蓋率
- [ ] 評估工具效能與可靠性
- [ ] 更新作業程序文件

總結

企業憑證生命週期管理是一項複雜但至關重要的工作。透過本文介紹的各個階段:

  1. 發現盤點:掌握所有憑證資產
  2. 申請核發:標準化憑證取得流程
  3. 部署安裝:自動化減少人為錯誤
  4. 監控提醒:及早發現問題
  5. 更新輪換:確保服務持續運作
  6. 撤銷廢棄:妥善處理不需要的憑證
  7. 治理合規:滿足法規與稽核需求

透過適當的工具、自動化與流程,企業可以有效降低憑證相關風險,確保數位資產的安全性。建議組織逐步導入自動化工具如 cert-manager、HashiCorp Vault 或商業 CLM 解決方案,以達到更高的管理成熟度。


參考資源

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