09 · Terraform
Greenfield Terraform stack
The full infrastructure source for the lab: VPC and routing, VyOS bootstrap, GRE/BYOIP toggles, operator scripts and Lambda handlers.
Download bundle (.zip)38 files
main.tf
data "http" "terraform_runner_ipv4" {
count = var.auto_detect_terraform_public_ip ? 1 : 0
url = "https://api.ipify.org"
}
locals {
# Legacy BYOIP detector (SSH healthy → withdraw AWS) is mutually exclusive with lab GRE+BYOIP flow.
legacy_byoip_detector_enabled = var.byoip_failover_detector_enabled && !var.lab_gre_byoip_flow_enabled
_terraform_runner_ip_raw = var.auto_detect_terraform_public_ip && length(data.http.terraform_runner_ipv4) > 0 ? trimspace(data.http.terraform_runner_ipv4[0].response_body) : ""
_terraform_runner_cidr = (
local._terraform_runner_ip_raw != "" && can(regex("^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9])$", local._terraform_runner_ip_raw))
) ? "${local._terraform_runner_ip_raw}/32" : null
admin_cidrs_effective = local._terraform_runner_cidr != null ? distinct(concat(var.admin_cidrs, [local._terraform_runner_cidr])) : var.admin_cidrs
imperva_pm_server_cidrs = [for ip in var.imperva_pm_server_ips : "${ip}/32"]
# LAN victim SG: SSH stays admin-only; HTTP/HTTPS are intentionally public through GRE/BYOIP.
lan_server_admin_tcp_ingress = flatten([
for port in [22] : [
for c in local.admin_cidrs_effective : {
port = port
cidr = c
}
]
])
mandatory_tags = {
"Owner email" = "alex.bakshtein@thalesgroup.com"
"Manager email" = "david.holmes@thalesgroup.com"
"Team email" = "ww.dis.imperva.se-ddos-sme@thalesgroup.com"
"Description" = "Lab Router for NW DDoS protection"
"Environment" = "Sales environment"
"Data Classification" = "OPEN"
}
aws_default_tags = merge(local.mandatory_tags, var.extra_tags)
vyos_ssh_line = var.create_router_keypair ? trimspace(replace(tls_private_key.router[0].public_key_openssh, "\n", " ")) : trimspace(var.vyos_ssh_public_key)
vyos_key_parts = split(" ", local.vyos_ssh_line)
vyos_ssh_key_type = length(local.vyos_key_parts) >= 2 ? local.vyos_key_parts[0] : "ssh-rsa"
vyos_ssh_key_data = length(local.vyos_key_parts) >= 2 ? local.vyos_key_parts[1] : ""
vmx_key_name = var.create_router_keypair ? aws_key_pair.router[0].key_name : var.router_key_name
router_private_key_path = trimspace(var.router_private_key_output_path) != "" ? trimspace(var.router_private_key_output_path) : "${path.module}/${var.name_prefix}-router.pem"
router_wan_external_eip = trimspace(var.router_wan_eip_allocation_id)
router_wan_uses_external_eip = local.router_wan_external_eip != ""
router_wan_eip_allocation_id = local.router_wan_uses_external_eip ? data.aws_eip.router_wan_external[0].id : aws_eip.router_wan_managed[0].id
router_wan_public_ip = local.router_wan_uses_external_eip ? data.aws_eip.router_wan_external[0].public_ip : aws_eip.router_wan_managed[0].public_ip
netflow_collector_ip = trimspace(var.netflow_collector_ip)
netflow_protected_subnet_ids = length(var.netflow_protected_subnet_ids) > 0 ? var.netflow_protected_subnet_ids : [
aws_subnet.internal.id
]
netflow_protected_subnet_cidrs = length(var.netflow_protected_subnet_cidrs) > 0 ? var.netflow_protected_subnet_cidrs : [
var.subnet_internal_cidr
]
netflow_bastion_user_data = var.netflow_exporter_enabled && var.netflow_reuse_bastion ? templatefile("${path.module}/user_data/netflow-exporter-bastion.sh.tpl", {
netflow_mirror_vni = tostring(var.netflow_mirror_vni)
netflow_collector_ip = local.netflow_collector_ip
netflow_collector_port = tostring(var.netflow_collector_port)
netflow_version = tostring(var.netflow_version)
netflow_sampling_rate = tostring(var.netflow_sampling_rate)
exporter_private_ip = aws_network_interface.netflow_exporter[0].private_ip
exporter_prefix_len = local.wan_prefix_len
wan_gw = local.wan_gw
}) : null
}
provider "aws" {
region = var.region
default_tags {
tags = local.aws_default_tags
}
}
check "manual_keypair_when_needed" {
assert {
condition = var.create_router_keypair || (
length(trimspace(var.router_key_name)) > 0 && length(trimspace(var.vyos_ssh_public_key)) > 0
)
error_message = "When create_router_keypair is false, set both router_key_name and vyos_ssh_public_key (full ssh-rsa/ssh-ed25519 line)."
}
}
check "byoip_detector_ssh_key" {
assert {
condition = !var.byoip_failover_detector_enabled || var.create_router_keypair || length(trimspace(var.byoip_detector_ssh_private_key_secret_arn)) > 0
error_message = "When byoip_failover_detector_enabled=true and create_router_keypair=false, set byoip_detector_ssh_private_key_secret_arn (Secrets Manager ARN of the PEM for VyOS SSH)."
}
}
check "lab_gre_byoip_flow_ssh_when_needed" {
assert {
condition = !var.lab_gre_byoip_flow_enabled || var.create_router_keypair || (
length(trimspace(var.lab_gre_byoip_flow_ssh_private_key_secret_arn)) > 0 || length(trimspace(var.byoip_detector_ssh_private_key_secret_arn)) > 0
)
error_message = "When lab_gre_byoip_flow_enabled=true and create_router_keypair=false, set lab_gre_byoip_flow_ssh_private_key_secret_arn or byoip_detector_ssh_private_key_secret_arn (Secrets Manager ARN of the PEM for VyOS SSH)."
}
}
check "terraform_runner_ip_authorized" {
assert {
condition = !var.auto_detect_terraform_public_ip || local._terraform_runner_cidr != null
error_message = "auto_detect_terraform_public_ip=true, but this runner's public IPv4 could not be detected or was not a valid IPv4 address. Set auto_detect_terraform_public_ip=false for non-interactive or air-gapped runs, or add your current public /32 manually to admin_cidrs."
}
}
check "netflow_exporter_configuration" {
assert {
condition = !var.netflow_exporter_enabled || (
var.netflow_reuse_bastion &&
var.create_ssh_bastion &&
local.netflow_collector_ip != ""
)
error_message = "When netflow_exporter_enabled=true, set netflow_collector_ip and keep netflow_reuse_bastion=true with create_ssh_bastion=true."
}
}
check "netflow_auto_mirror_not_enabled" {
assert {
condition = !var.netflow_auto_mirror_subnet_enis
error_message = "netflow_auto_mirror_subnet_enis is reserved for future EventBridge/Lambda automation. Add service ENIs to netflow_mirror_managed_eni_ids for now."
}
}
# Reliable AL2023 AMI ID per region (name-based data.aws_ami filters often break)
data "aws_ssm_parameter" "lan_al2023_ami" {
name = "/aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64"
}
resource "tls_private_key" "router" {
count = var.create_router_keypair ? 1 : 0
algorithm = "RSA"
rsa_bits = 4096
}
resource "aws_key_pair" "router" {
count = var.create_router_keypair ? 1 : 0
key_name = "${var.name_prefix}-router-ec2"
public_key = tls_private_key.router[0].public_key_openssh
tags = {
Name = "${var.name_prefix}-router-ec2-key"
}
}
resource "local_sensitive_file" "router_private_key" {
count = var.create_router_keypair ? 1 : 0
filename = local.router_private_key_path
content = tls_private_key.router[0].private_key_pem
file_permission = "0600"
}
data "aws_availability_zones" "available" {
state = "available"
}
locals {
az = var.availability_zone != "" ? var.availability_zone : data.aws_availability_zones.available.names[0]
wan_gw = cidrhost(var.subnet_wan_cidr, 1)
mgmt_gw = cidrhost(var.subnet_mgmt_cidr, 1)
internal_gw = cidrhost(var.subnet_internal_cidr, 1)
loopback_cidr = format(
"%s/%s",
cidrhost(var.advertised_prefix_cidr, tonumber(var.loopback_host_octet)),
split("/", var.advertised_prefix_cidr)[1]
)
mgmt_prefix_len = split("/", var.subnet_mgmt_cidr)[1]
wan_prefix_len = split("/", var.subnet_wan_cidr)[1]
internal_prefix_len = split("/", var.subnet_internal_cidr)[1]
gre_inner_ip = split("/", var.customer_gre_inner_ip_cidr)[0]
# Single map for vyos-cloud-init.yaml.tpl + vyos-recovery-configure.tpl + outputs
vyos_cloud_init_vars = {
vyos_hostname = var.vyos_hostname
ssh_key_type = local.vyos_ssh_key_type
ssh_key_data = local.vyos_ssh_key_data
mgmt_ip = var.router_mgmt_private_ip
mgmt_pl = local.mgmt_prefix_len
mgmt_gw = local.mgmt_gw
wan_ip = var.router_wan_private_ip
wan_pl = local.wan_prefix_len
internal_ip = var.router_internal_private_ip
internal_pl = local.internal_prefix_len
wan_gw = local.wan_gw
internal_gw = local.internal_gw
imperva_public_ip = var.imperva_gre_public_ip
imperva_pm_server_ips = var.imperva_pm_server_ips
gre_inner_cidr = var.customer_gre_inner_ip_cidr
gre_mtu = var.gre_mtu
imperva_bgp_neighbor = var.imperva_bgp_neighbor_ip
bgp_local_asn = var.bgp_local_asn
bgp_peer_asn = var.bgp_peer_asn
advertised_prefix = var.advertised_prefix_cidr
gre_inner_ip = local.gre_inner_ip
lan_service_enabled = var.create_lan_server
lan_server_private_ip = var.lan_server_private_ip
lan_server_advertised_ip = cidrhost(var.advertised_prefix_cidr, 100)
}
lab_vyos_advertise_local_b64 = base64encode(templatefile("${path.module}/user_data/lab-vyos-advertise-local.vbash.tpl", local.vyos_cloud_init_vars))
# Post-boot script holds GRE/BGP/statics so EC2 user-data stays under ~16KB (truncation was dropping routes/BGP).
vyos_postconfig_bootup = templatefile("${path.module}/user_data/vyos-postconfig-bootup.sh.tpl", local.vyos_cloud_init_vars)
vyos_user_data = templatefile("${path.module}/user_data/vyos-cloud-init.yaml.tpl", merge(local.vyos_cloud_init_vars, {
postconfig_bootup_b64 = base64encode(local.vyos_postconfig_bootup)
lab_vyos_advertise_local_b64 = local.lab_vyos_advertise_local_b64
}))
# Recovery template omits SSH material so terraform output is not marked sensitive.
vyos_recovery_template_vars = {
for k, v in local.vyos_cloud_init_vars : k => v
if !contains(["ssh_key_type", "ssh_key_data"], k)
}
}
# -----------------------------------------------------------------------------
# VPC
# -----------------------------------------------------------------------------
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.name_prefix}-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.name_prefix}-igw"
}
}
resource "aws_subnet" "wan" {
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_wan_cidr
availability_zone = local.az
map_public_ip_on_launch = true
tags = {
Name = "${var.name_prefix}-subnet-wan"
}
}
resource "aws_subnet" "mgmt" {
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_mgmt_cidr
availability_zone = local.az
tags = {
Name = "${var.name_prefix}-subnet-mgmt"
}
}
resource "aws_subnet" "internal" {
vpc_id = aws_vpc.main.id
cidr_block = var.subnet_internal_cidr
availability_zone = local.az
tags = {
Name = "${var.name_prefix}-subnet-internal"
}
# Internal subnet deletion waits for Lambda-managed ENIs to disappear.
timeouts {
delete = "45m"
}
}
resource "aws_eip" "nat" {
domain = "vpc"
tags = {
Name = "${var.name_prefix}-nat-eip"
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_nat_gateway" "main" {
allocation_id = aws_eip.nat.id
subnet_id = aws_subnet.wan.id
tags = {
Name = "${var.name_prefix}-nat"
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.name_prefix}-rt-public"
}
}
resource "aws_route_table_association" "wan" {
subnet_id = aws_subnet.wan.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
network_interface_id = aws_network_interface.router_internal.id
}
lifecycle {
# Divert/revert intentionally move this default route between VyOS and IGW.
ignore_changes = [route]
}
tags = {
Name = "${var.name_prefix}-rt-private"
}
}
# Mgmt subnet must use IGW (public RT), not NAT: router has EIP on mgmt ENI for SSH.
# If default route were NAT, SYN-ACK for inbound SSH would be SNAT'd to NAT EIP → TCP hang.
resource "aws_route_table_association" "mgmt" {
subnet_id = aws_subnet.mgmt.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "internal" {
subnet_id = aws_subnet.internal.id
route_table_id = aws_route_table.private.id
}
# -----------------------------------------------------------------------------
# Security groups
# -----------------------------------------------------------------------------
resource "aws_security_group" "router_wan" {
name_prefix = "${var.name_prefix}-router-wan-"
description = "WAN ENI: ICMP, egress (GRE is ${var.name_prefix}-router-wan-gre when split)"
vpc_id = aws_vpc.main.id
revoke_rules_on_delete = true
ingress {
description = "ICMP from Imperva (PMTUD)"
from_port = -1
to_port = -1
protocol = "icmp"
cidr_blocks = ["${var.imperva_gre_public_ip}/32"]
}
dynamic "ingress" {
for_each = local.imperva_pm_server_cidrs
content {
description = "ICMP from Imperva PM server"
from_port = -1
to_port = -1
protocol = "icmp"
cidr_blocks = [ingress.value]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name_prefix}-sg-router-wan"
}
lifecycle {
create_before_destroy = true
}
timeouts {
delete = "10m"
}
}
# GRE-only SG: attach/detach from WAN ENI via Lambda to break/restore tunnel without VyOS changes.
resource "aws_security_group" "router_wan_gre" {
name_prefix = "${var.name_prefix}-router-wan-gre-"
description = "WAN ENI: GRE (protocol 47) only from Imperva POP; toggle via lab GRE Lambda"
vpc_id = aws_vpc.main.id
revoke_rules_on_delete = true
ingress {
description = "GRE (IP protocol 47) from Imperva POP"
from_port = 0
to_port = 0
protocol = "47"
cidr_blocks = ["${var.imperva_gre_public_ip}/32"]
}
egress {
description = "Required by VPC SG (unused for GRE-only path)"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name_prefix}-sg-router-wan-gre"
}
lifecycle {
create_before_destroy = true
}
timeouts {
delete = "10m"
}
}
resource "aws_security_group" "router_mgmt" {
name_prefix = "${var.name_prefix}-router-mgmt-"
description = "Mgmt ENI: SSH from admin"
vpc_id = aws_vpc.main.id
revoke_rules_on_delete = true
dynamic "ingress" {
for_each = local.admin_cidrs_effective
content {
description = "SSH from admin"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [ingress.value]
}
}
dynamic "ingress" {
for_each = var.create_ssh_bastion ? [1] : []
content {
description = "SSH from bastion instance (jump host to VyOS private mgmt)"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.ssh_bastion[0].id]
}
}
dynamic "ingress" {
for_each = local.legacy_byoip_detector_enabled ? [1] : []
content {
description = "SSH from BYOIP detector Lambda (VPC)"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.byoip_detector_lambda[0].id]
}
}
dynamic "ingress" {
for_each = var.lab_gre_byoip_flow_enabled ? [1] : []
content {
description = "SSH from lab traffic path controller Lambda (VPC)"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.lab_byoip_flow_lambda[0].id]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name_prefix}-sg-router-mgmt"
}
lifecycle {
create_before_destroy = true
}
timeouts {
delete = "10m"
}
}
resource "aws_security_group" "router_internal" {
name_prefix = "${var.name_prefix}-router-internal-"
description = "Internal ENI: lab traffic"
vpc_id = aws_vpc.main.id
revoke_rules_on_delete = true
ingress {
description = "VPC internal to lab router"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = [var.vpc_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name_prefix}-sg-router-internal"
}
lifecycle {
create_before_destroy = true
}
timeouts {
delete = "10m"
}
}
data "aws_iam_policy_document" "ssh_bastion_ec2_assume" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
resource "aws_iam_role" "ssh_bastion" {
count = var.create_ssh_bastion ? 1 : 0
name_prefix = "${var.name_prefix}-bastion-"
assume_role_policy = data.aws_iam_policy_document.ssh_bastion_ec2_assume.json
tags = {
Name = "${var.name_prefix}-bastion-ssm-role"
}
}
resource "aws_iam_role_policy_attachment" "ssh_bastion_ssm" {
count = var.create_ssh_bastion ? 1 : 0
role = aws_iam_role.ssh_bastion[0].name
policy_arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}
resource "aws_iam_instance_profile" "ssh_bastion" {
count = var.create_ssh_bastion ? 1 : 0
name_prefix = "${var.name_prefix}-bastion-"
role = aws_iam_role.ssh_bastion[0].name
tags = {
Name = "${var.name_prefix}-bastion-instance-profile"
}
}
# Jump host: SSH to VyOS private mgmt IP (bypasses broken return path for mgmt EIP).
resource "aws_security_group" "ssh_bastion" {
count = var.create_ssh_bastion ? 1 : 0
name_prefix = "${var.name_prefix}-ssh-bastion-"
description = "SSH from admin_cidrs only; SSM works without inbound 22"
vpc_id = aws_vpc.main.id
revoke_rules_on_delete = true
dynamic "ingress" {
for_each = local.admin_cidrs_effective
content {
description = "SSH to jump host"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [ingress.value]
}
}
dynamic "ingress" {
for_each = var.netflow_exporter_enabled && var.netflow_reuse_bastion ? local.netflow_protected_subnet_cidrs : []
content {
description = "AWS Traffic Mirror VXLAN from protected subnet"
from_port = 4789
to_port = 4789
protocol = "udp"
cidr_blocks = [ingress.value]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name_prefix}-sg-ssh-bastion"
}
lifecycle {
create_before_destroy = true
}
timeouts {
delete = "10m"
}
}
resource "aws_instance" "ssh_bastion" {
count = var.create_ssh_bastion ? 1 : 0
ami = data.aws_ssm_parameter.lan_al2023_ami.value
instance_type = var.ssh_bastion_instance_type
subnet_id = aws_subnet.wan.id
vpc_security_group_ids = [aws_security_group.ssh_bastion[0].id]
key_name = local.vmx_key_name
associate_public_ip_address = true
iam_instance_profile = aws_iam_instance_profile.ssh_bastion[0].name
user_data = local.netflow_bastion_user_data
user_data_replace_on_change = true
root_block_device {
encrypted = true
}
metadata_options {
http_tokens = "required"
}
tags = {
Name = "${var.name_prefix}-ssh-bastion"
Role = "ssh-bastion"
}
depends_on = [aws_iam_role_policy_attachment.ssh_bastion_ssm]
}
resource "aws_network_interface" "netflow_exporter" {
count = var.netflow_exporter_enabled && var.netflow_reuse_bastion ? 1 : 0
subnet_id = aws_subnet.wan.id
security_groups = [aws_security_group.ssh_bastion[0].id]
tags = {
Name = "${var.name_prefix}-eni-netflow-exporter"
}
}
resource "aws_network_interface_attachment" "netflow_exporter" {
count = var.netflow_exporter_enabled && var.netflow_reuse_bastion ? 1 : 0
instance_id = aws_instance.ssh_bastion[0].id
network_interface_id = aws_network_interface.netflow_exporter[0].id
device_index = 1
}
resource "aws_eip" "netflow_exporter" {
count = var.netflow_exporter_enabled && var.netflow_reuse_bastion ? 1 : 0
domain = "vpc"
tags = {
Name = "${var.name_prefix}-netflow-exporter-eip"
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_eip_association" "netflow_exporter" {
count = var.netflow_exporter_enabled && var.netflow_reuse_bastion ? 1 : 0
allocation_id = aws_eip.netflow_exporter[0].id
network_interface_id = aws_network_interface.netflow_exporter[0].id
}
# -----------------------------------------------------------------------------
# Elastic IPs (WAN tunnel source + optional mgmt SSH)
# -----------------------------------------------------------------------------
removed {
from = aws_eip.router_wan
lifecycle {
destroy = false
}
}
data "aws_eip" "router_wan_external" {
count = local.router_wan_uses_external_eip ? 1 : 0
id = local.router_wan_external_eip
}
resource "aws_eip" "router_wan_managed" {
count = local.router_wan_uses_external_eip ? 0 : 1
domain = "vpc"
tags = {
Name = "${var.name_prefix}-router-wan-eip"
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_eip" "router_mgmt" {
domain = "vpc"
tags = {
Name = "${var.name_prefix}-router-mgmt-eip"
}
depends_on = [aws_internet_gateway.main]
}
# -----------------------------------------------------------------------------
# Router ENIs (device order: 0 mgmt fxp0, 1 WAN ge-0/0/0, 2 internal ge-0/0/1)
# -----------------------------------------------------------------------------
resource "aws_network_interface" "router_mgmt" {
subnet_id = aws_subnet.mgmt.id
private_ips = [var.router_mgmt_private_ip]
security_groups = [aws_security_group.router_mgmt.id]
source_dest_check = false
tags = {
Name = "${var.name_prefix}-eni-router-mgmt"
}
}
resource "aws_network_interface" "router_wan" {
subnet_id = aws_subnet.wan.id
private_ips = [var.router_wan_private_ip]
security_groups = [aws_security_group.router_wan.id, aws_security_group.router_wan_gre.id]
source_dest_check = false
tags = {
Name = "${var.name_prefix}-eni-router-wan"
}
# Lab GRE Lambda detaches the GRE-only SG for simulation; Terraform must not fight that attachment set.
lifecycle {
ignore_changes = [security_groups]
}
}
resource "aws_network_interface" "router_internal" {
subnet_id = aws_subnet.internal.id
private_ips = [var.router_internal_private_ip]
security_groups = [aws_security_group.router_internal.id]
source_dest_check = false
tags = {
Name = "${var.name_prefix}-eni-router-internal"
}
}
resource "aws_eip_association" "router_wan" {
allocation_id = local.router_wan_eip_allocation_id
network_interface_id = aws_network_interface.router_wan.id
}
resource "aws_eip_association" "router_mgmt" {
allocation_id = aws_eip.router_mgmt.id
network_interface_id = aws_network_interface.router_mgmt.id
}
resource "aws_instance" "vmx" {
ami = var.router_ami_id
instance_type = var.router_instance_type
key_name = local.vmx_key_name
disable_api_termination = false
network_interface {
device_index = 0
network_interface_id = aws_network_interface.router_mgmt.id
}
network_interface {
device_index = 1
network_interface_id = aws_network_interface.router_wan.id
}
network_interface {
device_index = 2
network_interface_id = aws_network_interface.router_internal.id
}
user_data = local.vyos_user_data
user_data_replace_on_change = false
root_block_device {
encrypted = true
}
lifecycle {
# Keep day-0 user_data in Terraform for new builds/recovery outputs, but do
# not recycle the live router when the bootstrap templates evolve.
ignore_changes = [user_data]
precondition {
condition = length(local.vyos_user_data) <= 15360
error_message = "VyOS user_data exceeds ~15KB; EC2 limit is 16KB and truncation breaks cloud-init (partial config, no default route/BGP). Shorten SSH key or template."
}
}
tags = {
Name = "${var.name_prefix}-vyos"
Role = "vyos-router"
}
depends_on = [
aws_eip_association.router_wan,
aws_eip_association.router_mgmt
]
}
# -----------------------------------------------------------------------------
# LAN server (internal subnet) + BYOIP .100 from advertised prefix
# -----------------------------------------------------------------------------
resource "aws_security_group" "lan_server" {
count = var.create_lan_server ? 1 : 0
name_prefix = "${var.name_prefix}-lan-server-"
description = "LAN victim: TCP 22/80/443 and ICMP echo only from admin_cidrs"
vpc_id = aws_vpc.main.id
revoke_rules_on_delete = true
ingress {
description = "HTTP from internet through GRE/BYOIP"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS from internet through GRE/BYOIP"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
dynamic "ingress" {
for_each = local.lan_server_admin_tcp_ingress
content {
description = "TCP ${ingress.value.port} from admin_cidrs"
from_port = ingress.value.port
to_port = ingress.value.port
protocol = "tcp"
cidr_blocks = [ingress.value.cidr]
}
}
dynamic "ingress" {
for_each = local.admin_cidrs_effective
content {
description = "ICMP echo from admin_cidrs"
from_port = 8
to_port = 0
protocol = "icmp"
cidr_blocks = [ingress.value]
}
}
# Bastion connects to LAN private IP with source = bastion private IP (not your public admin /32).
dynamic "ingress" {
for_each = var.create_lan_server && var.create_ssh_bastion ? [22, 80, 443] : []
content {
description = "TCP ${ingress.value} from bastion SG (private IP path)"
from_port = ingress.value
to_port = ingress.value
protocol = "tcp"
security_groups = [aws_security_group.ssh_bastion[0].id]
}
}
dynamic "ingress" {
for_each = var.create_lan_server && var.create_ssh_bastion ? [1] : []
content {
description = "ICMP echo from bastion SG"
from_port = 8
to_port = 0
protocol = "icmp"
security_groups = [aws_security_group.ssh_bastion[0].id]
}
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.name_prefix}-sg-lan-server"
}
lifecycle {
create_before_destroy = true
}
timeouts {
delete = "10m"
}
}
resource "aws_instance" "lan_server" {
count = var.create_lan_server ? 1 : 0
ami = var.lan_server_ami_id != "" ? var.lan_server_ami_id : data.aws_ssm_parameter.lan_al2023_ami.value
instance_type = var.lan_server_instance_type
subnet_id = aws_subnet.internal.id
vpc_security_group_ids = [aws_security_group.lan_server[0].id]
key_name = local.vmx_key_name
private_ip = var.lan_server_private_ip
root_block_device {
encrypted = true
}
metadata_options {
http_tokens = "required"
}
user_data = "${file("${path.module}/user_data/lan-server-bootstrap.sh")}\n# replace-trigger=${var.lan_server_replace_trigger}\n"
user_data_replace_on_change = true
tags = {
Name = "${var.name_prefix}-lan-server"
Role = "lan-by100"
}
lifecycle {
ignore_changes = [ami]
}
}
data "aws_eip" "lan_byoip" {
count = var.create_lan_server && trimspace(var.lan_server_byoip_allocation_id) != "" ? 1 : 0
id = trimspace(var.lan_server_byoip_allocation_id)
}