|
| 1 | +"""Helpers for building I-ALiRT's NOAA Site-to-Site VPN path.""" |
| 2 | + |
| 3 | +from aws_cdk import Stack |
| 4 | +from aws_cdk import aws_ec2 as ec2 |
| 5 | +from aws_cdk import aws_secretsmanager as secretsmanager |
| 6 | +from aws_cdk import aws_ssm as ssm |
| 7 | +from aws_cdk import custom_resources as cr |
| 8 | + |
| 9 | +from sds_data_manager.constructs import ialirt_vpn_construct, networking_construct |
| 10 | + |
| 11 | + |
| 12 | +def build_noaa_vpn_tgw( |
| 13 | + ialirt_stack: Stack, |
| 14 | + networking: networking_construct.NetworkingConstruct, |
| 15 | +) -> None: |
| 16 | + """Build the NOAA VPN Transit Gateway path for I-ALiRT. |
| 17 | +
|
| 18 | + Terminates NOAA's Site-to-Site VPN on a Transit Gateway attached to |
| 19 | + I-ALiRT's VPC, so decrypted traffic reaches I-ALiRT via the existing |
| 20 | + NAT Gateway / Elastic IP path. |
| 21 | +
|
| 22 | + Parameters |
| 23 | + ---------- |
| 24 | + ialirt_stack : Stack |
| 25 | + The I-ALiRT stack to parent these resources to. |
| 26 | + networking : networking_construct.NetworkingConstruct |
| 27 | + Networking construct providing the VPC to attach to the TGW. |
| 28 | +
|
| 29 | + """ |
| 30 | + # Retrieve the NOAA VPN pre-shared key from Secrets Manager. |
| 31 | + # Store the PSK under the key "psk" in a secret named |
| 32 | + # "/ialirt/noaa/noaa-vpn-psk" before deploying this stack. |
| 33 | + noaa_vpn_psk = ( |
| 34 | + secretsmanager.Secret.from_secret_name_v2( |
| 35 | + ialirt_stack, "NoaaVpnPsk", "/ialirt/noaa/noaa-vpn-psk" |
| 36 | + ) |
| 37 | + .secret_value_from_json("psk") |
| 38 | + .unsafe_unwrap() |
| 39 | + ) |
| 40 | + |
| 41 | + # Retrieve NOAA's border router IPs from SSM Parameter Store. |
| 42 | + # Store these before deploying: |
| 43 | + # aws ssm put-parameter --name "/ialirt/noaa-vpn/wash-ip" |
| 44 | + # --value "<ip>" --type String |
| 45 | + # aws ssm put-parameter --name "/ialirt/noaa-vpn/denv-ip" |
| 46 | + # --value "<ip>" --type String |
| 47 | + noaa_wash_ip = ssm.StringParameter.value_for_string_parameter( |
| 48 | + ialirt_stack, "/ialirt/noaa-vpn/wash-ip" |
| 49 | + ) |
| 50 | + noaa_denv_ip = ssm.StringParameter.value_for_string_parameter( |
| 51 | + ialirt_stack, "/ialirt/noaa-vpn/denv-ip" |
| 52 | + ) |
| 53 | + |
| 54 | + # Create a Transit Gateway (TGW) to terminate the IPSec tunnel from NOAA. |
| 55 | + ialirt_transit_gateway = ec2.CfnTransitGateway( |
| 56 | + ialirt_stack, |
| 57 | + "IalirtTransitGateway", |
| 58 | + # ASN is BGP's identifier for a distinct network/routing domain — |
| 59 | + # how two BGP peers identify themselves and each other. |
| 60 | + # Default ASN, but stated explicitly here. |
| 61 | + amazon_side_asn=64512, |
| 62 | + # Use our own route table below instead of TGW's hidden default one. |
| 63 | + default_route_table_association="disable", |
| 64 | + default_route_table_propagation="disable", |
| 65 | + description="I-ALiRT TGW for NOAA VPN", |
| 66 | + ) |
| 67 | + |
| 68 | + # Attach the VPC to the TGW via the private subnets, whose route tables |
| 69 | + # already have 0.0.0.0/0 -> NAT Gateway — so traffic arriving via the TGW |
| 70 | + # attachment gets forwarded there automatically. The NAT Gateway then |
| 71 | + # sends it out to the internet, where it reaches I-ALiRT's own Elastic |
| 72 | + # IP the same way every other partner's traffic already does. |
| 73 | + ialirt_vpc_attachment = ec2.CfnTransitGatewayVpcAttachment( |
| 74 | + ialirt_stack, |
| 75 | + "IalirtTransitGatewayVpcAttachment", |
| 76 | + transit_gateway_id=ialirt_transit_gateway.attr_id, |
| 77 | + vpc_id=networking.vpc.vpc_id, |
| 78 | + subnet_ids=[subnet.subnet_id for subnet in networking.vpc.private_subnets], |
| 79 | + ) |
| 80 | + |
| 81 | + ialirt_vpn = ialirt_vpn_construct.IalirtVpnConstruct( |
| 82 | + scope=ialirt_stack, |
| 83 | + construct_id="IalirtVpn", |
| 84 | + transit_gateway_id=ialirt_transit_gateway.attr_id, |
| 85 | + psk=noaa_vpn_psk, |
| 86 | + wash_ip=noaa_wash_ip, |
| 87 | + denv_ip=noaa_denv_ip, |
| 88 | + ) |
| 89 | + |
| 90 | + # Create a TGW route table. |
| 91 | + ialirt_tgw_route_table = ec2.CfnTransitGatewayRouteTable( |
| 92 | + ialirt_stack, |
| 93 | + "IalirtTransitGatewayRouteTable", |
| 94 | + transit_gateway_id=ialirt_transit_gateway.attr_id, |
| 95 | + ) |
| 96 | + tgw_route_table_id = ialirt_tgw_route_table.attr_transit_gateway_route_table_id |
| 97 | + |
| 98 | + # VPC's connection to the Transit Gateway will use our route table to |
| 99 | + # look up where to send traffic. |
| 100 | + ec2.CfnTransitGatewayRouteTableAssociation( |
| 101 | + ialirt_stack, |
| 102 | + "IalirtTgwRouteTableAssociationVpc", |
| 103 | + transit_gateway_attachment_id=ialirt_vpc_attachment.attr_id, |
| 104 | + transit_gateway_route_table_id=tgw_route_table_id, |
| 105 | + ) |
| 106 | + |
| 107 | + # Add the VPC's address range to our route table, so any other |
| 108 | + # attachment using this table (e.g. the NOAA VPN connections) can route |
| 109 | + # traffic destined for the VPC here. |
| 110 | + ec2.CfnTransitGatewayRouteTablePropagation( |
| 111 | + ialirt_stack, |
| 112 | + "IalirtTgwRouteTablePropagationVpc", |
| 113 | + transit_gateway_attachment_id=ialirt_vpc_attachment.attr_id, |
| 114 | + transit_gateway_route_table_id=tgw_route_table_id, |
| 115 | + ) |
| 116 | + |
| 117 | + # AWS::EC2::VPNConnection does not expose its Transit Gateway attachment |
| 118 | + # ID as a CloudFormation attribute, so look it up via a custom resource |
| 119 | + # that calls ec2:DescribeTransitGatewayAttachments, filtered to the VPN |
| 120 | + # connection's resource ID. |
| 121 | + for site, vpn_connection in ialirt_vpn.vpn_connections.items(): |
| 122 | + describe_vpn_attachment = cr.AwsSdkCall( |
| 123 | + service="EC2", |
| 124 | + action="describeTransitGatewayAttachments", |
| 125 | + parameters={ |
| 126 | + "Filters": [ |
| 127 | + { |
| 128 | + "Name": "resource-id", |
| 129 | + "Values": [vpn_connection.attr_vpn_connection_id], |
| 130 | + }, |
| 131 | + {"Name": "resource-type", "Values": ["vpn"]}, |
| 132 | + ] |
| 133 | + }, |
| 134 | + physical_resource_id=cr.PhysicalResourceId.of( |
| 135 | + f"IalirtVpnTgwAttachmentLookup{site}" |
| 136 | + ), |
| 137 | + ) |
| 138 | + vpn_attachment_lookup = cr.AwsCustomResource( |
| 139 | + ialirt_stack, |
| 140 | + f"IalirtVpnTgwAttachmentLookup{site}", |
| 141 | + on_create=describe_vpn_attachment, |
| 142 | + on_update=describe_vpn_attachment, |
| 143 | + policy=cr.AwsCustomResourcePolicy.from_sdk_calls( |
| 144 | + resources=cr.AwsCustomResourcePolicy.ANY_RESOURCE |
| 145 | + ), |
| 146 | + ) |
| 147 | + vpn_attachment_id = vpn_attachment_lookup.get_response_field( |
| 148 | + "TransitGatewayAttachments.0.TransitGatewayAttachmentId" |
| 149 | + ) |
| 150 | + |
| 151 | + # Add the transit gateway attachments to the route table. |
| 152 | + |
| 153 | + # Use this table to decide where to forward the traffic it receives. |
| 154 | + ec2.CfnTransitGatewayRouteTableAssociation( |
| 155 | + ialirt_stack, |
| 156 | + f"IalirtTgwRouteTableAssociationVpn{site}", |
| 157 | + transit_gateway_attachment_id=vpn_attachment_id, |
| 158 | + transit_gateway_route_table_id=tgw_route_table_id, |
| 159 | + ) |
| 160 | + # Installs NOAA's routes into the table so the VPC attachment can |
| 161 | + # find its way back to NOAA on the return path. |
| 162 | + ec2.CfnTransitGatewayRouteTablePropagation( |
| 163 | + ialirt_stack, |
| 164 | + f"IalirtTgwRouteTablePropagationVpn{site}", |
| 165 | + transit_gateway_attachment_id=vpn_attachment_id, |
| 166 | + transit_gateway_route_table_id=tgw_route_table_id, |
| 167 | + ) |
| 168 | + |
| 169 | + # Static route: send traffic for I-ALiRT EC2's Elastic IP into the VPC |
| 170 | + # attachment, where the NAT Gateway forwards it to the public internet. |
| 171 | + ec2.CfnTransitGatewayRoute( |
| 172 | + ialirt_stack, |
| 173 | + "IalirtTgwDefaultRouteToVpc", |
| 174 | + destination_cidr_block="0.0.0.0/0", |
| 175 | + transit_gateway_route_table_id=tgw_route_table_id, |
| 176 | + transit_gateway_attachment_id=ialirt_vpc_attachment.attr_id, |
| 177 | + ) |
| 178 | + |
| 179 | + # If it's a private address (10.x, 172.16-31.x, 192.168.x), |
| 180 | + # go back through the TGW instead of the public internet. |
| 181 | + def _add_return_routes(subnet_group: str, subnets: list) -> None: |
| 182 | + for cidr in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16"): |
| 183 | + cidr_suffix = cidr.split("/")[0].replace(".", "") |
| 184 | + for i, subnet in enumerate(subnets): |
| 185 | + route = ec2.CfnRoute( |
| 186 | + ialirt_stack, |
| 187 | + f"Ialirt{subnet_group}Subnet{i}ReturnRoute{cidr_suffix}", |
| 188 | + route_table_id=subnet.route_table.route_table_id, |
| 189 | + destination_cidr_block=cidr, |
| 190 | + transit_gateway_id=ialirt_transit_gateway.attr_id, |
| 191 | + ) |
| 192 | + route.add_dependency(ialirt_vpc_attachment) |
| 193 | + |
| 194 | + _add_return_routes("Private", networking.vpc.private_subnets) |
| 195 | + _add_return_routes("Public", networking.vpc.public_subnets) |
0 commit comments