ssl-tls-management について
Webサーバー、Kubernetesクラスター、内部サービスのTLS証明書と安全な通信を扱うガイドです。HTTPSの設定や証明書の更新・監視を担う運用担当者向けです。
主な機能
- 01Certbotによる証明書の発行と更新
- 02Kubernetesへのcert-manager導入
- 03鍵の生成と証明書チェーンの確認
- 04NginxとApacheのTLS設定
- 05証明書期限の監視とアラート
活用シーン
- 01新しいWebアプリでHTTPSを有効化
- 02Let's Encrypt証明書の更新を自動化
- 03Kubernetesサービス向け証明書の導入
- 04期限切れ間近または侵害された証明書への対応
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ bagelhole/ devops-security-agent-skills --skill ssl-tls-managementインストール後、Claude Code で/ssl-tls-managementを入力して読み込まれたか確認します。
詳細
SSL/TLS 管理
Web サーバー、Kubernetes クラスター、内部サービス全体で証明書を管理し、安全な通信を実現します。
このスキルを使用する場面
次のような場合にこのスキルを使用します。
- 新しい Web アプリケーションで HTTPS を設定する
- Let's Encrypt による証明書の更新を自動化する
- Kubernetes に cert-manager をデプロイする
- 内部サービス間通信の TLS を設定する
- コンプライアンス対応のため、暗号スイートと TLS バージョンを監査する
- 期限切れまたは侵害された証明書に対応する
前提条件
- パブリック証明書を取得するための DNS 管理権限があるドメイン名
- Web サーバーへの root/sudo アクセス
- Let's Encrypt 用の
certbotがインストールされていること opensslCLI が利用可能であること(ほとんどの Linux ディストリビューションではデフォルトでインストールされています)- cert-manager のデプロイに使用する Helm がインストールされた Kubernetes クラスター
- X.509 証明書の信頼チェーンに関する知識
Certbot を使用した Let's Encrypt
インストールと証明書の発行
# Install certbot (Ubuntu/Debian)
apt update && apt install -y certbot python3-certbot-nginx
# Obtain certificate for nginx (interactive)
certbot --nginx -d example.com -d www.example.com
# Non-interactive mode for automation
certbot certonly --nginx \
-d example.com \
-d www.example.com \
--non-interactive \
--agree-tos \
--email [email protected]
# Standalone mode (when no web server is running)
certbot certonly --standalone \
-d example.com \
--preferred-challenges http
# DNS challenge (for wildcard certs)
certbot certonly --manual \
--preferred-challenges dns \
-d "*.example.com" \
-d example.com
# Using DNS plugin for automation (Cloudflare example)
pip install certbot-dns-cloudflare
cat > /etc/letsencrypt/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini
certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d "*.example.com" \
-d example.com
更新の自動化
# Test renewal
certbot renew --dry-run
# Systemd timer (preferred over cron)
cat > /etc/systemd/system/certbot-renewal.service << 'EOF'
[Unit]
Description=Certbot certificate renewal
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
EOF
cat > /etc/systemd/system/certbot-renewal.timer << 'EOF'
[Unit]
Description=Run certbot renewal twice daily
[Timer]
OnCalendar=*-*-* 00,12:00:00
RandomizedDelaySec=3600
Persistent=true
[Install]
WantedBy=timers.target
EOF
systemctl enable --now certbot-renewal.timer
# Verify timer is active
systemctl list-timers certbot-renewal.timer
# Renewal hooks for post-renewal actions
mkdir -p /etc/letsencrypt/renewal-hooks/deploy
cat > /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh << 'HOOK'
#!/bin/bash
systemctl reload nginx
# Also reload other services using the cert
systemctl reload haproxy 2>/dev/null || true
HOOK
chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-services.sh
Kubernetes 用 cert-manager
インストール
# Install with Helm
helm repo add jetstack https://charts.jetstack.io
helm repo update
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--version v1.14.0 \
--set installCRDs=true \
--set prometheus.enabled=true
# Verify installation
kubectl get pods -n cert-manager
kubectl get crds | grep cert-manager
ClusterIssuer の設定
# letsencrypt-staging (use for testing first)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-staging
solvers:
- http01:
ingress:
class: nginx
---
# letsencrypt-prod
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod
solvers:
- http01:
ingress:
class: nginx
---
# DNS challenge solver (for wildcard certs with Cloudflare)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: [email protected]
privateKeySecretRef:
name: letsencrypt-prod-dns
solvers:
- dns01:
cloudflare:
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
---
# Self-signed CA issuer for internal services
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-ca
spec:
selfSigned: {}
Certificate リソース
# Public-facing certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-cert
namespace: default
spec:
secretName: example-tls
issuerRef:
name: letsencrypt-prod
kind: ClusterIssuer
dnsNames:
- example.com
- www.example.com
duration: 2160h # 90 days
renewBefore: 720h # 30 days before expiry
---
# Wildcard certificate
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-cert
namespace: default
spec:
secretName: wildcard-tls
issuerRef:
name: letsencrypt-prod-dns
kind: ClusterIssuer
dnsNames:
- "*.example.com"
- example.com
---
# Ingress with automatic TLS
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- example.com
secretName: example-tls
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80
OpenSSL コマンドのリファレンス
# Generate a private key
openssl genrsa -out server.key 4096
# Generate an ECDSA key (preferred for performance)
openssl ecparam -genkey -name prime256v1 -out server-ec.key
# Generate a CSR (Certificate Signing Request)
openssl req -new -key server.key -out server.csr \
-subj "/C=US/ST=California/L=San Francisco/O=Acme Corp/CN=example.com"
# Generate CSR with SAN (Subject Alternative Names)
openssl req -new -key server.key -out server.csr -config <(cat <<EOF
[req]
default_bits = 4096
distinguished_name = dn
req_extensions = san
prompt = no
[dn]
CN = example.com
O = Acme Corp
C = US
[san]
subjectAltName = DNS:example.com,DNS:www.example.com,DNS:api.example.com
EOF
)
# Generate self-signed certificate (development/testing)
openssl req -x509 -nodes -days 365 -newkey rsa:4096 \
-keyout selfsigned.key -out selfsigned.crt \
-subj "/CN=localhost"
# View certificate details
openssl x509 -in cert.pem -noout -text
# Check certificate expiration date
openssl x509 -in cert.pem -noout -dates
# Check remote certificate
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | \
openssl x509 -noout -dates -subject -issuer
# Verify certificate chain
openssl verify -CAfile ca-bundle.crt server.crt
# Check certificate chain from remote server
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
openssl x509 -noout -text
# Convert PEM to PKCS12
openssl pkcs12 -export -out cert.pfx -inkey server.key -in server.crt -certfile ca.crt
# Convert PKCS12 to PEM
openssl pkcs12 -in cert.pfx -out cert.pem -nodes
# Test TLS connection and cipher negotiation
openssl s_client -connect example.com:443 -tls1_3
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384'
強固な TLS 設定
Nginx
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Protocol versions
ssl_protocols TLSv1.2 TLSv1.3;
# Cipher suites (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;
ssl_prefer_server_ciphers off;
# Session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
# HTTP to HTTPS redirect (in separate server block)
}
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
Apache
<VirtualHost *:443>
ServerName example.com
SSLEngine on
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
SSLHonorCipherOrder off
SSLUseStapling on
SSLStaplingCache shmcb:/tmp/stapling_cache(128000)
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
</VirtualHost>
証明書の監視
#!/bin/bash
# cert-monitor.sh - Monitor certificate expiration across hosts
WARN_DAYS=30
CRIT_DAYS=7
HOSTS=(
"example.com:443"
"api.example.com:443"
"admin.example.com:443"
)
for host in "${HOSTS[@]}"; do
expiry=$(echo | openssl s_client -connect "$host" -servername "${host%%:*}" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$expiry" ]; then
echo "ERROR: Cannot connect to $host"
continue
fi
expiry_epoch=$(date -d "$expiry" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -le "$CRIT_DAYS" ]; then
echo "CRITICAL: $host expires in $days_left days ($expiry)"
elif [ "$days_left" -le "$WARN_DAYS" ]; then
echo "WARNING: $host expires in $days_left days ($expiry)"
else
echo "OK: $host expires in $days_left days ($expiry)"
fi
done
Prometheus の cert-manager メトリクス
# Alert on expiring certificates in Kubernetes
groups:
- name: cert-manager
rules:
- alert: CertificateExpiringSoon
expr: certmanager_certificate_expiration_timestamp_seconds - time() < 7 * 24 * 3600
for: 1h
labels:
severity: critical
annotations:
summary: "Certificate {{ $labels.name }} expires in less than 7 days"
- alert: CertificateNotReady
expr: certmanager_certificate_ready_status{condition="True"} == 0
for: 15m
labels:
severity: warning
annotations:
summary: "Certificate {{ $labels.name }} is not ready"
トラブルシューティング
| 問題 | 原因 | 解決策 |
|---|---|---|
| Certbot が「connection refused」で失敗する | ファイアウォールでポート 80 がブロックされている | ACME HTTP-01 チャレンジ用にポート 80 を開放します |
| 「Too many certificates already issued」 | Let's Encrypt のレート制限に達している | テストにはステージングエンドポイントを使用し、レート制限がリセットされるまで待ちます |
| cert-manager のチャレンジが pending のまま停止する | Ingress または DNS の設定に誤りがある | kubectl describe challenge を確認し、DNS レコードを検証します |
| 混在コンテンツの警告が表示される | HTTPS ページ上のリソースが HTTP を使用している | すべてのアセット URL を HTTPS に更新し、CSP ヘッダーを使用します |
| OCSP ステープリングが機能しない | リゾルバーが設定されていない | nginx に resolver ディレクティブを追加し、外部 DNS を検証します |
| 中間証明書が欠落している | 不完全なチェーンが配信されている | cert.pem ではなく fullchain.pem を使用し、openssl s_client -showcerts で検証します |
| TLS ハンドシェイクに失敗する | クライアントが提示された暗号に対応していない | TLS 1.2 のサポートを追加し、暗号スイートとの互換性を確認します |
ベストプラクティス
- systemd タイマーまたは cert-manager を使って更新を自動化します
- アラートを設定して有効期限を監視します(30日前と7日前に警告)
- TLS 1.2 と TLS 1.3 のみを使用します
- 長い max-age と includeSubDomains を設定して HSTS を有効にします
- ハンドシェイクのパフォーマンスを向上させるため、OCSP ステープリングを有効にします
- 可能な場合は、パフォーマンス向上のため ECDSA キーを使用します
- SSL Labs(ssllabs.com/ssltest)で設定をテストします
- 適切なファイル権限(0600)を設定して秘密鍵を保護します
- 期限切れになってからではなく、期限前に証明書をローテーションします
- すべてのサービスの証明書一覧を管理します
関連スキル
- hashicorp-vault - PKI 管理
- waf-setup - Web 保護
- zero-trust - mTLS と ID ベースのアクセス
コメント
Markdown 対応