VPNは午前3時にも接続に応答しますが、証明書に署名するマシンは引き出しの中で電源が切られたままです。サーバー証明書を検証するクライアントがあなたのCAに問い合わせることはないため、CAはどの接続に対しても到達可能である必要はありません。計画が必要なのは、証明書を発行、更新、または失効させるその日にルート鍵を施錠したままにする部分です。

接続が実際に検証するもの

OpenVPNクライアントが接続すると、サーバーのリーフ証明書から、すでに信頼しているCA証明書まで署名チェーンをたどり、有効期間を確認し、プロファイルがそう指定していれば失効リストを参照します。ルートCAを参照することは決してありません。サーバーはクライアント証明書について同じことを鏡像的に行います。接続時点で存在するのは、リーフ、中間、そして以前に配布した信頼バンドルだけです。

実際に失敗するのは次の三つのいずれかです。発行時に署名鍵が利用できない、失効リストが古くなっている、またはクライアントの信頼バンドルがサーバーが提示するチェーンより古い。最初はプロセスの問題で、残り二つは配布の問題です。どれもルート鍵がオンラインであることを必要とせず、トンネル認証に秘密証明書を要求するマネージドVPNサービスは署名権限をプロバイダーに移すだけです。Azureのポイント対サイトは逆に動作します。ルート証明書をアップロードすると、その下で署名された証明書が信頼されるため、誰が署名し、誰が検証し、どのように証明書を早期に取り消すのかを確認してください。

決してアドレスを持たないホストでルートを構築する

オフラインのルートはめったに使われません。中間CA証明書に署名するときと、後にそれを更新または失効させるときだけです。それ以外はすべて一段下で行われます。ルートの作業はネットワークから切り離したマシンで、可能ならライブシステムから行い、誰が部屋にいたかを記録します。コマンドはOpenSSL 1.1.1以降を前提としています。-addextは古いビルドでは利用できないからです。

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out root.key
openssl req -x509 -new -key root.key -sha256 -days 7300 \
  -subj "/CN=Example VPN Root CA" -out root.crt \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"

署名ホストで中間を作成し、その要求だけをオフラインのマシンに持ち込みます。ルートはその要求をパス長ゼロで署名するため、中間CAはエンドポイント証明書に署名できますが、その下に別のCAを作ることはできません。この一つの拡張が、侵害された署名ホストがルートと同等の権限を作るのを防ぎます。ECDSAはすべてのクライアントがOpenVPN 2.4以降を実行している場合にのみ使用し、そうでなければRSA鍵を生成して同じ流れを保ちます。

openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out intermediate.key
openssl req -new -key intermediate.key -subj "/CN=Example VPN Intermediate CA" -out intermediate.csr
openssl x509 -req -in intermediate.csr -CA root.crt -CAkey root.key \
  -CAcreateserial -days 3650 -sha256 -extfile intermediate.ext -out intermediate.crt

intermediate.extにはbasicConstraints=critical,CA:TRUE,pathlen:0、keyUsage=critical,keyCertSign,cRLSign、authorityKeyIdentifier=keyid:alwaysが含まれます。署名ホストを再接続する前に、ルート鍵を金庫に戻します。ルートが中間を失効させられる必要がある場合は、openssl x509 -reqではなくルートレベルのindex.txtに対してopenssl ca -config root.cnf -extensions intermediate_caで署名し、シリアル番号が記録されるようにします。その後、得られたルートリストをクライアントプロファイル内に配布し、ファイルベースのcrl-verifyが起動時にそれを読み取ります。クライアントには両方のCA証明書を含むバンドルを配布してください。リーフだけではルートに対して検証されません。

中間からリーフを発行し、データベースを使う

シリアル番号が記録されないため、openssl x509 -reqでの署名は失効が必要になるまでしか機能しません。最初の証明書からopenssl caを使い、シリアル番号、有効期限、失効状態を一つのデータベースに保ちます。データベースファイルを一度作成してから、設定をそれらに向けます:

mkdir -p /etc/vpn-ca/intermediate/newcerts
touch /etc/vpn-ca/intermediate/index.txt
echo 1000 > /etc/vpn-ca/intermediate/serial
echo 1000 > /etc/vpn-ca/intermediate/crlnumber
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = /etc/vpn-ca/intermediate
database = $dir/index.txt
new_certs_dir = $dir/newcerts
serial = $dir/serial
crlnumber = $dir/crlnumber
certificate = $dir/intermediate.crt
private_key = $dir/intermediate.key
default_md = sha256
default_days = 365
default_crl_days = 30
unique_subject = no
policy = policy_any
[ policy_any ]
commonName = supplied
[ server_cert ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = @server_names
[ client_cert ]
basicConstraints = CA:FALSE
keyUsage = critical, digitalSignature
extendedKeyUsage = clientAuth
[ server_names ]
DNS.1 = vpn.example.com

鍵用途の行はECDSA鍵を前提としており、RSAの場合はkeyEnciphermentを追加し、各クライアントに独自のコモンネームを与えてください。なぜならOpenVPNがそのフィールドで接続を識別し、CN=vpn-clientを共有する二台のノートPCはログでも、client-config-dirのルールでも、ユーザーごとのプッシュでも区別できず、失効判断を正当化する帰属を失います。リーフを次のように発行して検証します:

openssl req -new -key alice.key -subj "/CN=alice-laptop" -out alice.csr
openssl ca -config intermediate.cnf -extensions client_cert -in alice.csr -out alice.crt -batch
openssl verify -CAfile ca-bundle.crt -untrusted intermediate.crt alice.crt

署名鍵はVPNホストにコピーせずポリシーの境界の背後に置き、要求を認証してコモンネームを許可リストと照合し証明書だけを返す小さなサービスとします。各シリアル番号を記録し、ログは証明書とそれを求めた人物を結ぶ唯一のリンクです。

ルートを起こさずに失効を公開する

クライアント証明書を失効させるのは中間であり、ルートではありません。ルートリストが重要になるのは中間が焼失したときだけです。crl-verifyでサーバーを中間リストに向けます。ファイルベースのCRLはサービス起動時に読み込まれるため、再生成のたびに再読み込みしてください。ディレクトリ形式は接続時に状態を読み、シリアルごとに一つのファイルを消費します。

同じデータベースから二つのコマンドで失効させ、再生成します:

openssl ca -config intermediate.cnf -revoke /etc/vpn-ca/intermediate/newcerts/1001.pem
openssl ca -config intermediate.cnf -gencrl -out /etc/openvpn/crl.pem

次に時計を監視します。CRLには次回更新フィールドがあり、その日付を過ぎると検証側のピアはリストを無視せず拒否します。default_crl_days = 30で毎月再生成すれば余裕が残り、毎年では残りません。リーフの寿命を短くする方が信頼できる制御です。失効はリストが参照される場所でしか適用されないからです。

チェーンをテストし、その後で儀式を文書化する

設計を信頼する前に四つの失敗を再現します。openssl verify -CAfile ca-bundle.crt -untrusted intermediate.crt server.crtでチェーンを検証します。期限切れのクライアント証明書で接続し、サーバーが拒否することを確認します。テストクライアントを失効させ、再読み込みし、リストが有効になることを確認します。ホストの時計を一日進めます。誤った時計は、他の場所では有効に見える証明書を発行したり拒否したりするからです。

オフラインのルートは、書類がそろっている場合にのみ制御になります。儀式のための二名の指名、署名したすべてのシリアル番号の記録、マシンとは別に保管する暗号化バックアップ、隔離したホストでの復元テストです。同じ儀式の間に予備の中間CAに署名しておけば、署名ホストが故障しても真夜中にルートを解錠する必要はありません。

完了と呼ぶ前に、五つのことを確認します。ルート鍵がネットワーク接続されたホストにないこと、中間鍵が署名ホストだけに存在すること、クライアントバンドルが両方のCA証明書を含むこと、CRLに将来の次回更新があること、チェーン内のすべての有効期限について誰かがカレンダー登録を持っていることです。一つでも欠けていれば、オフラインに見える構成は単に文書化されていないだけです。

関連記事