TLS/SSL
TLSはインターネット通信の暗号化を担うプロトコル。ハンドシェイク、証明書、暗号スイートの仕組みを理解し、安全な通信の基盤を学ぶ。
82 分で読めます40,681 文字
TLS/SSL
TLSはインターネット通信の暗号化を担うプロトコル。ハンドシェイク、証明書、暗号スイートの仕組みを理解し、安全な通信の基盤を学ぶ。
前提知識
このガイドを理解するには以下の知識が必要です:
- 公開鍵暗号の基礎 — RSA/ECDSA等の非対称暗号の仕組み
この章で学ぶこと
- TLSハンドシェイクの流れを理解する
- 証明書の仕組みと検証プロセスを把握する
- TLS 1.3の改善点を学ぶ
- 暗号スイートの選定と設定を実践する
- 実務でのTLS設定とトラブルシューティングを習得する
1. TLSの基本
TLS(Transport Layer Security):
→ 通信の暗号化、認証、完全性を提供
→ HTTPS = HTTP + TLS
歴史:
SSL 2.0 (1995) → 脆弱、使用禁止
SSL 3.0 (1996) → POODLE攻撃、使用禁止
TLS 1.0 (1999) → 非推奨(2021年にRFC 8996で正式廃止)
TLS 1.1 (2006) → 非推奨(2021年にRFC 8996で正式廃止)
TLS 1.2 (2008) → 現在広く使用
TLS 1.3 (2018) → 最新、推奨
※ SSLは全バージョン廃止。「SSL証明書」は慣習的な名称
TLSが提供する3つの機能:
① 機密性(Confidentiality): 通信内容の暗号化
② 認証(Authentication): 通信相手の本人確認
③ 完全性(Integrity): データの改ざん検知
TLSの位置づけ(OSI参照モデル):
アプリケーション層: HTTP, SMTP, FTP
────────────────────────────────
TLS(セッション〜プレゼンテーション層)
────────────────────────────────
トランスポート層: TCP
────────────────────────────────
ネットワーク層: IP
→ TLSはTCPの上、アプリケーションの下に位置
→ アプリケーションプロトコルに依存しない
→ HTTP以外にもSMTPS, FTPS, LDAPS等で使用
1.1 暗号技術の基礎
TLSが使用する暗号技術:
① 共通鍵暗号(対称暗号):
→ 同じ鍵で暗号化と復号
→ 高速(大量データの暗号化に適する)
→ 鍵の配送が課題
→ AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305
平文 ──[共通鍵]──→ 暗号文 ──[共通鍵]──→ 平文
② 公開鍵暗号(非対称暗号):
→ 公開鍵で暗号化、秘密鍵で復号
→ 低速(鍵交換・署名に使用)
→ 鍵の配送問題を解決
→ RSA, ECDSA, Ed25519
平文 ──[公開鍵]──→ 暗号文 ──[秘密鍵]──→ 平文
③ ハッシュ関数:
→ 任意長データから固定長ダイジェストを生成
→ 一方向性(逆算不可能)
→ 衝突耐性(同じハッシュになる入力を見つけるのが困難)
→ SHA-256, SHA-384, SHA-512
④ MAC(Message Authentication Code):
→ メッセージの完全性と認証を保証
→ HMAC: ハッシュ関数ベースのMAC
→ AEAD: 暗号化と認証を同時に行う(GCM, Poly1305)
⑤ 鍵交換アルゴリズム:
→ 安全でないチャネル上で共通鍵を確立
→ Diffie-Hellman(DH)
→ Elliptic Curve Diffie-Hellman(ECDH)
→ 一時鍵(Ephemeral): ECDHE → 前方秘匿性を提供
TLSでの使い分け:
鍵交換: ECDHE(公開鍵暗号ベース)
認証: RSA or ECDSA(公開鍵暗号)
データ暗号化: AES-GCM or ChaCha20(共通鍵暗号)
完全性: AEAD(GCM, Poly1305)が暗号化と同時に提供
2. TLS 1.2 ハンドシェイク
TLS 1.2 ハンドシェイク(2 RTT):
クライアント サーバー
│ │
① │── ClientHello ──→ │
│ 対応TLSバージョン │
│ 対応暗号スイート一覧 │
│ クライアントランダム │
│ セッションID │
│ SNI(Server Name Indication)│
│ │
② │←── ServerHello ── │
│ 選択されたTLSバージョン │
│ 選択された暗号スイート │
│ サーバーランダム │
│ セッションID │
│ │
③ │←── Certificate ── │
│ サーバー証明書チェーン │
│ │
④ │←── ServerKeyExchange ── │
│ 鍵交換パラメータ(ECDHE等) │
│ 署名(秘密鍵による) │
│ │
⑤ │←── ServerHelloDone ── │
│ │
│ [クライアント側で証明書検証]│
│ │
⑥ │── ClientKeyExchange ──→ │
│ 鍵交換パラメータ │
│ │
│ [両者がプリマスターシークレットを算出]
│ [マスターシークレット → セッション鍵を導出]
│ │
⑦ │── ChangeCipherSpec ──→ │
│── Finished ──→ │
│ (ハンドシェイク全体のハッシュを暗号化)
│ │
⑧ │←── ChangeCipherSpec ── │
│←── Finished ── │
│ │
│ 暗号化通信開始 │
所要時間: 2 RTT(東京-US間: 約200ms)
2.1 各ステップの詳細解説
ClientHello の詳細:
→ TLSバージョン: 最高でTLS 1.2を提示
→ CipherSuites: 優先順位付きの暗号スイートリスト
→ Compression Methods: 通常null(圧縮はCRIME攻撃の原因)
→ Extensions:
- server_name (SNI): 接続先ドメイン名
- supported_groups: 対応する楕円曲線(P-256, X25519等)
- signature_algorithms: 対応する署名アルゴリズム
- session_ticket: セッション再開用チケット
SNI(Server Name Indication):
→ 1つのIPで複数ドメインのTLS証明書を使い分けるための拡張
→ ClientHelloに接続先ドメイン名を含める
→ ただしClientHelloは平文 → ドメイン名が見える
→ ESNI(Encrypted SNI)/ ECH(Encrypted Client Hello)で解決
セッション再開(Session Resumption):
① Session ID:
→ サーバー側にセッション状態を保存
→ クライアントがIDを提示して再開
→ サーバーのメモリ消費が課題
② Session Ticket:
→ セッション状態を暗号化してクライアントに渡す
→ サーバーはステートレス
→ チケット暗号化鍵の管理が課題
再開時: 1 RTT(フルハンドシェイク不要)
2.2 鍵導出プロセス
TLS 1.2の鍵導出:
1. プリマスターシークレット:
→ ECDHE: 共有シークレットを算出
→ 両者のDHパラメータから同じ値を導出
2. マスターシークレット:
master_secret = PRF(
pre_master_secret,
"master secret",
ClientHello.random + ServerHello.random
)
3. セッション鍵の導出:
key_block = PRF(
master_secret,
"key expansion",
ServerHello.random + ClientHello.random
)
key_blockから以下を切り出し:
→ client_write_MAC_key
→ server_write_MAC_key
→ client_write_key(暗号化鍵)
→ server_write_key(暗号化鍵)
→ client_write_IV
→ server_write_IV
PRF = 擬似乱数関数(HMAC-SHA256ベース)
ポイント:
→ クライアントとサーバーで別々の鍵を使用
→ MACと暗号化で別々の鍵を使用
→ ランダム値を含めることでセッションごとにユニークな鍵
3. TLS 1.3 ハンドシェイク
TLS 1.3 ハンドシェイク(1 RTT):
クライアント サーバー
│ │
① │── ClientHello ──→ │
│ 対応暗号スイート │
│ 鍵共有パラメータ(推測) │ ← 1RTTの秘密
│ supported_versions拡張 │
│ │
② │←── ServerHello ── │
│ 選択された暗号スイート │
│ 鍵共有パラメータ │
│ │
│←── {EncryptedExtensions} ── │ ← ここから暗号化
│←── {Certificate} ── │
│←── {CertificateVerify} ── │
│←── {Finished} ── │
│ │
③ │── {Finished} ──→ │
│ │
│ 暗号化通信開始 │
改善点:
① 1 RTT: 鍵交換を最初から送信(推測ベース)
② 0-RTT: 再接続時はデータも同時送信可能
③ 脆弱な暗号スイートを廃止
④ ハンドシェイクも暗号化(証明書が見えない)
⑤ CertificateVerify追加(サーバー認証の強化)
3.1 TLS 1.3の鍵スケジュール
TLS 1.3の鍵導出(HKDF ベース):
HKDF = HMAC-based Key Derivation Function
→ TLS 1.2のPRFよりも明確で安全な設計
鍵導出の流れ:
PSK(Pre-Shared Key、なければ0)
│
▼
Early Secret ── Derive-Secret → client_early_traffic_secret
│ (0-RTTデータ用)
│
ECDHE共有シークレット
│
▼
Handshake Secret
│
├── Derive-Secret → client_handshake_traffic_secret
│ (ハンドシェイク暗号化用)
├── Derive-Secret → server_handshake_traffic_secret
│
▼
Master Secret
│
├── Derive-Secret → client_application_traffic_secret
│ (アプリケーションデータ用)
├── Derive-Secret → server_application_traffic_secret
│
├── Derive-Secret → exporter_master_secret
└── Derive-Secret → resumption_master_secret
(セッション再開用)
ポイント:
→ 段階的な鍵導出で用途別の鍵を生成
→ ハンドシェイクとアプリケーションデータで別鍵
→ 鍵のコンプロマイズ範囲を限定
3.2 0-RTT(Early Data)
0-RTT 再接続:
クライアント ── ClientHello + アプリデータ ──→ サーバー
→ 接続確立とデータ送信を同時に
前提:
→ 以前の接続でPSK(Pre-Shared Key)を取得済み
→ PSKからearly_traffic_secretを導出
→ 最初のリクエストを暗号化して送信
利点:
→ 0 RTTでデータ送信開始
→ Webパフォーマンスの大幅改善
リスク:
→ リプレイ攻撃: 攻撃者がパケットを再送可能
→ 前方秘匿性なし: PSKが漏洩すると0-RTTデータは復号可能
対策:
→ 冪等なリクエストのみ(GET等)で使用
→ サーバー側でリプレイ検知を実装
→ Single-Use Ticketの使用
→ Anti-Replay Window の設定
設定例(Nginx):
ssl_early_data on;
proxy_set_header Early-Data $ssl_early_data;
→ バックエンドで Early-Data: 1 を見て冪等性を判断
3.3 TLS 1.3で廃止された機能
TLS 1.3で廃止された暗号・機能:
暗号アルゴリズム:
✗ RSA鍵交換(前方秘匿性なし)
✗ CBC モードの暗号(パディングオラクル攻撃)
✗ RC4(多数の脆弱性)
✗ DES, 3DES(鍵長が短い)
✗ MD5, SHA-1(衝突攻撃が実用的)
✗ 静的DH(前方秘匿性なし)
✗ カスタムDHグループ(弱いパラメータの可能性)
✗ 圧縮(CRIME攻撃)
✗ 再ネゴシエーション(三重ハンドシェイク攻撃)
プロトコル機能:
✗ ChangeCipherSpec メッセージ(不要に)
✗ セッションID による再開(PSKベースに変更)
✗ ランダムなセッションIDの生成
なぜ廃止が重要か:
→ 選択肢が少ない = 設定ミスの可能性が低い
→ 安全でない構成が不可能になる
→ 実装の複雑さが軽減
→ 監査が容易に
4. 証明書
X.509 証明書の構造:| バージョン: v3 |
|---|
| シリアル番号: 01:AB:CD:EF:... |
| 署名アルゴリズム: SHA-256 with RSA |
| 発行者: Let's Encrypt Authority X3 |
| 有効期間: 2024-01-01 〜 2024-03-31 |
| サブジェクト: example.com |
| 公開鍵: RSA 2048bit / ECDSA P-256 |
| 拡張: |
| SANs: example.com, www.example.com |
| Key Usage: Digital Signature |
| Extended Key Usage: Server Auth |
| Basic Constraints: CA:FALSE |
| CRL Distribution Points: ... |
| Authority Information Access: ... |
| OCSP Responder: http://ocsp... |
| CA Issuers: http://... |
| 署名値: 3A:4B:5C:... |
4.1 証明書チェーン
証明書チェーン: ┌────────────────┐
│ ルートCA証明書 │ ← OSやブラウザに内蔵(信頼の起点)
│ (自己署名) │ 約150〜200個の信頼済みCA
└───────┬────────┘│ 署名 ┌───────▼────────┐
│ 中間CA証明書 │ ← ルートCAが署名
│ │ ルートCAのオフライン保護のため
└───────┬────────┘│ 署名| example.com |
|---|
検証プロセス:
1. サーバー証明書の署名を中間CAの公開鍵で検証
2. 中間CA証明書の署名をルートCAの公開鍵で検証
3. ルートCA証明書がOS/ブラウザの信頼ストアに存在するか確認
4. 証明書の有効期限を確認
5. ドメイン名(SAN)の一致を確認
6. 失効確認(OCSP / CRL)
7. Certificate Transparency(CT)ログの確認
中間CA証明書の重要性:
→ サーバーは中間CA証明書も送信する必要がある
→ 送信漏れ → 「信頼されていない証明書」エラー
→ 特にAndroid等で問題になりやすい(信頼ストアが異なる)
→ ssl-cert-check や SSL Labs でチェーン不備を検出可能
クロスサイン(Cross-signing):
→ 新しいCAが古いルートCAにも署名してもらう
→ 古いデバイスでも信頼チェーンが成立
→ Let's Encrypt: ISRG Root X1 + DST Root CA X3(クロスサイン)
4.2 証明書の種類と選定
証明書の種類:
DV(ドメイン検証):
→ ドメインの所有のみ確認
→ Let's Encrypt等(無料)
→ 発行まで数分
→ 個人サイト、スタートアップ向け
OV(組織検証):
→ 組織の実在を確認
→ 企業サイト向け
→ 発行まで数日
→ 年間$50〜$300程度
EV(拡張検証):
→ 厳格な組織審査
→ 金融機関等
→ 発行まで数週間
→ 年間$150〜$1,000程度
→ ブラウザ表示の差は縮小傾向(アドレスバーの組織名表示廃止)
ワイルドカード証明書:
→ *.example.com で全サブドメインに対応
→ ただし1階層のみ(*.sub.example.com は不可)
→ DNSチャレンジが必要(HTTP-01では不可)
マルチドメイン証明書(SAN証明書):
→ 1枚の証明書に複数ドメインを含める
→ example.com + example.org + example.net
→ 管理の簡素化
選定ガイドライン:
個人サイト → Let's Encrypt(DV、無料)
一般企業サイト → DV or OV
金融・医療 → OV or EV
マイクロサービス → 内部CA(自己署名)
複数サブドメイン → ワイルドカード
4.3 証明書の失効
証明書の失効確認:
① CRL(Certificate Revocation List):
→ CAが失効証明書のリストを公開
→ クライアントがダウンロードして照合
→ 欠点: リストが大きくなる、更新頻度が低い
② OCSP(Online Certificate Status Protocol):
→ リアルタイムで証明書の有効性を確認
→ CAのOCSPレスポンダーに問い合わせ
クライアント ──→ OCSPレスポンダー
│
← "good" / "revoked" / "unknown"
欠点:
→ レイテンシ増加(追加のネットワークリクエスト)
→ CAがダウンすると確認不可
→ プライバシー問題(CAにアクセス先がわかる)
③ OCSP Stapling:
→ サーバーがOCSPレスポンスを事前取得してTLSハンドシェイクに含める
→ クライアントはCAに問い合わせ不要
→ レイテンシ削減 + プライバシー向上
設定例(Nginx):
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
④ OCSP Must-Staple:
→ 証明書にOCSP Staling必須の拡張を含める
→ Stapled応答がない場合は接続を拒否
→ 失効確認のソフトフェイル問題を解決
⑤ CRLite(Firefox実装):
→ CRLをBloomフィルターで圧縮してブラウザに配布
→ 全証明書の失効状態をローカルで確認
→ OCSP不要、プライバシー保護
5. 暗号スイート
暗号スイートの構成(TLS 1.2):
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
│ │ │ │ │ │ │
│ │ │ │ │ │ └── ハッシュ関数
│ │ │ │ │ └─ モード
│ │ │ │ └─ 鍵長
│ │ │ └── 共通鍵暗号
│ │ └── 認証アルゴリズム
│ └── 鍵交換アルゴリズム
└── プロトコル
各コンポーネントの選択肢:
鍵交換: ECDHE(推奨), DHE, RSA(非推奨)
認証: RSA, ECDSA, Ed25519
暗号化: AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305
ハッシュ: SHA-256, SHA-384
TLS 1.3の暗号スイート(簡素化):
TLS_AES_256_GCM_SHA384
TLS_AES_128_GCM_SHA256
TLS_CHACHA20_POLY1305_SHA256
→ 鍵交換は常にECDHE(前方秘匿性あり)
→ 認証は証明書で別途処理
→ 選択肢を減らしてセキュリティ向上
→ AEAD暗号のみ(CBC廃止)
5.1 前方秘匿性(Forward Secrecy)
前方秘匿性(Forward Secrecy / Perfect Forward Secrecy):
→ サーバーの秘密鍵が漏洩しても、過去の通信は復号できない
→ 毎回新しい一時鍵(ephemeral key)を生成
→ ECDHE: Elliptic Curve Diffie-Hellman Ephemeral
RSA鍵交換(前方秘匿性なし):
1. クライアント → プリマスターシークレットをRSA公開鍵で暗号化
2. サーバー → RSA秘密鍵で復号
→ 秘密鍵が漏洩 → 全過去通信を復号可能
攻撃シナリオ:
① 攻撃者が暗号化通信を長期間記録
② サーバーの秘密鍵を何らかの方法で入手
③ 記録した全通信を復号
→ "Record now, decrypt later" 攻撃
ECDHE鍵交換(前方秘匿性あり):
1. 両者が一時的なDHパラメータを交換
2. 共有シークレットを算出
3. 一時鍵は使い捨て(メモリから消去)
→ 秘密鍵が漏洩しても一時鍵は復元不可能
→ 過去の通信は安全
楕円曲線の選択:
→ X25519: 最速、推奨、Daniel J. Bernstein設計
→ P-256: NIST標準、広くサポート
→ P-384: 高セキュリティ要件向け
→ P-521: 最高セキュリティ(パフォーマンスに影響)
ポスト量子暗号(Post-Quantum):
→ 量子コンピュータでECDHEが破られる可能性
→ NIST PQC標準化(2024年):
ML-KEM(旧CRYSTALS-Kyber)
ML-DSA(旧CRYSTALS-Dilithium)
→ ハイブリッド方式: X25519 + ML-KEM-768
→ Chrome/Firefox: X25519Kyber768 の試験実装
→ TLS 1.3で先行対応可能
5.2 AES-GCM vs ChaCha20-Poly1305
AES-GCM:
→ AES暗号 + Galois/Counter Modeの認証付き暗号
→ ハードウェアアクセラレーション(AES-NI)対応
→ Intel/AMDプロセッサで非常に高速
→ サーバーサイドのデファクトスタンダード
ChaCha20-Poly1305:
→ ChaCha20ストリーム暗号 + Poly1305 MAC
→ ソフトウェア実装で高速(AES-NI非対応環境向け)
→ モバイルデバイス(ARM)で有利
→ Google開発、広く採用
選択の指針:
デスクトップ/サーバー: AES-256-GCM(AES-NI活用)
モバイル: ChaCha20-Poly1305(省電力)
推奨: 両方サポートし、クライアントに選ばせる
暗号モードの比較:
GCM(Galois/Counter Mode):
→ AEAD: 暗号化 + 認証を同時
→ カウンターモードベース → 並列処理可能
→ nonce再利用で致命的な脆弱性
→ 96ビットnonce + 32ビットカウンター
CBC(Cipher Block Chaining):
→ TLS 1.3で廃止
→ パディングオラクル攻撃に脆弱
→ MACの処理順序の問題(Encrypt-then-MAC vs MAC-then-Encrypt)
→ 直列処理のみ → 遅い
6. 実務でのTLS設定
6.1 Nginx設定
# /etc/nginx/conf.d/ssl.conf
# 推奨TLS設定(Mozilla Intermediate Configuration)
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;
# TLSバージョン
ssl_protocols TLSv1.2 TLSv1.3;
# 暗号スイート(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:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers off; # TLS 1.3ではクライアント優先
# DH パラメータ(TLS 1.2 DHE用)
ssl_dhparam /etc/nginx/dhparam.pem;
# 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;
# セッション設定
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # 前方秘匿性を厳密に保つなら無効化
# セキュリティヘッダー
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
# TLS 1.3 0-RTT(オプション)
ssl_early_data on;
proxy_set_header Early-Data $ssl_early_data;
}
# HTTPからHTTPSへのリダイレクト
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}6.2 Let's Encrypt自動化
# certbot インストール(Ubuntu)
$ sudo apt install certbot python3-certbot-nginx
# 証明書取得(Nginx自動設定)
$ sudo certbot --nginx -d example.com -d www.example.com
# 証明書取得(DNSチャレンジ、ワイルドカード用)
$ sudo certbot certonly --manual --preferred-challenges dns \
-d '*.example.com' -d example.com
# 自動更新のテスト
$ sudo certbot renew --dry-run
# 自動更新(cron / systemd timer)
$ sudo crontab -e
0 0,12 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
# systemd timerの確認
$ systemctl list-timers | grep certbot
# 証明書の状態確認
$ sudo certbot certificates# ACME プロトコルの仕組み
#
# 1. アカウント登録
# クライアント → Let's Encrypt: 公開鍵を登録
#
# 2. 認証チャレンジ
# HTTP-01: http://example.com/.well-known/acme-challenge/<TOKEN>
# DNS-01: _acme-challenge.example.com TXT <TOKEN>
# TLS-ALPN-01: TLSハンドシェイク中にトークンを提示
#
# 3. チャレンジ検証
# Let's Encrypt → example.com: トークンを確認
#
# 4. 証明書発行
# CSR(Certificate Signing Request)を送信
# 署名済み証明書を受信
#
# HTTP-01 vs DNS-01:
# HTTP-01: 簡単、ポート80のアクセス必要、ワイルドカード不可
# DNS-01: DNSレコード操作が必要、ワイルドカード可能、CDN背後でも使用可能6.3 内部通信のTLS(mTLS)
mTLS(mutual TLS / 相互TLS認証):
→ クライアントもサーバーも証明書を提示
→ サービスメッシュやマイクロサービス間通信で使用
通常のTLS:
クライアント → サーバーの証明書を検証(一方向)
mTLS:
クライアント → サーバーの証明書を検証
サーバー → クライアントの証明書を検証(双方向)
用途:
→ Kubernetes のサービス間通信(Istio, Linkerd)
→ ゼロトラストネットワーク
→ APIゲートウェイの認証
→ IoTデバイスの認証
Nginx設定:
ssl_client_certificate /etc/nginx/ca.crt;
ssl_verify_client on;
ssl_verify_depth 2;
→ クライアント証明書がない場合: 403 Forbidden
→ 無効な証明書の場合: 400 Bad Request
# Istio での mTLS設定(PeerAuthentication)
apiVersion: security.istio.io/v1beta1
kind: PeerAuthentication
metadata:
name: default
namespace: istio-system
spec:
mtls:
mode: STRICT # 全通信でmTLS必須
---
# DestinationRule(クライアント側設定)
apiVersion: networking.istio.io/v1alpha3
kind: DestinationRule
metadata:
name: default
spec:
host: "*.local"
trafficPolicy:
tls:
mode: ISTIO_MUTUAL7. TLSの脆弱性と攻撃
7.1 歴史的な脆弱性
主要なTLS関連の脆弱性:
① BEAST(2011):
→ TLS 1.0のCBCモードに対する攻撃
→ 選択平文攻撃でCookieを復号
→ 対策: TLS 1.1以上に移行
② CRIME(2012):
→ TLS圧縮を利用した攻撃
→ 圧縮率の差からシークレットを推測
→ 対策: TLS圧縮の無効化
③ Lucky Thirteen(2013):
→ CBCモードのパディング処理のタイミング差を悪用
→ 対策: constant-time実装、AEAD暗号へ移行
④ Heartbleed(2014):
→ OpenSSLのハートビート拡張の実装バグ
→ サーバーメモリの最大64KB漏洩
→ 秘密鍵、セッションデータ等が漏洩
→ CVE-2014-0160
→ 対策: OpenSSLアップデート、証明書再発行
⑤ POODLE(2014):
→ SSL 3.0のCBCパディング処理の脆弱性
→ SSL 3.0への ダウングレード攻撃と組み合わせ
→ 対策: SSL 3.0の無効化
⑥ FREAK(2015):
→ 輸出グレード暗号(512ビットRSA)への ダウングレード
→ 対策: 輸出グレード暗号の無効化
⑦ Logjam(2015):
→ DHE鍵交換の弱いパラメータ(512/1024ビット)
→ 対策: 2048ビット以上のDHパラメータ
⑧ ROBOT(2017):
→ RSA鍵交換のパディングオラクル
→ Bleichenbacher攻撃の変種
→ 対策: RSA鍵交換の無効化(ECDHE使用)
⑨ RACCOON(2020):
→ DH鍵交換のタイミングサイドチャネル
→ 対策: constant-time実装
教訓:
→ TLS 1.3は上記の多くの攻撃を根本的に排除
→ 古いプロトコルバージョンを無効化する重要性
→ ソフトウェアの定期的なアップデート
7.2 ダウングレード攻撃とその対策
ダウングレード攻撃:
→ クライアントとサーバーの間に入り、
弱い暗号スイートやTLSバージョンを強制
攻撃手法:
1. ClientHelloを改ざん(対応バージョンをTLS 1.0に書き換え)
2. サーバーはTLS 1.0で応答
3. 弱い暗号で通信が確立
4. 攻撃者が復号
対策:
① TLS_FALLBACK_SCSV:
→ フォールバック時に特殊な暗号スイートを送信
→ サーバーがダウングレードを検知して接続拒否
② TLS 1.3のダウングレード防止:
→ ServerHello.random の末尾8バイトに固定値
→ TLS 1.2: 0x44 0x4F 0x57 0x4E 0x47 0x52 0x44 0x01
→ TLS 1.1以下: 0x44 0x4F 0x57 0x4E 0x47 0x52 0x44 0x00
→ クライアントがこれを検出して接続拒否
③ 古いバージョンの無効化:
→ サーバー側でTLS 1.0/1.1を無効に
→ ssl_protocols TLSv1.2 TLSv1.3;
④ HSTS Preload:
→ ブラウザがHTTP接続自体を拒否
→ https://hstspreload.org/ で登録
8. Certificate Transparency(CT)
Certificate Transparency:
→ 証明書の発行を公開ログに記録する仕組み
→ 不正な証明書の発行を検知
背景:
→ 過去にCAが不正な証明書を発行した事例
→ DigiNotar(2011): *.google.com の偽証明書
→ Symantec(2017): 信頼取り消し
仕組み:
1. CAが証明書をCTログサーバーに登録
2. CTログサーバーがSCT(Signed Certificate Timestamp)を返却
3. SCTが証明書に埋め込まれるか、TLSハンドシェイクで送信
4. ブラウザがSCTを検証| CA | ──→ | CTログ | ──→ | モニター |
|---|
│ │ │
│ SCT │ 公開ログ │ 不正検知
▼ ▼ ▼| サーバー | ブラウザ | 管理者 |
|---|
CTログの監視:
→ crt.sh: 発行された証明書の検索
→ https://crt.sh/?q=example.com
→ 不正な証明書の発行を早期に検知
Chrome要件:
→ 2018年以降、CT対応が必須
→ SCTが2つ以上必要
→ CT非対応の証明書は「Not Transparent」警告
9. 実務での確認コマンド
# TLS接続の詳細確認
$ openssl s_client -connect example.com:443
# TLS 1.3での接続を強制
$ openssl s_client -connect example.com:443 -tls1_3
# 証明書チェーンの表示
$ openssl s_client -connect example.com:443 -showcerts
# 証明書の内容を確認
$ echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -text -noout
# 証明書の有効期限確認
$ echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -dates
# 証明書のSAN(サブジェクト代替名)確認
$ echo | openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -noout -ext subjectAltName
# OCSP Staplingの確認
$ echo | openssl s_client -connect example.com:443 -status 2>/dev/null | \
grep -A 20 "OCSP Response"
# TLSバージョンとスイートの確認
$ curl -v https://example.com 2>&1 | grep -E '(SSL|TLS)'
# 特定の暗号スイートでの接続テスト
$ openssl s_client -connect example.com:443 \
-cipher ECDHE-RSA-AES256-GCM-SHA384
# 対応する暗号スイートの一覧取得
$ nmap --script ssl-enum-ciphers -p 443 example.com
# SSL Labs でオンラインチェック(グレードA+を目指す)
# https://www.ssllabs.com/ssltest/
# testssl.sh(包括的なTLSテストツール)
$ git clone https://github.com/drwetter/testssl.sh
$ ./testssl.sh example.com
# certigo(証明書の詳細表示)
$ certigo connect example.com:443
# SSLyze(Python製TLSスキャナー)
$ pip install sslyze
$ sslyze example.com9.1 証明書管理の自動化スクリプト
#!/bin/bash
# check-cert-expiry.sh - 証明書有効期限の一括確認
DOMAINS=(
"example.com"
"api.example.com"
"www.example.com"
)
WARN_DAYS=30
for domain in "${DOMAINS[@]}"; do
expiry=$(echo | openssl s_client -connect "$domain:443" 2>/dev/null | \
openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -z "$expiry" ]; then
echo "[ERROR] $domain: 証明書取得失敗"
continue
fi
expiry_epoch=$(date -d "$expiry" +%s 2>/dev/null || \
date -j -f "%b %d %T %Y %Z" "$expiry" +%s 2>/dev/null)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt 0 ]; then
echo "[EXPIRED] $domain: ${days_left}日前に期限切れ"
elif [ "$days_left" -lt "$WARN_DAYS" ]; then
echo "[WARNING] $domain: 残り${days_left}日 (${expiry})"
else
echo "[OK] $domain: 残り${days_left}日 (${expiry})"
fi
done# Python での証明書有効期限確認
import ssl
import socket
from datetime import datetime
def check_certificate(hostname: str, port: int = 443) -> dict:
"""証明書情報を取得して返す"""
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()
# 有効期限
not_after = datetime.strptime(
cert['notAfter'], '%b %d %H:%M:%S %Y %Z'
)
days_left = (not_after - datetime.utcnow()).days
# SAN(Subject Alternative Names)
sans = []
for type_name, value in cert.get('subjectAltName', []):
if type_name == 'DNS':
sans.append(value)
# 発行者
issuer = dict(x[0] for x in cert['issuer'])
return {
'hostname': hostname,
'issuer': issuer.get('organizationName', 'Unknown'),
'not_before': cert['notBefore'],
'not_after': cert['notAfter'],
'days_left': days_left,
'sans': sans,
'serial_number': cert.get('serialNumber', ''),
'version': cert.get('version', ''),
'tls_version': ssock.version(),
'cipher': ssock.cipher(),
}
# 使用例
domains = ['example.com', 'api.example.com', 'www.example.com']
for domain in domains:
try:
info = check_certificate(domain)
status = "OK" if info['days_left'] > 30 else "WARNING"
print(f"[{status}] {domain}: 残り{info['days_left']}日, "
f"発行者={info['issuer']}, TLS={info['tls_version']}")
except Exception as e:
print(f"[ERROR] {domain}: {e}")10. HSTS(HTTP Strict Transport Security)
HSTS:
→ ブラウザにHTTPS接続を強制する仕組み
→ HTTPへのダウングレードを防止
ヘッダー:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
max-age: HTTPSを強制する期間(秒)
→ 63072000 = 2年
→ 最低でも31536000(1年)を推奨
includeSubDomains: サブドメインも対象
→ 全サブドメインがHTTPS対応必要
preload: HSTS Preload Listに登録
→ ブラウザに事前組み込み
→ 初回アクセスからHTTPS強制
HSTS Preload:
→ https://hstspreload.org/ で登録申請
→ Chrome, Firefox, Safari, Edge に組み込み
→ 一度登録すると解除が困難(数ヶ月〜1年)
登録要件:
① 有効なSSL証明書
② HTTPからHTTPSへリダイレクト
③ 全サブドメインがHTTPS
④ max-age が31536000以上
⑤ includeSubDomains ディレクティブ
⑥ preload ディレクティブ
注意事項:
→ 段階的にmax-ageを増やす
300 → 86400 → 604800 → 2592000 → 63072000
→ includeSubDomainsは全サブドメインの確認後に追加
→ preloadは十分テスト後に有効化
→ 設定ミスでサイトにアクセス不能になるリスク
11. TLSのパフォーマンス最適化
TLSのパフォーマンス最適化:
① セッション再開:
→ Session Ticket: サーバーステートレス
→ 1-RTTハンドシェイクに短縮
② TLS 1.3 0-RTT:
→ 再接続時の最初のリクエストが0-RTT
→ ただしリプレイリスク
③ OCSP Stapling:
→ クライアントのOCSP問い合わせを削減
→ ハンドシェイク時間の短縮
④ 証明書の最適化:
→ ECDSA証明書(RSAより小さい)
RSA 2048: 約256バイト
ECDSA P-256: 約64バイト
→ 証明書チェーンの最小化
→ 不要な中間CA証明書を含めない
⑤ TLS Record Size:
→ 初回は小さなレコードサイズ(1369バイト)
→ TCPスロースタートとの整合
→ 安定後に大きなレコードサイズ(16KB)
→ Dynamic Record Sizingの活用
⑥ TCP Fast Open + TLS 1.3:
→ TCP接続確立とTLSハンドシェイクを並行
→ 合計1-RTTで暗号化通信開始
⑦ ハードウェアアクセラレーション:
→ AES-NI: AES暗号化のハードウェア支援
→ QAT(Quick Assist Technology): Intelのオフロードデバイス
→ SSL/TLS専用ハードウェア(HSM)
パフォーマンス計測:
$ curl -o /dev/null -s -w "\
TCP: %{time_connect}s\n\
TLS: %{time_appconnect}s\n\
TTFB: %{time_starttransfer}s\n" \
https://example.com
TLS handshake目標:
TLS 1.2: < 100ms(同一リージョン)
TLS 1.3: < 50ms(同一リージョン)
TLS 1.3 0-RTT: < 10ms(再接続時)
12. HTTP/2, HTTP/3とTLS
HTTP/2 と TLS:
→ HTTP/2はTLS 1.2以上が必須(事実上)
→ ALPN(Application-Layer Protocol Negotiation)で HTTP/2 をネゴシエート
→ ALPNはTLSハンドシェイク中に行われる(追加RTT不要)
ClientHello:
ALPN: h2, http/1.1
ServerHello:
ALPN: h2
→ HTTP/2で通信開始
HTTP/3 と QUIC:
→ HTTP/3はQUIC上で動作
→ QUIC = UDP上のトランスポートプロトコル
→ TLS 1.3がQUICに組み込み(別プロトコルではない)
TCP + TLS 1.3:
TCP 3-way handshake: 1 RTT
TLS handshake: 1 RTT
合計: 2 RTT
QUIC:
QUIC handshake(TLS 1.3組み込み): 1 RTT
合計: 1 RTT
QUIC 0-RTT:
再接続時: 0 RTT
→ 接続確立と同時にデータ送信
QUICのメリット:
→ Head-of-Line Blocking の解消
→ コネクションマイグレーション(IP変更に対応)
→ 組み込みの暗号化(平文通信不可)
→ ユーザー空間での実装(カーネル変更不要)
Nginx QUIC設定:
server {
listen 443 quic reuseport;
listen 443 ssl;
http2 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
# ...TLS設定は通常と同じ
}
13. 実務でのトラブルシューティング
よくあるTLS問題と解決策:
① 証明書エラー: "NET::ERR_CERT_AUTHORITY_INVALID"
原因: 中間CA証明書の未送信
確認: openssl s_client -connect host:443 -showcerts
解決: fullchain.pemを使用(中間CA含む)
② 証明書エラー: "NET::ERR_CERT_DATE_INVALID"
原因: 証明書の期限切れ
確認: openssl x509 -noout -dates
解決: 証明書の更新、自動更新の設定
③ Mixed Content:
原因: HTTPSページ内にHTTPリソース
確認: Chrome DevTools Console
解決: 全リソースをHTTPS化、CSPのupgrade-insecure-requests
④ HSTS関連エラー:
原因: 証明書が無効なのにHSTSでHTTPS強制
確認: chrome://net-internals/#hsts
解決: 証明書の修正、HSSTSエントリの削除
⑤ SNI関連エラー:
原因: 古いクライアントがSNI非対応
確認: openssl s_client -connect host:443 -servername host
解決: デフォルト証明書の設定、古いクライアントの切り捨て
⑥ Protocol/Cipher Mismatch:
原因: クライアントとサーバーで共通の暗号スイートなし
確認: sslyze / testssl.sh で対応暗号確認
解決: 暗号スイート設定の見直し
⑦ TLS接続タイムアウト:
原因: ファイアウォール、プロキシ、MTU問題
確認: tcpdump でパケットキャプチャ
解決: ファイアウォールルール確認、MTU調整
⑧ クライアント証明書エラー(mTLS):
原因: クライアント証明書がない、または無効
確認: curl --cert client.crt --key client.key https://...
解決: クライアント証明書の配布、CA証明書の設定
14. mTLS(相互TLS認証)の実践
mTLS(mutual TLS):
→ 通常のTLSに加えて、クライアントも証明書で認証
→ サーバー → クライアント認証 + クライアント → サーバー認証
通常のTLS:
クライアント ───────→ サーバー
← サーバー証明書
「サーバーは本物か?」のみ検証
mTLS:
クライアント ───────→ サーバー
← サーバー証明書
→ クライアント証明書
「サーバーは本物か?」+ 「クライアントは本物か?」を相互検証
用途:
① マイクロサービス間通信: サービス同士の認証
② IoTデバイス認証: デバイス証明書による接続制御
③ VPNの代替: BeyondCorp / ゼロトラスト
④ 金融系API: 強固な認証が必要なケース
14.1 mTLS証明書の作成と設定
# プライベートCA(Root CA)の作成
# 1. ルートCA秘密鍵の生成
$ openssl ecparam -genkey -name prime256v1 -out root-ca.key
# 2. ルートCA証明書の生成(10年間有効)
$ openssl req -new -x509 -sha256 -key root-ca.key \
-out root-ca.crt -days 3650 \
-subj "/C=JP/O=MyOrg/CN=MyOrg Root CA"
# サーバー証明書の作成
# 3. サーバー秘密鍵
$ openssl ecparam -genkey -name prime256v1 -out server.key
# 4. CSR(証明書署名要求)の作成
$ openssl req -new -sha256 -key server.key -out server.csr \
-subj "/C=JP/O=MyOrg/CN=api.example.com"
# 5. サーバー証明書の署名(SANを含む)
$ cat > server-ext.cnf << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = api.example.com
DNS.2 = *.api.example.com
EOF
$ openssl x509 -req -sha256 -in server.csr -CA root-ca.crt \
-CAkey root-ca.key -CAcreateserial -out server.crt \
-days 365 -extfile server-ext.cnf
# クライアント証明書の作成
# 6. クライアント秘密鍵
$ openssl ecparam -genkey -name prime256v1 -out client.key
# 7. クライアントCSR
$ openssl req -new -sha256 -key client.key -out client.csr \
-subj "/C=JP/O=MyOrg/CN=service-a"
# 8. クライアント証明書の署名
$ cat > client-ext.cnf << EOF
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature
extendedKeyUsage = clientAuth
EOF
$ openssl x509 -req -sha256 -in client.csr -CA root-ca.crt \
-CAkey root-ca.key -CAcreateserial -out client.crt \
-days 365 -extfile client-ext.cnf14.2 Nginx での mTLS 設定
server {
listen 443 ssl http2;
server_name api.example.com;
# サーバー証明書
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# mTLS設定
ssl_client_certificate /etc/nginx/certs/root-ca.crt; # クライアント証明書のCA
ssl_verify_client on; # クライアント証明書を必須化
ssl_verify_depth 2; # 証明書チェーンの深さ
# クライアント証明書情報をバックエンドに転送
location / {
proxy_pass http://backend;
proxy_set_header X-Client-DN $ssl_client_s_dn;
proxy_set_header X-Client-Serial $ssl_client_serial;
proxy_set_header X-Client-Verify $ssl_client_verify;
}
# 証明書検証に失敗した場合
# ssl_verify_client on; の場合、Nginx が 400 Bad Request を返す
# ssl_verify_client optional; にすると検証はするが必須でない
}14.3 Node.js での mTLS 接続
const https = require('https');
const fs = require('fs');
// mTLSサーバー
const serverOptions = {
key: fs.readFileSync('/path/to/server.key'),
cert: fs.readFileSync('/path/to/server.crt'),
ca: fs.readFileSync('/path/to/root-ca.crt'), // クライアント証明書のCA
requestCert: true, // クライアント証明書を要求
rejectUnauthorized: true // 検証失敗時に拒否
};
const server = https.createServer(serverOptions, (req, res) => {
const clientCert = req.socket.getPeerCertificate();
console.log('Client CN:', clientCert.subject.CN);
console.log('Client Org:', clientCert.subject.O);
res.writeHead(200);
res.end(`Hello, ${clientCert.subject.CN}!`);
});
server.listen(443);
// mTLSクライアント
const clientOptions = {
hostname: 'api.example.com',
port: 443,
path: '/api/data',
method: 'GET',
key: fs.readFileSync('/path/to/client.key'),
cert: fs.readFileSync('/path/to/client.crt'),
ca: fs.readFileSync('/path/to/root-ca.crt')
};
const clientReq = https.request(clientOptions, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => console.log('Response:', data));
});
clientReq.end();15. TLSセキュリティの監査とテスト
15.1 自動セキュリティ監査ツール
# testssl.sh — 包括的なTLS設定テスト
$ git clone --depth 1 https://github.com/drwetter/testssl.sh.git
$ cd testssl.sh
$ ./testssl.sh https://example.com
# 出力例:
# Testing protocols via sockets
# SSLv2 not offered (OK)
# SSLv3 not offered (OK)
# TLS 1 not offered (OK)
# TLS 1.1 not offered (OK)
# TLS 1.2 offered (OK)
# TLS 1.3 offered (OK)
#
# Testing vulnerabilities
# Heartbleed not vulnerable (OK)
# CCS not vulnerable (OK)
# Ticketbleed not vulnerable (OK)
# ROBOT not vulnerable (OK)
# CRIME not vulnerable (OK)
# BREACH potentially vulnerable (WARN)
# POODLE not vulnerable (OK)
# DROWN not vulnerable (OK)
# LOGJAM not vulnerable (OK)
# BEAST not vulnerable (OK)
# sslyze — Python製のTLSスキャナー
$ pip install sslyze
$ sslyze example.com
# SSL Labs API — Webベースの詳細スキャン
$ curl "https://api.ssllabs.com/api/v3/analyze?host=example.com" | jq .
# nmap — TLS暗号スイートの列挙
$ nmap --script ssl-enum-ciphers -p 443 example.com15.2 TLSセキュリティチェックリスト
TLS設定チェックリスト(2024年版):
プロトコル:
[x] TLS 1.3 を有効化
[x] TLS 1.2 を有効化(互換性のため)
[x] TLS 1.1 以下を無効化
[x] SSLv2, SSLv3 を無効化
暗号スイート:
[x] AEADのみ許可(AES-GCM, ChaCha20-Poly1305)
[x] 前方秘匿性を提供する鍵交換のみ(ECDHE)
[x] RSA鍵交換を無効化
[x] 3DES, RC4, DES を無効化
[x] CBC モードを無効化(推奨)
証明書:
[x] RSA 2048bit以上 or ECDSA P-256以上
[x] SHA-256以上の署名アルゴリズム
[x] 中間CA証明書を含むフルチェーン
[x] SANにワイルドカードでなく具体的なドメインを列挙(可能な場合)
[x] 自動更新の設定と監視
[x] Certificate Transparency の有効化
セキュリティヘッダー:
[x] HSTS: max-age=31536000; includeSubDomains; preload
[x] HSTS Preload List への登録
[x] Expect-CT(レポートモード)
パフォーマンス:
[x] OCSP Stapling の有効化
[x] TLSセッション再開(Session Ticket / Session ID)
[x] TLS 1.3の0-RTT(安全なリクエストのみ)
[x] 適切なTLSレコードサイズ
運用:
[x] 秘密鍵の安全な保管(ファイルパーミッション 600)
[x] 証明書期限の監視とアラート
[x] 定期的なセキュリティスキャン(SSL Labs A+)
[x] インシデント時の証明書失効手順の整備
16. ポスト量子暗号とTLSの未来
量子コンピュータの脅威:
現在の公開鍵暗号:
RSA: 素因数分解問題に基づく → Shorのアルゴリズムで解読可能
ECDHE: 楕円曲線離散対数問題 → Shorのアルゴリズムで解読可能
→ 十分な量子ビット数が実現された場合、現在のTLSは破綻
タイムライン(推定):
2024-2030: 量子コンピュータの発展期
2030-2040: 暗号関連の量子コンピュータが実用化の可能性
現在: "Harvest Now, Decrypt Later" 攻撃への対策が急務
→ 現在暗号化された通信を保存し、将来量子コンピュータで解読
NIST ポスト量子暗号標準(2024年策定):
① ML-KEM(Module-Lattice-Based Key Encapsulation):
→ 旧称: CRYSTALS-Kyber
→ 鍵交換用(TLSのECDHEの代替)
→ 格子問題に基づく → 量子コンピュータでも解読困難
→ Chromeが実験的に ML-KEM-768 を実装済み
② ML-DSA(Module-Lattice-Based Digital Signature):
→ 旧称: CRYSTALS-Dilithium
→ デジタル署名用(証明書の署名の代替)
③ SLH-DSA(Stateless Hash-Based Digital Signature):
→ 旧称: SPHINCS+
→ ハッシュベースの署名(保守的な選択肢)
ハイブリッド方式(移行期の推奨アプローチ):
→ 従来の暗号 + ポスト量子暗号を組み合わせ
→ どちらか一方が破られても安全
TLS 1.3 での実装:
鍵交換: X25519 + ML-KEM-768(X25519MLKEM768)
→ 両方の共有秘密を結合して最終鍵を導出
→ 従来の安全性は維持しつつ量子耐性を追加
Chrome/Edge の対応状況(2024年〜):
→ X25519MLKEM768 をデフォルトで有効化
→ ハンドシェイクサイズが約1KB増加
→ 一部の古いミドルボックスで接続問題の報告あり
実務での推奨対応:
今すぐ:
✓ 暗号アジリティの確保(暗号スイートの変更が容易な設計)
✓ TLS 1.3の完全移行
✓ 長期保存データの暗号化レベルの見直し
2025-2030:
✓ ハイブリッド方式のテスト導入
✓ ライブラリ/フレームワークのPQC対応状況の監視
✓ HSMのポスト量子対応の確認
2030以降:
✓ 完全なポスト量子暗号への移行
✓ 古い暗号スイートの廃止
まとめ
| 概念 | ポイント |
|---|---|
| TLS | 暗号化 + 認証 + 完全性 |
| TLS 1.2 | 2 RTT ハンドシェイク |
| TLS 1.3 | 1 RTT(0-RTT再接続)、脆弱暗号廃止 |
| 証明書 | X.509、CA署名チェーン、DV/OV/EV |
| 前方秘匿性 | ECDHE で過去の通信を保護 |
| 暗号スイート | AES-GCM / ChaCha20-Poly1305、AEAD必須 |
| HSTS | HTTPS強制、Preload Listで初回から保護 |
| mTLS | 相互認証、マイクロサービス間通信 |
| CT | 証明書発行の透明性、不正発行の検知 |
| HTTP/3 | QUIC上でTLS 1.3組み込み、1-RTT |
| ポスト量子 | ML-KEM/ML-DSA、ハイブリッド方式で先行対応 |
FAQ
Q1: TLS 1.2とTLS 1.3の主な違いは何ですか?
A1: パフォーマンス、セキュリティ、シンプルさが大幅に改善されています。
主要な違い:
-
ハンドシェイクの高速化:
- TLS 1.2: 2 RTT(往復通信が2回必要)
- TLS 1.3: 1 RTT(往復通信が1回)
- TLS 1.3(再接続): 0-RTT(データと同時送信可能)
- 実測: TLS 1.2で200ms → TLS 1.3で100ms(東京-US間)
-
暗号化範囲の拡大:
- TLS 1.2: アプリケーションデータのみ暗号化
- TLS 1.3: ハンドシェイクの大部分も暗号化(証明書が見えない)
- プライバシー向上: 接続先サーバーの証明書が傍受者に見えない
-
脆弱な暗号の廃止:
- RSA鍵交換(前方秘匿性なし)→ 廃止
- CBC モード(パディングオラクル攻撃)→ 廃止
- 圧縮(CRIME攻撃)→ 廃止
- 再ネゴシエーション → 廃止
- → 常にECDHEによる前方秘匿性、AEADによる認証付き暗号
-
鍵導出の改善:
- TLS 1.2: PRF(Pseudo-Random Function)
- TLS 1.3: HKDF(HMAC-based KDF)— より明確で安全な設計
- 段階的鍵導出: 0-RTT鍵、ハンドシェイク鍵、アプリケーション鍵が分離
-
選択肢の削減によるセキュリティ向上:
- TLS 1.2: 約40種類の暗号スイート
- TLS 1.3: 5種類のみ(TLS_AES_128_GCM_SHA256等)
- → 設定ミスのリスク低減、監査が容易
実務での推奨: TLS 1.3をメインに、互換性のためTLS 1.2も許可。TLS 1.0/1.1は無効化。
Q2: 証明書チェーンの仕組みと、なぜ中間CA証明書が必要なのですか?
A2: 信頼の連鎖を構築しつつ、ルートCAの秘密鍵を安全に保つための設計です。
証明書チェーンの構造:
┌────────────────┐
│ ルートCA証明書 │ ← OS/ブラウザに内蔵(約150-200個)
│ (自己署名) │ 秘密鍵はオフライン保管(HSM内、物理的隔離)
└───────┬────────┘ 侵害されると全インターネットが影響│ 署名┌───────▼────────┐
│ 中間CA証明書 │ ← ルートCAが署名
│ │ 秘密鍵はオンライン(証明書発行に使用)
└───────┬────────┘ 侵害されてもルート失効で対処可能│ 署名┌───────▼────────┐
│ サーバー証明書 │ ← 中間CAが署名(Let's Encrypt等)
│ example.com │ 有効期限: DV証明書は通常90日
└────────────────┘
中間CAが必要な理由:
-
ルートCAの保護:
- ルートCAの秘密鍵は厳重に隔離(エアギャップ環境)
- 証明書発行の度にルートCAにアクセスするのは非現実的
- 中間CAがオンラインで発行処理を担当
-
リスクの分散:
- 中間CAが侵害されても、その中間CAのみ失効すればよい
- ルートCAが侵害されると、全ての派生証明書が無効に
- 実例: 2011年のDigiNotar侵害 → ルートCA失効 → ブラウザから削除
-
柔軟な運用:
- 用途別・地域別に複数の中間CAを使い分け
- DV専用、EV専用の中間CAで分離
- 中間CAの有効期限更新が容易
検証プロセス(ブラウザ側):
1. サーバー証明書の署名を中間CAの公開鍵で検証
2. 中間CA証明書の署名をルートCAの公開鍵で検証
3. ルートCA証明書がOS/ブラウザの信頼ストアに存在するか確認
4. 各証明書の有効期限を確認
5. ドメイン名(SAN)の一致を確認
6. 失効確認(OCSP Stapling / CRL / CRLite)
7. Certificate Transparencyログの確認
よくある問題と対処法:
問題1: 中間CA証明書の送信漏れ
# 症状: "信頼されていない証明書" エラー
# 原因: サーバーがサーバー証明書のみ送信、中間CAを含まない
# 確認:
$ openssl s_client -connect example.com:443 -showcerts
# 対処: fullchain.pem を使用(サーバー証明書+中間CA証明書)
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
# cert.pemではなくfullchain.pemを指定問題2: 証明書の順序が逆
正しい順序: サーバー証明書 → 中間CA証明書 → ルートCA証明書(省略可)
誤った順序: 中間CA → サーバー証明書 → エラー
クロスサイン: 新しいルートCAの普及期に、古いルートCAにも署名してもらうテクニック。
- Let's Encrypt: ISRG Root X1(新)+ DST Root CA X3(旧)でクロスサイン
- 古いAndroidデバイスでもDST経由で信頼チェーン成立
- 2024年現在はISRG Root X1がほぼ全デバイスで信頼済み
Q3: Let's Encryptの仕組みと実務での使い方を教えてください。
A3: ACMEプロトコルによる自動化されたドメイン検証で、無料のDV証明書を90日間発行します。
Let's Encryptの特徴:
- 完全無料: DV(ドメイン検証)証明書
- 自動化: certbotで取得・更新を自動化
- 有効期限: 90日(意図的に短く設定 → 自動更新の促進)
- 信頼: ISRG(Internet Security Research Group)運営、主要ブラウザで信頼済み
- 制限: ワイルドカード証明書はDNSチャレンジのみ、レート制限あり
ACMEプロトコルのフロー:
1. アカウント登録
クライアント → Let's Encrypt: 公開鍵を送信
→ アカウントURLを取得
2. 証明書リクエスト
クライアント → Let's Encrypt: example.comの証明書をリクエスト
3. チャレンジ提示
Let's Encrypt → クライアント: 以下のいずれかで認証せよ
① HTTP-01チャレンジ:
http://example.com/.well-known/acme-challenge/<TOKEN>
に特定の内容を配置
→ Let's Encryptがアクセスして確認
→ ポート80が必要、ワイルドカード不可
② DNS-01チャレンジ:
_acme-challenge.example.com TXT "<TOKEN>"
にDNSレコードを設定
→ Let's EncryptがDNS問い合わせで確認
→ ワイルドカード証明書可能、ポート80不要
→ DNSプロバイダーのAPI必要(自動化のため)
③ TLS-ALPN-01チャレンジ:
TLSハンドシェイク中にトークンを含む証明書を提示
→ HTTP-01の代替、ポート443のみ使用
4. チャレンジ応答
クライアント: トークンを配置 → Let's Encryptに通知
5. 検証
Let's Encrypt: トークンを確認 → 成功
6. 証明書発行
クライアント: CSR(Certificate Signing Request)を送信
Let's Encrypt: 署名済み証明書を返却
実務での設定例:
# certbotインストール(Ubuntu)
$ sudo apt update
$ sudo apt install certbot python3-certbot-nginx
# Nginx用の証明書取得(自動設定)
$ sudo certbot --nginx -d example.com -d www.example.com
# → Nginxの設定ファイルを自動編集
# → 証明書を/etc/letsencrypt/live/example.com/に配置
# 証明書のみ取得(手動設定)
$ sudo certbot certonly --webroot -w /var/www/html \
-d example.com -d www.example.com
# ワイルドカード証明書(DNSチャレンジ必須)
$ sudo certbot certonly --manual --preferred-challenges dns \
-d '*.example.com' -d example.com
# → DNSレコード設定を指示される:
# _acme-challenge.example.com TXT "abc123..."
# → 設定後にEnterで検証
# 自動更新の設定(systemd timer、Ubuntuでは自動設定済み)
$ systemctl list-timers | grep certbot
# または cron
$ sudo crontab -e
0 0,12 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
# → 1日2回チェック、期限30日以内なら更新
# 更新のテスト(実際には更新しない)
$ sudo certbot renew --dry-run
# 証明書の確認
$ sudo certbot certificates
Found the following certs:
Certificate Name: example.com
Domains: example.com www.example.com
Expiry Date: 2024-03-31 12:00:00+00:00 (VALID: 89 days)
Certificate Path: /etc/letsencrypt/live/example.com/fullchain.pem
Private Key Path: /etc/letsencrypt/live/example.com/privkey.pemNginx設定(手動の場合):
server {
listen 443 ssl http2;
server_name example.com;
# Let's Encrypt証明書
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# ACME チャレンジパス(更新時に必要)
location /.well-known/acme-challenge/ {
root /var/www/html;
allow all;
}
}
# HTTPからHTTPSへリダイレクト
server {
listen 80;
server_name example.com;
# ACMEチャレンジのみ許可
location /.well-known/acme-challenge/ {
root /var/www/html;
allow all;
}
# それ以外はHTTPSへ
location / {
return 301 https://$host$request_uri;
}
}レート制限(2024年時点):
- 登録ドメイン/週: 50証明書
- 重複証明書/週: 5証明書
- アカウント登録: 10アカウント/3時間/IP
- → 本番前にステージング環境で十分テスト
ステージング環境:
$ sudo certbot --nginx --staging \
-d example.com -d www.example.com
# → レート制限が緩い、信頼されない証明書(テスト用)トラブルシューティング:
# ログ確認
$ sudo tail -f /var/log/letsencrypt/letsencrypt.log
# 証明書の手動削除
$ sudo certbot delete --cert-name example.com
# Nginxの設定確認
$ sudo nginx -t
# ポート80が開いているか確認(HTTP-01チャレンジ)
$ curl http://example.com/.well-known/acme-challenge/test代替クライアント:
- acme.sh: シェルスクリプトのみ、依存なし
- Caddy: Webサーバーに証明書取得機能を内蔵
- Traefik: リバースプロキシに証明書取得機能を内蔵
次に読むべきガイド
参考文献
- RFC 8446. "The Transport Layer Security (TLS) Protocol Version 1.3." IETF, 2018.
- RFC 5246. "The Transport Layer Security (TLS) Protocol Version 1.2." IETF, 2008.
- RFC 8996. "Deprecating TLS 1.0 and TLS 1.1." IETF, 2021.
- RFC 6960. "X.509 Internet Public Key Infrastructure Online Certificate Status Protocol - OCSP." IETF, 2013.
- RFC 6962. "Certificate Transparency." IETF, 2013.
- RFC 6797. "HTTP Strict Transport Security (HSTS)." IETF, 2012.
- Mozilla. "Security/Server Side TLS." Mozilla Wiki, 2024.
- SSL Labs. "SSL/TLS Deployment Best Practices." Qualys, 2024.
- NIST. "SP 800-52 Rev. 2: Guidelines for the Selection, Configuration, and Use of TLS Implementations." 2019.
- Cloudflare. "What is TLS?" Cloudflare Learning Center, 2024.