Use Case — Site-to-Site VPN: US Office ↔ AWS VPC
| On-prem side | US HQ office, UniFi UDM Pro Max — 10.12.0.0/16 |
| AWS side | US AWS prod VPC, us-west-2 — 10.1.0.0/20 |
| Tunnel | IPsec site-to-site (IKEv2), static routing |
| DNS | UDM built-in DNS → conditional forward → Route 53 Resolver inbound endpoint |
1. Architecture
Office (UDM Pro Max) AWS us-west-2 prod VPC (10.1.0.0/20)
10.12.0.0/16 ┌───────────────────────────────────┐
┌─────────────────────┐ IPsec tunnel │ Virtual Private Gateway (VGW) │
│ VLANs 20/30/40/80 ──┼──── (IKEv2, 2 tunnels) ───┼──▶ attached to VPC │
│ UDM built-in DNS │ over WAN │ │
│ (dnsmasq) │ │ Route 53 Resolver INBOUND endpoint│
└─────────┬───────────┘ │ 10.1.4.10 (app1, AZ-a) │
│ forward aws.corp.example.com │ 10.1.5.10 (app2, AZ-b) │
└──────────── DNS/53 over tunnel ──────▶│ ▲ Private Hosted Zone │
└──┼────────────────────────────────┘
aws.corp.example.com
Two flows ride the tunnel: data (office subnets ↔ VPC subnets) and DNS (office clients querying AWS private names, forwarded by the UDM to the Route 53 Resolver inbound endpoint inside the VPC).
This terminates the tunnel on a Virtual Private Gateway attached directly to the VPC (simplest for one office ↔ one VPC). In the full hub-and-spoke design you would instead terminate on the regional Transit Gateway and reach all VPCs behind it — same UDM config, different AWS endpoint.
2. Addressing and routing
| Office prefix advertised to AWS | 10.12.0.0/16 (Guest/IoT VLANs excluded by firewall policy) |
| AWS prefix advertised to office | 10.1.0.0/20 (the VPC) |
| Resolver inbound endpoint IPs | 10.1.4.10, 10.1.5.10 (two AZs) |
| Private hosted zone | aws.corp.example.com |
On AWS, route propagation from the VGW adds 10.12.0.0/16 to the VPC route tables. On the UDM, the tunnel's remote subnet (10.1.0.0/20) is installed as a route automatically. Because both sides sit under the US 10.0.0.0/12, the route tables stay compact.
3. Part A — AWS side (Terraform)
Extends the existing us-west-2 prod VPC (aws_vpc.main, subnets keyed app1/app2, route tables private/data).
###############################################################################
# Site-to-Site VPN: US office (UDM Pro Max) -> AWS us-west-2 prod VPC
###############################################################################
variable "udm_public_ip" {
type = string
description = "Public (WAN) IP of the US office UDM Pro Max"
}
variable "office_cidr" {
type = string
default = "10.12.0.0/16"
}
# --- Customer Gateway = the UDM ------------------------------------------------
resource "aws_customer_gateway" "us_office" {
bgp_asn = 65000 # required by the resource even for static routing
ip_address = var.udm_public_ip
type = "ipsec.1"
tags = { Name = "cgw-us-office-udm" }
}
# --- Virtual Private Gateway attached to the VPC -------------------------------
resource "aws_vpn_gateway" "main" {
vpc_id = aws_vpc.main.id
amazon_side_asn = 64512
tags = { Name = "vgw-us-aws-usw2-prod" }
}
# --- The VPN connection (static routing, IKEv2, hardened tunnel params) --------
resource "aws_vpn_connection" "us_office" {
customer_gateway_id = aws_customer_gateway.us_office.id
vpn_gateway_id = aws_vpn_gateway.main.id
type = "ipsec.1"
static_routes_only = true
# Tunnel 1 (mirror these onto tunnel2_* for the second, redundant tunnel)
tunnel1_ike_versions = ["ikev2"]
tunnel1_phase1_encryption_algorithms = ["AES256"]
tunnel1_phase2_encryption_algorithms = ["AES256"]
tunnel1_phase1_integrity_algorithms = ["SHA2-256"]
tunnel1_phase2_integrity_algorithms = ["SHA2-256"]
tunnel1_phase1_dh_group_numbers = [14]
tunnel1_phase2_dh_group_numbers = [14]
tunnel2_ike_versions = ["ikev2"]
tunnel2_phase1_encryption_algorithms = ["AES256"]
tunnel2_phase2_encryption_algorithms = ["AES256"]
tunnel2_phase1_integrity_algorithms = ["SHA2-256"]
tunnel2_phase2_integrity_algorithms = ["SHA2-256"]
tunnel2_phase1_dh_group_numbers = [14]
tunnel2_phase2_dh_group_numbers = [14]
tags = { Name = "vpn-us-office-to-usw2" }
}
# Static route telling AWS the office prefix is reachable over this tunnel.
resource "aws_vpn_connection_route" "office" {
destination_cidr_block = var.office_cidr
vpn_connection_id = aws_vpn_connection.us_office.id
}
# Propagate VGW routes into the tiers that should reach the office.
resource "aws_vpn_gateway_route_propagation" "private" {
for_each = toset(local.azs)
vpn_gateway_id = aws_vpn_gateway.main.id
route_table_id = aws_route_table.private[each.key].id
}
resource "aws_vpn_gateway_route_propagation" "data" {
vpn_gateway_id = aws_vpn_gateway.main.id
route_table_id = aws_route_table.data.id
}
# Allow the office prefix toward in-VPC workloads (example SG).
resource "aws_security_group" "from_office" {
name = "sg-allow-from-us-office"
description = "Allow traffic from the US office over the VPN"
vpc_id = aws_vpc.main.id
ingress {
description = "All from office (tighten per app)"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [var.office_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "sg-allow-from-us-office" }
}
###############################################################################
# Route 53 Resolver INBOUND endpoint (office -> AWS private DNS)
###############################################################################
resource "aws_security_group" "r53_inbound" {
name = "sg-r53-resolver-inbound"
description = "DNS from the US office to the Route 53 Resolver inbound endpoint"
vpc_id = aws_vpc.main.id
ingress {
description = "DNS UDP from office"
from_port = 53
to_port = 53
protocol = "udp"
cidr_blocks = [var.office_cidr]
}
ingress {
description = "DNS TCP from office"
from_port = 53
to_port = 53
protocol = "tcp"
cidr_blocks = [var.office_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = { Name = "sg-r53-resolver-inbound" }
}
resource "aws_route53_resolver_endpoint" "inbound" {
name = "r53-inbound-usw2"
direction = "INBOUND"
security_group_ids = [aws_security_group.r53_inbound.id]
ip_address {
subnet_id = aws_subnet.this["app1"].id # 10.1.4.0/24, AZ-a
ip = "10.1.4.10"
}
ip_address {
subnet_id = aws_subnet.this["app2"].id # 10.1.5.0/24, AZ-b
ip = "10.1.5.10"
}
tags = { Name = "r53-inbound-usw2" }
}
# Example private hosted zone the office will resolve through the endpoint.
resource "aws_route53_zone" "private" {
name = "aws.corp.example.com"
vpc {
vpc_id = aws_vpc.main.id
}
}
###############################################################################
# Outputs needed to configure the UDM
###############################################################################
output "vpn_tunnel1_address" { value = aws_vpn_connection.us_office.tunnel1_address }
output "vpn_tunnel2_address" { value = aws_vpn_connection.us_office.tunnel2_address }
output "vpn_tunnel1_psk" {
value = aws_vpn_connection.us_office.tunnel1_preshared_key
sensitive = true
}
output "vpn_tunnel2_psk" {
value = aws_vpn_connection.us_office.tunnel2_preshared_key
sensitive = true
}
output "resolver_inbound_ips" {
value = [for a in aws_route53_resolver_endpoint.inbound.ip_address : a.ip]
}
4. Part B — UDM Pro Max side (UniFi Network)
4.1 The IPsec tunnel
Settings → VPN → Site-to-Site VPN → Manual IPsec:
| Field | Value |
|---|---|
| Remote Gateway | vpn_tunnel1_address (AWS tunnel 1 outside IP) |
| Pre-Shared Key | vpn_tunnel1_psk |
| Local subnets | 10.12.0.0/16 (or list Corporate/App/Data/Cloud-Transit VLANs only) |
| Remote subnets | 10.1.0.0/20 |
| Key Exchange | IKEv2 |
| Encryption | AES-256 |
| Hash | SHA-256 |
| DH / PFS group | 14 |
The tunnel parameters must match the AWS side exactly (the Terraform above sets AES-256 / SHA2-256 / DH14, IKEv2). AWS provisions two tunnel endpoints for HA; configure tunnel 1 as primary and add tunnel 2 as a second site-to-site entry for failover. Seamless dual-tunnel failover is cleanest with BGP (see Notes).
The office firewall already permits Corporate/Application/Data → Cloud Transit; that zone policy now routes across this tunnel. Guest and IoT remain blocked, so they never traverse the VPN even though 10.12.0.0/16 is the encryption domain.
4.2 Split DNS → Route 53
Goal: an office client asking for db.aws.corp.example.com is answered with the record's private VPC IP, resolved by Route 53 inside AWS.
- Point clients at the UDM. In each VLAN's DHCP settings, hand out the UDM as the DNS server (its per-VLAN gateway IP, e.g.
10.12.20.1). All client lookups now hit the UDM's built-in DNS.
Fallback if your UniFi version lacks Forward Domain in the UI. The built-in DNS is dnsmasq, so the same result comes from a server=/<domain>/<resolver-ip> directive, applied via a udm-utilities on-boot script (SSH access required):
# /mnt/data/on_boot.d/10-aws-dns.sh
cat > /run/dnsmasq.conf.d/aws-dns.conf <<'EOF'
server=/aws.corp.example.com/10.1.4.10
server=/aws.corp.example.com/10.1.5.10
EOF
kill -9 "$(cat /run/dnsmasq.pid)"
On-boot scripts are a community mechanism, not an Ubiquiti-supported feature — prefer the native Forward Domain UI where available; use the script only as a fallback, and re-test after firmware upgrades.
Add a Forward Domain (Settings → DNS → Forward Domain, in current UniFi Network):
| Domain | Forward to |
|---|---|
aws.corp.example.com |
10.1.4.10, 10.1.5.10 |
Add the reverse zone too if you need PTR lookups for VPC IPs (e.g. 1.10.in-addr.arpa → same resolver IPs). Everything else continues to your normal upstream resolver.
5. Verification
- Tunnel up: AWS console → Site-to-Site VPN shows both tunnels
UP; UDM → VPN shows the peer connected. - Data path: from an office Application-VLAN host,
ping/curla private IP in10.1.x.x; from an EC2 instance, reach an office host in10.12.x.x. - DNS: from an office client,
nslookup db.aws.corp.example.comreturns a10.1.x.xprivate IP (proves the query reached the Route 53 inbound endpoint). A public name likeexample.comstill resolves via the normal upstream (proves only the AWS domain is conditionally forwarded).
6. Notes
- Reverse resolution (AWS → office). For EC2 to resolve on-prem names, add a Route 53 Resolver OUTBOUND endpoint plus a forwarding rule sending
corp.example.comto the UDM's DNS IP (10.12.10.1). That's the mirror of the inbound flow. - BGP option. Set
static_routes_only = false, give the UDM and CGW real ASNs, and enable BGP on the UDM. AWS then advertises10.1.0.0/20and learns10.12.0.0/16dynamically, and dual-tunnel failover becomes automatic — preferable for production. - Transit Gateway. To reach every US AWS VPC (not just this one), attach the VPN to a Transit Gateway instead of the VGW and let the office learn the summarized
10.0.0.0/14. The UDM config is unchanged except the remote subnet. - Tighten the encryption domain. Instead of
10.12.0.0/16, list only the VLANs that need AWS (10.12.20.0/24,10.12.30.0/24,10.12.40.0/24,10.12.80.0/24) so Guest/IoT are excluded at the IPsec layer as well as the firewall. - Secrets. The tunnel PSKs are sensitive Terraform outputs — retrieve them with
terraform output -raw vpn_tunnel1_pskand enter them in the UniFi UI; don't commit them. - DNS resolution of VPC interface endpoints. If you use PrivateLink/interface endpoints with private DNS, forwarding
<region>.amazonaws.com-style zones (or the specific service domains) to the resolver makes those resolve to private IPs from the office too.