- Authors
- Name
- はじめに
- 1. SSL/TLSの動作原理
- 2. 証明書の種類
- 3. Let's Encrypt + Certbot 実践
- 4. Nginx SSL設定(A+グレード)
- 5. AWS ACM証明書管理
- 6. 証明書の自動更新
- 7. 証明書デバッグコマンド集
- 8. トラブルシューティング
- 9. 高度な設定
- 10. 運用チェックリスト
- まとめ
はじめに
HTTPSはもはや選択ではなく必須です。ブラウザはHTTPサイトに「安全ではありません」という警告を表示し、検索エンジンはHTTPSをランキングシグナルとして活用しています。しかし、SSL/TLS証明書を正しく理解し運用することは、思ったより複雑です。証明書の種類選択、正しい設定、自動更新、チェーン構成など、見落としやすいポイントが多くあります。
この記事では、SSL/TLSの動作原理から実践的な運用まで、一気にまとめます。
1. SSL/TLSの動作原理
対称暗号化 vs 非対称暗号化
SSL/TLSは2つの暗号化方式を組み合わせて使用します。
| 区分 | 対称暗号化 | 非対称暗号化 |
|---|---|---|
| 鍵 | 1つの鍵で暗号化/復号化 | 公開鍵(暗号化)+ 秘密鍵(復号化) |
| 速度 | 高速 | 低速(100〜1000倍) |
| アルゴリズム | AES-256, ChaCha20 | RSA, ECDSA, Ed25519 |
| 用途 | 実データの転送 | 鍵交換、証明書署名 |
| 鍵長 | 128/256 bit | 2048/4096 bit (RSA) |
TLS 1.3 ハンドシェイクの流れ
TLS 1.3ではハンドシェイクが1-RTTに短縮されました。
Client Server
| |
|--- ClientHello (supported ciphers, key share) ->|
| |
|<-- ServerHello (chosen cipher, key share, |
| EncryptedExtensions, Certificate, |
| CertificateVerify, Finished) ---------------|
| |
|--- Finished (encrypted) ---------------------->|
| |
|<========= Application Data (encrypted) =======>|
TLS 1.3の主な改善点:
- 1-RTTハンドシェイク:TLS 1.2の2-RTTから1-RTTに短縮
- 0-RTT再接続:再接続時に追加の遅延なしでデータ送信が可能
- 強化されたセキュリティ:脆弱な暗号スイートの削除(RC4、3DES、CBCモードなど)
- Forward Secrecy必須:すべての鍵交換でECDHEまたはDHEを使用
証明書検証フロー
[ブラウザ] -> サーバー証明書を受信
-> 証明書の有効期間を確認
-> ドメイン名の一致を確認
-> 証明書チェーンの検証(サーバー証明書 → 中間CA → ルートCA)
-> OCSP/CRLで失効状態を確認
-> 検証成功 → 鍵アイコンを表示
2. 証明書の種類
検証レベル別の分類
| 種類 | 検証レベル | 発行時間 | コスト | 用途 |
|---|---|---|---|---|
| DV(Domain Validation) | ドメイン所有権のみ確認 | 数分 | 無料〜低額 | 個人ブログ、小規模サービス |
| OV(Organization Validation) | 組織の実在確認 | 1〜3日 | 中程度 | 企業Webサイト |
| EV(Extended Validation) | 法的実体の確認 | 1〜2週間 | 高額 | 金融、EC |
適用範囲別の分類
| 種類 | 適用範囲 | 例 | コスト |
|---|---|---|---|
| 単一ドメイン | 1つのFQDN | www.example.com | 最も安価 |
| ワイルドカード | 同レベルのすべてのサブドメイン | *.example.com | 中程度 |
| SAN/マルチドメイン | 複数ドメインを1つの証明書に | example.com、example.org | ドメインごとに追加費用 |
ワイルドカード証明書の注意点:
# *.example.comがカバーする範囲
www.example.com # O カバーされる
api.example.com # O カバーされる
example.com # X カバーされない(SANに別途追加が必要)
sub.api.example.com # X カバーされない(2段階サブドメイン)
3. Let's Encrypt + Certbot 実践
Certbotのインストール
# Ubuntu/Debian
sudo apt update
sudo apt install -y certbot python3-certbot-nginx
# CentOS/Rocky Linux
sudo dnf install -y epel-release
sudo dnf install -y certbot python3-certbot-nginx
# macOS(開発用)
brew install certbot
# Docker
docker run -it --rm \
-v /etc/letsencrypt:/etc/letsencrypt \
-v /var/lib/letsencrypt:/var/lib/letsencrypt \
certbot/certbot certonly --help
証明書発行方式
Webroot方式(サービス停止なし)
# Nginxが実行中の状態で発行
sudo certbot certonly --webroot \
-w /var/www/html \
-d example.com \
-d www.example.com \
--email admin@example.com \
--agree-tos \
--no-eff-email
# Nginx設定に.well-knownパスの追加が必要
# location /.well-known/acme-challenge/ {
# root /var/www/html;
# }
Standalone方式(ポート80が必要)
# ポート80を使用するサービスを一時停止する必要あり
sudo systemctl stop nginx
sudo certbot certonly --standalone \
-d example.com \
-d www.example.com
sudo systemctl start nginx
DNS方式(ワイルドカード証明書発行時に必須)
# DNS TXTレコードで検証
sudo certbot certonly --manual \
--preferred-challenges dns \
-d "*.example.com" \
-d example.com
# 自動化のためのDNSプラグイン使用(Cloudflareの例)
sudo pip install certbot-dns-cloudflare
# APIトークンファイルの作成
cat > /etc/letsencrypt/cloudflare.ini << 'EOF'
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
EOF
chmod 600 /etc/letsencrypt/cloudflare.ini
# Cloudflare DNSで自動発行
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d "*.example.com" \
-d example.com
発行された証明書ファイルの構造
/etc/letsencrypt/live/example.com/
├── cert.pem # サーバー証明書
├── chain.pem # 中間CA証明書
├── fullchain.pem # cert.pem + chain.pem(Nginxで使用)
├── privkey.pem # 秘密鍵
└── README
4. Nginx SSL設定(A+グレード)
基本SSL設定
# /etc/nginx/sites-available/example.com
server {
listen 80;
server_name example.com www.example.com;
# HTTP -> HTTPSリダイレクト
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com www.example.com;
# 証明書設定
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# SSLプロトコル(TLS 1.2、1.3のみ許可)
ssl_protocols TLSv1.2 TLSv1.3;
# 暗号スイート(サーバー優先)
ssl_prefer_server_ciphers on;
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';
# ECDHカーブ
ssl_ecdh_curve X25519:prime256v1:secp384r1;
# SSLセッションキャッシュ
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
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;
# DHパラメータ(2048ビット以上)
ssl_dhparam /etc/nginx/dhparam.pem;
# セキュリティヘッダー
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;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline';" always;
root /var/www/example.com;
index index.html;
}
DHパラメータの生成
# 2048ビットDHパラメータの生成(約1〜2分かかります)
sudo openssl dhparam -out /etc/nginx/dhparam.pem 2048
# 4096ビット(より安全ですが5〜10分かかります)
sudo openssl dhparam -out /etc/nginx/dhparam.pem 4096
SSL Labsテスト
# 設定の検証
sudo nginx -t
# 設定の適用
sudo systemctl reload nginx
# SSL LabsでA+グレードを確認
# https://www.ssllabs.com/ssltest/analyze.html?d=example.com
# CLIで確認
curl -sS https://api.ssllabs.com/api/v3/analyze?host=example.com | jq '.endpoints[0].grade'
5. AWS ACM証明書管理
ACM証明書の発行
# AWS CLIで証明書をリクエスト
aws acm request-certificate \
--domain-name example.com \
--subject-alternative-names "*.example.com" \
--validation-method DNS \
--region ap-northeast-2
# 検証状態の確認
aws acm describe-certificate \
--certificate-arn arn:aws:acm:ap-northeast-2:123456789012:certificate/abc-123 \
--query 'Certificate.DomainValidationOptions'
TerraformによるACM + Route53の自動化
# ACM証明書リソース
resource "aws_acm_certificate" "main" {
domain_name = "example.com"
subject_alternative_names = ["*.example.com"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
# Route53 DNS検証レコードの自動作成
resource "aws_route53_record" "cert_validation" {
for_each = {
for dvo in aws_acm_certificate.main.domain_validation_options : dvo.domain_name => {
name = dvo.resource_record_name
type = dvo.resource_record_type
record = dvo.resource_record_value
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
ttl = 60
type = each.value.type
zone_id = data.aws_route53_zone.main.zone_id
}
# 証明書検証完了の待機
resource "aws_acm_certificate_validation" "main" {
certificate_arn = aws_acm_certificate.main.arn
validation_record_fqdns = [for record in aws_route53_record.cert_validation : record.fqdn]
}
# ALBに証明書を関連付け
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = aws_acm_certificate_validation.main.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.main.arn
}
}
ACM vs Let's Encrypt 比較
| 項目 | AWS ACM | Let's Encrypt |
|---|---|---|
| コスト | 無料(AWSサービス連携時) | 無料 |
| 有効期間 | 13ヶ月(自動更新) | 90日(自動更新が必要) |
| ワイルドカード | 対応 | 対応(DNS検証必須) |
| 使用範囲 | AWS ALB、CloudFront、API GW | どこでも使用可能 |
| 証明書エクスポート | 不可(Private CA除く) | 可能 |
| 管理負荷 | ほぼなし | Certbot管理が必要 |
6. 証明書の自動更新
Cronによる更新
# Certbot更新テスト
sudo certbot renew --dry-run
# Cronジョブの登録
sudo crontab -e
# 毎日午前3時に更新確認(期限30日以内の場合自動更新)
0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
Systemd Timerによる更新(推奨)
# /etc/systemd/system/certbot-renewal.service
cat > /etc/systemd/system/certbot-renewal.service << 'EOF'
[Unit]
Description=Certbot Renewal
After=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx"
ExecStartPost=/bin/bash -c 'echo "Certbot renewal completed at $(date)" >> /var/log/certbot-renewal.log'
EOF
# /etc/systemd/system/certbot-renewal.timer
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
# 有効化
sudo systemctl daemon-reload
sudo systemctl enable --now certbot-renewal.timer
# タイマーの状態確認
sudo systemctl list-timers | grep certbot
更新監視スクリプト
#!/bin/bash
# /usr/local/bin/check-ssl-expiry.sh
DOMAINS=("example.com" "api.example.com" "admin.example.com")
ALERT_DAYS=14
SLACK_WEBHOOK="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
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)
if [ -z "$expiry_date" ]; then
echo "ERROR: Cannot connect to $domain"
continue
fi
expiry_epoch=$(date -d "$expiry_date" +%s 2>/dev/null || date -j -f "%b %d %T %Y %Z" "$expiry_date" +%s)
current_epoch=$(date +%s)
days_left=$(( (expiry_epoch - current_epoch) / 86400 ))
echo "$domain: $days_left days until expiry ($expiry_date)"
if [ "$days_left" -lt "$ALERT_DAYS" ]; then
curl -s -X POST "$SLACK_WEBHOOK" \
-H 'Content-type: application/json' \
-d "{\"text\":\"SSL証明書期限切れ警告:$domain - 残り${days_left}日\"}"
fi
done
# 実行権限の付与とcronの登録
chmod +x /usr/local/bin/check-ssl-expiry.sh
echo "0 9 * * * /usr/local/bin/check-ssl-expiry.sh" | sudo crontab -
7. 証明書デバッグコマンド集
# 証明書情報の確認
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -text -noout
# 有効期限のみ確認
openssl x509 -in /etc/letsencrypt/live/example.com/cert.pem -noout -enddate
# リモートサーバーの証明書確認
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -text
# 証明書チェーンの確認
echo | openssl s_client -showcerts -servername example.com -connect example.com:443 2>/dev/null
# 証明書と秘密鍵のマッチング確認
openssl x509 -noout -modulus -in cert.pem | openssl md5
openssl rsa -noout -modulus -in privkey.pem | openssl md5
# 2つの値が一致すればマッチしている
# 証明書SANの確認
openssl x509 -in cert.pem -noout -ext subjectAltName
# PEM → PFX(PKCS12)変換
openssl pkcs12 -export -out cert.pfx \
-inkey privkey.pem -in cert.pem -certfile chain.pem
# PFX → PEM変換
openssl pkcs12 -in cert.pfx -out cert.pem -nodes
8. トラブルシューティング
問題1:証明書の期限切れ(ERR_CERT_DATE_INVALID)
# 現在の有効期限を確認
echo | openssl s_client -connect example.com:443 2>/dev/null \
| openssl x509 -noout -dates
# 即座に更新
sudo certbot renew --force-renewal --cert-name example.com
sudo systemctl reload nginx
問題2:証明書チェーンの不完全(ERR_CERT_AUTHORITY_INVALID)
# チェーンの検証
openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt \
/etc/letsencrypt/live/example.com/fullchain.pem
# Nginxでfullchain.pemを使用しているか確認
# 誤った設定:
# ssl_certificate /etc/letsencrypt/live/example.com/cert.pem;
# 正しい設定:
# ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
問題3:Mixed Content警告
# HTMLからhttp://リソースを検索
grep -rn "http://" /var/www/html/ --include="*.html" --include="*.js" --include="*.css"
# CSPヘッダーで自動アップグレード
# Nginx設定に追加:
# add_header Content-Security-Policy "upgrade-insecure-requests;" always;
問題4:SSLハンドシェイクの失敗
# サポートされているプロトコルの確認
nmap --script ssl-enum-ciphers -p 443 example.com
# 特定のTLSバージョンで接続テスト
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
# クライアントがサポートしていない暗号スイートの問題
# → ssl_ciphers設定に互換性のあるスイートを追加
問題5:HSTS関連の問題
# HSTSヘッダーが設定されるとHTTPに戻れなくなる
# テスト時にはmax-ageを短く設定
add_header Strict-Transport-Security "max-age=300" always;
# 本番環境では十分に長く設定
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
問題6:Let's Encrypt Rate Limit
# Rate Limitの詳細
# - 同一ドメイン:週5回の発行(更新を除く)
# - 重複証明書:週5回
# - アカウントごと:3時間に300リクエスト
# テスト時にはstagingサーバーを使用
sudo certbot certonly --staging \
--webroot -w /var/www/html \
-d test.example.com
# staging証明書を削除して本番発行
sudo certbot delete --cert-name test.example.com
9. 高度な設定
mTLS(相互TLS認証)
server {
listen 443 ssl http2;
server_name api.example.com;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
# クライアント証明書を要求
ssl_client_certificate /etc/nginx/ssl/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
location / {
# クライアント証明書情報をバックエンドに転送
proxy_set_header X-Client-Cert-DN $ssl_client_s_dn;
proxy_set_header X-Client-Cert-Serial $ssl_client_serial;
proxy_pass http://backend;
}
}
クライアント証明書の生成
# CA鍵と証明書の生成
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/C=KR/ST=Seoul/O=MyOrg/CN=Internal CA"
# クライアント鍵とCSRの生成
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr \
-subj "/C=KR/ST=Seoul/O=MyOrg/CN=client1"
# クライアント証明書の発行
openssl x509 -req -days 365 \
-in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-out client.crt
# テスト
curl --cert client.crt --key client.key https://api.example.com/
Certificate Transparency(CT)ログの確認
# CTログでドメイン証明書を検索
# https://crt.sh/?q=example.com
# CLIで確認
curl -s "https://crt.sh/?q=example.com&output=json" | jq '.[0:5] | .[] | {id, name_value, not_after}'
10. 運用チェックリスト
初期設定チェックリスト
- TLS 1.2以上のみ許可(
ssl_protocols TLSv1.2 TLSv1.3) - 強力な暗号スイートのみ使用(ECDHE + AES-GCM/ChaCha20)
- HSTSヘッダーの設定(
Strict-Transport-Security) - HTTP → HTTPSリダイレクト(301)
- OCSP Staplingの有効化
- DHパラメータ2048ビット以上の生成
- SSL LabsでA+グレードの確認
- Mixed Contentがないことの確認
自動更新チェックリスト
- Certbot自動更新の設定(cronまたはsystemd timer)
- 更新テスト(
certbot renew --dry-run) - 更新後のNginx reloadフックの設定
- 期限切れ監視スクリプトのデプロイ
- Slack/Emailアラートの設定(期限切れ14日前)
定期点検チェックリスト
- 毎月証明書の有効期限を確認
- 四半期ごとにSSL Labsで再テスト
- 新しいセキュリティ脆弱性パッチの確認(OpenSSL、Nginx)
- CTログで不正な証明書発行を監視
- セキュリティヘッダーの変更点を確認
インシデント対応チェックリスト
- 証明書期限切れ時の即時更新手順を文書化
- 証明書失効(revoke)手順の把握
- 緊急証明書再発行手順の準備
- ロールバック計画の策定(以前の証明書のバックアップ)
- インシデント発生時の通知チャネルの指定
まとめ
SSL/TLS証明書管理は一度設定して終わりではありません。Let's Encryptの90日有効期間ポリシーは、自動化を強制する良い慣行です。この記事で取り上げた内容を基に、安全で自動化された証明書管理体制を構築してください。
まとめ:
- TLS 1.3をデフォルトに使用しつつ、互換性のためにTLS 1.2も許可
- Let's Encrypt + Certbotで無料証明書の自動発行と更新
- Nginx設定はSSL Labs A+グレードを目標に最適化
- 自動更新はsystemd timerで設定し、監視は必須
- トラブルシューティングは
openssl s_clientコマンドから開始