Ubuntuで構築するOpenVPNサーバー:Google Authenticator 2FAとEasy-RSA PKIによるセキュアなVPN

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

現場の課題:クライアント証明書のみによるVPNアクセスの落とし穴

昨年、当社のエンジニアリングチームは3つのタイムゾーンにまたがり、12名から45名へと急拡大しました。開発者、データベース管理者、委託先パートナー全員が、Ubuntu上のステージング環境やプライベートなPostgreSQLクラスターに常時アクセスできる環境を必要としていました。多くの成長企業と同様に、私たちも当初は標準的なOpenVPNサーバーを構築し、Easy-RSAで発行したクライアント証明書のみでアクセスを制御していました。

危機感を持ったきっかけは、あるエンジニアが混雑した空港のターミナルに会社のThinkPadを置き忘れたことでした。ノートPCにはLUKSによるフルディスク暗号化が施されていたため、データ流出自体は免れました。しかし、このインシデントを受けて緊急の社内セキュリティ監査を実施したところ、憂慮すべき実態が明らかになりました。なんと4名の開発者が、暗号化されていない個人のUSBメモリに.ovpn設定ファイルをバックアップしていたのです。さらに悪いことに、パスワード入力の手間を省くため、複数のクライアント秘密鍵がパスフレーズなし(空)に設定されていました。

証明書のみに依存するアクセス制御は、「証明書を持っている人物=正当なユーザー」という単純な前提に基づいています。万が一、侵害された端末から暗号化されていない秘密鍵が攻撃者に抜き取られてしまえば、ネットワークの境界防御は瞬時に崩壊します。追加の認証ステップがないため、攻撃者はプライベートサブネットへ簡単に侵入できてしまうのです。私たちには、暗号化されたデバイスIDと、アウトオブバンドで発行される一時的な認証要素を組み合わせた構成が不可欠でした。

根本原因の分析:標準的なVPN運用がスケール時に破綻する理由

インシデント後のポストモーテム(事後検証)により、標準的なOpenVPN運用に共通する3つの構造的弱点が判明しました。

  • 静的クレデンシャルの露出リスク: 統合型の.ovpnプロファイルは、CA証明書、クライアント証明書、秘密鍵を単一の平文ファイルにまとめて保持します。このファイルを入手した人物は誰でも、即座にVPN接続を確立できてしまいます。
  • 証明書失効(Revocation)のタイムラグ: Easy-RSAはCRL(証明書失効リスト)をサポートしていますが、紛失した鍵の失効処理には一定の時間を要します。管理者が./easyrsa revokeを実行し、CRLを再生成して公開する必要があります。今回のケースでは、ノートPCを紛失してからセキュリティチームにインシデント報告のチケットが届くまでに14時間もの空白が生じていました。
  • パスフレーズ疲れによるセキュリティの形骸化: 再接続のたびに16文字のキーパスフレーズの入力を求められると、開発者は危険なショートカットに走りがちです。実際、2名のエンジニアがパスフレーズをハードコードしたシェルスクリプトを作成し、接続を自動化していました。

静的な暗号鍵だけでは、ハードウェアの盗難や設定ファイルの流出からネットワーク境界を防御することはできません。「ファイルを所持していること」を証明するだけでは不十分なのです。

移行候補の比較:リモートチームに最適なソリューションの選定

私たちは10名のエンジニアによる検証グループを立ち上げ、4週間かけて3つの代替アーキテクチャを評価しました。

1. 静的公開鍵を用いたWireGuard

WireGuardは圧倒的なパフォーマンスを示しました。ベンチマークテストでは、1GbpsリンクにおいてCPU使用率をほとんど消費することなく、920Mbpsのスループットを記録しました。しかし、WireGuardは設計思想として対話型のチャレンジ/レスポンス機構を意図的に排除しています。TOTP(Time-based One-Time Password)を導入するには、カスタムのポートノッキングスクリプトや、iptablesを動的に書き換えるサイドカーデーモンを自作しなければなりません。境界防御の要を脆い自作スクリプトに委ねることは避けるべきだと判断しました。

2. 商用ゼロトラストプラットフォーム(Tailscale、Cloudflare Zero Trust)

クラウド管理型のSASEプラットフォームは、OktaやGoogle WorkspaceなどのIDプロバイダー(IdP)と連携することで、多要素認証を極めて容易に実現できます。しかし、1ユーザーあたり月額5〜15ドルのサブスクリプション費用が発生します。エンジニア45名と委託先15名のチーム規模では、年間9,000ドルの固定費となります。さらに、SOC 2やHIPAAのコンプライアンス要件により、機微な患者データパイプラインをサードパーティのコントロールプレーン経由でルーティングすることが認められませんでした。

3. Easy-RSA PKIとPAM Google Authenticatorを組み合わせたOpenVPN

OpenVPNは、openvpn-plugin-auth-pam.soライブラリを介してLinuxのPAM(Pluggable Authentication Modules)とネイティブに統合できます。このアーキテクチャでは、接続試行ごとに2つの独立した要素を検証します。まずEasy-RSAのクライアント証明書を用いてハードウェアプロファイルを検証し、次にユーザーのLinuxアカウントパスワードと、Google Authenticatorが生成する30秒ごとに変化するワンタイムコードを照合します。

これにより、万が一.ovpnプロファイル単体が盗まれても完全に無力化されます。攻撃者はエンジニアのアカウントパスワードと手元の物理スマートフォンの両方を手に入れない限り侵入できません。さらに、このスタック全体は標準的なUbuntuサーバー上で動作するため、アカウント数に応じたライセンス費用も一切発生しません。

推奨アプローチ:Ubuntu 22.04および24.04 LTSでの本番環境セットアップ

当社ではこのアーキテクチャを、常時約35セッションの同時接続をホストするUbuntu 22.04 LTS仮想マシン(2 vCPU、4 GB RAM)にデプロイしました。サーバー負荷が12%を超えることは稀で、ハンドシェイクの平均レイテンシは1.5秒未満を維持しています。以下の手順に沿ってシステムを構成してください。

ステップ 1: 必要なパッケージのインストール

aptリポジトリを更新し、OpenVPN、Easy-RSA、PAM Google Authenticatorモジュール、およびiptables関連ツールをインストールします。

sudo apt update && sudo apt install -y openvpn easy-rsa libpam-google-authenticator iptables iptables-persistent

ステップ 2: Easy-RSA PKIの初期化

認証局(CA)用のクリーンなディレクトリを作成し、サーバー側の暗号化アセットを生成します。

make-cadir ~/openvpn-ca
cd ~/openvpn-ca
./easyrsa init-pki
./easyrsa build-ca nopass
./easyrsa gen-dh
./easyrsa build-server-full server nopass
openvpn --genkey secret ta.key

生成されたCA証明書、Diffie-Hellmanパラメータ、サーバー鍵、およびTLS認証鍵をOpenVPNサーバーのディレクトリにコピーします。

sudo cp pki/ca.crt pki/dh.pem pki/issued/server.crt pki/private/server.key ta.key /etc/openvpn/server/

ステップ 3: 2FA向けLinux PAMの設定

OpenVPN専用のPAM設定ファイルを新規作成します。

sudo nano /etc/pam.d/openvpn

次の3行のディレクティブを記述します。

auth required pam_google_authenticator.so forward_pass
auth required pam_unix.so use_first_pass
account required pam_unix.so

forward_pass引数を指定することで、パスワードと6桁のTOTPコードを連結した単一文字列での入力を受け付けられるようになります。PAMは入力値から認証用トークンを抽出して検証し、残りの生パスワードをpam_unix.soに渡します。

ステップ 4: OpenVPNサーバーデーモンの設定

メインのデーモン設定ファイルを/etc/openvpn/server/server.confに保存します。

port 1194
proto udp
dev tun
ca ca.crt
cert server.crt
key server.key
dh dh.pem
tls-auth ta.key 0
server 10.8.0.0 255.255.255.0
ifconfig-pool-persist /var/log/openvpn/ipp.txt
push "redirect-gateway def1 bypass-dhcp"
push "dhcp-option DNS 1.1.1.1"
push "dhcp-option DNS 8.8.8.8"
keepalive 10 120
cipher AES-256-GCM
auth SHA256
user nobody
group nogroup
persist-key
persist-tun
status /var/log/openvpn/openvpn-status.log
verb 3
explicit-exit-notify 1

# 2要素PAM統合
plugin /usr/lib/x86_64-linux-gnu/openvpn/plugins/openvpn-plugin-auth-pam.so openvpn
verify-client-cert require
username-as-common-name
reneg-sec 0

username-as-common-nameディレクティブは、セキュリティ上極めて重要な不変条件を強制します。ログインユーザー名とクライアント証明書に埋め込まれたCommon Name(CN)が完全に一致していなければなりません。また、reneg-sec 0を指定して1時間ごとの鍵再ネゴシエーションを無効化することで、作業中にアクティブセッションが突然切断されるのを防ぎます。

ステップ 5: IPフォワーディングとNATマスカレードの有効化

sysctlでIPv4パケットルーティングを有効化します。

sudo sed -i 's/#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf
sudo sysctl -p

ip route show defaultコマンドでパブリックネットワークインターフェース(通常はeth0やens3など)を確認します。NATルーティングルールを適用し、OpenVPNサービスを起動します。

sudo iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
sudo netfilter-persistent save
sudo systemctl enable --now openvpn-server@server

ステップ 6: ユーザーアカウントとTOTPキーのプロビジョニング

各エンジニア向けに、対話型シェルアクセスを持たないシステムユーザーを作成します。

sudo useradd -m -s /usr/sbin/nologin devuser1
sudo passwd devuser1

作成したユーザー権限でGoogle Authenticatorプロファイルを初期化します。

sudo -u devuser1 google-authenticator -t -d -f -r 3 -R 30 -W

このコマンドは、時間ベースのトークン生成を設定し、トークンの再利用を防止し、ブルートフォース攻撃対策として30秒あたり3回のリクエスト制限を設け、シードファイルを~/.google_authenticatorに書き込みます。出力されたQRコードをエンジニアに共有し、認証アプリでスキャンしてもらいます。また、緊急用のスクラッチコード(バックアップコード)5組は社内のパスワードマネージャーに保管してください。

ステップ 7: クライアント設定プロファイルの構築

システムのユーザー名とCommon Nameが完全に一致するクライアント証明書を発行します。

cd ~/openvpn-ca
./easyrsa build-client-full devuser1 nopass

暗号化ブロックをインラインで埋め込んだクライアント設定ファイル(devuser1.ovpn)を作成します。

client
dev tun
proto udp
remote 203.0.113.10 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
key-direction 1

# 対話型認証プロンプトをトリガー
auth-user-pass
reneg-sec 0

<ca>
# /etc/openvpn/server/ca.crt を貼り付け
</ca>
<cert>
# ~/openvpn-ca/pki/issued/devuser1.crt を貼り付け
</cert>
<key>
# ~/openvpn-ca/pki/private/devuser1.key を貼り付け
</key>
<tls-auth>
# /etc/openvpn/server/ta.key を貼り付け
</tls-auth>

ステップ 8: 日常の運用とクライアント認証フロー

OpenVPN GUI、Tunnelblick、またはコマンドラインから接続すると、標準的なログインプロンプトが表示されます。ユーザー名フィールドにはdevuser1を入力します。パスワードフィールドには、Unixアカウントのパスワードに6桁のTOTPコードをそのまま末尾に連結して入力します(例:SecretPassphrase839201)。

過去6か月間で、この構成は原因不明の切断を1件も出すことなく、4,200件以上のユニークセッションを安定して処理しました。ハードウェアの紛失はもはや非常事態ではありません。攻撃者がアクティブな.ovpnプロファイルを不正取得したとしても、物理的な認証デバイスが手元にない限り、社内プライベートネットワークには一切アクセスできません。

Share: