第15章 HTTPS化とSSL/TLS証明書管理
LPIC-2 208.2 相当
前章ではcertbotによる自動証明書取得を扱いましたが、証明書の仕組みそのもの(TLSハンドシェイク・X.509構造・CA階層)や、opensslによる手動操作、SSL/TLSのセキュリティ設定は別に理解しておく必要があります。この章では、HTTPSの内部動作から、opensslコマンド体系、Apacheでの安全なHTTPS設定、Let's Encryptとの対応づけ、実際のトラブルシューティングまでを一気通貫で扱います。
TLSハンドシェイクの流れ
HTTPS通信を開始する前に、クライアントとサーバーはTLSハンドシェイクと呼ばれる手順を踏み、暗号方式の合意・サーバーの身元確認・共通鍵の共有を行います。TLS 1.2とTLS 1.3では、このやり取りにかかる往復回数(RTT)が異なります。
TLS 1.2のハンドシェイク(アプリケーションデータ送信まで2往復)
クライアント サーバー
|--- ClientHello(対応バージョン・暗号スイート候補・乱数)--->|
|<-- ServerHello(選択したバージョン・暗号スイート・乱数) ---|
|<-- Certificate(サーバー証明書チェーン) -------------------|
|<-- ServerKeyExchange / ServerHelloDone ---------------------|
|--- ClientKeyExchange -------------------------------------->|
|--- ChangeCipherSpec / Finished ----------------------------->|
|<-- ChangeCipherSpec / Finished -------------------------------|
|=== 以後、暗号化されたアプリケーションデータ ===================|
TLS 1.3のハンドシェイク(アプリケーションデータ送信まで1往復)
クライアント サーバー
|--- ClientHello(鍵交換に使う情報を先に含めて送信) --------->|
|<-- ServerHello + 鍵交換情報 ----------------------------------|
|<-- {EncryptedExtensions, Certificate,
| CertificateVerify, Finished}(この時点で暗号化開始) ------|
|--- Finished -------------------------------------------------->|
|=== 以後、暗号化されたアプリケーションデータ ===================|
TLS 1.2では、クライアントが使う鍵交換方式をサーバーの応答を見てから決めるため、実質2往復(2-RTT)が必要でした。TLS 1.3では、クライアントが最初のClientHelloの時点で「おそらく使われるであろう鍵交換方式」の情報を先に添えて送ることで、多くの場合1往復(1-RTT)でハンドシェイクが完了し、接続確立までの待ち時間が短縮されます。さらにTLS 1.3では、再接続時に0-RTT(初回のデータ送信すら待たずに送る)の仕組みも用意されていますが、再送攻撃への耐性の面で制約があるため、用途を選んで使う必要があります。またTLS 1.3では、RC4・3DES・CBCモードの暗号など、脆弱性が指摘されていた古い暗号方式がプロトコルレベルで廃止されている点も、1.2との大きな違いです。
公開鍵証明書の構造
HTTPSで使われる証明書は、X.509という標準形式に従います。証明書には主に次の情報が含まれます。
| 項目 | 意味 |
|---|---|
| Subject(サブジェクト) | この証明書が誰(どのドメイン・組織)に対して発行されたかを表す情報 |
| Issuer(発行者) | この証明書に署名したCA(認証局)の情報 |
| 有効期限(Validity) | Not Before(有効開始日時)とNot After(有効終了日時) |
| SAN(Subject Alternative Name) | この証明書が有効なホスト名(ドメイン)の一覧。複数のドメイン・サブドメインを1枚の証明書でカバーする場合に列挙される |
| 公開鍵(Public Key) | このサーバーの公開鍵本体と、そのアルゴリズム(RSA、EC等)・鍵長 |
| 署名アルゴリズム(Signature Algorithm) | CAがこの証明書に署名する際に使ったアルゴリズム(例:sha256WithRSAEncryption) |
| 拇印(Fingerprint) | 証明書全体をハッシュ化した値。証明書そのものを一意に識別・照合するために使われる |
ERR_CERT_COMMON_NAME_INVALIDなど)が出る典型的な失敗パターンになります。
認証局の階層とチェーンの検証
実運用の証明書は、多くの場合3段階の階層で構成されます。
認証局の階層構造
ルートCA(自己署名、オフライン厳重管理、OS/ブラウザの信頼ストアに事前登録)
└─ 中間CA(ルートCAが署名。日常の証明書発行はこちらが担当)
└─ サーバー証明書(中間CAが署名。実際にサイトへ導入するもの)
ルートCAの秘密鍵は極めて重要な資産であり、万一漏洩すると、そのルートを信頼する全世界のクライアントに影響が及びます。そのためルートCAは通常オフラインで厳重に管理され、日常の証明書発行業務には、ルートCAが署名した「中間CA」を使うのが一般的です。仮に中間CAの鍵が漏洩しても、その中間CAだけを無効化(失効)すればよく、ルートCA自体を交換する事態を避けられます。これが中間CAを挟む主な理由です。
クライアントが証明書チェーンを検証する順序は、サーバー証明書から始めて、信頼できるルートに到達するまで遡ります。
- サーバーから提示された「サーバー証明書」のIssuerを確認し、対応する中間CA証明書の署名でその内容を検証する
- その中間CA証明書のIssuerを確認し、対応するルートCA証明書の署名でその内容を検証する
- そのルートCA証明書が、クライアント(OSやブラウザ)の信頼ストアに含まれているかを確認する。含まれていれば、鎖全体が信頼できると判断される
この検証の途中で必要な中間CA証明書がサーバーから提示されないと、クライアントは鎖を最後まで遡れず、unable to get local issuer certificateのようなエラーになります。
opensslコマンド体系:鍵の生成
| コマンド | 用途 |
|---|---|
openssl genrsa | RSA秘密鍵を生成する伝統的な専用コマンド |
openssl genpkey | RSA・EC・Ed25519など複数のアルゴリズムに対応した、現在推奨される汎用の鍵生成コマンド |
openssl ecparam -genkey | 楕円曲線暗号(EC)の鍵ペアを生成する専用コマンド |
$ openssl genrsa -out server.key 2048
Generating RSA private key, 2048 bit long modulus (2 primes)
....................+++++
............+++++
e is 65537 (0x010001)
$ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out server.key # 正常時は無出力
# genrsaの現代的な等価コマンド。genpkeyはRSA以外の鍵種別も同じ書式で扱えるため、
# OpenSSLのドキュメントでは新規にはこちらの使用が推奨されている
$ openssl ecparam -genkey -name prime256v1 -out server-ec.key # 正常時は無出力
# prime256v1(P-256)曲線を使ったEC鍵ペアを生成。RSAより鍵長が短く高速
この行のこの値に注目: 直前のopenssl genrsaが生成中のドット(.や+)を進捗表示として画面に出すのに対し、genpkey・ecparam -genkeyはどちらも成功時に何も表示しません。鍵ファイルが実際に生成できたかどうかは、ls -l server.keyやopenssl rsa -in server.key -noout -checkのような後続コマンドで確認します。
opensslコマンド体系:CSRの作成
$ openssl req -new -key server.key -out server.csr
You are about to be asked to enter information that will be incorporated
into your certificate request.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Shinjuku
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Example Corp
Organizational Unit Name (eg, section) []:IT Department
Common Name (e.g. server FQDN or YOUR name) []:www.example.com
Email Address []:
| 項目 | 意味 |
|---|---|
| C(Country) | 国名(2文字のISOコード、例:JP) |
| ST(State) | 都道府県・州 |
| L(Locality) | 市区町村 |
| O(Organization) | 組織名 |
| OU(Organizational Unit) | 部署名 |
| CN(Common Name) | 証明書の主題となる名前。サーバー証明書ではドメイン名を指定するのが慣例 |
前述の通り、CNとSANは似た役割に見えますが、現代のブラウザはCNを実質的に無視し、SANフィールドに列挙されたホスト名だけを見ます。そのため、CSRを作る際はCNだけでなく、SANに必要なホスト名(wwwありなし両方など)を漏れなく含めることが重要です。
毎回対話入力するのではなく、-subjオプションで非対話的に指定することもできます。
$ openssl req -new -key server.key -out server.csr \
-subj "/C=JP/ST=Tokyo/L=Shinjuku/O=Example Corp/OU=IT Department/CN=www.example.com"
ただし-subjだけではSANを含められません。SANを含めるには、設定ファイルを用意して-configと-extensionsで指定します。
san.cnf(SANを含めるための設定ファイルの全文)
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req
[dn]
C = JP
ST = Tokyo
L = Shinjuku
O = Example Corp
OU = IT Department
CN = www.example.com
[v3_req]
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.example.com
DNS.2 = example.com
DNS.3 = api.example.com
$ openssl req -new -key server.key -out server.csr -config san.cnf -extensions v3_req # 正常時は無出力
# promptがnoのため対話入力なしで、[dn]セクションの内容とSANを含むCSRが生成される
$ openssl req -in server.csr -noout -subject -ext subjectAltName
subject=C = JP, ST = Tokyo, L = Shinjuku, O = Example Corp, OU = IT Department, CN = www.example.com
X509v3 Subject Alternative Name:
DNS:www.example.com, DNS:example.com, DNS:api.example.com
この行のこの値に注目: -subject -ext subjectAltNameで確認すると、生成されたCSRにsan.cnfで指定した3つのホスト名がすべてX509v3 Subject Alternative Nameとして正しく含まれているかを、対話プロンプトなしですぐに検証できます。ここで意図したホスト名が抜けていれば、san.cnfの[alt_names]セクションの記述漏れを疑います。
自己署名証明書の作成
社内システムや開発環境など、商用CAの証明書が不要な場合は、自分自身で署名する「自己署名証明書」を使うこともできます。鍵の生成からCSRの作成、署名までを1コマンドにまとめて実行できます。
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout server.key -out server.crt
# -x509オプションでCSRではなく自己署名済みの証明書を直接生成
# -newkey rsa:2048 で鍵の生成も同時に行い、-nodesで秘密鍵をパスフレーズなしで保存する
# (Apache等のサービスが起動時にパスフレーズ入力を求められないようにするため)
自己署名証明書は、ブラウザにあらかじめ組み込まれている信頼済みCAのリストに含まれないため、アクセスするとブラウザが警告を表示します。社内限定の用途であれば、その自己署名証明書(またはそれを発行した自前のCA証明書)をクライアント側に個別にインストールして信頼させる運用が一般的です。
証明書・鍵・CSRの内容確認
$ openssl req -in server.csr -noout -text # CSRの内容(SAN含む)を表示
Certificate Request:
Data:
Version: 1 (0x0)
Subject: C = JP, ST = Tokyo, L = Shinjuku, O = Example Corp, CN = www.example.com
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
RSA Public-Key: (2048 bit)
Attributes:
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:www.example.com, DNS:example.com, DNS:api.example.com
Signature Algorithm: sha256WithRSAEncryption
$ openssl x509 -in server.crt -noout -text # 証明書の内容全体を表示
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 4096 (0x1000)
Signature Algorithm: sha256WithRSAEncryption
Issuer: C = US, O = Let's Encrypt, CN = R3
Validity
Not Before: Aug 20 00:00:00 2026 GMT
Not After : Nov 18 23:59:59 2026 GMT
Subject: CN = www.example.com
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:www.example.com, DNS:example.com
Signature Algorithm: sha256WithRSAEncryption
$ openssl x509 -in server.crt -noout -dates -subject -issuer
notBefore=Aug 20 00:00:00 2026 GMT
notAfter=Nov 18 23:59:59 2026 GMT
subject=C = JP, O = Example Corp, CN = www.example.com
issuer=C = US, O = Let's Encrypt, CN = R3
$ openssl rsa -in server.key -noout -check # 秘密鍵ファイル自体の整合性チェック
RSA key ok
この行のこの値に注目: -text付きの出力は、CSR・証明書の全項目を人間が読める形式でダンプします。CSRのRequested ExtensionsにSANが正しく含まれているか、証明書のValidity(有効期間)・Issuer(発行者)が意図した通りかを、この出力全体から確認できます。項目数が多いため、実運用ではgrepや、次に示す-dates -subject -issuerのように必要な項目だけに絞って確認することも多くあります。
この行のこの値に注目:-dates -subject -issuerを組み合わせると、有効期限の確認・監視スクリプトに必要な最小限の情報だけを簡潔に取得できます。RSA key okが表示されれば、鍵ファイル自体が破損していないことが確認できます。
鍵と証明書の対応関係を検証する
実務で頻出するトラブルとして、誤って別の鍵と証明書の組み合わせをApacheに設定してしまうケースがあります。鍵・証明書・CSRのそれぞれから公開鍵の「モジュラス」(RSAの場合の公開鍵の核となる数値)を取り出し、そのハッシュ値を比較することで、正しい組み合わせかどうかを機械的に検証できます。
$ openssl x509 -noout -modulus -in server.crt | openssl md5
(stdin)= 8f14e45fceea167a5a36dedd4bea2543
$ openssl rsa -noout -modulus -in server.key | openssl md5
(stdin)= 8f14e45fceea167a5a36dedd4bea2543
$ openssl req -noout -modulus -in server.csr | openssl md5
(stdin)= 8f14e45fceea167a5a36dedd4bea2543
この行のこの値に注目:3つのコマンドが出力するハッシュ値がすべて一致していれば、証明書・秘密鍵・CSRが同じ鍵ペアに由来する正しい組み合わせであることが確認できます。1つでも値が異なる場合は、別の鍵で作られた証明書を取り違えて配置してしまっている可能性が高く、Apacheではkey values mismatchのようなエラーで起動に失敗します。
形式変換
| 変換 | コマンド例 |
|---|---|
| PEM → DER | openssl x509 -inform PEM -in server.crt -outform DER -out server.der |
| DER → PEM | openssl x509 -inform DER -in server.der -outform PEM -out server.pem |
| PEM一式 → PKCS#12 | openssl pkcs12 -export -out cert.p12 -inkey server.key -in server.crt -certfile intermediate-ca.crt |
PEMはBase64テキストで-----BEGIN CERTIFICATE-----のような区切りを持つ形式、DERはそのバイナリ表現です。OpenSSL系のツール(Apache、nginx等)は主にPEMを扱いますが、Windows系の環境やJavaのキーストアではDERやPKCS#12(拡張子.p12/.pfx、秘密鍵と証明書チェーンを1つのパスワード保護されたファイルにまとめた形式)が求められることがあり、その際にこれらの変換コマンドを使います。
接続の検査
$ openssl s_client -connect example.com:443 -servername example.com
CONNECTED(00000003)
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
depth=1 C = US, O = Let's Encrypt, CN = R3
verify return:1
depth=0 CN = example.com
verify return:1
---
Certificate chain
0 s:CN = example.com
i:C = US, O = Let's Encrypt, CN = R3
1 s:C = US, O = Let's Encrypt, CN = R3
i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
---
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
Session-ID: 3F2A1B...
Verify return code: 0 (ok)
この行のこの値に注目:Certificate chainには、サーバーが実際に提示している証明書の並び(s=Subject、i=Issuer)が表示され、途中の中間CAが正しく含まれているかをここで確認できます。ProtocolとCipherは実際にネゴシエートされたTLSバージョンと暗号スイート、Verify return code: 0 (ok)は検証が正常に完了したことを示します。0以外の値(例:20 (unable to get local issuer certificate))が出た場合は、チェーンの検証に問題があります。
証明書チェーン全体(中間・ルートを含む生のPEMデータ)を取得したい場合は-showcertsを追加します。
$ openssl s_client -connect example.com:443 -servername example.com -showcerts
CONNECTED(00000003)
depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1
verify return:1
---
Certificate chain
0 s:CN = example.com
i:C = US, O = Let's Encrypt, CN = R3
-----BEGIN CERTIFICATE-----
MIIFXTCCBEWgAwIBAgISA1b2c3d4e5f6...(省略、実際のBase64データが続く)
-----END CERTIFICATE-----
1 s:C = US, O = Let's Encrypt, CN = R3
i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
-----BEGIN CERTIFICATE-----
MIIEVzCCAj+gAwIBAgIRALBXPpFzlydw...(省略)
-----END CERTIFICATE-----
---
SSL-Session:
Protocol : TLSv1.3
Verify return code: 0 (ok)
この行のこの値に注目: -showcertsを付けない場合との違いは、各証明書についてs:(Subject)・i:(Issuer)の要約情報だけでなく、-----BEGIN CERTIFICATE-----で始まる実際のPEM形式データそのものが出力される点です。この出力をそのままファイルに保存すれば、サーバーが提示している証明書チェーンを、その場で検証用のファイルとして取り出せます。
古いプロトコルバージョンが無効化されているかを確認したい場合、明示的にバージョンを指定して接続を試みます。無効化されていれば接続は失敗(handshake failure等)します。
$ openssl s_client -connect example.com:443 -tls1_1
140234... error:1409442E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version
# TLS 1.1が無効化されているサーバーでは、このようにハンドシェイクの時点で拒否される
簡易CAの構築
社内向けなどで、自前の小さな認証局を運用したい場合、openssl caサブコマンドと専用のディレクトリ構成・設定ファイルを使います。
$ mkdir -p demoCA/{certs,newcerts,private,crl} # 正常時は無出力
$ touch demoCA/index.txt # 正常時は無出力
$ echo 1000 > demoCA/serial # 正常時は無出力(リダイレクト先ファイルに書き込まれる)
$ openssl genrsa -out demoCA/private/cakey.pem 4096
Generating RSA private key, 4096 bit long modulus (2 primes)
.......................................................................++++
.....................................................................................................................++++
e is 65537 (0x010001)
$ openssl req -x509 -new -nodes -key demoCA/private/cakey.pem -days 3650 -out demoCA/cacert.pem
You are about to be asked to enter information that will be incorporated
into your certificate request.
-----
Country Name (2 letter code) [AU]:JP
State or Province Name (full name) [Some-State]:Tokyo
Locality Name (eg, city) []:Shinjuku
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Example Corp Internal CA
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:Example Corp Internal Root CA
Email Address []:
この行のこの値に注目: CA用の鍵はサーバー証明書用より長い4096ビットで生成するのが一般的です(CA証明書は長期間・多数の証明書発行の基盤になるため、より高い強度が求められます)。openssl req -x509で自己署名のCA証明書を作る際のCommon Nameには、実在のホスト名ではなく「このCAが何であるか」を表す名前(この例ではExample Corp Internal Root CA)を入力するのが慣習です。
index.txtは発行済み証明書の台帳(発行日・失効状態などを記録するデータベース)、serialは次に発行する証明書に割り当てるシリアル番号を保持するファイルです。openssl.cnfの[ca]セクションで、これらのパスをまとめて定義します。
openssl.cnf の [ca] セクション(抜粋)
[ca]
default_ca = CA_default
[CA_default]
dir = ./demoCA
database = $dir/index.txt
serial = $dir/serial
certificate = $dir/cacert.pem
private_key = $dir/private/cakey.pem
new_certs_dir = $dir/newcerts
default_days = 365
default_md = sha256
policy = policy_match
[policy_match]
countryName = match
organizationName = match
commonName = supplied
$ openssl ca -config openssl.cnf -in server.csr -out server.crt
# CSRに対して自前のCA(cakey.pem)で署名し、証明書を発行する。
# index.txtとserialが自動的に更新される
Using configuration from openssl.cnf
Check that the request matches the signature
Signature ok
Certificate Details:
Serial Number: 4096 (0x1000)
Validity
Not Before: Aug 21 00:00:00 2026 GMT
Not After : Aug 21 00:00:00 2027 GMT
Subject:
countryName = JP
organizationName = Example Corp
commonName = www.example.com
Certificate is to be certified until Aug 21 00:00:00 2027 GMT (365 days)
Sign the certificate? [y/n]:y
1 out of 1 certificate requests certified, commit? [y/n]y
Write out database with 1 new entries
Data Base Updated
この行のこの値に注目: openssl caは対話的に2回の確認(Sign the certificate?とcommit?)を求めます。yと答えると、最終行のData Base Updatedが表示され、index.txtに発行記録が追記されると同時にserialファイルの番号が次の値へ進みます。policy_matchでmatch指定した項目(この例ではcountryName・organizationName)がCA証明書の値と一致しないCSRは、この時点でエラーになり署名が拒否されます。
Apacheへの組み込み
$ sudo a2enmod ssl # Debian系:mod_sslを有効化して再起動
Enabling module ssl.
See /usr/share/doc/apache2/README.Debian.gz on how to configure SSL and create self-signed certificates.
To activate the new configuration, you need to run:
systemctl restart apache2
$ sudo systemctl restart apache2 # 正常時は無出力
この行のこの値に注目: a2enmod実行後のメッセージは、モジュールを有効化しただけではまだ反映されておらず、systemctl restart apache2を実行して初めて設定が反映されることを示しています。この案内メッセージに従わずに再起動を忘れると、SSL関連の設定を追加してもmod_sslがロードされないままで、意図した通りに動作しません。
RHEL系:httpd.confでの明示的なロード(通常はmod_ssl導入時に自動追記される)
LoadModule ssl_module modules/mod_ssl.so
| ディレクティブ | 意味 |
|---|---|
SSLEngine on | そのVirtualHostでSSL/TLSを有効化する |
SSLCertificateFile | サーバー証明書(Apache 2.4.8以降は中間CA証明書を連結して1ファイルにまとめることも可) |
SSLCertificateKeyFile | サーバーの秘密鍵 |
SSLCertificateChainFile | 中間CA証明書を別ファイルとして指定する古い書式(Apache 2.4.8以降は非推奨。SSLCertificateFileへの連結が推奨) |
SSLCACertificateFile | クライアント証明書認証(mTLS)で信頼するCA証明書を、複数連結した1つのファイルとして指定 |
SSLCACertificatePath | 同じくクライアント証明書認証で信頼するCA証明書群を、c_rehashでハッシュ名リンクを張ったディレクトリとして指定 |
SSLCACertificateFile/SSLCACertificatePathは、サーバー自身の証明書チェーンを補完するものではなく、クライアント証明書認証で「どのCAが発行したクライアント証明書を信頼するか」を指定する設定です。サーバー証明書のチェーンを補完したい場合は、SSLCertificateFileにサーバー証明書と中間CA証明書を連結するか、(古い構成であれば)SSLCertificateChainFileを使う必要があります。この2つの設定を混同すると、意図した効果が得られません。
中間CA証明書を含めないと一部のクライアントでエラーになるのは、すべてのクライアントが最新の中間CA証明書をあらかじめキャッシュしているとは限らないためです。サーバー自身がルート直下までの完全なチェーンを提示することで、クライアント側のキャッシュ状態に関わらず、誰でも信頼できるルートまで検証を完了できるようになります。
完全なVirtualHost設定例(80番からのリダイレクト付き)
<VirtualHost *:80>
ServerName www.example.com
Redirect permanent / https://www.example.com/
</VirtualHost>
<VirtualHost *:443>
ServerName www.example.com
SSLEngine on
SSLCertificateFile /etc/ssl/certs/server-fullchain.crt
SSLCertificateKeyFile /etc/ssl/private/server.key
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite HIGH:!aNULL:!MD5:!3DES
SSLHonorCipherOrder on
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
ServerTokens Prod
ServerSignature Off
TraceEnable off
</VirtualHost>
SNIによるバーチャルホスティング
名前ベースのバーチャルホストは、HTTPではHostヘッダーで振り分けられますが、HTTPSでは通信が暗号化される前にどの証明書を提示すべきかが決まっている必要があり、単純にはいきません。SNI以前は、1つのIPアドレスに複数のドメインのHTTPS証明書を割り当てることができず、ドメインごとに別々のIPアドレスを用意する必要がありました。
この課題を解決するのがSNI(Server Name Indication)で、TLSのハンドシェイク開始時(ClientHelloの中)にクライアントが接続先のホスト名を平文で送ることで、サーバー側は証明書を提示する前に、どのバーチャルホストへのアクセスかを把握し、正しい証明書を選んで応答できます。HTTPの名前ベースバーチャルホストで使われていたNameVirtualHostディレクティブは、Apache 2.4以降では複数のVirtualHostブロックが同じIP:ポートを共有していれば自動的に有効になるため、明示的な指定は不要になっています。現在の主要ブラウザ・OS標準のTLSライブラリはほぼ例外なくSNIに対応しており、対応していないのはWindows XP上の古いブラウザなど、ごく限られたレガシー環境に限られます。
セキュリティ設定
SSL/TLSには複数のバージョンがありますが、SSLv2・SSLv3や、古いTLS 1.0/1.1には既知の脆弱性があるため、明示的に無効化する必要があります。
Apacheでの推奨設定
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLCipherSuite HIGH:!aNULL:!MD5:!3DES
SSLHonorCipherOrder on
ServerTokens Prod
ServerSignature Off
TraceEnable off
| ディレクティブ | 意味 |
|---|---|
SSLProtocol -all +TLSv1.2 +TLSv1.3 | いったん全プロトコルを無効化(-all)してから、TLS 1.2と1.3だけを明示的に有効化する書き方。SSLv2/v3、TLS1.0/1.1は結果的にすべて無効化される |
SSLCipherSuite | 使用を許可する暗号スイートの条件式。HIGHで強い暗号のみを許可し、!を付けたaNULL(認証なし)・MD5・3DESを明示的に除外するのが推奨値 |
SSLHonorCipherOrder on | クライアントが提示した優先順位ではなく、サーバー側が設定した優先順位で暗号スイートを選ぶよう強制する |
ServerTokens Prod | レスポンスヘッダーのServerに、Apacheのバージョンやモジュール情報を含めず「Apache」とだけ表示する |
ServerSignature Off | 自動生成されるエラーページ末尾のバージョン情報表示を抑制する |
TraceEnable off | HTTPのTRACEメソッドを無効化する |
ServerTokens・ServerSignatureは、攻撃者に対して「このApacheの正確なバージョンや導入モジュール」という手がかりを与えないための設定です。TraceEnable offは、クロスサイトトレーシング(XST)と呼ばれる攻撃手法を防ぐための設定です。TRACEメソッドが有効だと、本来JavaScriptから読み取れないはずのCookie(HttpOnly属性付き)の内容が、TRACEレスポンスのエコーバックを介して読み取られてしまう可能性があります。
一度HTTPSで接続してきたブラウザに対して「今後は常にHTTPSでアクセスすること」を指示するHSTS(HTTP Strict Transport Security)も、実務で広く使われています。
Apacheの例(mod_headersが必要)
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
max-ageは、この指示をブラウザが記憶しておく秒数です。includeSubDomainsを付けると、サブドメインを含めてHTTPS強制の対象になります。HSTSを有効化する前に、実際に全てのサブドメインでHTTPSが正しく提供できているかを確認しておかないと、一部のサブドメインにアクセスできなくなる事故につながる点に注意してください。
Let's Encrypt/certbotとの対応づけ
ここまではopensslによる手動での鍵・証明書操作を扱いましたが、実務ではLet's Encryptのような無料の認証局と、取得・更新を自動化するcertbotツールが広く使われています。
$ sudo certbot --apache -d example.com -d www.example.com
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Requesting a certificate for example.com and www.example.com
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/example.com/fullchain.pem
Key is saved at: /etc/letsencrypt/live/example.com/privkey.pem
This certificate expires on 2026-11-19.
These files will be updated when the certificate renews.
Deploying certificate
Successfully deployed certificate for example.com to /etc/apache2/sites-available/example-com-le-ssl.conf
Successfully deployed certificate for www.example.com to /etc/apache2/sites-available/example-com-le-ssl.conf
Congratulations! You have successfully enabled HTTPS on https://example.com and https://www.example.com
この行のこの値に注目: Certificate is saved at:・Key is saved at:で、実際に発行された証明書・秘密鍵の配置先パスが分かります。Successfully deployed certificate ...は、certbotが対象のApache VirtualHost設定ファイル(この例では自動生成されたexample-com-le-ssl.conf)にSSLCertificateFile等のディレクティブを自動追記したことを示しており、手動でApacheの設定を書き換える必要はありません。
certbotは内部的に、ここまで説明してきた手順の多くを自動で肩代わりしています。
- 秘密鍵とCSRを内部で自動生成する
- ACMEチャレンジ(Let's Encryptがドメインの所有権を確認する仕組み)を実行する。標準的なhttp-01チャレンジでは、
/.well-known/acme-challenge/以下に指定されたファイルを一時的に配置し、Let's EncryptのサーバーがそのURLへ実際にアクセスできることでドメイン所有を確認する - 発行された証明書一式(サーバー証明書・中間CA証明書)を
/etc/letsencrypt/live/ドメイン名/以下に配置する --apacheプラグイン使用時は、該当VirtualHostにSSLCertificateFile等のディレクティブを自動で追記し、80番からのHTTPSへのリダイレクトも設定する- systemdタイマー(またはcronジョブ)を登録し、有効期限が近づいた証明書を自動更新する
Let's Encryptの証明書の有効期間は90日と短いため、手動更新は現実的ではありません。
$ sudo certbot renew --dry-run # 実際には更新せず、更新処理が正常に動くかを検証
Saving debug log to /var/log/letsencrypt/letsencrypt.log
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/example.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Simulating renewal of an existing certificate for example.com and www.example.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
new certificate deployed with reload of apache server; fullchain is
/etc/letsencrypt/live/example.com/fullchain.pem
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/example.com/fullchain.pem (success)
$ systemctl list-timers | grep certbot # 自動更新タイマーが登録されているか確認
Thu 2026-08-22 03:24:16 JST 17h left Wed 2026-08-20 03:18:42 JST 17h ago certbot.timer certbot.service
この行のこの値に注目: --dry-runは本番用のLet's Encryptサーバーではなく、テスト用のステージング環境に対して更新処理一式(証明書取得〜Apacheへのデプロイ〜リロード)を実際に試すため、証明書のレート制限を消費せずに動作確認できます。最終行にCongratulations, all simulated renewals succeededと表示されれば、実際の更新も問題なく動くはずだと判断できます。systemctl list-timersの出力にある左から2列目(この例では17h left)は、次回のタイマー起動までの残り時間です。
証明書の期限切れを防ぐ運用
自動更新の仕組みがあっても、更新処理自体の失敗(DNS変更、ファイアウォールの設定変更、certbotのバージョン不整合など)に気づかず、証明書が突然失効してしまう事故は珍しくありません。監視の仕組みを別途用意しておくことが重要です。
check_cert_expiry.sh(有効期限が14日を切ったら警告するチェックスクリプト)
#!/bin/bash
CERT=/etc/letsencrypt/live/example.com/cert.pem
THRESHOLD_DAYS=14
END_DATE=$(openssl x509 -enddate -noout -in "$CERT" | cut -d= -f2)
END_EPOCH=$(date -d "$END_DATE" +%s)
NOW_EPOCH=$(date +%s)
DAYS_LEFT=$(( (END_EPOCH - NOW_EPOCH) / 86400 ))
if [ "$DAYS_LEFT" -lt "$THRESHOLD_DAYS" ]; then
echo "WARNING: $CERT expires in $DAYS_LEFT days" >&2
exit 1
fi
echo "OK: $CERT expires in $DAYS_LEFT days"
exit 0
このようなスクリプトをcronで定期実行し、監視システム(Nagios/Zabbixなど)のプラグインとして組み込んだり、失敗時にメールやチャット通知を送るようにしておくと、certbotの自動更新が何らかの理由で失敗していた場合にも、期限切れ前に気づけます。
トラブルシューティング
| エラー | 典型的な原因 | 切り分け方法 |
|---|---|---|
ERR_CERT_COMMON_NAME_INVALID | SANにアクセスしたホスト名が含まれていない | openssl x509 -in server.crt -noout -textでSubject Alternative Nameの内容を確認する |
unable to get local issuer certificate | 中間CA証明書がサーバーから提示されていない | openssl s_client -showcertsで提示されているチェーンに中間証明書が含まれているか確認する |
key values mismatch | 設定されている秘密鍵と証明書が対になっていない | 前述のmodulusのMD5ハッシュ比較(openssl x509 -noout -modulusとopenssl rsa -noout -modulusの一致確認)で切り分ける |
この3つは実務でも頻出のトラブルで、いずれもこの章で扱ったopensslコマンド(-textによる内容表示、-showcertsによるチェーン確認、modulusのハッシュ比較)だけで、ブラウザの表面的なエラーメッセージから一歩踏み込んで原因を切り分けられます。