A
Arun's Blog
← Back to all posts

Site to Site AWS VPN - Using Only Windows Servers

VPCVPNWindowsTerraform
TL;DR

Establish a Site-to-Site VPN between AWS and your on-premise network using a Windows Server as your router - no third-party networking equipment required. Use Terraform to create AWS VPN resources, then configure Windows Server with Routing and Remote Access (RRAS) for IKEv2 VPN connectivity.

Introduction

Do you rely solely on Windows servers in your environment and lack access to third-party networking equipment? Are you looking to establish a site-to-site VPN connection to your AWS environment but unsure how to do so with your existing setup? If so, fear not, because it's possible to achieve this with just a Windows Server acting as a router. In this blog post, we'll guide you through the steps needed to establish a secure connection between your LAN and AWS, enabling bidirectional communication between your servers and the cloud platform.

Prerequisites

  • Windows Server to be used as a router
    • I am using Server Operating System 2019
  • An AWS account
  • An AWS IAM object with adequate rights
  • An AWS VPC with an EC2 instance for testing connectivity
    • I am using Terraform to create all AWS objects
Important

The Windows Server must have a public IP address accessible from the internet. Ensure your firewall allows UDP ports 500 and 4500 for IKE negotiation, as well as ESP protocol (IP protocol 50) for encrypted traffic.

AWS Tasks

Create VPC

resource "aws_vpc" "vpc" {
    cidr_block = "192.168.0.0/16"
}

Create Subnets

#Private Subnet in AZ-A
resource "aws_subnet" "prisub1" {
cidr_block = 192.168.0.0/24
vpc_id = aws_vpc.vpc.id
availability_zone = data.aws_availability_zones.available.names[0]
}

#Private Subnet in AZ-B
resource "aws_subnet" "prisub2" {
cidr_block = 192.168.1.0/24
vpc_id = aws_vpc.vpc.id
availability_zone = data.aws_availability_zones.available.names[1]
}

Create and Attach Route Table

resource "aws_route_table" "routetableprivate" {
vpc_id = aws_vpc.vpc.id
}

resource "aws_route_table_association" "prisub1" {
subnet_id = aws_subnet.prisub1.id
route_table_id = aws_route_table.routetableprivate.id
}

resource "aws_route_table_association" "prisub2" {
subnet_id = aws_subnet.prisub2.id
route_table_id = aws_route_table.routetableprivate.id
}

resource "aws_vpn_gateway_route_propagation" "routepropagation" {
  vpn_gateway_id = aws_vpn_gateway.vgw.id
  route_table_id = aws_route_table.routetableprivate.id
}
# this will enable routing to on-premise CIDR via propagation upon successful connection to the VPN

Create and Attach Virtual Private Gateway

resource "aws_vpn_gateway" "vgw" {
vpc_id = aws_vpc.vpc.id
}

resource "aws_vpn_gateway_attachment" "vgw_attachment" {
  vpc_id         = aws_vpc.vpc.id
  vpn_gateway_id = aws_vpn_gateway.vgw.id
}

Create Customer Gateway

resource "aws_customer_gateway" "cgw" {
  bgp_asn    = 65000
  ip_address = "20.85.247.126" #this is your public IP address you will obtain from your Windows Server 
  type       = "ipsec.1"
}

Create Site to Site VPN

resource "aws_vpn_connection" "vpn" {
  vpn_gateway_id      = aws_vpn_gateway.vgw.id
  customer_gateway_id = aws_customer_gateway.cgw.id
  type                = "ipsec.1"
  static_routes_only  = true
  tunnel1_preshared_key = "abc123xyz987" 
  #this can be anything you want and will be used when configuring the VPN on the Windows Server
}

resource "aws_vpn_connection_route" "remote" {
  destination_cidr_block = "10.0.0.0/24" #this is your LAN CIDR where your Windows Server sits
  vpn_connection_id      = aws_vpn_connection.vpn.id
}

output "tunnel1IP" {
  value = aws_vpn_connection.vpn.tunnel1_address
  #this will be the AWS gateway which will be used when configuring the VPN on the Windows Server
}
Pro Tip

Use static routes for Windows Server RRAS since BGP configuration on Windows is more complex. The static_routes_only = true setting simplifies the setup significantly.

Create Test EC2

data "aws_ssm_parameter" "linux" {
  name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
}

resource "aws_security_group" "sg-private" {
  name        = "allow_onprem"
  vpc_id      = aws_vpc.vpc.id

  ingress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["10.0.0.0/24"]
  }
  
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
}

resource "aws_instance" "privateLinux" {
    ami = "${data.aws_ssm_parameter.linux.value}"
    instance_type = "t3.medium"
    subnet_id = aws_subnet.prisub1.id
    vpc_security_group_ids = [aws_security_group.sg-private.id]
}

output "privateLinuxIP" {
    value = aws_instance.privateLinux.private_ip
}

Windows Server Tasks

These tasks can be done via PowerShell.

Install Routing Services

Install-WindowsFeature -Name Routing -IncludeManagementTools

Install-RemoteAccess -VpnType VpnS2S

$tunnel1IP = <input the tunnel IP address terraform outputs>
$psk = 'abc123xyz987'
Add-VpnS2SInterface -Name AWS1 -Destination $tunnel1IP -Protocol IKEv2 -AuthenticationMethod PSKOnly -SharedSecret $psk -IPv4Subnet 192.168.0.0/16:100

restart-service RemoteAccess; start-sleep 15; Connect-VpnS2SInterface -Name AWS1 -PassThru
Note

The IPv4Subnet parameter (192.168.0.0/16:100) specifies the AWS VPC CIDR with a metric of 100. This creates the necessary route for traffic destined to AWS to use the VPN tunnel.

Install and Configure Remote Access with IPSec

Install-RemoteAccess -VpnType VpnS2S

$tunnel1IP = <input the tunnel IP address terraform outputs>
$psk = 'abc123xyz987'
Add-VpnS2SInterface -Name AWS1 -Destination $tunnel1IP -Protocol IKEv2 -AuthenticationMethod PSKOnly -SharedSecret $psk -IPv4Subnet 192.168.0.0/16:100

restart-service RemoteAccess; start-sleep 15; Connect-VpnS2SInterface -Name AWS1 -PassThru

Verification

As AWS never initiates a connection, you will have to initiate the connection over the Site to Site VPN. An easy way to achieve this is by pinging the test AWS instance we created via Terraform. The private IP address for the EC2 instance has been outputted when running Terraform.

ping a.b.c.d -t #continuous ping; replace a.b.c.d with the private IP address of the test EC2 instance
Important

AWS VPN tunnels go DOWN after 10 seconds of inactivity. Configure a persistent ping or monitoring tool to keep the tunnel alive, or implement Dead Peer Detection (DPD) on your Windows Server.

Confirm Inbound and Outbound connection

Get-NetIPsecQuickModeSA

You should see both, Inbound and Outbound connections from your server to AWS.

To take this one step further, you can set the default gateway for other machines in your LAN to the IP Address of your Windows Server acting as a router to AWS so that you can communicate from our LAN servers to AWS, and vice versa.

Conclusion

With Windows Server Operating Systems, establishing a Site to Site IPSec VPN tunnel to AWS is easy and allows you to efficiently direct traffic through a Windows Server functioning as a router. This solution is ideal for creating a fast and straightforward lab environment that allows you to test Site to Site VPN connectivity and dependencies that rely on a Site to Site VPN connection.

Troubleshooting

Issue Possible Cause Solution
VPN connection fails to establish Firewall blocking IPSec ports Ensure UDP 500 (IKE), UDP 4500 (NAT-T), and IP protocol 50 (ESP) are allowed through your firewall. Check Windows Firewall rules.
Tunnel shows connected but no traffic passes Route not added or incorrect subnet Verify the IPv4Subnet in Add-VpnS2SInterface matches the AWS VPC CIDR. Check route table with Get-NetRoute.
"The remote server did not respond" error AWS tunnel IP incorrect or not reachable Verify the tunnel1_address output from Terraform. Test connectivity with Test-NetConnection $tunnel1IP -Port 500.
Connection drops after short period AWS DPD timeout - no traffic keeping tunnel alive Configure a persistent ping script to the AWS EC2 or use network monitoring. AWS tunnels timeout after 10 seconds of inactivity.
Pre-shared key mismatch error PSK in Windows doesn't match AWS VPN Verify tunnel1_preshared_key in Terraform matches the -SharedSecret parameter in PowerShell. Keys are case-sensitive.