TLS/証明書
TLS ハンドシェイクから証明書チェーン、Let's Encrypt による自動化、mTLS まで、安全な通信の基盤技術を体系的に学ぶ
TLS/証明書
TLS ハンドシェイクから証明書チェーン、Let's Encrypt による自動化、mTLS まで、安全な通信の基盤技術を体系的に学ぶ
この章で学ぶこと
- TLS ハンドシェイクの仕組み — クライアントとサーバ間で暗号化通信が確立されるまでの全ステップ
- 証明書チェーンと PKI — ルート CA から中間 CA、サーバ証明書に至る信頼の連鎖
- 実運用での証明書管理 — Let's Encrypt による自動化と mTLS による双方向認証
前提知識
- 公開鍵暗号の基本(RSA、楕円曲線暗号、電子署名)
- TCP/IP ネットワーキング(3ウェイハンドシェイク、ポート)
- HTTP プロトコルの基本動作
関連ガイド
- 鍵管理 — 暗号鍵のライフサイクルと安全な保管方法
- ネットワークセキュリティ基礎 — ファイアウォール・IDS/IPS による多層防御
- API セキュリティ — TLS の上に構築する API 保護
- DNS セキュリティ — DANE/TLSA による DNS ベースの証明書検証
1. TLS の全体像
TLS とは何か
TLS (Transport Layer Security) はトランスポート層の上で動作する暗号化プロトコルである。SSL の後継として策定され、現在の推奨バージョンは TLS 1.3 (RFC 8446) である。
TLS は以下の 3 つのセキュリティ特性を提供する:
- 機密性 (Confidentiality): 通信内容の暗号化
- 完全性 (Integrity): データ改竄の検知
- 認証 (Authentication): 通信相手の身元確認
+-------------------+
| Application | HTTP, SMTP, IMAP, etc.
+-------------------+
| TLS | 暗号化・認証・完全性
| +-------------+ |
| | Record | | データの分割・暗号化・MAC
| +-------------+ |
| | Handshake | | 鍵交換・認証・パラメータ交渉
| +-------------+ |
| | Alert | | エラー通知・接続終了
| +-------------+ |
| | Change Cipher| | 暗号仕様の切り替え (TLS 1.2)
| +-------------+ |
+-------------------+
| TCP | 信頼性のある配送
+-------------------+
| IP | ルーティング
+-------------------+
TLS のバージョン比較
| バージョン | 状態 | ハンドシェイク RTT | 主な特徴 | 廃止理由/脆弱性 |
|---|---|---|---|---|
| SSL 2.0 | 廃止 (2011) | 2-RTT | - | 根本的な設計欠陥 |
| SSL 3.0 | 廃止 (2015) | 2-RTT | - | POODLE 脆弱性 |
| TLS 1.0 | 廃止 (2020) | 2-RTT | SSL 3.0 の改良 | BEAST 脆弱性 |
| TLS 1.1 | 廃止 (2020) | 2-RTT | CBC 改善 | Lucky13、モダン暗号スイート未対応 |
| TLS 1.2 | 現役 | 2-RTT | AEAD 暗号スイート | GCM 推奨、CBC は非推奨 |
| TLS 1.3 | 推奨 | 1-RTT (0-RTT可) | ハンドシェイク簡素化 | 前方秘匿性必須 |
TLS 1.3 で廃止されたもの
+------------------------------------------------------------------+
| TLS 1.3 で廃止された機能・アルゴリズム |
|------------------------------------------------------------------|
| |
| [鍵交換] |
| - RSA 鍵交換 (静的RSA) → ECDHE のみに |
| 理由: 前方秘匿性なし。サーバ秘密鍵の漏洩で過去の通信全て復号可能 |
| |
| [暗号アルゴリズム] |
| - RC4 → 廃止 (統計的バイアス) |
| - 3DES → 廃止 (64bitブロック、Sweet32攻撃) |
| - CBC モード → 廃止 (パディングオラクル) |
| - DES → 廃止 (鍵長不足) |
| |
| [ハッシュ] |
| - MD5 → 廃止 (衝突攻撃) |
| - SHA-1 → 廃止 (衝突攻撃) |
| |
| [プロトコル機能] |
| - 圧縮 → 廃止 (CRIME/BREACH 攻撃) |
| - 再ネゴシエーション → 廃止 (三者間攻撃) |
| - ChangeCipherSpec メッセージ → 廃止 |
| - カスタム暗号スイート定義 → 5 スイートのみに限定 |
+------------------------------------------------------------------+
2. TLS 1.3 ハンドシェイク
ハンドシェイクの流れ
Client Server
| |
|--- ClientHello -----------------------> |
| + supported_versions: [TLS 1.3] |
| + key_share: (x25519 公開鍵) |
| + signature_algorithms: [Ed25519, |
| ECDSA-P256, RSA-PSS] |
| + psk_key_exchange_modes |
| + supported_groups: [x25519, P-256] |
| |
| <--- ServerHello --------------------- |
| + key_share: (x25519 公開鍵) |
| [ここからサーバ→クライアント暗号化] |
| <--- EncryptedExtensions ------------- |
| <--- Certificate --------------------- |
| (サーバ証明書チェーン) |
| <--- CertificateVerify -------------- |
| (ハンドシェイクメッセージの署名) |
| <--- Finished ----------------------- |
| (ハンドシェイクの MAC) |
| |
|--- Finished -------------------------> |
| [ここからクライアント→サーバ暗号化] |
| |
|========= 暗号化通信開始 ================|
TLS 1.3 の暗号スイート
TLS 1.3 で使用可能な 5 つの暗号スイート:
TLS_AES_256_GCM_SHA384 最も推奨
TLS_AES_128_GCM_SHA256 推奨
TLS_CHACHA20_POLY1305_SHA256 モバイル/ARM に最適
TLS_AES_128_CCM_SHA256 IoT 向け
TLS_AES_128_CCM_8_SHA256 IoT (短い認証タグ)
暗号スイートの構成要素 (TLS 1.3):
[AEAD暗号] + [ハッシュ]
鍵交換: ECDHE (x25519 または P-256) — 暗号スイート外で選択
署名: Ed25519, ECDSA, RSA-PSS — 暗号スイート外で選択
注: TLS 1.2 では暗号スイートが鍵交換+認証+暗号+MACの4要素を
含んでいたが、TLS 1.3 では分離された。
TLS 1.2 と 1.3 のハンドシェイク比較
| 項目 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| ラウンドトリップ | 2-RTT | 1-RTT |
| 0-RTT 再接続 | 不可 | 可能 (Early Data) |
| 鍵交換 | RSA / ECDHE | ECDHE のみ |
| 暗号スイート数 | 数十個 | 5個 |
| ハンドシェイク暗号化 | なし (平文) | ServerHello 以降暗号化 |
| 前方秘匿性 | オプション | 必須 |
| 圧縮 | サポート | 廃止 |
| 再ネゴシエーション | あり | なし (KeyUpdate で代替) |
| セッション再開 | セッションID/チケット | PSK ベース |
| ServerHello の内容 | 平文 | 暗号化 |
OpenSSL でハンドシェイクを確認
# コード例1: TLS 接続の詳細確認
# TLS 1.3 ハンドシェイクの詳細を表示
openssl s_client -connect example.com:443 -tls1_3 -msg
# 証明書チェーンを確認
openssl s_client -connect example.com:443 -showcerts
# 使用される暗号スイートの確認
openssl s_client -connect example.com:443 -cipher 'TLS_AES_256_GCM_SHA384'
# サーバがサポートする暗号スイート一覧
nmap --script ssl-enum-ciphers -p 443 example.com
# testssl.sh による包括的なTLSテスト
testssl.sh https://example.com
# sslyze による詳細分析
sslyze --regular example.com
# curl でTLSバージョンとプロトコル情報を表示
curl -vI https://example.com 2>&1 | grep -E "SSL|TLS|subject|issuer"0-RTT (Early Data) の仕組みと注意点
0-RTT の流れ (PSK ベースの再接続):
Client Server
| |
|--- ClientHello -----------------------> |
| + pre_shared_key (前回のセッションから) |
| + early_data (アプリケーションデータ) |
| |
| <--- ServerHello --------------------- |
| <--- Finished ----------------------- |
| |
|--- Finished -------------------------> |
| |
| 0-RTT で送ったデータは既にサーバで処理済み |
利点: 再接続時のレイテンシがゼロ
リスク: リプレイ攻撃が可能
対策:
+-- べき等でない操作 (POST, DELETE) には使用しない
+-- Anti-Replay 機構をサーバ側で実装
+-- Nginx: ssl_early_data on; + Early-Data ヘッダの確認
+-- サーバ側で同一 PSK の 0-RTT を一度だけ受け入れる
前方秘匿性 (Forward Secrecy) の仕組み
前方秘匿性 (PFS) なし (静的RSA鍵交換):
Client Server
|--- RSA暗号化(premaster secret) --> |
| サーバの公開鍵で暗号化 |
| |
問題: サーバの秘密鍵が将来漏洩した場合、
過去に記録した通信全てを復号できる
前方秘匿性あり (ECDHE 鍵交換):
Client Server
|--- ECDHE公開鍵 ------------------> |
|<--- ECDHE公開鍵 ------------------|
| 両者が独立に共有秘密を計算 |
| 一時的な鍵ペアは破棄 |
| |
利点: 各セッションで異なる鍵ペアを使用するため、
サーバの秘密鍵が漏洩しても過去の通信は復号不可能
TLS 1.3 では PFS が必須 (ECDHE のみ)
3. 証明書チェーンと PKI
証明書チェーンの構造
+---------------------------+
| Root CA 証明書 | 自己署名 / OS・ブラウザに内蔵
| (例: DigiCert Root) | 有効期間: 20-30年
| 自分自身の秘密鍵で署名 |
+---------------------------+
|
| 署名
v
+---------------------------+
| 中間 CA 証明書 | Root CA が署名
| (例: DigiCert SHA2) | 有効期間: 5-10年
| エンドエンティティに署名 | Root CA のオフライン保護
+---------------------------+
|
| 署名
v
+---------------------------+
| サーバ証明書 | 中間 CA が署名
| (例: *.example.com) | 有効期間: 最大398日
| サーバが TLS で提示 | (CA/B Forum 規定)
+---------------------------+
なぜ中間 CA が必要か:
1. Root CA の秘密鍵をオフラインで保護(HSM に格納)
2. Root CA の侵害時の被害範囲を限定
3. 証明書失効時に中間 CA のみ失効すれば済む
4. 異なるポリシー/用途ごとに中間 CA を分離
証明書の中身を確認
# コード例2: 証明書の詳細確認コマンド
# 証明書の内容をデコード
openssl x509 -in server.crt -text -noout
# 出力例:
# Certificate:
# Data:
# Version: 3 (0x2)
# Serial Number: 04:00:00:00:00:01:2f:...
# Signature Algorithm: sha256WithRSAEncryption
# Issuer: C=US, O=DigiCert Inc, CN=DigiCert SHA2 ...
# Validity:
# Not Before: Jan 1 00:00:00 2025 GMT
# Not After : Dec 31 23:59:59 2025 GMT
# Subject: CN=*.example.com
# Subject Public Key Info:
# Public Key Algorithm: id-ecPublicKey
# Public-Key: (256 bit)
# X509v3 extensions:
# X509v3 Subject Alternative Name:
# DNS:*.example.com, DNS:example.com
# X509v3 Key Usage: critical
# Digital Signature
# X509v3 Extended Key Usage:
# TLS Web Server Authentication
# Authority Information Access:
# OCSP - URI:http://ocsp.digicert.com
# CA Issuers - URI:http://cacerts.digicert.com/...
# X509v3 CRL Distribution Points:
# URI:http://crl3.digicert.com/...
# 証明書チェーンの検証
openssl verify -CAfile ca-bundle.crt server.crt
# リモートサーバの証明書チェーン全体を表示
openssl s_client -connect example.com:443 -showcerts 2>/dev/null | \
openssl x509 -noout -subject -issuer -dates
# 証明書のフィンガープリント
openssl x509 -in server.crt -fingerprint -sha256 -noout
# CSR (証明書署名要求) の確認
openssl req -in server.csr -text -noout
# 秘密鍵と証明書の一致確認
openssl x509 -in server.crt -modulus -noout | openssl md5
openssl rsa -in server.key -modulus -noout | openssl md5
# 同じハッシュ値なら一致X.509 証明書の主要フィールド
+-----------------------------------------------------+
| X.509 v3 Certificate |
|-----------------------------------------------------|
| Version: 3 (v3) |
| Serial Number: 一意の識別子 (CA内で一意) |
| Signature Algorithm: sha256WithRSAEncryption |
| or ecdsa-with-SHA256 |
| Issuer: 発行者 (CA) の Distinguished Name|
| Validity: |
| Not Before: 発行日時 |
| Not After: 有効期限 |
| Subject: 所有者の Distinguished Name |
| Public Key: 公開鍵 (RSA 2048+ / ECDSA P-256)|
| Extensions: |
| - Subject Alt Name (SAN): ドメイン一覧 (必須) |
| - Key Usage: digitalSignature, keyEncipherment |
| - Extended Key Usage: serverAuth, clientAuth |
| - Basic Constraints: CA:FALSE / CA:TRUE |
| - Authority Key Identifier: 発行者鍵の識別子 |
| - Subject Key Identifier: 証明書鍵の識別子 |
| - CRL Distribution Points: CRL の取得先 |
| - Authority Info Access: OCSP レスポンダ URL |
| - Certificate Policies: 発行ポリシー OID |
| - SCT List: Certificate Transparency ログ |
| Signature: CA の電子署名 |
+-----------------------------------------------------+
証明書の種類と検証レベル
| 種類 | 略称 | 検証内容 | 発行時間 | 用途 | ブラウザ表示 |
|---|---|---|---|---|---|
| ドメイン検証 | DV | ドメイン制御の確認のみ | 数分 | 一般的なWebサイト | 鍵マーク |
| 組織検証 | OV | + 組織の実在確認 | 1-3日 | 企業サイト | 鍵マーク + 組織名 |
| 拡張検証 | EV | + 厳格な組織審査 | 1-4週間 | 金融・EC サイト | 鍵マーク + 組織名 |
| ワイルドカード | WC | *.example.com | DV/OV に準ずる | 多数のサブドメイン | 同上 |
| マルチドメイン | SAN | 複数ドメインを1枚に | DV/OV に準ずる | 複数サイトの統合 | 同上 |
4. 証明書の失効チェック
OCSP と CRL の比較
+------------------------------------------------------------------+
| 証明書失効チェックの仕組み |
|------------------------------------------------------------------|
| |
| [CRL (Certificate Revocation List)] |
| CA が定期的に失効証明書リストを公開 |
| クライアントがリストをダウンロードして検証 |
| |
| 問題点: |
| - リストが巨大化 (数MB) |
| - 更新頻度に依存 (リアルタイム性なし) |
| - ダウンロード失敗時のフォールバック (fail-open) |
| |
| [OCSP (Online Certificate Status Protocol)] |
| クライアントが個別の証明書について CA に問い合わせ |
| |
| 問題点: |
| - レイテンシの増加 (CA への往復) |
| - プライバシー (CA にアクセス先が漏れる) |
| - OCSP レスポンダの障害リスク |
| |
| [OCSP Stapling (推奨)] |
| サーバが CA から OCSP レスポンスを事前に取得 |
| TLS ハンドシェイク時にクライアントに提供 |
| |
| 利点: |
| - クライアントが CA に直接問い合わせる必要がない |
| - プライバシーが保護される |
| - レイテンシの増加なし |
+------------------------------------------------------------------+
| 方式 | リアルタイム性 | プライバシー | パフォーマンス | 信頼性 |
|---|---|---|---|---|
| CRL | 低 (定期更新) | 高 | 低 (大きなリスト) | 中 |
| OCSP | 高 | 低 (CA に漏れる) | 中 (往復あり) | CA依存 |
| OCSP Stapling | 高 | 高 | 高 | サーバ依存 |
| CRLite (実験的) | 高 | 高 | 高 | ブラウザ内蔵 |
# コード例3: Nginx での OCSP Stapling 設定
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
# OCSP Stapling の有効化
ssl_stapling on;
ssl_stapling_verify on;
# OCSP レスポンダへの接続に使用する CA 証明書
ssl_trusted_certificate /etc/nginx/ssl/ca-chain.crt;
# OCSP レスポンスの DNS 解決
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
}Certificate Transparency (CT)
Certificate Transparency の仕組み:
CA が証明書を発行する際:
1. CA が証明書を CT ログサーバに提出
2. CT ログサーバが SCT (Signed Certificate Timestamp) を返す
3. SCT が証明書に埋め込まれる
4. ブラウザが SCT を検証し、CT ログに記録されていることを確認
目的:
- 不正な証明書の発行を検知 (例: 2011年 DigiNotar 事件)
- ドメイン所有者が自分のドメインの証明書を監視
- 透明性による CA の信頼性向上
Chrome の要件:
- 2018年4月以降、全ての新規証明書は CT ログへの登録が必須
- 最低 2 つの CT ログからの SCT が必要
監視ツール:
- crt.sh: https://crt.sh/?q=example.com
- Google CT Dashboard
- certspotter (CLI ツール)
# コード例4: CT ログの監視
# crt.sh で特定ドメインの全証明書を確認
curl -s "https://crt.sh/?q=%25.example.com&output=json" | \
python3 -m json.tool | head -50
# certspotter でリアルタイム監視
# certspotter watch example.com5. Let's Encrypt による自動化
ACME プロトコルの仕組み
ACME (Automatic Certificate Management Environment) - RFC 8555:
クライアント (certbot) Let's Encrypt CA
| |
|--- (1) アカウント登録 ------------> |
| JWK公開鍵 + 利用規約同意 |
| |
| <--- アカウント作成完了 ---------- |
| |
|--- (2) 証明書発行リクエスト ------> |
| (ドメイン: example.com) |
| |
| <--- (3) チャレンジ発行 ---------- |
| (HTTP-01 or DNS-01) |
| |
|--- (4) チャレンジ応答 ------------> |
| HTTP-01: /.well-known/acme- |
| challenge/{token} にトークン配置 |
| DNS-01: _acme-challenge TXT |
| レコードにトークン設定 |
| |
| <--- (5) 検証実行 ---------------- |
| CA が HTTP/DNS でトークンを確認 |
| |
| <--- (6) 検証完了 ---------------- |
| |
|--- (7) CSR 送信 ------------------> |
| |
| <--- (8) 証明書発行 -------------- |
| (チェーン付き証明書) |
certbot による証明書取得
# コード例5: certbot の各種使用方法
# Nginx 用に証明書を取得・設定(最も簡単)
sudo certbot --nginx -d example.com -d www.example.com
# Apache 用
sudo certbot --apache -d example.com
# スタンドアロンモード(既存のWebサーバを停止して使用)
sudo certbot certonly --standalone -d example.com
# Webroot モード(既存のWebサーバを停止せずに使用)
sudo certbot certonly --webroot -w /var/www/html -d example.com
# DNS-01 チャレンジでワイルドカード証明書を取得
sudo certbot certonly --manual --preferred-challenges dns \
-d "*.example.com" -d "example.com"
# DNS-01 + Cloudflare プラグインで自動化
sudo certbot certonly \
--dns-cloudflare \
--dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
-d "*.example.com" -d "example.com"
# 自動更新のテスト
sudo certbot renew --dry-run
# 自動更新の設定
# systemd timer (推奨)
# /etc/systemd/system/certbot-renewal.timer
# [Timer]
# OnCalendar=*-*-* 03:00:00
# RandomizedDelaySec=3600
# crontab による自動更新(代替)
# 0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"
# 証明書の情報確認
sudo certbot certificates
# 証明書の手動失効
sudo certbot revoke --cert-path /etc/letsencrypt/live/example.com/cert.pemチャレンジ方式の比較
| 方式 | 用途 | 自動化 | ワイルドカード | ポート要件 | DNS 変更 |
|---|---|---|---|---|---|
| HTTP-01 | Web サーバ | 容易 | 不可 | 80 | 不要 |
| DNS-01 | 任意 | DNS API 必要 | 可能 | 不要 | 必要 |
| TLS-ALPN-01 | 443 ポートのみ | 中程度 | 不可 | 443 | 不要 |
ACME クライアントの比較
| クライアント | 言語 | 特徴 | 用途 |
|---|---|---|---|
| certbot | Python | 公式、最も普及 | 汎用 |
| acme.sh | Shell | 軽量、依存関係なし | シンプルな環境 |
| lego | Go | シングルバイナリ | Docker/CI |
| cert-manager | Go | K8s ネイティブ | Kubernetes |
| Caddy (内蔵) | Go | 自動TLS | Webサーバ |
| Traefik (内蔵) | Go | 自動TLS | リバースプロキシ |
6. mTLS (Mutual TLS)
通常の TLS と mTLS の違い
通常の TLS (一方向認証):
Client ---- サーバ証明書を検証 ---> Server
(クライアントは認証されない)
用途: 一般的な HTTPS
mTLS (双方向認証):
Client ---- サーバ証明書を検証 ---> Server
Client <--- クライアント証明書を検証 --- Server
(双方が認証される)
用途: マイクロサービス間通信、ゼロトラスト、API 認証
mTLS ハンドシェイクの詳細
Client Server
| |
|--- ClientHello -----------------------> |
| |
| <--- ServerHello --------------------- |
| <--- EncryptedExtensions ------------- |
| <--- CertificateRequest ------------- | ← mTLS: クライアント証明書を要求
| (許可される CA の一覧) |
| <--- Certificate --------------------- |
| <--- CertificateVerify -------------- |
| <--- Finished ----------------------- |
| |
|--- Certificate -----------------------> | ← mTLS: クライアント証明書を提示
|--- CertificateVerify ----------------> | ← mTLS: クライアントの署名
|--- Finished -------------------------> |
| |
|========= 双方向認証された暗号化通信 ======|
mTLS の設定例 (Nginx)
# コード例6: Nginx での mTLS 設定
server {
listen 443 ssl;
server_name api.example.com;
# サーバ証明書
ssl_certificate /etc/nginx/ssl/server.crt;
ssl_certificate_key /etc/nginx/ssl/server.key;
# TLS プロトコルと暗号スイートの設定
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;
# クライアント証明書の検証
ssl_client_certificate /etc/nginx/ssl/ca.crt; # 信頼するCA証明書
ssl_verify_client on; # 必須 (optional にすると選択的になる)
ssl_verify_depth 2; # チェーンの最大深度
# CRL (証明書失効リスト) の設定
ssl_crl /etc/nginx/ssl/ca.crl;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
location / {
# クライアント証明書の情報をバックエンドに転送
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;
proxy_set_header X-Client-Fingerprint $ssl_client_fingerprint;
proxy_pass http://backend;
}
}Go でのクライアント証明書生成と mTLS クライアント
// コード例7: Go での mTLS 実装(サーバ + クライアント)
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net/http"
"os"
"time"
)
// ===== CA と証明書の生成 =====
func generateCA() (*x509.Certificate, *ecdsa.PrivateKey, error) {
caKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"MyOrg Internal CA"},
CommonName: "MyOrg Root CA",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
}
certDER, _ := x509.CreateCertificate(
rand.Reader, template, template, &caKey.PublicKey, caKey,
)
cert, _ := x509.ParseCertificate(certDER)
return cert, caKey, nil
}
func generateClientCert(caCert *x509.Certificate, caKey *ecdsa.PrivateKey,
commonName string) error {
// クライアント鍵ペア生成
clientKey, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
// 証明書テンプレート
template := &x509.Certificate{
SerialNumber: big.NewInt(2),
Subject: pkix.Name{
Organization: []string{"MyOrg"},
CommonName: commonName,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
}
// CA で署名
certDER, _ := x509.CreateCertificate(
rand.Reader, template, caCert, &clientKey.PublicKey, caKey,
)
// PEM 書き出し
certFile, _ := os.Create(commonName + ".crt")
pem.Encode(certFile, &pem.Block{Type: "CERTIFICATE", Bytes: certDER})
certFile.Close()
keyDER, _ := x509.MarshalECPrivateKey(clientKey)
keyFile, _ := os.Create(commonName + ".key")
pem.Encode(keyFile, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})
keyFile.Close()
return nil
}
// ===== mTLS サーバ =====
func startMTLSServer(caCertPool *x509.CertPool) {
tlsConfig := &tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
ClientCAs: caCertPool,
MinVersion: tls.VersionTLS13,
}
server := &http.Server{
Addr: ":8443",
TLSConfig: tlsConfig,
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// クライアント証明書の情報を取得
if len(r.TLS.PeerCertificates) > 0 {
clientCert := r.TLS.PeerCertificates[0]
fmt.Fprintf(w, "Hello, %s (verified by mTLS)\n",
clientCert.Subject.CommonName)
}
})
server.ListenAndServeTLS("server.crt", "server.key")
}
// ===== mTLS クライアント =====
func createMTLSClient(clientCert, clientKey, caCert string) (*http.Client, error) {
cert, err := tls.LoadX509KeyPair(clientCert, clientKey)
if err != nil {
return nil, err
}
caCertPEM, _ := os.ReadFile(caCert)
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCertPEM)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{cert},
RootCAs: caCertPool,
MinVersion: tls.VersionTLS13,
}
return &http.Client{
Transport: &http.Transport{TLSClientConfig: tlsConfig},
Timeout: 30 * time.Second,
}, nil
}Python での mTLS クライアント
# コード例8: Python mTLS クライアント
import httpx
import ssl
def create_mtls_client(
client_cert: str,
client_key: str,
ca_cert: str,
) -> httpx.Client:
"""mTLS 対応の HTTP クライアントを作成"""
ssl_context = ssl.create_default_context(
purpose=ssl.Purpose.SERVER_AUTH,
cafile=ca_cert,
)
ssl_context.load_cert_chain(
certfile=client_cert,
keyfile=client_key,
)
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
return httpx.Client(
verify=ssl_context,
timeout=30.0,
)
# 使用例
client = create_mtls_client(
client_cert="client.crt",
client_key="client.key",
ca_cert="ca.crt",
)
response = client.get("https://api.example.com/data")
print(response.json())7. Nginx / Apache の TLS 設定ベストプラクティス
Mozilla SSL Configuration Generator 準拠の設定
# コード例9: Nginx TLS 設定(Modern プロファイル)
server {
listen 443 ssl http2;
server_name example.com;
# 証明書
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# プロトコル(TLS 1.3 のみ — Modern)
ssl_protocols TLSv1.3;
# TLS 1.2 も含める場合(Intermediate):
# ssl_protocols TLSv1.2 TLSv1.3;
# 暗号スイート
# TLS 1.3 の場合は ssl_ciphers の設定は不要(TLS 1.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 使用時)
# openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
# ssl_dhparam /etc/nginx/ssl/dhparam.pem;
# セッションキャッシュ
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # PFS を完全に保証するため
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
# 0-RTT (TLS 1.3)
ssl_early_data on;
# セキュリティヘッダ
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 → HTTPS リダイレクト(別の server ブロックで)
}
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}8. 証明書の自動管理と監視
証明書有効期限の監視
# コード例10: 証明書有効期限監視スクリプト
import ssl
import socket
import datetime
import json
from typing import Optional
class CertificateMonitor:
"""TLS 証明書の有効期限を監視"""
def __init__(self, warning_days: int = 30, critical_days: int = 7):
self.warning_days = warning_days
self.critical_days = critical_days
def check_certificate(self, hostname: str, port: int = 443) -> dict:
"""証明書情報を取得して有効期限をチェック"""
context = ssl.create_default_context()
try:
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.datetime.strptime(
cert["notAfter"], "%b %d %H:%M:%S %Y %Z"
)
days_remaining = (not_after - datetime.datetime.utcnow()).days
# ステータス判定
if days_remaining < 0:
status = "EXPIRED"
elif days_remaining <= self.critical_days:
status = "CRITICAL"
elif days_remaining <= self.warning_days:
status = "WARNING"
else:
status = "OK"
# SAN (Subject Alternative Name) の取得
san = []
for entry_type, value in cert.get("subjectAltName", []):
if entry_type == "DNS":
san.append(value)
return {
"hostname": hostname,
"status": status,
"days_remaining": days_remaining,
"not_after": not_after.isoformat(),
"issuer": dict(x[0] for x in cert["issuer"]),
"subject": dict(x[0] for x in cert["subject"]),
"san": san,
"serial_number": cert.get("serialNumber"),
"version": cert.get("version"),
}
except ssl.SSLCertVerificationError as e:
return {"hostname": hostname, "status": "VERIFICATION_ERROR",
"error": str(e)}
except (socket.timeout, ConnectionRefusedError) as e:
return {"hostname": hostname, "status": "CONNECTION_ERROR",
"error": str(e)}
def check_multiple(self, hostnames: list[str]) -> list[dict]:
"""複数ホストの証明書を一括チェック"""
results = []
for hostname in hostnames:
result = self.check_certificate(hostname)
results.append(result)
if result["status"] in ("CRITICAL", "EXPIRED"):
print(f"[{result['status']}] {hostname}: "
f"{result.get('days_remaining', 'N/A')} days remaining")
return results
# 使用例
monitor = CertificateMonitor(warning_days=30, critical_days=7)
domains = [
"example.com",
"api.example.com",
"app.example.com",
]
results = monitor.check_multiple(domains)
print(json.dumps(results, indent=2, default=str))Kubernetes での cert-manager
# コード例11: cert-manager による自動証明書管理
# ClusterIssuer (Let's Encrypt)
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: admin@example.com
privateKeySecretRef:
name: letsencrypt-production-key
solvers:
- http01:
ingress:
class: nginx
- dns01:
cloudflare:
email: admin@example.com
apiTokenSecretRef:
name: cloudflare-api-token
key: api-token
selector:
dnsZones:
- "example.com"
---
# Certificate リソース
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: example-com-tls
namespace: default
spec:
secretName: example-com-tls-secret
issuerRef:
name: letsencrypt-production
kind: ClusterIssuer
dnsNames:
- example.com
- "*.example.com"
duration: 2160h # 90日
renewBefore: 720h # 30日前に更新
---
# Ingress での使用
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-production"
spec:
tls:
- hosts:
- example.com
secretName: example-com-tls-secret
rules:
- host: example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web-service
port:
number: 809. エッジケース
エッジケース 1: SNI (Server Name Indication) と仮想ホスト
TLS ハンドシェイクの ClientHello に含まれる SNI 拡張により、同一 IP アドレスで複数ドメインの証明書を使い分けられる。ただし、SNI は平文で送信されるため、接続先ドメイン名がネットワーク上で可視。TLS 1.3 の Encrypted Client Hello (ECH) でこれを暗号化する提案がある。
エッジケース 2: 証明書ピンニングの問題
HPKP (HTTP Public Key Pinning) は証明書の公開鍵ハッシュをブラウザにピン留めする仕組みだったが、運用ミスによるサイト到達不能のリスクが高く、Chrome 72 で廃止された。代替として DANE/TLSA レコード(DNSSEC 前提)や Certificate Transparency の監視が推奨される。
エッジケース 3: ワイルドカード証明書と SAN の制限
ワイルドカード証明書(*.example.com)は 1 レベルのサブドメインのみカバーする。sub.sub.example.com はカバーされない。また、SAN に最大何ドメインまで含められるかは CA ごとに異なり(Let's Encrypt は 100 まで)、大量のドメインを持つ場合は複数の証明書が必要。
エッジケース 4: 時刻同期の影響
証明書の有効期間チェックはクライアントの時刻に依存する。NTP が正しく設定されていないクライアントでは、有効な証明書でも検証に失敗する場合がある。特に IoT デバイスや組み込みシステムで問題になりやすい。
10. アンチパターン
アンチパターン 1: 証明書検証の無効化
# NG: 本番環境で証明書検証を無効化
import requests
response = requests.get("https://api.example.com", verify=False)
# WARNING: InsecureRequestWarning が出るが無視される
# OK: 正しい CA バンドルを指定
response = requests.get("https://api.example.com", verify="/path/to/ca-bundle.crt")
# OK: 環境変数で CA バンドルを指定
# export REQUESTS_CA_BUNDLE=/path/to/ca-bundle.crtなぜ危険か: 中間者攻撃 (MITM) が可能になり、通信内容の盗聴・改竄のリスクがある。開発環境でも自己署名 CA を作成して正しく検証すべきである。verify=False はセキュリティスキャナで自動検出される重大な問題。
アンチパターン 2: 古い TLS バージョンの許可
# NG: TLS 1.0/1.1 を許可
ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3;
# OK: TLS 1.2 以上のみ許可
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
ssl_prefer_server_ciphers on;なぜ危険か: TLS 1.0/1.1 には既知の脆弱性 (BEAST, POODLE, Lucky13) があり、PCI DSS でも使用が禁止されている。2020年に主要ブラウザが TLS 1.0/1.1 のサポートを終了。
アンチパターン 3: 秘密鍵のハードコーディング
# NG: ソースコードに秘密鍵を埋め込む
PRIVATE_KEY = """-----BEGIN EC PRIVATE KEY-----
MHQCAQEEIBkg4LVWM...
-----END EC PRIVATE KEY-----"""
# OK: 環境変数またはシークレットマネージャから取得
import os
key_path = os.environ["TLS_KEY_PATH"]
with open(key_path) as f:
private_key = f.read()
# OK: AWS Secrets Manager / HashiCorp Vault から取得
import boto3
client = boto3.client("secretsmanager")
secret = client.get_secret_value(SecretId="tls/server-key")アンチパターン 4: 証明書更新の手動運用
NG: 証明書の更新を手動で行い、カレンダーで管理
→ 更新忘れでサービス障害が発生
→ 2020年: Microsoft Teams が証明書期限切れで数時間のダウン
OK: cert-manager, certbot, Caddy などで自動更新
→ 証明書の有効期限を Prometheus/Grafana で監視
→ 30日前にアラート、7日前にクリティカルアラート
11. 演習
演習 1(基礎): TLS 接続の確認
以下のコマンドを実行し、TLS 接続の詳細を観察せよ:
# 1. 任意のサイトの TLS バージョンと暗号スイートを確認
openssl s_client -connect google.com:443 -tls1_3
# 2. 証明書チェーンの発行者を確認
openssl s_client -connect github.com:443 -showcerts 2>/dev/null | \
grep -E "subject|issuer"
# 質問:
# - TLS 1.3 で使用されている暗号スイートは?
# - 証明書チェーンは何階層か?
# - Root CA は何か?演習 2(中級): 自己署名 CA と証明書の作成
OpenSSL を使用して以下を作成せよ:
- Root CA の鍵ペアと自己署名証明書
- 中間 CA の鍵ペアと Root CA で署名した証明書
- サーバ証明書(SAN 付き)
- 証明書チェーンの検証
演習 3(上級): mTLS サービス間通信
Go または Python で以下を実装せよ:
- 内部 CA の構築
- サーバ証明書とクライアント証明書の自動生成
- mTLS で保護された HTTP サーバ
- mTLS クライアントからの API 呼び出し
- クライアント証明書の失効チェック
12. パフォーマンスに関する考察
TLS ハンドシェイクのレイテンシ
| 設定 | レイテンシ (概算) | 最適化方法 |
|---|---|---|
| TLS 1.2 フルハンドシェイク | ~100ms | - |
| TLS 1.3 フルハンドシェイク | ~50ms | 1-RTT |
| TLS 1.3 0-RTT | ~0ms | PSK 再利用 |
| TLS セッション再開 (1.2) | ~50ms | セッションチケット |
| ECDSA 署名検証 | ~0.1ms | RSA より高速 |
| RSA 2048 署名検証 | ~0.3ms | - |
| OCSP ステープリング | 節約 ~50ms | CA への問い合わせ不要 |
暗号アルゴリズムのスループット比較
暗号化スループット (Intel Xeon, single core):
AES-256-GCM: ~5 GB/s (AES-NI ハードウェア)
AES-128-GCM: ~6 GB/s
ChaCha20-Poly1305: ~2 GB/s (ソフトウェア)
ChaCha20-Poly1305: ~4 GB/s (AVX2)
AES-256-CBC: ~1 GB/s (非推奨)
鍵交換:
ECDHE (x25519): ~50,000 ops/sec
ECDHE (P-256): ~30,000 ops/sec
RSA-2048 鍵交換: ~15,000 ops/sec
→ AES-NI 対応の x86 サーバでは AES-GCM が最速
→ ARM (モバイル/Raspberry Pi) では ChaCha20 が高速
実践演習
演習1: 基本的な実装
以下の要件を満たすコードを実装してください。
要件:
- 入力データの検証を行うこと
- エラーハンドリングを適切に実装すること
- テストコードも作成すること
# 演習1: 基本実装のテンプレート
class Exercise1:
"""基本的な実装パターンの演習"""
def __init__(self):
self.data = []
def validate_input(self, value):
"""入力値の検証"""
if value is None:
raise ValueError("入力値がNoneです")
return True
def process(self, value):
"""データ処理のメインロジック"""
self.validate_input(value)
self.data.append(value)
return self.data
def get_results(self):
"""処理結果の取得"""
return {
'count': len(self.data),
'data': self.data
}
# テスト
def test_exercise1():
ex = Exercise1()
assert ex.process(1) == [1]
assert ex.process(2) == [1, 2]
assert ex.get_results()['count'] == 2
try:
ex.process(None)
assert False, "例外が発生するべき"
except ValueError:
pass
print("全テスト合格!")
test_exercise1()演習2: 応用パターン
基本実装を拡張して、以下の機能を追加してください。
# 演習2: 応用パターン
from typing import List, Dict, Optional
from datetime import datetime
class AdvancedExercise:
"""応用パターンの演習"""
def __init__(self, max_size: int = 100):
self._items: List[Dict] = []
self._max_size = max_size
self._created_at = datetime.now()
def add(self, key: str, value: any) -> bool:
"""アイテムの追加(サイズ制限付き)"""
if len(self._items) >= self._max_size:
return False
self._items.append({
'key': key,
'value': value,
'timestamp': datetime.now().isoformat()
})
return True
def find(self, key: str) -> Optional[Dict]:
"""キーによる検索"""
for item in reversed(self._items):
if item['key'] == key:
return item
return None
def remove(self, key: str) -> bool:
"""キーによる削除"""
for i, item in enumerate(self._items):
if item['key'] == key:
self._items.pop(i)
return True
return False
def stats(self) -> Dict:
"""統計情報"""
return {
'total_items': len(self._items),
'max_size': self._max_size,
'usage_percent': len(self._items) / self._max_size * 100,
'uptime': str(datetime.now() - self._created_at)
}
# テスト
def test_advanced():
ex = AdvancedExercise(max_size=3)
assert ex.add("a", 1) == True
assert ex.add("b", 2) == True
assert ex.add("c", 3) == True
assert ex.add("d", 4) == False # サイズ制限
assert ex.find("b")['value'] == 2
assert ex.remove("b") == True
assert ex.find("b") is None
stats = ex.stats()
assert stats['total_items'] == 2
print("応用テスト全合格!")
test_advanced()演習3: パフォーマンス最適化
以下のコードのパフォーマンスを改善してください。
# 演習3: パフォーマンス最適化
import time
from functools import lru_cache
# 最適化前(O(n^2))
def slow_search(data: list, target: int) -> int:
"""非効率な検索"""
for i in range(len(data)):
for j in range(i + 1, len(data)):
if data[i] + data[j] == target:
return (i, j)
return (-1, -1)
# 最適化後(O(n))
def fast_search(data: list, target: int) -> tuple:
"""ハッシュマップを使った効率的な検索"""
seen = {}
for i, num in enumerate(data):
complement = target - num
if complement in seen:
return (seen[complement], i)
seen[num] = i
return (-1, -1)
# ベンチマーク
def benchmark():
import random
data = list(range(5000))
random.shuffle(data)
target = data[100] + data[4000]
start = time.time()
result1 = slow_search(data, target)
slow_time = time.time() - start
start = time.time()
result2 = fast_search(data, target)
fast_time = time.time() - start
print(f"非効率版: {slow_time:.4f}秒")
print(f"効率版: {fast_time:.6f}秒")
print(f"高速化率: {slow_time/fast_time:.0f}倍")
benchmark()ポイント:
- アルゴリズムの計算量を意識する
- 適切なデータ構造を選択する
- ベンチマークで効果を測定する
13. FAQ
Q1. TLS 1.3 の 0-RTT は安全か?
0-RTT (Early Data) は再接続時のレイテンシを削減するが、リプレイ攻撃のリスクがある。べき等でない操作 (POST による状態変更など) には使用すべきでない。Nginx では ssl_early_data on; で有効化し、バックエンドで Early-Data: 1 ヘッダを確認して保護する。Google は検索クエリ(べき等な GET)で 0-RTT を使用している。
Q2. 証明書の有効期間はどのくらいが適切か?
CA/Browser Forum の規定により、公開証明書の最大有効期間は 398 日 (約13ヶ月) である。Let's Encrypt は 90 日で発行し、自動更新を前提とする設計になっている。短い有効期間は鍵の危殆化リスクを低減する。Apple は 2025 年から 45 日への短縮を提案している。
Q3. 自己署名証明書を使ってよい場面は?
開発環境、内部マイクロサービス間の mTLS、テスト環境に限定すべきである。公開サービスでは必ず信頼された CA の証明書を使用する。自己署名でも CA を作成し、チェーンを正しく構成する運用が望ましい。
Q4. ECC (楕円曲線暗号) と RSA のどちらを選ぶべきか?
新規構築では ECC (P-256 or Ed25519) を推奨。RSA 2048 と同等の安全性を ECC 256 で実現でき、鍵サイズ・署名サイズが大幅に小さい。RSA は互換性が高いが、将来的には ECC が主流になる。Let's Encrypt は ECC 証明書をデフォルトで発行している。
Q5. TLS の終端はどこで行うべきか?
ロードバランサ/リバースプロキシで TLS を終端し、バックエンドへは HTTP で通信するパターンが一般的。ただし、ゼロトラスト環境では mTLS でバックエンドまで暗号化を維持すべき。サービスメッシュ (Istio, Linkerd) を使うと mTLS を透過的に導入できる。
FAQ
Q1: このトピックを学ぶ上で最も重要なポイントは何ですか?
実践的な経験を積むことが最も重要です。理論だけでなく、実際にコードを書いて動作を確認することで理解が深まります。
Q2: 初心者がよく陥る間違いは何ですか?
基礎を飛ばして応用に進むことです。このガイドで説明している基本概念をしっかり理解してから、次のステップに進むことをお勧めします。
Q3: 実務ではどのように活用されていますか?
このトピックの知識は、日常的な開発業務で頻繁に活用されます。特にコードレビューやアーキテクチャ設計の際に重要になります。
まとめ
| 項目 | 要点 |
|---|---|
| TLS バージョン | TLS 1.3 を推奨、最低でも TLS 1.2 |
| ハンドシェイク | TLS 1.3 は 1-RTT で完了し前方秘匿性を必須化 |
| 暗号スイート | AES-GCM / ChaCha20-Poly1305 + ECDHE |
| 証明書チェーン | Root CA → 中間 CA → サーバ証明書の信頼の連鎖 |
| 失効チェック | OCSP Stapling を推奨、CT ログで監視 |
| Let's Encrypt | ACME プロトコルで証明書の取得・更新を自動化 |
| mTLS | クライアント証明書による双方向認証でゼロトラスト実現 |
| 証明書管理 | 自動更新必須、秘密鍵はシークレットマネージャで保護 |
| 監視 | 証明書の有効期限を常時監視し期限切れを防止 |
| 0-RTT | レイテンシ削減だがリプレイリスクあり、べき等操作のみ |
次に読むべきガイド
- 鍵管理 — 暗号鍵のライフサイクルと安全な保管方法
- ネットワークセキュリティ基礎 — ファイアウォール・IDS/IPS による多層防御
- APIセキュリティ — TLS の上に構築する API 保護
- DNSセキュリティ — DANE/TLSA による DNS ベースの証明書検証
参考文献
- RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3 (2018) — https://datatracker.ietf.org/doc/html/rfc8446
- RFC 8555 — Automatic Certificate Management Environment (ACME) — https://datatracker.ietf.org/doc/html/rfc8555
- Mozilla SSL Configuration Generator — https://ssl-config.mozilla.org/ — サーバソフトウェア別の推奨 TLS 設定
- Let's Encrypt Documentation — https://letsencrypt.org/docs/ — ACME プロトコルと証明書自動化の公式ガイド
- Qualys SSL Labs — SSL/TLS Best Practices — https://github.com/ssllabs/research/wiki/SSL-and-TLS-Deployment-Best-Practices
- RFC 6960 — X.509 Internet PKI Online Certificate Status Protocol - OCSP — https://datatracker.ietf.org/doc/html/rfc6960
- Certificate Transparency (RFC 6962) — https://certificate.transparency.dev/
- cert-manager Documentation — https://cert-manager.io/docs/