背景と導入の理由
オンプレミスのインフラストラクチャをAWS、Azure、Google Cloudに接続することは、日常的によくある課題です。Linux上でIPsecのサイト間(Site-to-Site)トンネルを構築する際、ポリシーベース(Policy-based)またはルートベース(Route-based)の2つのモデルから選択する必要があります。
従来のIPsec構成の多くは、ポリシーベースのトンネルを採用していました。この方式では、Linuxカーネルが送信パケットをセキュリティポリシーデータベース(SPD: Security Policy Database)と照合します。パケットが設定済みのサブネットペア(例えば 192.168.1.0/24 から 10.0.0.0/16 への通信)に一致すると、カーネルによってパケットが暗号化されます。
これは正常に動作するものの、トラブルシューティングが極めて困難です。ip link コマンドを実行してもネットワークインターフェースが表示されず、標準のLinuxルーティングテーブルからトンネルへトラフィックを直接転送することもできません。さらに厄介なのは、DevOpsチームが新たに 10.50.0.0/16 といったサブネットを追加するたびに、ipsec.conf を更新してセキュリティアソシエーション(SA: Security Association)を再ネゴシエーションしなければならず、一時的な通信切断が発生することです。
ルートベースVPNは、IPsecを仮想トンネルインターフェース(VTI: Virtual Tunnel Interface)に紐付けることで、このアーキテクチャ上の課題を解決します。ハードコードされたIP範囲に一致させる代わりに、暗号化ポリシーを mark = 42 のような数値マークに関連付けます。トンネルはワイルドカードセレクタ(0.0.0.0/0 <=> 0.0.0.0/0)をネゴシエーションし、vti0 という仮想インターフェースを経由してトラフィックをルーティングします。
vti0 は物理ネットワークカードとまったく同様に扱うことができます。ip route add を使ったサブネットの転送、iftop によるリアルタイムの帯域幅モニタリング、nftables によるトラフィックフィルタリング、さらにはBGPフェイルオーバーを目的としたFRRouting(FRR)などの動的ルーティングデーモンの実行も可能です。
ここでは、パブリックインターネットを介した2つのエンドポイント間にトンネルを構築します:
- ローカルゲートウェイ(オフィス / データセンター): パブリックIP
198.51.100.10、ローカルLAN192.168.1.0/24、トンネルVTI IP10.254.0.1/30 - クラウドゲートウェイ(AWS EC2 / GCP Compute Engine / リモートVM): パブリックIP
203.0.113.20、クラウドVPC10.0.0.0/16、トンネルVTI IP10.254.0.2/30 - XFRMマークID:
42(VTIインターフェースとカーネルのIPsec状態を紐付け)
インストール
以下のコマンドはUbuntu 22.04/24.04およびDebian 12を対象としています。RHELやRocky Linuxをご利用の場合は、apt を dnf に置き換え、strongswan と strongswan-libcharon をインストールしてください。
1. strongSwanパッケージのインストール
公式リポジトリからstrongSwanと関連する暗号化モジュールを取得します:
sudo apt update
sudo apt install -y strongswan strongswan-pki libcharon-extra-plugins
2. Linuxカーネルモジュールの有効化
VTIは ip_vti と xfrm4_tunnel という2つのカーネルモジュールに依存します。これらを直ちにロードします:
sudo modprobe ip_vti
sudo modprobe xfrm4_tunnel
システムの再起動後もこれらのドライバーが自動ロードされるよう、/etc/modules-load.d/ipsec-vti.conf に設定を保存します:
echo "ip_vti" | sudo tee -a /etc/modules-load.d/ipsec-vti.conf
echo "xfrm4_tunnel" | sudo tee -a /etc/modules-load.d/ipsec-vti.conf
3. カーネルパラメータのチューニング(sysctl)
ゲートウェイがインターフェース間でパケットを転送するため、IPフォワーディングを有効にしておく必要があります。また、Linuxのリバースパスフィルタ(rp_filter)はデフォルトで厳格モード(1)になっています。厳格モードでは、ルーティングテーブルの不一致によって検証に失敗するため、vti0 から返送されるパケットがサイレントに破棄されてしまいます。ルーズモード(2)に設定することで、このサイレントパケットロスを防止できます。
以下のネットワーク設定を /etc/sysctl.d/99-ipsec-vti.conf に書き込みます:
sudo tee /etc/sysctl.d/99-ipsec-vti.conf <<EOF
net.ipv4.ip_forward = 1
net.ipv4.conf.all.forwarding = 1
net.ipv4.conf.default.rp_filter = 2
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.disable_policy = 1
net.ipv4.conf.all.disable_policy = 1
EOF
sudo sysctl --system
設定手順
ステップ1: strongSwan IPsecポリシーの設定
ローカルゲートウェイで /etc/ipsec.conf を開きます。ルーティングされたすべてのトラフィックを捕捉するため、leftsubnet と rightsubnet の双方に 0.0.0.0/0 を指定します。mark=42 パラメータは、ID 42でマークされたパケットのみを暗号化するようXFRMカーネル層に指示します:
sudo tee /etc/ipsec.conf <<'EOF'
config setup
charondebug="ike 2, knl 2, cfg 2, net 1, esp 1"
uniqueids=yes
conn %default
keyexchange=ikev2
ike=aes256-sha256-modp2048!
esp=aes256-sha256-modp2048!
dpdaction=restart
dpddelay=30s
dpdtimeout=120s
auto=start
conn vti-tunnel
left=198.51.100.10
leftid=198.51.100.10
leftsubnet=0.0.0.0/0
right=203.0.113.20
rightid=203.0.113.20
rightsubnet=0.0.0.0/0
mark=42
authby=secret
type=tunnel
EOF
ステップ2: 事前共有鍵(PSK)の保存
強力な秘密鍵(32文字以上)を生成し、/etc/ipsec.secrets に保存します。rootユーザーのみが読み取れるようにファイルパーミッションを制限します:
sudo tee /etc/ipsec.secrets <<EOF
198.51.100.10 203.0.113.20 : PSK "K9#mQ2$zL8!vP5*xR7@wT1^bY4&cN3(e"
EOF
sudo chmod 600 /etc/ipsec.secrets
ステップ3: VTIデバイスのプロビジョニング
Linuxトンネルインターフェースをマーク 42 に紐付けます。標準的なEthernetのMTUは1500バイトです。ESPおよびSHA256によるIPsecカプセル化はおよそ50〜73バイトのオーバーヘッドを追加するため、パケットの断片化(フラグメンテーション)を防ぐためにトンネルのMTUを 1418 に下げます:
# マーク42に関連付けられたトンネルデバイスを作成
sudo ip tunnel add vti0 mode vti local 198.51.100.10 remote 203.0.113.20 key 42
# ポイントツーポイントの/30トンネルIPアドレスを設定
sudo ip addr add 10.254.0.1/30 peer 10.254.0.2 dev vti0
# インターフェースを起動
sudo ip link set dev vti0 up
# IPsecオーバーヘッドを考慮して安全なMTU値を設定
sudo ip link set dev vti0 mtu 1418
# 新しい仮想インターフェースにルーズモードのパスフィルタリングを適用
sudo sysctl -w net.ipv4.conf.vti0.rp_filter=2
sudo sysctl -w net.ipv4.conf.vti0.disable_policy=1
ステップ4: vti0経由のクラウドサブネットのルーティング
クラウドVPC(10.0.0.0/16)へトラフィックを向けるには、ルートエントリを1つ追加するだけです。vti0 に送られたパケットには自動的にマーク42が付与され、XFRMによって捕捉されて暗号化されます:
sudo ip route add 10.0.0.0/16 dev vti0
後からセカンダリVPC(例えば 172.16.0.0/20)を追加する必要が生じた場合でも、ip route add 172.16.0.0/20 dev vti0 を実行するだけで済みます。IPsecのリロードも、ネゴシエーションに伴うダウンタイムも発生しません。
ステップ5: インターフェースライフサイクルの自動化
手動で実行したコマンドは再起動時に失われます。strongSwanの leftupdown フックを利用して、フェーズ2の確立時に vti0 を自動起動し、トンネル切断時にはクリーンに削除するようにします。
このスクリプトを /usr/local/sbin/ipsec-vti.sh として保存します:
sudo tee /usr/local/sbin/ipsec-vti.sh <<'EOF'
#!/usr/bin/env bash
VTI_IF="vti0"
VTI_LOCAL="10.254.0.1/30"
VTI_REMOTE="10.254.0.2"
REMOTE_SUBNET="10.0.0.0/16"
VTI_KEY="42"
case "${PLUTO_VERB}" in
up-client)
ip tunnel add "${VTI_IF}" mode vti local "${PLUTO_ME}" remote "${PLUTO_PEER}" key "${VTI_KEY}"
ip addr add "${VTI_LOCAL}" peer "${VTI_REMOTE}" dev "${VTI_IF}"
ip link set dev "${VTI_IF}" mtu 1418 up
sysctl -w "net.ipv4.conf.${VTI_IF}.rp_filter=2" > /dev/null
sysctl -w "net.ipv4.conf.${VTI_IF}.disable_policy=1" > /dev/null
ip route replace "${REMOTE_SUBNET}" dev "${VTI_IF}"
;;
down-client)
ip link set dev "${VTI_IF}" down 2>/dev/null
ip tunnel del "${VTI_IF}" 2>/dev/null
;;
esac
EOF
sudo chmod +x /usr/local/sbin/ipsec-vti.sh
/etc/ipsec.conf 内の conn vti-tunnel ブロックに leftupdown=/usr/local/sbin/ipsec-vti.sh を追加します。
ステップ6: strongSwanの起動
サービスを再起動してIKEデーモンを初期化し、接続を確立します:
sudo systemctl restart strongswan-starter
動作確認とモニタリング
リンクの状態を、IKE/ESPステータス、トンネルインターフェースのメトリクス、エンドツーエンドのping、生パケットキャプチャの4段階で確認します。
1. IKEおよびESP SAの確認
ipsec statusall を使用してstrongSwanのステートマシンを確認します:
sudo ipsec statusall
フェーズ1およびフェーズ2のアソシエーションが確立(ESTABLISHED)されていることを確認します:
Security Associations (1 up, 0 connecting):
vti-tunnel[1]: ESTABLISHED 3 minutes ago, 198.51.100.10[198.51.100.10]...203.0.113.20[203.0.113.20]
vti-tunnel{1}: INSTALLED, TUNNEL, reqid 1, ESP SPIs: c3a48e10_a2099df3
vti-tunnel{1}: 0.0.0.0/0 === 0.0.0.0/0, mark 42
0.0.0.0/0 === 0.0.0.0/0, mark 42 のステータスが INSTALLED になっていれば、マーク42に一致するトラフィックを暗号化する準備がカーネルで整っていることを示しています。
2. インターフェースカウンタの確認
仮想インターフェースを通過するパケットの流れを確認します:
ip -s link show dev vti0
RX および TX のバイトカウンタを確認します。TX が増加しているにもかかわらず RX が0のままである場合は、クラウド側のファイアウォール(AWSのセキュリティグループやGCPのファイアウォールルール)を確認してください。UDPポート500および4500、さらにIPプロトコル50(ESP)が開放されていることを確認します。
3. エンドツーエンドのレイテンシとMTUのテスト
クラウドVPC内のホスト(例: 10.0.1.15)に対してICMPエコー要求を送信します:
ping -c 4 10.0.1.15
次に、Path MTUの問題を検証します。Don’t Fragment(DF)ビットをセットした1418バイトのパケットを送信します(ICMPペイロード1390バイト + IPv4ヘッダー20バイト + ICMPヘッダー8バイト = 1418バイト):
ping -M do -s 1390 -c 2 10.0.1.15
Frag needed and DF set (mtu = 1418) と表示された場合は、疎通するまでペイロードサイズを8バイトずつ減らし、その値に合わせて vti0 のMTUを下げてください。また、大容量のHTTP/SSHペイロードでのパケットドロップを防ぐために、iptables でTCP MSSクランピングを適用することも可能です:
sudo iptables -t mangle -A FORWARD -o vti0 -p tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
4. リアルタイムパケットキャプチャ
vti0 上の平文トラフィックと、WANアダプタ(eth0)上の暗号化トラフィックを監視して、暗号化が正常に行われていることを確認します。
パブリックインターフェース上で、暗号化されたESPパケットのみが表示されることを確認します:
sudo tcpdump -n -i eth0 host 203.0.113.20 and proto 50
別のターミナルで vti0 を監視し、暗号化されていないアプリケーションペイロードを確認します:
sudo tcpdump -n -i vti0 icmp
もし eth0 で受信ESPパケットが確認できるにもかかわらず vti0 に何も表示されない場合は、dmesg | grep xfrm を確認してください。多くの場合、リバースパスフィルタリング(rp_filter)またはマークIDの不一致が原因です。

