【Terraform】AWS ⇔ オンプレ VPN接続

AWS

AWSにVPN Gatewayを作成し、オンプレミス環境と接続をしてみました。
前回Terraformで作成した環境を拡張して接続を行います。

NW構成図

自宅検証用の固定IPを利用して接続を行いました。
Azureの時と同様、オンプレ側でVPN接続する機器は家で眠っていたFortigate 50EをVPN接続専用機として利用しました。
AWS環境にPrivate Subnetを作成していなったため、新たに追加しVPN Gatewayをアタッチします。
IPSecでトンネルを2本接続しBGPで経路交換を行い、VPN Gatewayで受信した経路はPrivate Subnetに再配布します。
VPN GatewayはデフォルトでMED属性が付与されているのでActive-Standby構成になります。
AWS側のプライベートAS「64512」はデフォルト設定のため変更可能です。

ファイル追加

TFファイル

プライベートサブネットを作成し、ルートテーブル、SGを作成します。
ルートテーブルはBGPで運用するので経路設定はしません。

network.tf
~~~省略~~~

# プライベートサブネット追加
resource "aws_subnet" "tf_subnet_private" {
  vpc_id                  = aws_vpc.tf_vpc.id
  cidr_block              = "172.31.200.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = false
  tags = {
    Name = "tf_subnet_private"
  }
}
# Private用ルートテーブル
resource "aws_route_table" "private" {
  vpc_id = aws_vpc.tf_vpc.id
}
# サブネットとルートテーブルの紐づけ
resource "aws_route_table_association" "tf_rt_private" {
  subnet_id      = aws_subnet.tf_subnet_private.id
  route_table_id = aws_route_table.private.id
}
# PrivateSG作成
resource "aws_security_group" "tf_sg_private" {
  name        = "tf_sg_private"
  description = "PrivateSG Managed by Terraform"
  vpc_id      = aws_vpc.tf_vpc.id

  tags = {
    Name = "tf_sg_private"
  }
}
# Ingress、Egressルールを追加
resource "aws_security_group_rule" "ingress_private" {
  for_each = var.ingress_rules_private

  type              = "ingress"
  from_port         = each.value.from_port
  to_port           = each.value.to_port
  protocol          = each.value.protocol
  cidr_blocks       = each.value.cidr_blocks
  description       = each.value.description
  security_group_id = aws_security_group.tf_sg_private.id
}
resource "aws_security_group_rule" "egress_private" {
  for_each = var.egress_rules_private

  type              = "egress"
  from_port         = each.value.from_port
  to_port           = each.value.to_port
  protocol          = each.value.protocol
  cidr_blocks       = each.value.cidr_blocks
  description       = each.value.description
  security_group_id = aws_security_group.tf_sg_private.id
}

EC2にPrivate Instanceを作成します。
SSH認証鍵は構築時の鍵を使いまわします。

aws_ec2.tf
~~~省略~~~

# EC2インスタンス作成
resource "aws_instance" "private_server" {
  ami           = data.aws_ssm_parameter.al2023_ami.value
  instance_type = "t2.micro"
  key_name      = aws_key_pair.sshpub_key.key_name
  vpc_security_group_ids = [aws_security_group.tf_sg_private.id]
  subnet_id              = aws_subnet.tf_subnet_private.id

  tags = {
    Name = "private_server"
  }
}

VPN Gateway、VPN Connectionを作成します。
FortigateとAzure間の接続で実績があるフェーズ1、フェーズ2の暗号化アルゴリズム、ハッシュ関数、鍵長はAzure接続時に合わせました。

vpn_gateway.tf
#########################
# VGW
#########################
resource "aws_vpn_gateway" "vgw" {
  vpc_id          = aws_vpc.tf_vpc.id
  amazon_side_asn = 64512  #AWSデフォルト

  tags = {
    Name = "tf_vgw"
  }
}

resource "aws_customer_gateway" "fortigate" {
  bgp_asn    = 65001
  ip_address = "x.x.x.x" #オンプレグローバルIP
  type       = "ipsec.1"

  tags = {
    Name = "fortigate-cgw"
  }
}
resource "aws_vpn_gateway_route_propagation" "vpn_propagation" {
  vpn_gateway_id = aws_vpn_gateway.vgw.id
  route_table_id = aws_route_table.private.id
}

resource "aws_vpn_connection" "fortigate_vpn" {
  vpn_gateway_id      = aws_vpn_gateway.vgw.id
  customer_gateway_id = aws_customer_gateway.fortigate.id
  type                = "ipsec.1"

  tunnel1_inside_cidr = "169.254.100.0/30"
  tunnel2_inside_cidr = "169.254.101.0/30"

  tunnel1_preshared_key = "AWS_Ph1_IPsec" # 事前共有鍵
  tunnel2_preshared_key = "AWS_Ph2_IPsec" # 事前共有鍵

  static_routes_only = false

  # Tunnel1 Phase1
  tunnel1_phase1_encryption_algorithms = ["AES256"]
  tunnel1_phase1_integrity_algorithms    = ["SHA2-256"]
  tunnel1_phase1_dh_group_numbers     = [14]

  # Tunnel1 Phase2
  tunnel1_phase2_encryption_algorithms = ["AES256"]
  tunnel1_phase2_integrity_algorithms    = ["SHA2-256"]
  tunnel1_phase2_dh_group_numbers     = [2]

  # Tunnel2 Phase1(AWSは冗長)
  tunnel2_phase1_encryption_algorithms = ["AES256"]
  tunnel2_phase1_integrity_algorithms    = ["SHA2-256"]
  tunnel2_phase1_dh_group_numbers     = [14]

  # Tunnel2 Phase2(AWSは冗長)
  tunnel2_phase2_encryption_algorithms = ["AES256"]
  tunnel2_phase2_integrity_algorithms    = ["SHA2-256"]
  tunnel2_phase2_dh_group_numbers     = [2]

  tags = {
    Name = "fortigate-ipsec"
  }
}

output "vpn_tunnel1" {
  value = {
    aws_public_ip = aws_vpn_connection.fortigate_vpn.tunnel1_address
    aws_bgp_ip     = aws_vpn_connection.fortigate_vpn.tunnel1_vgw_inside_address
    fortigate_ip     = aws_vpn_connection.fortigate_vpn.tunnel1_cgw_inside_address
    tunnel_cidr     = aws_vpn_connection.fortigate_vpn.tunnel1_inside_cidr
  }
}
output "vpn_tunnel2" {
  value = {
    aws_public_ip = aws_vpn_connection.fortigate_vpn.tunnel2_address
    aws_bgp_ip     = aws_vpn_connection.fortigate_vpn.tunnel2_vgw_inside_address
    fortigate_ip     = aws_vpn_connection.fortigate_vpn.tunnel2_cgw_inside_address
    tunnel_cidr      = aws_vpn_connection.fortigate_vpn.tunnel2_inside_cidr
  }
}

NSGにオンプレとWebserver間の通信を許可出します。

variables.tf
~~~省略~~~

variable "ingress_rules_private" {
  description = "Ingress rules"
  type = map(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))
  default = {}
}
variable "egress_rules_private" {
  description = "Egress rules"
  type = map(object({
    from_port   = number
    to_port     = number
    protocol    = string
    cidr_blocks = list(string)
    description = string
  }))
  default = {}
}

NSGはステートフルのためIngressのみ許可を行います。

terraform.tfvars
~~~省略~~~

# Privateインスタンス用
ingress_rules_private = {
  http = {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["10.200.0.0/16"]  #送信元
    description = "From On-Premises"
  }
  icmp = {
    from_port   = -1
    to_port     = -1
    protocol    = "icmp"
    cidr_blocks = ["10.200.0.0/16"]  #送信元
    description = "From On-Premises"
  }
}

egress_rules_private = {}

Fortigate

トンネル、IPSec、BGP、ポリシーを追加します。
ここで注意しなければいけないのが、通信が発生しないとIPSecを張らないことです。
LAN側からトンネル内を通過する通信を発生させるためにポリシーで許可することで、BGPネイバーの確立ができるようになります。

HCL
config system interface
  edit "aws-vpn-1"
    set ip 169.254.100.2 255.255.255.255
    set allowaccess ping
    set type tunnel
    set remote-ip 169.254.100.1 255.255.255.252
  next
  edit "aws-vpn-2"
    set ip 169.254.101.2 255.255.255.255
    set allowaccess ping
    set type tunnel
    set remote-ip 169.254.101.1 255.255.255.252
  next
end

config vpn ipsec phase1-interface
  edit aws-vpn-1
    set interface wan
    set ike-version 2
    set peertype any
    set proposal aes256-sha256
    set dhgrp 14
    set remote-gw x.x.x.x #トンネル1グローバルIP
    set psksecret AWS_Ph1_IPsec #事前共有鍵
  next
  edit aws-vpn-2
    set interface wan
    set ike-version 2
    set peertype any
    set proposal aes256-sha256
    set dhgrp 14
    set remote-gw y.y.y.y #トンネル2グローバルIP
    set psksecret AWS_Ph2_IPsec #事前共有鍵
  next
end

config vpn ipsec phase2-interface
  edit aws-vpn-p2-1
    set phase1name aws-vpn-1
    set proposal aes256-sha256
    set dhgrp 2
    set src-subnet 0.0.0.0 0.0.0.0
    set dst-subnet 0.0.0.0 0.0.0.0
  next
  edit aws-vpn-p2-2
    set phase1name aws-vpn-2
    set proposal aes256-sha256
    set dhgrp 2
    set src-subnet 0.0.0.0 0.0.0.0
    set dst-subnet 0.0.0.0 0.0.0.0
  next
end

config router bgp
  set as 65001
  set router-id 10.201.0.1
  config neighbor
    edit 169.254.100.1
      set remote-as 64512
      set update-source "aws-vpn-1"
    next
    edit 169.254.101.1
      set remote-as 64512
      set update-source "aws-vpn-2"
    next
  end
end
config firewall address
  edit "onprem-real"
    set subnet 10.200.0.0 255.255.0.0
  next
  edit "aws-vpc"
   set subnet 172.31.0.0 255.255.0.0
  next
end


config firewall policy
    edit 1
        set name "lan-to-AWS-1"
        set srcintf "lan"
        set dstintf "aws-vpn-1"
        set srcaddr "onprem-real"
        set dstaddr "aws-vpc"
        set action accept
        set schedule "always"
        set service "ALL"
    next
    edit 2
        set name "lan-to-AWS-2"
        set srcintf "lan"
        set dstintf "aws-vpn-2"
        set srcaddr "onprem-real"
        set dstaddr "aws-vpc"
        set action accept
        set schedule "always"
        set service "ALL"
    next
end

接続確認

Fortigate確認

IKE SA確認

Tunnel×2本接続するため、SAが2つ作成されていれば問題ありません。

# diagnose vpn ike gateway list
 
vd: root/0
name: aws-vpn-1
version: 2
interface: ppp1 42
addr: x.x.x.x:4500 -> y.y.y.y:4500
virtual-interface-addr: 169.254.100.2 -> 169.254.100.1
created: 13s ago
nat: peer
PPK: no
IKE SA: created 1/1  established 1/1  time 230/230/230 ms
IPsec SA: created 1/1  established 1/1  time 230/230/230 ms
 
  id/spi: 17855 xxxxxxxxxxxxxxxxxxxxxxxxxxxx
  direction: initiator
  status: established 13-13s ago = 230ms
  proposal: aes256-sha256
  child: no
  SK_ei: xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx
  SK_er: xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx
  SK_ai: xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx
  SK_ar: xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx
  PPK: no
  message-id sent/recv: 2/1
  lifetime/rekey: 86400/86086
  DPD sent/recv: 00000000/00000000
 
vd: root/0
name: aws-vpn-2
version: 2
interface: ppp1 42
addr: x.x.x.x:4500 -> z.z.z.z:4500
virtual-interface-addr: 169.254.101.2 -> 169.254.101.1
created: 7s ago
nat: peer
PPK: no
IKE SA: created 1/1  established 1/1  time 190/190/190 ms
IPsec SA: created 1/1  established 1/1  time 190/190/190 ms
 
  id/spi: 17856 xxxxxxxxxxxxxxxxxxxxxxxxxxxx
  direction: initiator
  status: established 7-7s ago = 190ms
  proposal: aes256-sha256
  child: no
  SK_ei: xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx
  SK_er: xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx
  SK_ai: xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx
  SK_ar: xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxx
  PPK: no
  message-id sent/recv: 2/0
  lifetime/rekey: 86400/86092
  DPD sent/recv: 00000000/00000000

IPSec SA確認

トンネル×2本のため、2つSAが作成されていれば問題ありません。
stat: rxp=30 txp=31のようにカウントアップすれば、BGPも通せていることになります。

# diagnose vpn tunnel list
list all ipsec tunnel in vd 0
------------------------------------------------------
name=aws-vpn-1 ver=2 serial=5 x.x.x.x:4500->y.y.y.y:4500 dst_mtu=1454
bound_if=42 lgwy=static/1 tun=intf/0 mode=auto/1 encap=none/512 options[0200]=frag-rfc  run_state=0 accept_traffic=1 overlay_id=0
 
proxyid_num=1 child_num=0 refcnt=14 ilast=2 olast=2 ad=/0
stat: rxp=30 txp=31 rxb=4264 txb=2038
dpd: mode=on-demand on=1 idle=20000ms retry=3 count=0 seqno=0
natt: mode=keepalive draft=0 interval=10 remote_port=4500
proxyid=aws-vpn-p2-1 proto=0 sa=1 ref=2 serial=4
  src: 0:0.0.0.0/0.0.0.0:0
  dst: 0:0.0.0.0/0.0.0.0:0
  SA:  ref=3 options=10202 type=00 soft=0 mtu=1374 expire=42813/0B replaywin=1024
       seqno=20 esn=0 replaywin_lastseq=0000001e itn=0 qat=0 hash_search_len=1
  life: type=01 bytes=0/0 timeout=42902/43200
  dec: spi=1f946e7d esp=aes key=32 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
       ah=sha256 key=32 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  enc: spi=c8a4e342 esp=aes key=32 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
       ah=sha256 key=32 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  dec:pkts/bytes=30/1927, enc:pkts/bytes=31/4476
run_tally=1
------------------------------------------------------
name=aws-vpn-2 ver=2 serial=6 x.x.x.x:4500->z.z.z.z:4500 dst_mtu=1454
bound_if=42 lgwy=static/1 tun=intf/0 mode=auto/1 encap=none/512 options[0200]=frag-rfc  run_state=0 accept_traffic=1 overlay_id=0
 
proxyid_num=1 child_num=0 refcnt=15 ilast=4 olast=4 ad=/0
stat: rxp=28 txp=29 rxb=3936 txb=1941
dpd: mode=on-demand on=1 idle=20000ms retry=3 count=0 seqno=0
natt: mode=keepalive draft=0 interval=10 remote_port=4500
proxyid=aws-vpn-p2-2 proto=0 sa=1 ref=2 serial=2
  src: 0:0.0.0.0/0.0.0.0:0
  dst: 0:0.0.0.0/0.0.0.0:0
  SA:  ref=3 options=10202 type=00 soft=0 mtu=1374 expire=42815/0B replaywin=1024
       seqno=1e esn=0 replaywin_lastseq=0000001c itn=0 qat=0 hash_search_len=1
  life: type=01 bytes=0/0 timeout=42898/43200
  dec: spi=1f946e7e esp=aes key=32 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
       ah=sha256 key=32 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  enc: spi=cdf2517c esp=aes key=32 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
       ah=sha256 key=32 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
  dec:pkts/bytes=28/1750, enc:pkts/bytes=29/4212
run_tally=1

BGP受信ルート確認

トンネルの経由のネットワークが2経路見えていればOKです。
ここで気づいたのですが、メトリックが付与されているためトンネル2がベストパスになります。
想定と違いましたが、物理経路は同じなので良しとしましょう。
AWS側でメトリック調整する項目がないので運になるのかな?

# get router info bgp network
BGP table version is 5, local router ID is 10.201.0.1
Status codes: s suppressed, d damped, h history, * valid, > best, i - internal,
              S Stale
Origin codes: i - IGP, e - EGP, ? - incomplete
 
   Network          Next Hop            Metric LocPrf Weight RouteTag Path
*> 10.200.0.0/16    0.0.0.0                       100  32768        0 i <-/1>
*> 172.31.0.0       169.254.101.1          100             0        0 64512 i <-/1>
*                   169.254.100.1          200             0        0 64512 i <-/->

AWS確認

AWS側の確認ですが、

IPSec接続状況

ikev1、ikev2の選択はできないため、オンプレルータ側に依存するようです。

BGP受信ルート

BGPピアの接続状況を確認する項目が見当たりませんでした。
VGWに伝搬設定を入れているため、VPCに経路が流れてくることを確認しました。
VGW上でいろいろ確認できるのかな?オンプレおじさんには見当がつきませんでした。

SG

インバウンドルールのみ追加されているのを確認します。

EC2インスタンス

いまさらながらプライベートIPは固定IPを振るべきでした。
疎通確認だけなので問題ないですが、

オンプレから接続確認

今回Webサーバ機能はインストールしていないため、ICMPで疎通確認します。
オンプレセグメントに配置しているPCからAWSのプライベートVPCのEC2インスタンス宛に確認します。

>tracert -d 172.31.200.196

172.31.200.196 へのルートをトレースしています。経由するホップ数は最大 30 です

  1     1 ms     1 ms     1 ms  10.200.0.254
  2    11 ms    11 ms    11 ms  172.31.200.196

トレースを完了しました。

最後に

ECMPをする場合、Fortigate側でメトリックを無視する設定をしなければいけないため今回はやめておきました。
柔軟なネットワークを考えた場合、Directconnectで接続しないとECMPやルート制限、接続数上限など制約が多そうです。
ただ、Directconnectはロケーションが必要になるので個人で検証することはできないです。

インターネット接続する場合は、デフォルトルートを配布してオンプレ経由させるか、NAT Gatewayを入れる必要があります。
NAT Gatewayはコストがかかるのでコストを抑える場合は、デフォルトルート配布してオンプレ経由がいいと思います。
サーバ機能のインストールはTerraformで完結できますが、IPSecやBGPの通信確立した後にインストール処理を実行し、処理順番や処理遅延など考える必要があり、面倒なので省略しました。
本番環境で実行する場合は、ネットワークの疎通確認が完了してから、EC2インスタンスを再作成してCloud-initでインストールするのが確実かなと思いました。