Skip to content
Published on

SSL/TLS証明書完全ガイド:発行から自動更新、トラブルシューティングまで

Authors
  • Name
    Twitter

はじめに

HTTPSはもはや選択ではなく必須です。ブラウザはHTTPサイトに「安全ではありません」という警告を表示し、検索エンジンはHTTPSをランキングシグナルとして活用しています。しかし、SSL/TLS証明書を正しく理解し運用することは、思ったより複雑です。証明書の種類選択、正しい設定、自動更新、チェーン構成など、見落としやすいポイントが多くあります。

この記事では、SSL/TLSの動作原理から実践的な運用まで、一気にまとめます。

1. SSL/TLSの動作原理

対称暗号化 vs 非対称暗号化

SSL/TLSは2つの暗号化方式を組み合わせて使用します。

区分対称暗号化非対称暗号化
1つの鍵で暗号化/復号化公開鍵(暗号化)+ 秘密鍵(復号化)
速度高速低速(100〜1000倍)
アルゴリズムAES-256, ChaCha20RSA, ECDSA, Ed25519
用途実データの転送鍵交換、証明書署名
鍵長128/256 bit2048/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. 1-RTTハンドシェイク:TLS 1.2の2-RTTから1-RTTに短縮
  2. 0-RTT再接続:再接続時に追加の遅延なしでデータ送信が可能
  3. 強化されたセキュリティ:脆弱な暗号スイートの削除(RC4、3DES、CBCモードなど)
  4. 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つのFQDNwww.example.com最も安価
ワイルドカード同レベルのすべてのサブドメイン*.example.com中程度
SAN/マルチドメイン複数ドメインを1つの証明書にexample.comexample.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 ACMLet'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日有効期間ポリシーは、自動化を強制する良い慣行です。この記事で取り上げた内容を基に、安全で自動化された証明書管理体制を構築してください。

まとめ:

  1. TLS 1.3をデフォルトに使用しつつ、互換性のためにTLS 1.2も許可
  2. Let's Encrypt + Certbotで無料証明書の自動発行と更新
  3. Nginx設定はSSL Labs A+グレードを目標に最適化
  4. 自動更新はsystemd timerで設定し、監視は必須
  5. トラブルシューティングopenssl s_clientコマンドから開始