Skip to content

Commit a8e0c63

Browse files
authored
Merge branch 'dev' into optional-reconstructed-ephemeris
2 parents 1b91b1c + 83478cb commit a8e0c63

4 files changed

Lines changed: 346 additions & 0 deletions

File tree

sds_data_manager/constructs/ialirt_processing_construct.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,10 @@ def create_ecs_security_group(self):
107107
"params": ["kiel"],
108108
"ports": [7564],
109109
},
110+
"noaa": {
111+
"params": ["noaa"],
112+
"ports": [7565],
113+
},
110114
"uksa": {
111115
"params": ["uksa"],
112116
"ports": [7566, 7567],
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
"""Configure the I-ALiRT VPN connections to NOAA N-Wave."""
2+
3+
from aws_cdk import aws_ec2 as ec2
4+
from constructs import Construct
5+
6+
7+
class IalirtVpnConstruct(Construct):
8+
"""NOAA N-Wave customer gateways and VPN connections for I-ALiRT."""
9+
10+
def __init__(
11+
self,
12+
scope: Construct,
13+
construct_id: str,
14+
transit_gateway_id: str,
15+
psk: str,
16+
wash_ip: str,
17+
denv_ip: str,
18+
**kwargs,
19+
) -> None:
20+
"""Create NOAA N-Wave customer gateways and VPN connections.
21+
22+
Parameters
23+
----------
24+
scope : Construct
25+
Parent construct.
26+
construct_id : str
27+
A unique string identifier for this construct.
28+
transit_gateway_id : str
29+
The Transit Gateway to attach the VPN connections to. A Transit
30+
Gateway is used (rather than a Virtual Private Gateway) because a
31+
VGW cannot route decrypted VPN traffic to a NAT Gateway, and a NAT
32+
Gateway is required so NOAA's traffic reaches I-ALiRT via its
33+
stable Elastic IP instead of an EC2 private IP that changes
34+
whenever the Auto Scaling Group replaces the instance.
35+
psk : str
36+
Pre-shared key for IKE authentication. Pass a CDK token from
37+
``secret_value_from_json(...).unsafe_unwrap()`` so the value is
38+
resolved by CloudFormation at deploy time and never appears in
39+
the template.
40+
wash_ip : str
41+
NOAA border router public IP at McLean, VA (WASH), retrieved from SSM.
42+
denv_ip : str
43+
NOAA border router public IP at Denver, CO (DENV), retrieved from SSM.
44+
kwargs : dict
45+
Keyword arguments.
46+
"""
47+
super().__init__(scope, construct_id, **kwargs)
48+
49+
# Define the crypto settings for the IPSec tunnel, as specified
50+
# in the N-Wave ICD (NOAA0550).
51+
#
52+
# Phase 1 (IKE) — the handshake phase where both sides authenticate each other
53+
# and agree on encryption keys. Uses pre-shared key (PSK)
54+
# resolved at deploy time.
55+
# - IKEv2 only (NOAA requirement)
56+
# - AES-256 encryption
57+
# - SHA2-256 integrity
58+
# - DH group 14 for key exchange
59+
# - 28800s (8 hour) lifetime
60+
#
61+
# Phase 2 (ESP) — the data phase where actual traffic is encrypted.
62+
# - AES-128 or AES-256 encryption
63+
# - HMAC-SHA2-256-128 integrity
64+
# - DH group 14 (PFS — Perfect Forward Secrecy)
65+
# - 3600s (1 hour) lifetime
66+
tunnel = ec2.CfnVPNConnection.VpnTunnelOptionsSpecificationProperty(
67+
pre_shared_key=psk,
68+
ike_versions=[
69+
ec2.CfnVPNConnection.IKEVersionsRequestListValueProperty(value="ikev2")
70+
],
71+
phase1_encryption_algorithms=[
72+
ec2.CfnVPNConnection.Phase1EncryptionAlgorithmsRequestListValueProperty(
73+
value="AES256"
74+
)
75+
],
76+
phase1_integrity_algorithms=[
77+
ec2.CfnVPNConnection.Phase1IntegrityAlgorithmsRequestListValueProperty(
78+
value="SHA2-256"
79+
)
80+
],
81+
phase1_dh_group_numbers=[
82+
ec2.CfnVPNConnection.Phase1DHGroupNumbersRequestListValueProperty(
83+
value=14
84+
)
85+
],
86+
phase1_lifetime_seconds=28800,
87+
phase2_encryption_algorithms=[
88+
ec2.CfnVPNConnection.Phase2EncryptionAlgorithmsRequestListValueProperty(
89+
value="AES128"
90+
),
91+
ec2.CfnVPNConnection.Phase2EncryptionAlgorithmsRequestListValueProperty(
92+
value="AES256"
93+
),
94+
],
95+
phase2_integrity_algorithms=[
96+
ec2.CfnVPNConnection.Phase2IntegrityAlgorithmsRequestListValueProperty(
97+
value="SHA2-256"
98+
)
99+
],
100+
phase2_dh_group_numbers=[
101+
ec2.CfnVPNConnection.Phase2DHGroupNumbersRequestListValueProperty(
102+
value=14
103+
)
104+
],
105+
phase2_lifetime_seconds=3600,
106+
)
107+
108+
# Customer Gateway - AWS's record of NOAA's router so that AWS can recognize
109+
# and accept the incoming encrypted packets.
110+
111+
# Every AWS Site-to-Site VPN connection automatically provisions
112+
# two auto-assigned tunnel IPs.
113+
# These LASP IKE Gateways must be given to NOAA.
114+
self.vpn_connections: dict[str, ec2.CfnVPNConnection] = {}
115+
for site, ip in {"WASH": wash_ip, "DENV": denv_ip}.items():
116+
# AWS needs to know the router's public IP and ASN to establish the tunnel.
117+
# bgp_asn=64583 is NOAA's ASN per the ICD. This must match the ASN
118+
# configured on NOAA's actual router — AWS silently rejects the BGP
119+
# session (not the tunnel itself) if the peer AS doesn't match what's
120+
# registered here.
121+
cgw = ec2.CfnCustomerGateway(
122+
self,
123+
f"NoaaCustomerGateway{site}",
124+
bgp_asn=64583,
125+
ip_address=ip,
126+
type="ipsec.1",
127+
)
128+
129+
# Create the VPN connection between our Transit Gateway (TGW) and
130+
# NOAA's customer gateway. Each connection gets two tunnels by
131+
# default (AWS requirement for redundancy) — both use the same
132+
# crypto settings. BGP is used (static_routes_only=False) so that
133+
# if one site (WASH or DENV) goes down, BGP automatically reroutes
134+
# traffic through the other. Data flows one way: NOAA sends to us.
135+
# We do not send to NOAA.
136+
self.vpn_connections[site] = ec2.CfnVPNConnection(
137+
self,
138+
f"NoaaVpnConnection{site}",
139+
customer_gateway_id=cgw.ref,
140+
transit_gateway_id=transit_gateway_id,
141+
type="ipsec.1",
142+
static_routes_only=False,
143+
vpn_tunnel_options_specifications=[tunnel, tunnel],
144+
)
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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)

sds_data_manager/utils/stackbuilder.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
spice_monitoring_construct,
4040
website_hosting,
4141
)
42+
from sds_data_manager.utils import ialirt_noaa_vpn
4243

4344

4445
def build_sds(
@@ -434,6 +435,8 @@ def build_sds(
434435
account_name=account_name,
435436
)
436437

438+
ialirt_noaa_vpn.build_noaa_vpn_tgw(ialirt_stack, networking)
439+
437440
reprocessing_tools_construct = instrument_lambdas.ReprocessingTools(
438441
scope=sdc_stack,
439442
construct_id="ReprocessingTools",

0 commit comments

Comments
 (0)