複数のAmazon VPCのプライベートネットワークをOpenVPNを使って接続する

openvpn icon

今月に入ってまだJavaのコードを読み書きしてないとか、想定外です。都元ダイスケです。

さて、ネットワークまわりが苦手な人にとってはギョッとする単語が並んだタイトルですが。私も苦手なところからスタートしたので、皆様に分かりやすくお伝えしようと思います。

まずはおさらい

プライベートネットワーク

IPアドレスには「グローバルIPアドレス」と「プライベートIPアドレス」があることはご存知だと思います。前者はインターネットの世界において一意であることが保証されているため、インターネット上で「発信元」や「宛先」として機能します。対して後者は、インターネットの世界から見ると一意ではないため、「発信元」や「宛先」として機能しません。ただし、インターネットではなく、ローカルエリアネットワーク (LAN) の中では(大抵の場合は)一意であるように設計されるため、LAN内に閉じた(=インターネットを介さない)通信は、プライベートIPアドレスが「発信元」や「宛先」として機能します。

具体的には、プライベートIPアドレスしか持たないサーバに対しては、LAN内からはHTTPリクエストやping等を投げらるが、インターネットを介した場合は投げられない、ということです。

VPCの場合のネットワークの解釈

openvpn-ping

1つのVPC内に複数のサブネットを作った場合、これは同じLAN内にあると考えられます。従って、各サブネット内にEC2インスタンスをデプロイした場合、相互にpingによる疎通が可能です。

しかし、複数のVPCを作成し、それぞれにサブネットを作った場合、それぞれが別のLANに属していると考えます。従って、pingによる疎通は不可能ということになります。

もちろん、それぞれにグローバルIPアドレスを振れば相互にアクセス可能にはなりますが、グローバルIPアドレスは有限であり、貴重なリソースです。また、平文のパケットがインターネットを流れることになります。

VPNとは

openvpn-vpn

VPNというのは、インターネットによって隔てられた複数の「プライベートネットワーク」を、あたかも同じLAN内にあるかのように接続するシステムです。

VPNをインターネット接続を利用して実現し、さらにそのインターネット接続は暗号化するのが「インターネットVPN」です。(これに対し、通信事業社が独自に構築した閉鎖IPネットワークを介して実現するVPNをIP-VPNと呼びます。)

インターネットVPNには「SSL-VPN」と「IPsec-VPN」という2つの代表的な規格があります。今回は、前者SSL-VPNの実装であるOpenVPNを利用した、2つのVPC間のVPN接続をご紹介します。

本エントリのゴール

  • Terminal-AにSSHログインして、Terminal-Bにping疎通ができること
  • Terminal-BにSSHログインして、Terminal-Aにping疎通ができること

以上をこのエントリのゴールとしたいと思います。また、物理的に離れた2拠点間のインターネット接続を介したVPN通信であることを強調するため、敢えて別のAWSリージョンに、それぞれのVPC環境を構築してみます。実験をするだけなので、リージョンはどこでも構いません。普段利用していないまっさらなリージョンを2つ選びましょう。本エントリでは「アイルランド eu-west-1」と「サンパウロ sa-east-1」を利用してみました。

VPC環境構築

openvpn-create-vpcs

両リージョンに、ほぼ同じVPC・サブネット・EC2インスタンスの構成を作るわけですが、それぞれのネットワークアドレスやIPアドレスは重複しないように設定します。各VPC内には2つのサブネットを作りますが、どちらもpublic subnetとします。Start VPC Wizardからは「VPC with a Single Public Subnet Only」を選んで、後からpublic subnetを1つ追加するのが楽だと思います。その場合、後から作ったsubnetに対して自動生成されたRoute Tableは、関連付けを解除して、2つのsubnetで同じRoute Tableを共有するように設定しておいてください。これによって、両者ともpublic subnetとして機能します。使わなくなったRoute Tableは削除してしまっても構いません。

続いて、各サブネットにEC2インスタンスを1台ずつ起動します。全てmicroインスタンスで充分です。各インスタンスのローカルIPアドレスは、自動付与でも構いませんが、認識しやすいように各自で指定しておくことをお薦めします。また、これらのEC2インスタンスには、すべてEIPによりグローバルIPアドレスも振っておきましょう。

VPNの接続自体は、Terminal-A/B が属するサブネット(図の右半分)をprivate subnetにし、EIPは VPN Instance-A/Bにのみ振る、という構成であっても成立します。しかし、本エントリのゴールを確認するために、Terminal-A/Bに直接SSHログインできた方が便利なので、全てをpublic subnetとし、さらに4台の全サーバにEIPを振ってしまいます。より厳密に検証したい方は、Terminalが属するサブネットはprivate subnetとして構築し、EIPはVPN Instance-A/B の2つに振りましょう。そして、VPN Instance-A を Terminal-Aに対する踏み台(bastion)として利用すれば検証もできる思います。

次に「vpntest-ping」というVPC Security Groupを作り、All ICMPを許可する設定します。また、SSHのためのVPC Security Groupも作成し、設定します。これらを全インスタンスに紐付けて(default + vpntest-ping + vpntest-ssh になるように)下さい。

openvpn-secg

ここまで出来たら、各EC2インスタンス(4台)にSSHで接続し、相互にpingを撃ってみてください。想定通り、同じVPC内ではpingが通り、VPCをまたごうとするとと通らないと思います。ここがスタート地点ですね。

openvpn-before-ping-local

openvpn-before-vertical

VPN構築

VPC Security Groupの設定

openvpn-udp1194-secg-2

VPN Instance-AとBの間で、VPNのための通信ができるよう、UDP/1194ポートを開けたVPC Security Group「vpntest-openvpn」を作成し、VPN Instance-A/Bに割り当てます。今回は実験ですので 0.0.0.0/0 に対して許可しても構いませんが、運用時はそれぞれ相手のVPN InstanceのEIP/32を指定すると、よりセキュアになります。ちなみにTCPではなくUDPです。筆者はここで少々ハマりました。

Route Table の調整

openvpn-aws

次に、Route Tableの設定を調整します。デフォルトの状態では以下の項目が設定済みです。

  • 宛先が自身のVPC内のパケットは、ローカルで処理する
  • それ以外の全宛先のパケットは、Internet Gatewayを通じて外に出す

VPC-Aにおいて、ここに「宛先が10.1.0.0/16 (つまりVPC-B内)のパケットは、VPN Instance-Aに渡して処理させる」という設定を追加します。逆に、VPC-Bにおいては「宛先が10.0.0.0/16(つまりVPC-A内)のパケットは、VPN Instance-Bに渡して処理させる」という設定を追加します。VPN接続によって別の拠点に送るべきパケットは、すべてVPN Instanceに送りつけて処理してもらうわけです。

下のスクリーンショットは、VPC-A側のRoute Table設定です。VPC-B側のネットワーク(10.1.0.0/16)宛のパケットは、VPN Instance-Aに向けています。

openvpn-rtb-1

openvpn-rtb-2

Source/Dest. Check の無効化

VPCのEC2インスタンスは、Source/Dest. Checkというものを行っています。自分が宛先ではないパケットは、送信してもEC2に届かないようになっています。今回、VPN Instanceは、他のVPC宛のパケットを間接的に受け取って転送(フォワード)する役割を果たしますので、このチェックが邪魔になります。従って、VPN Instance-A/B両者において、このチェックを無効(disable)に設定します。

ちなみに、VPCにおけるNATインスタンスも、外部への通信を中継し、転送する役割がありますので、このチェックが無効になっています。

openvpn-disable-sdcheck-1

openvpn-disable-sdcheck-2

IPv4フォワーディングの有効化

上で説明したチェックはAWSインフラレベルのチェックでした。同様のチェックがOSレベルでも実施されており(AWSインフラレベルの「Security Group」に対して、OSレベルの「iptables」のようなものです。)、それも無効にする必要があります。

VPN Instance-A/B両者において、/etc/sysctl.conf 内の net.ipv4.ip_forward の値を0から1に変更します。

net.ipv4.ip_forward = 1

続いて、設定を反映させるために、下記のコマンドを実行します。

$ sudo /sbin/sysctl -p

OpenVPNのインストールと設定

VPC Instance-AにおけるOpenVPNのインストールと設定

いよいよOpenVPNをインストールし、設定していきます。下記1行目はインストール、2行目で暗号化のための鍵を生成しています。ちなみに、今回採用する暗号化方式は共通鍵方式であるため、ここで生成された鍵を後ほどVPC Instance-Bにコピーする必要があります。

$ sudo yum -y install openvpn
$ sudo openvpn --genkey --secret /etc/openvpn/openvpn-key.txt
$ sudo cat /etc/openvpn/openvpn-key.txt

続いて、設定ファイルを作成します。 /etc/openvpn/a-to-b.conf というファイルに下記の内容を記述します。「{VPN Instance-BのEIP}」は適宜置換してください。

port 1194
proto udp
dev tun
secret "/etc/openvpn/openvpn-key.txt"

remote {VPN Instance-BのEIP}
route 10.1.0.0 255.255.0.0

ifconfig 10.254.0.1 10.254.0.2

status openvpn-status.log
verb 3

VPC-AのOpenVPNを起動します。

$ sudo openvpn --config /etc/openvpn/a-to-b.conf
Wed Feb  6 08:56:58 2013 OpenVPN 2.2.2 x86_64-redhat-linux-gnu [SSL] [LZO2] [EPOLL] [PKCS11] [eurephia] built on Oct 14 2012
Wed Feb  6 08:56:58 2013 NOTE: OpenVPN 2.1 requires '--script-security 2' or higher to call user-defined scripts or executables
Wed Feb  6 08:56:58 2013 Static Encrypt: Cipher 'BF-CBC' initialized with 128 bit key
Wed Feb  6 08:56:58 2013 Static Encrypt: Using 160 bit message hash 'SHA1' for HMAC authentication
Wed Feb  6 08:56:58 2013 Static Decrypt: Cipher 'BF-CBC' initialized with 128 bit key
Wed Feb  6 08:56:58 2013 Static Decrypt: Using 160 bit message hash 'SHA1' for HMAC authentication
Wed Feb  6 08:56:58 2013 Socket Buffers: R=[229376->131072] S=[229376->131072]
Wed Feb  6 08:56:58 2013 ROUTE default_gateway=10.0.0.1
Wed Feb  6 08:56:58 2013 TUN/TAP device tun0 opened
Wed Feb  6 08:56:58 2013 TUN/TAP TX queue length set to 100
Wed Feb  6 08:56:58 2013 /sbin/ip link set dev tun0 up mtu 1500
Wed Feb  6 08:56:58 2013 /sbin/ip addr add dev tun0 local 10.254.0.1 peer 10.254.0.2
Wed Feb  6 08:56:58 2013 /sbin/ip route add 10.1.0.0/16 via 10.254.0.2
Wed Feb  6 08:56:58 2013 Data Channel MTU parms [ L:1544 D:1450 EF:44 EB:4 ET:0 EL:0 ]
Wed Feb  6 08:56:58 2013 Local Options hash (VER=V4): '6d3adec9'
Wed Feb  6 08:56:58 2013 Expected Remote Options hash (VER=V4): '00859611'
Wed Feb  6 08:56:58 2013 UDPv4 link local (bound): [undef]:1194
Wed Feb  6 08:56:58 2013 UDPv4 link remote: XXX.XXX.XXX.XXX:1194
Wed Feb  6 08:57:08 2013 Peer Connection Initiated with XXX.XXX.XXX.XXX:1194
Wed Feb  6 08:57:10 2013 Initialization Sequence Completed

VPC Instance-BにおけるOpenVPNのインストールと設定

こちらも同様に、OpenVPNをインストールし、VPN Instance-Aで生成した鍵をコピーします。ここでは簡単に、コンソールからのコピペで共有しました。

$ sudo yum -y install openvpn
$ sudo vi /etc/openvpn/openvpn-key.txt
$ sudo chmod 600 /etc/openvpn/openvpn-key.txt

同じように、設定ファイルを作成します。 /etc/openvpn/b-to-a.conf というファイルに下記の内容を記述します。「{VPN Instance-AのEIP}」は適宜置換してください。

port 1194
proto udp
dev tun
secret "/etc/openvpn/openvpn-key.txt"

remote {VPN Instance-AのEIP}
route 10.0.0.0 255.255.0.0

ifconfig 10.254.0.2 10.254.0.1

status openvpn-status.log
verb 3

VPC-BのOpenVPNを起動します。

$ sudo openvpn --config /etc/openvpn/b-to-a.conf
Wed Feb  6 08:57:05 2013 OpenVPN 2.2.2 x86_64-redhat-linux-gnu [SSL] [LZO2] [EPOLL] [PKCS11] [eurephia] built on Oct 14 2012
Wed Feb  6 08:57:05 2013 NOTE: OpenVPN 2.1 requires '--script-security 2' or higher to call user-defined scripts or executables
Wed Feb  6 08:57:05 2013 Static Encrypt: Cipher 'BF-CBC' initialized with 128 bit key
Wed Feb  6 08:57:05 2013 Static Encrypt: Using 160 bit message hash 'SHA1' for HMAC authentication
Wed Feb  6 08:57:05 2013 Static Decrypt: Cipher 'BF-CBC' initialized with 128 bit key
Wed Feb  6 08:57:05 2013 Static Decrypt: Using 160 bit message hash 'SHA1' for HMAC authentication
Wed Feb  6 08:57:05 2013 Socket Buffers: R=[229376->131072] S=[229376->131072]
Wed Feb  6 08:57:05 2013 ROUTE default_gateway=10.1.0.1
Wed Feb  6 08:57:05 2013 TUN/TAP device tun0 opened
Wed Feb  6 08:57:05 2013 TUN/TAP TX queue length set to 100
Wed Feb  6 08:57:05 2013 /sbin/ip link set dev tun0 up mtu 1500
Wed Feb  6 08:57:05 2013 /sbin/ip addr add dev tun0 local 10.254.0.2 peer 10.254.0.1
Wed Feb  6 08:57:05 2013 /sbin/ip route add 10.0.0.0/16 via 10.254.0.1
Wed Feb  6 08:57:05 2013 Data Channel MTU parms [ L:1544 D:1450 EF:44 EB:4 ET:0 EL:0 ]
Wed Feb  6 08:57:05 2013 Local Options hash (VER=V4): '00859611'
Wed Feb  6 08:57:05 2013 Expected Remote Options hash (VER=V4): '6d3adec9'
Wed Feb  6 08:57:05 2013 UDPv4 link local (bound): [undef]:1194
Wed Feb  6 08:57:05 2013 UDPv4 link remote: YYY.YYY.YYY.YYY:1194
Wed Feb  6 08:57:08 2013 Peer Connection Initiated with YYY.YYY.YYY.YYY:1194
Wed Feb  6 08:57:10 2013 Initialization Sequence Completed

ゴールの検証

以上で、VPN接続が確立したはずです。各インスタンスから相互にpingを飛ばし合って、全組み合わせで問題なくpingの到達応答があることを確認します。さらにtracerouteで通信経路も確認してみます。

まずは、アイルランドTerminal-AからサンパウロTerminal-Bへ。

[ec2-user@ip-10-0-1-30 ~]$ ping -c2 10.1.1.40
PING 10.1.1.40 (10.1.1.40) 56(84) bytes of data.
64 bytes from 10.1.1.40: icmp_seq=1 ttl=62 time=223 ms
64 bytes from 10.1.1.40: icmp_seq=2 ttl=62 time=223 ms

--- 10.1.1.40 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1224ms
rtt min/avg/max/mdev = 223.128/223.225/223.322/0.097 ms

[ec2-user@ip-10-0-1-30 ~]$ traceroute 10.1.1.40
traceroute to 10.1.1.40 (10.1.1.40), 30 hops max, 60 byte packets
 1  10.0.0.10 (10.0.0.10)  0.407 ms  0.376 ms  0.363 ms
 2  10.254.0.2 (10.254.0.2)  223.067 ms  223.245 ms  223.257 ms
 3  10.1.1.40 (10.1.1.40)  223.765 ms  224.012 ms  224.006 ms

続いて、サンパウロTerminal-BからアイルランドTerminal-Aへ。

[ec2-user@ip-10-1-1-40 ~]$ ping -c2 10.0.1.30
PING 10.0.1.30 (10.0.1.30) 56(84) bytes of data.
64 bytes from 10.0.1.30: icmp_seq=1 ttl=62 time=223 ms
64 bytes from 10.0.1.30: icmp_seq=2 ttl=62 time=227 ms

--- 10.0.1.30 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1229ms
rtt min/avg/max/mdev = 223.428/225.529/227.631/2.154 ms

[ec2-user@ip-10-1-1-40 ~]$ traceroute 10.0.1.30
traceroute to 10.0.1.30 (10.0.1.30), 30 hops max, 60 byte packets
 1  10.1.0.20 (10.1.0.20)  0.253 ms  0.225 ms  0.308 ms
 2  10.254.0.1 (10.254.0.1)  223.440 ms  223.608 ms  223.634 ms
 3  10.0.1.30 (10.0.1.30)  223.615 ms  223.804 ms  223.833 ms

以上で、本エントリーのゴールは達成です。お疲れ様でした!

ちなみに、今回はOpenVPNのログが見えるように、コマンドラインでOpenVPNを起動しましたが、以下のようにデーモンとして起動も可能です。通常はこちらの方式を利用すると思います。

$ sudo service openvpn start

また、今回と同じ設定で、アカウントをまたいだVPCを接続することも、簡単にできますね。

参考文献

Connecting Multiple VPCs with EC2 Instances (SSL)