Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions sds_data_manager/constructs/ialirt_processing_construct.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ def create_ecs_security_group(self):
"185.83.168.248/32": [7566, 7567], # UKSA
"41.74.156.19/32": [7568], # SANSA
"41.74.156.20/32": [7568], # SANSA
# TODO: replace TBD with NOAA5026 private VPN IP and confirm data port
# "TBD/32": [1], # NOAA5026 (SWFO-OCONUS VRF, via Site-to-Site VPN)
Comment thread
laspsandoval marked this conversation as resolved.
}

for ip_range, ports in partner_access.items():
Expand Down
133 changes: 133 additions & 0 deletions sds_data_manager/constructs/ialirt_vpn_construct.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Configure the I-ALiRT VPN connections to NOAA N-Wave."""

from aws_cdk import aws_ec2 as ec2
from constructs import Construct


class IalirtVpnConstruct(Construct):
Comment thread
laspsandoval marked this conversation as resolved.
"""NOAA N-Wave customer gateways and VPN connections for I-ALiRT."""

def __init__(
self,
scope: Construct,
construct_id: str,
vpn_gateway: ec2.CfnVPNGateway,
psk: str,
wash_ip: str,
denv_ip: str,
**kwargs,
) -> None:
"""Create NOAA N-Wave customer gateways and VPN connections.

Parameters
----------
scope : Construct
Parent construct.
construct_id : str
A unique string identifier for this construct.
vpn_gateway : ec2.CfnVPNGateway
The Virtual Private Gateway to attach the VPN connections to.
psk : str
Pre-shared key for IKE authentication. Pass a CDK token from
``secret_value_from_json(...).to_string()`` so the value is resolved
by CloudFormation at deploy time and never appears in the template.
wash_ip : str
NOAA border router public IP at McLean, VA (WASH), retrieved from SSM.
denv_ip : str
NOAA border router public IP at Denver, CO (DENV), retrieved from SSM.
kwargs : dict
Keyword arguments.
"""
super().__init__(scope, construct_id, **kwargs)

# Define the crypto settings for the IPSec tunnel, as specified
# in the N-Wave ICD (NOAA0550).
#
# Phase 1 (IKE) — the handshake phase where both sides authenticate each other
# and agree on encryption keys. Uses pre-shared key (PSK)
# resolved at deploy time.
# - IKEv2 only (NOAA requirement)
# - AES-256 encryption
# - SHA2-256 integrity
# - DH group 14 for key exchange
# - 28800s (8 hour) lifetime
#
# Phase 2 (ESP) — the data phase where actual traffic is encrypted.
# - AES-128 or AES-256 encryption
# - HMAC-SHA2-256-128 integrity
# - DH group 14 (PFS — Perfect Forward Secrecy)
# - 3600s (1 hour) lifetime
tunnel = ec2.CfnVPNConnection.VpnTunnelOptionsSpecificationProperty(
pre_shared_key=psk,
ike_versions=[
ec2.CfnVPNConnection.IKEVersionsRequestListValueProperty(value="ikev2")
],
phase1_encryption_algorithms=[
ec2.CfnVPNConnection.Phase1EncryptionAlgorithmsRequestListValueProperty(
value="AES256"
)
],
phase1_integrity_algorithms=[
ec2.CfnVPNConnection.Phase1IntegrityAlgorithmsRequestListValueProperty(
value="SHA2-256"
)
],
phase1_dh_group_numbers=[
ec2.CfnVPNConnection.Phase1DHGroupNumbersRequestListValueProperty(
value=14
)
],
phase1_lifetime_seconds=28800,
phase2_encryption_algorithms=[
ec2.CfnVPNConnection.Phase2EncryptionAlgorithmsRequestListValueProperty(
value="AES128"
),
ec2.CfnVPNConnection.Phase2EncryptionAlgorithmsRequestListValueProperty(
value="AES256"
),
],
phase2_integrity_algorithms=[
ec2.CfnVPNConnection.Phase2IntegrityAlgorithmsRequestListValueProperty(
value="SHA2-256"
)
],
phase2_dh_group_numbers=[
ec2.CfnVPNConnection.Phase2DHGroupNumbersRequestListValueProperty(
value=14
)
],
phase2_lifetime_seconds=3600,
)

# Customer Gateway - AWS's record of NOAA's router so that AWS can recognize
# and accept the incoming encrypted packets.

# Every AWS Site-to-Site VPN connection automatically provisions
# two auto-assigned tunnel IPs.
# These LASP IKE Gateways must be given to NOAA.
for site, ip in {"WASH": wash_ip, "DENV": denv_ip}.items():
# AWS needs to know the router's public IP and ASN to establish the tunnel.
# bgp_asn=64583 is NOAA's ASN per the ICD.
cgw = ec2.CfnCustomerGateway(
self,
f"NoaaCustomerGateway{site}",
bgp_asn=64583,
ip_address=ip,
type="ipsec.1",
)

# Create the VPN connection between our Virtual Private Gateway (VGW)
# and NOAA's customer gateway. Each connection gets two tunnels by default
# (AWS requirement for redundancy) — both use the same crypto settings.
# BGP is used (static_routes_only=False) so that if one site (WASH or DENV)
# goes down, BGP automatically reroutes traffic through the other.
# Data flows one way: NOAA sends to us. We do not send to NOAA.
ec2.CfnVPNConnection(
self,
f"NoaaVpnConnection{site}",
customer_gateway_id=cgw.ref,
vpn_gateway_id=vpn_gateway.ref,
type="ipsec.1",
static_routes_only=False,
vpn_tunnel_options_specifications=[tunnel, tunnel],
)
38 changes: 38 additions & 0 deletions sds_data_manager/constructs/networking_construct.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Configure the networking components."""

import aws_cdk as cdk
from aws_cdk import aws_ec2 as ec2
from constructs import Construct

Expand Down Expand Up @@ -52,3 +53,40 @@ def __init__(
),
],
)

# Create the Virtual Private Gateway (VGW).
# The VGW decrypts incoming IPSec packets from NOAA and hands them into the VPC.
self.vpn_gateway = self._create_vpn_gateway()
Comment thread
laspsandoval marked this conversation as resolved.

def _create_vpn_gateway(self) -> ec2.CfnVPNGateway:
"""Create a Virtual Private Gateway and attach it to the VPC."""
# Create the Virtual Private Gateway (VGW).
vpn_gateway = ec2.CfnVPNGateway(
self,
"VpnGateway",
# IPSec version 1 is the standard protocol for encrypted VPN tunnels.
Comment thread
laspsandoval marked this conversation as resolved.
type="ipsec.1",
tags=[cdk.CfnTag(key="Name", value="ialirt-vpn-gateway")],
)

# Attach the VGW to the VPC so decrypted traffic can enter the VPC.
attachment = ec2.CfnVPCGatewayAttachment(
self,
"VpnGatewayAttachment",
vpc_id=self.vpc.vpc_id,
vpn_gateway_id=vpn_gateway.ref,
)

# Adds VPN route propagation to each public subnet so that regardless of
# which AZ the I-ALiRT EC2 lands in, it can receive traffic from the VPN.
# Must depend on the attachment being complete first.
for i, subnet in enumerate(self.vpc.public_subnets):
Comment thread
laspsandoval marked this conversation as resolved.
propagation = ec2.CfnVPNGatewayRoutePropagation(
self,
f"RoutePropagate{i}",
route_table_ids=[subnet.route_table.route_table_id],
vpn_gateway_id=vpn_gateway.ref,
)
propagation.node.add_dependency(attachment)

return vpn_gateway
36 changes: 36 additions & 0 deletions sds_data_manager/utils/stackbuilder.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from aws_cdk import aws_ec2 as ec2
from aws_cdk import aws_lambda as lambda_
from aws_cdk import aws_rds as rds
from aws_cdk import aws_secretsmanager as secretsmanager
from aws_cdk import aws_ssm as ssm

from sds_data_manager.constructs import (
api_gateway_construct,
Expand All @@ -25,6 +27,7 @@
ialirt_processing_construct,
ialirt_realtime_construct,
ialirt_schedule_fetch_construct,
ialirt_vpn_construct,
indexer_lambda_construct,
instrument_lambdas,
lambda_layer_construct,
Expand Down Expand Up @@ -461,6 +464,39 @@ def build_sds(
account_name=account_name,
)

# Retrieve the NOAA VPN pre-shared key from Secrets Manager.
# Store the PSK under the key "psk" in a secret named "noaa-vpn-psk"
# before deploying this stack.
noaa_vpn_psk = (
secretsmanager.Secret.from_secret_name_v2(
ialirt_stack, "NoaaVpnPsk", "noaa-vpn-psk"
)
.secret_value_from_json("psk")
.to_string()
Comment thread
laspsandoval marked this conversation as resolved.
)

# Retrieve NOAA's border router IPs from SSM Parameter Store.
# Store these before deploying:
# aws ssm put-parameter --name "/ialirt/noaa-vpn/wash-ip"
# --value "<ip>" --type String
# aws ssm put-parameter --name "/ialirt/noaa-vpn/denv-ip"
# --value "<ip>" --type String
noaa_wash_ip = ssm.StringParameter.value_for_string_parameter(
ialirt_stack, "/ialirt/noaa-vpn/wash-ip"
)
noaa_denv_ip = ssm.StringParameter.value_for_string_parameter(
ialirt_stack, "/ialirt/noaa-vpn/denv-ip"
)

ialirt_vpn_construct.IalirtVpnConstruct(
scope=ialirt_stack,
construct_id="IalirtVpn",
vpn_gateway=networking.vpn_gateway,
psk=noaa_vpn_psk,
wash_ip=noaa_wash_ip,
denv_ip=noaa_denv_ip,
)
Comment thread
laspsandoval marked this conversation as resolved.


def build_backup(scope: App, env: Environment, source_account: str):
"""Build backup bucket with permissions for replication from source_account.
Expand Down
Loading