From cf5efb06ec7b255240acfa27dab1ba25ceb9694c Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 28 Sep 2026 16:51:29 -0400 Subject: [PATCH] fix(infra): keep the file share off the public internet (PLAT-77) The instance has no public IP. Office routes use the syslog VPN gateway and other egress uses a NAT gateway. DLM targets the tagged data volume, and replacement detaches stop the instance first. --- README.md | 4 +++- terraform/dlm.tf | 2 +- terraform/ec2.tf | 16 ++++++++++++---- terraform/hcp_iam.tf | 16 ++++++++++++++++ terraform/network.tf | 45 +++++++++++++++++++++++++++++++++++++++++--- terraform/outputs.tf | 2 +- 6 files changed, 75 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a56f5b7..c57d4db 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,9 @@ Prod changes go through HCP Terraform workspace `file-share-prod` (manual apply The management-account CDK workflow no longer runs on push. `workflow_dispatch` remains for an explicit rollback of that stack. -Clients use the private IP. Office routing must include `10.40.20.0/24` on the existing syslog IPsec before SMB from the office will work. A check from `10.10.70.0/24` on 2026-09-28 reached the gateway for `10.40.10.254` and got no hop-1 reply for `10.40.20.1`. +The instance has no public IP. Its route table sends `10.10.0.0/16` and `10.30.0.0/16` through the syslog VPN gateway and everything else through a NAT gateway in the syslog public subnet. `10.10.0.0/16` is the Ronkonkoma office LAN and `10.30.0.0/16` is the Locust office LAN, the same pair the syslog VPN already routes. `10.20.0.0/16` is the management VPC and is not routed here. + +Clients use the private IP. Office routing must include `10.40.20.0/24` on the existing syslog IPsec before SMB from the office will work. A check from `10.10.70.0/24` on 2026-09-28 reached the gateway for `10.40.10.254` and got no hop-1 reply for `10.40.20.1`. The nightly DLM policy targets volumes tagged `file-share-backup=true`. Tag the copied volume with that key at cutover. ## Expanding Storage diff --git a/terraform/dlm.tf b/terraform/dlm.tf index f497c59..8161899 100644 --- a/terraform/dlm.tf +++ b/terraform/dlm.tf @@ -4,7 +4,7 @@ resource "aws_dlm_lifecycle_policy" "nightly" { state = "ENABLED" policy_details { - resource_types = ["INSTANCE"] + resource_types = ["VOLUME"] target_tags = { "file-share-backup" = "true" diff --git a/terraform/ec2.tf b/terraform/ec2.tf index d251b33..3988306 100644 --- a/terraform/ec2.tf +++ b/terraform/ec2.tf @@ -6,7 +6,7 @@ resource "aws_instance" "this" { subnet_id = aws_subnet.file_share.id vpc_security_group_ids = [aws_security_group.file_share.id] iam_instance_profile = aws_iam_instance_profile.this.name - associate_public_ip_address = true + associate_public_ip_address = false user_data = local.user_data user_data_replace_on_change = false @@ -25,12 +25,20 @@ resource "aws_instance" "this" { Name = "file-share" "file-share-backup" = "true" } + + lifecycle { + postcondition { + condition = self.public_ip == null || self.public_ip == "" + error_message = "file-share must not have a public IP." + } + } } resource "aws_volume_attachment" "data" { count = local.create_instance ? 1 : 0 - device_name = "/dev/xvdf" - volume_id = var.data_volume_id - instance_id = aws_instance.this[0].id + device_name = "/dev/xvdf" + volume_id = var.data_volume_id + instance_id = aws_instance.this[0].id + stop_instance_before_detaching = true } diff --git a/terraform/hcp_iam.tf b/terraform/hcp_iam.tf index 964a59a..b59f6b4 100644 --- a/terraform/hcp_iam.tf +++ b/terraform/hcp_iam.tf @@ -295,16 +295,27 @@ data "aws_iam_policy_document" "hcptf_apply_services" { sid = "Ec2Network" effect = "Allow" actions = [ + "ec2:AllocateAddress", + "ec2:AssociateAddress", "ec2:AssociateRouteTable", + "ec2:CreateNatGateway", + "ec2:CreateRoute", + "ec2:CreateRouteTable", "ec2:AuthorizeSecurityGroupEgress", "ec2:AuthorizeSecurityGroupIngress", "ec2:CreateSecurityGroup", "ec2:CreateSubnet", "ec2:CreateTags", + "ec2:DeleteNatGateway", + "ec2:DeleteRoute", + "ec2:DeleteRouteTable", "ec2:DeleteSecurityGroup", "ec2:DeleteSubnet", "ec2:DeleteTags", "ec2:DescribeAccountAttributes", + "ec2:DescribeAddresses", + "ec2:DescribeAddressesAttribute", + "ec2:DescribeNatGateways", "ec2:DescribeAvailabilityZones", "ec2:DescribeImages", "ec2:DescribeInstanceAttribute", @@ -326,7 +337,9 @@ data "aws_iam_policy_document" "hcptf_apply_services" { "ec2:DescribeVpcAttribute", "ec2:DescribeVpcs", "ec2:DescribeVpnGateways", + "ec2:DisassociateAddress", "ec2:DisassociateRouteTable", + "ec2:ReleaseAddress", "ec2:ModifySecurityGroupRules", "ec2:ModifySubnetAttribute", "ec2:RevokeSecurityGroupEgress", @@ -415,6 +428,8 @@ data "aws_iam_policy_document" "hcptf_plan_refresh" { effect = "Allow" actions = [ "ec2:DescribeAccountAttributes", + "ec2:DescribeAddresses", + "ec2:DescribeAddressesAttribute", "ec2:DescribeAvailabilityZones", "ec2:DescribeIamInstanceProfileAssociations", "ec2:DescribeImages", @@ -424,6 +439,7 @@ data "aws_iam_policy_document" "hcptf_plan_refresh" { "ec2:DescribeInstanceTypes", "ec2:DescribeInstances", "ec2:DescribeInternetGateways", + "ec2:DescribeNatGateways", "ec2:DescribeNetworkInterfaces", "ec2:DescribePrefixLists", "ec2:DescribeRouteTables", diff --git a/terraform/network.tf b/terraform/network.tf index 8331770..ee96b0e 100644 --- a/terraform/network.tf +++ b/terraform/network.tf @@ -21,7 +21,7 @@ data "aws_vpn_gateway" "syslog" { attached_vpc_id = data.aws_vpc.syslog.id } -data "aws_route_table" "syslog_public" { +data "aws_subnet" "syslog_public" { vpc_id = data.aws_vpc.syslog.id filter { @@ -30,11 +30,50 @@ data "aws_route_table" "syslog_public" { } } +resource "aws_eip" "nat" { + domain = "vpc" + + tags = { + Name = "file-share-nat" + } +} + +resource "aws_nat_gateway" "file_share" { + allocation_id = aws_eip.nat.id + subnet_id = data.aws_subnet.syslog_public.id + + tags = { + Name = "file-share" + } +} + +resource "aws_route_table" "file_share" { + vpc_id = data.aws_vpc.syslog.id + + tags = { + Name = "file-share" + } +} + +resource "aws_route" "office" { + for_each = toset(local.office_lan_cidrs) + + route_table_id = aws_route_table.file_share.id + destination_cidr_block = each.value + gateway_id = data.aws_vpn_gateway.syslog.id +} + +resource "aws_route" "nat" { + route_table_id = aws_route_table.file_share.id + destination_cidr_block = "0.0.0.0/0" + nat_gateway_id = aws_nat_gateway.file_share.id +} + resource "aws_subnet" "file_share" { vpc_id = data.aws_vpc.syslog.id cidr_block = local.subnet_cidr availability_zone = local.subnet_az - map_public_ip_on_launch = true + map_public_ip_on_launch = false tags = { Name = "file-share" @@ -55,5 +94,5 @@ resource "aws_subnet" "file_share" { resource "aws_route_table_association" "file_share" { subnet_id = aws_subnet.file_share.id - route_table_id = data.aws_route_table.syslog_public.id + route_table_id = aws_route_table.file_share.id } diff --git a/terraform/outputs.tf b/terraform/outputs.tf index bc1eaad..e7e3c62 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -4,7 +4,7 @@ output "private_ip" { } output "public_ip" { - description = "Egress address for package install and SSM. Not a client endpoint." + description = "Always empty. Egress uses the NAT gateway. Clients use private_ip." value = one(aws_instance.this[*].public_ip) }