← ブログに戻る

OpenVPNはなぜ.ovpnファイルとパスワードだけでVPNに繋がるのか

OpenVPNの体験は不思議なほど単純である。管理者から.ovpnファイルを受け取り、OpenVPN ConnectやTunnelblickにドラッグして、ユーザー名とパスワードを入れる。数秒後、10.8.0.6のような社内IPが自分に振られていて、社内のWikiやリポジトリに手が届く。

そのあいだにOpenVPNは何をしたのか。ファイル1個とパスワード1個で、証明書検証、鍵交換、ルーティング設定、DNS書き換えまで全部済んでいる。「起動して認証を通しただけ」に見えるのは、その手前で下ごしらえが終わっているからだ。

この記事では、.ovpnファイルの中身から始めて、接続確立の各段階で何が起きているかを順に見ていく。目的は「なぜこんなに簡単に見えるのか」を説明することで、それは「どこに複雑さが隠されているか」の話でもある。

前提として、この記事ではWireGuard・OpenVPN・Tailscaleの違いの記事で書いた「OpenVPNは制御チャネルにTLSを使う」という一段を、細かく分解する。

OpenVPN接続確立シーケンス: Phase 1 tls-cryptとmutual TLS、Phase 2 Key Method 2でデータチャネル鍵派生、Phase 3 auth-user-passでパスワード認証、Phase 4 PUSH_REQUEST/REPLYでifconfig・route・DNSが降ってくる、Phase 5 tun作成とルーティング適用

.ovpnファイルに書かれていること

まず入口の.ovpnファイルを開いてみる。典型的にはこういう中身になっている。

client
dev tun
proto udp
remote vpn.example.com 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
auth-user-pass

<ca>
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgIUJc...(CA証明書)
-----END CERTIFICATE-----
</ca>

<cert>
-----BEGIN CERTIFICATE-----
MIIDXjCCAkagAwIBAgIRAO...(クライアント証明書)
-----END CERTIFICATE-----
</cert>

<key>
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w...(クライアント秘密鍵)
-----END PRIVATE KEY-----
</key>

<tls-crypt>
-----BEGIN OpenVPN Static key V1-----
6acef03f62675b4b1bbd03e...(HMAC/暗号鍵)
-----END OpenVPN Static key V1-----
</tls-crypt>

情報量が多いように見えるが、5つのグループに分かれている。

接続先と輸送層: remoteprotovpn.example.comの1194番/UDPに繋ぎに行く、というアドレス指定である。DNSも普段の解決器で引く。この時点ではまだ暗号化トンネルは影も形もない。

動作モード: client, dev tunclientは「サーバーがpushする設定を受け取るモード」で、内部的にはpulltls-clientを有効にする。あとで大事になる。

サーバー検証の材料: <ca>ブロックに埋め込まれたCA証明書と、remote-cert-tls server。サーバー証明書がこのCAで署名されていて、なおかつサーバー用の拡張フィールドを持っている場合だけ有効、というチェックが走る。

自分を証明する材料: <cert><key>のペア。クライアント証明書と秘密鍵で、これがTLSの相互認証(mTLS)の材料になる。

制御チャネルの覆い: <tls-crypt>ブロック。あとで説明するが、TLSハンドシェイクそのものの外側に、もう一層のHMAC+暗号化を掛ける鍵である。

ここまで来て気付くことがある。パスワードはどこにも書かれていない。書かれているのはauth-user-passという指示子だけで、これは「接続時にユーザー名とパスワードを聞け」という設定である。パスワードは.ovpnとは別のチャネルで人間から入る。

つまり「.ovpn 1個 + パスワード」の構成のうち、認証に効いているのは主にクライアント証明書のほうで、パスワードは追加された第二要素という位置付けである。

Phase 1: 制御チャネルのTLSハンドシェイク

openvpn --config client.ovpnを叩くと最初に走るのは、.ovpnremoteで指定された宛先へのUDPパケット送信である。OpenVPNは独自のフォーマットを使うが、その中で運ばれる中身の主役はTLSだ。

TLSといってもTCPソケットの上で走るTLSではなく、OpenVPNの制御チャネルとして、TLSレコードをOpenVPNパケットの中にカプセル化して運ぶ。UDPの上でTLSを喋るために、OpenVPNは自分でシーケンス番号と再送を管理している。DTLSのような既製の仕組みではなく、独自にやっている。

制御チャネルで走るのはmutual TLSの完全なハンドシェイクである。

  • サーバーは自分の証明書を送る。クライアントは.ovpn<ca>で検証し、remote-cert-tls serverのチェックを通す
  • クライアントは<cert>の証明書を送る。サーバーは自分のCAで検証する
  • Diffie-Hellman鍵交換でTLSのマスターシークレットが確立する
  • 対称暗号スイート(TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384など)が決まる

ここで確立するのはTLSの鍵である。データチャネル(実際のIPパケットを流す方)の鍵ではない。それは次の段階でこの上に組み立てられる。

Phase 2: データチャネル鍵の派生

TLSのマスターシークレットができたら、双方はそこからデータチャネル用の対称鍵を作る必要がある。OpenVPN 2.0以降の既定はKey Method 2と呼ばれる方式で、これはこう動く。

  1. クライアントとサーバーはそれぞれ乱数を生成し、TLS制御チャネル越しに交換する
  2. TLS PRF(疑似乱数関数)を「マスターシークレット、ラベル "OpenVPN"、client_random + server_random」で叩き、鍵材料を導出する
  3. その鍵材料をスライスして、暗号鍵とHMAC鍵、送信方向と受信方向それぞれ用に配る

疑似コードにするとこうなる。

key_material = TLS_PRF(
    secret = tls_master_secret,
    label  = "OpenVPN",
    seed   = client_random || server_random
)
[cipher_key_c2s, hmac_key_c2s, cipher_key_s2c, hmac_key_s2c] = split(key_material)

なぜTLSのマスターシークレットをそのまま使わないのか。TLSレコード層の鍵とは別の鍵材料が欲しいからだ。TLSは制御チャネルとして使うだけで、データパケットの暗号化はOpenVPN側で別立てにする。層が分かれていることでTLS実装の中を通さずにIPパケットを暗号化でき、これがOpenVPNの性能の要になる。

なお最近のOpenVPNは、TLS実装がRFC 5705のExported Keying Material (EKM)をサポートしていれば、そちらを優先する。key-derivation tls-ekmという名前で機能ネゴシエーションに乗ってくる。中身の考え方は同じだが、TLSライブラリ側が正式にサポートしているAPIを使う点で綺麗になっている。

ここまでで、暗号化されたトンネル自体は張れている。ただしこの時点ではまだ、クライアントは自分のVPN内IPを知らない。ルーティングもDNSも変わっていない。

Phase 3: PUSH_REQUEST / PUSH_REPLY

クライアントの.ovpnにはclientが書かれているので、内部的にpullが有効になっている。このpullが次の一手を決める。

トンネルが張れた瞬間、クライアントは制御チャネル越しにテキストのメッセージを1本送る。

PUSH_REQUEST

サーバーはこれを受けて、そのクライアントに配布する設定をPUSH_REPLYという1本のメッセージにまとめて返す。中身はこういう見た目になる。

PUSH_REPLY,route 10.0.0.0 255.255.255.0,route-gateway 10.8.0.1,
topology subnet,ping 10,ping-restart 60,
ifconfig 10.8.0.6 255.255.255.0,peer-id 3,cipher AES-256-GCM,
dhcp-option DNS 10.8.0.1,dhcp-option DOMAIN corp.example.com

このメッセージを受け取って初めて、クライアントは自分に振られたVPN内IPを知る。10.8.0.6とか、そういうやつだ。ここが実務的に重要な設計判断で、IPアドレスや社内ルーティング、DNSは全部サーバー側で決まる。クライアントの.ovpnにはネットワーク構成をハードコードしないのが素直な使い方になる。

サーバー側の設定はたいていserver.confにこう書いてある。

push "route 10.0.0.0 255.255.255.0"
push "dhcp-option DNS 10.8.0.1"
push "dhcp-option DOMAIN corp.example.com"

これがそのままPUSH_REPLYに載って全クライアントに配布される。ユーザーごとに違う設定を配りたければ、Client Config Directory (CCD)にクライアント証明書のCN名でファイルを置く。

Phase 4: tunインタフェースを開く

PUSH_REPLYを受け取ってからようやく、OpenVPNは仮想ネットワークインタフェース(tun0など)を実際にOSに作らせる。順番が逆に見えるかもしれないが、これには理由がある。tunに割り当てるIPアドレスとサブネットとルーティングテーブルの中身が、PUSH_REPLYで降ってきてから初めて確定するからだ。

OSレベルでは、この段階で複数の副作用が同時に走る。

  • tun0インタフェースの作成と10.8.0.6/24の割り当て
  • ルーティングテーブルへのroute 10.0.0.0/24 via tun0の追加
  • DNS解決器の書き換え(macOSならscutil、Linuxならsystemd-resolved/etc/resolv.conf、Windowsならnetsh)
  • redirect-gatewayが指定されていればデフォルトルートまで奪う

ここが完了して初めて、ping 10.0.0.5が通るようになる。ユーザーが「繋がった」と感じるのはこの瞬間である。

Phase 5: パスワードはどこで効いているのか

auth-user-passがあった場合の話をしていなかった。パスワード認証は、実はここまでのシーケンスの中に別途差し込まれている。

具体的には、TLSハンドシェイクの直後、PUSH_REQUESTを送る前に、クライアントは制御チャネル越しにAUTH_LOGINまたはCLIENT_LISTという形でユーザー名/パスワードをサーバーへ送る。サーバー側は--auth-user-pass-verifyスクリプトやPAM、LDAP、Access Serverの認証プラグインなどにこれを流し、成否だけ返す。

パスワードが違えばここで接続は落ちる。この段階に到達した時点で証明書のmTLSは既に成立しているので、パスワードは常に証明書認証の上に載る追加要素として動く。逆に言えば、パスワードだけを盗んでも証明書ファイルが手元になければ.ovpnは使えない。

管理者側で「証明書は要らない、パスワードだけで運用したい」という構成にすることは可能だが、その場合でもサーバー証明書の検証は必ず走る。「認証が完全にパスワードだけ」というOpenVPN構成は実際には存在しない。制御チャネルがTLSである以上、少なくともサーバー側の証明書は毎回検証される。

補足: tls-authとtls-cryptの層

制御チャネルの手前にもう一層ある。.ovpnにあった<tls-crypt>ブロックがそれだ。

これは対称鍵1つで、両端に事前配布しておくものだ。役割は2つある。

HMAC firewall: 制御パケット(TLSハンドシェイクを含む)にHMACを付ける。HMACが合わないパケットはTLSレコードとして復号する前に破棄される。だからサーバーは、まず.ovpnに埋まった鍵を持っていない相手のパケットは一切処理しない。ポートスキャンをかけても何も返ってこないし、tls-authを使えば1194/UDPをネットに晒したままでもTLS実装の脆弱性を突かれるリスクを減らせる。

制御チャネル全体の暗号化(tls-cryptのみ): tls-authはHMACだけだが、tls-cryptは加えて制御パケット全体を対称暗号化する。TLSハンドシェイクのマジックバイトが外から見えなくなるので、DPIで「これはOpenVPNだ」と判別されにくくなる。厳しい環境からVPNを張るときに効く。

どちらもデータチャネルの機密性には関与しない。データチャネルはPhase 2で導出した鍵で暗号化されており、そこはTLSの外側で自前で回している。tls-auth/tls-cryptが守っているのは常に制御チャネルの入口である。

「起動するだけ」の裏にあったもの

最初の疑問に戻る。なぜ.ovpnとパスワードだけで繋がるのか。今なら順を追って答えられる。

  • 接続先アドレス、CA証明書、クライアント証明書、鍵、暗号スイート、tls-crypt鍵はすべて.ovpnに事前に埋め込まれている
  • TLSハンドシェイクは.ovpnの証明書だけで成立する。パスワードは要らない
  • データチャネルの対称鍵はTLS PRF(またはEKM)で毎回作られる。.ovpnにも設定にも書かれていない
  • IPアドレス、ルーティング、DNSはサーバーからPUSH_REPLYで降ってくる。クライアント側には書かない
  • パスワードは追加要素として制御チャネル上を1往復するだけで、TLS認証を置き換えない

言い換えると、.ovpnを1個渡すという行為は、接続に必要な信頼のかたまりを一度に配ることである。CA証明書は「このサーバーは本物」の材料で、クライアント証明書と秘密鍵は「自分は本物」の材料で、tls-crypt鍵は「TLSにさえ辿り着かせない門番」の材料である。パスワードはその上の管理職に相当する。

裏返すと、.ovpnファイルは秘密鍵を含んでいる以上、それ自体が秘密である。メールで送っていい類のものではない。ファイル1個で繋がる、というOpenVPNの体験の裏にあるのは、そこに全部が入っているという事実であり、これは同時にファイルが漏れると本人性が漏れるという事実でもある。

OpenVPNが「起動して認証を通すだけ」で済んでいるのではなく、始める前に本人性の材料を全部渡し終えている、と言うほうが正確だろう。楽なのは接続の瞬間の話であって、その前段の証明書発行と鍵配布は誰かが手で回している。

参考