AWSTemplateFormatVersion: '2010-09-09'
Description: >
  EG334S VPN #1 - AWS PUBLIC CLOUD SIDE (deploy in eu-west-1 / Ireland).
  Builds VPC2 (10.1.0.0/16), a Virtual Private Gateway, a Customer Gateway
  pointing at the on-prem strongSwan Elastic IP, and a Site-to-Site VPN
  connection (static routing) back to the on-prem CIDR 10.0.0.0/16. It also
  creates CloudTrail, this VPC's Flow Log, SNS and VPN alarms, and can add the
  staged second VPN connection to Azure.
  DEPLOY THIS STACK SECOND - it needs the StrongSwanElasticIP output from the
  us-east-1 stack as the OnPremStrongSwanEIP parameter.

# ---------------------------------------------------------------------------
# Parameters
# ---------------------------------------------------------------------------
Parameters:
  KeyPairName:
    Type: AWS::EC2::KeyPair::KeyName
    Description: Existing EC2 key pair in eu-west-1 for SSH access.

  OnPremStrongSwanEIP:
    Type: String
    Description: >
      The Elastic IP from the us-east-1 stack output (StrongSwanElasticIP).
      This is the Customer Gateway address. NO DEFAULT - you must supply it.
    AllowedPattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}$'

  OnPremCidr:
    Type: String
    Default: 10.0.0.0/16
    Description: CIDR of the simulated on-prem VPC (us-east-1). The VPN static route target.

  AdminCidr:
    Type: String
    Description: >
      Required administrator public IPv4 address as a single-host /32. There is
      deliberately no permissive default.
    AllowedPattern: '^((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])/32$'
    ConstraintDescription: Supply one valid public IPv4 address followed by /32; 0.0.0.0/0 is rejected.

  InstanceType:
    Type: String
    Default: t3.micro
    AllowedValues: [t3.micro, t3.small]

  CentralLogBucketName:
    Type: String
    Description: >
      Central logging bucket output by the us-east-1 stack. This stack sends
      its VPC Flow Log there and creates the account's multi-region CloudTrail.

  AzureCidr:
    Type: String
    Default: 10.2.0.0/16
    Description: CIDR of the Azure virtual network reached through VPN #2.

  CreateAzureVpn:
    Type: String
    Default: 'false'
    AllowedValues: ['true', 'false']
    Description: >
      Set true only after the Azure Bicep deployment has created its VPN
      gateway public IP and that address is supplied below.

  AzureGatewayPublicIp:
    Type: String
    Default: ''
    Description: >
      Public IP output from the Azure Bicep deployment. Required when
      CreateAzureVpn is true.

  MonitoringEmail:
    Type: String
    Default: ''
    Description: >
      Optional email endpoint for the monitoring SNS topic. AWS sends a
      confirmation message; alerts are not delivered until it is confirmed.

  Vpn1Tunnel1OutsideIp:
    Type: String
    Default: ''
    Description: >
      First outside tunnel IP from VPN #1. Supply both VPN #1 tunnel IPs on a
      stack update after AWS creates the connection to enable its alarm.

  Vpn1Tunnel2OutsideIp:
    Type: String
    Default: ''
    Description: Second outside tunnel IP from VPN #1.

  Vpn2Tunnel1OutsideIp:
    Type: String
    Default: ''
    Description: >
      First outside tunnel IP from VPN #2. Supply both VPN #2 tunnel IPs on a
      stack update after AWS creates the connection to enable its alarm.

  Vpn2Tunnel2OutsideIp:
    Type: String
    Default: ''
    Description: Second outside tunnel IP from VPN #2.

Conditions:
  ShouldCreateAzureVpn: !Equals [!Ref CreateAzureVpn, 'true']
  HasMonitoringEmail: !Not [!Equals [!Ref MonitoringEmail, '']]
  HasVpn1TunnelIps: !And
    - !Not [!Equals [!Ref Vpn1Tunnel1OutsideIp, '']]
    - !Not [!Equals [!Ref Vpn1Tunnel2OutsideIp, '']]
  HasVpn2TunnelIps: !And
    - !Condition ShouldCreateAzureVpn
    - !Not [!Equals [!Ref Vpn2Tunnel1OutsideIp, '']]
    - !Not [!Equals [!Ref Vpn2Tunnel2OutsideIp, '']]

# ---------------------------------------------------------------------------
# Resources
# ---------------------------------------------------------------------------
Resources:

  # ---- Network ----
  Vpc2:
    Type: AWS::EC2::VPC
    Properties:
      CidrBlock: 10.1.0.0/16
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - { Key: Name, Value: EG334S-awspublic-vpc2 }

  InternetGateway:
    Type: AWS::EC2::InternetGateway
    Properties:
      Tags:
        - { Key: Name, Value: EG334S-awspublic-igw }

  IgwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref Vpc2
      InternetGatewayId: !Ref InternetGateway

  PublicSubnet:
    Type: AWS::EC2::Subnet
    Properties:
      VpcId: !Ref Vpc2
      CidrBlock: 10.1.1.0/24
      MapPublicIpOnLaunch: true
      AvailabilityZone: !Select [0, !GetAZs '']
      Tags:
        - { Key: Name, Value: EG334S-awspublic-subnet }

  RouteTable:
    Type: AWS::EC2::RouteTable
    Properties:
      VpcId: !Ref Vpc2
      Tags:
        - { Key: Name, Value: EG334S-awspublic-rt }

  SubnetRouteTableAssoc:
    Type: AWS::EC2::SubnetRouteTableAssociation
    Properties:
      SubnetId: !Ref PublicSubnet
      RouteTableId: !Ref RouteTable

  DefaultRoute:
    Type: AWS::EC2::Route
    DependsOn: IgwAttachment
    Properties:
      RouteTableId: !Ref RouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway

  # ---- AWS-managed VPN side ----
  Vgw:
    Type: AWS::EC2::VPNGateway
    Properties:
      Type: ipsec.1
      Tags:
        - { Key: Name, Value: EG334S-awspublic-vgw }

  VgwAttachment:
    Type: AWS::EC2::VPCGatewayAttachment
    Properties:
      VpcId: !Ref Vpc2
      VpnGatewayId: !Ref Vgw

  # Auto-install the on-prem route (learned via the VPN) into the route table.
  RoutePropagation:
    Type: AWS::EC2::VPNGatewayRoutePropagation
    DependsOn: VgwAttachment
    Properties:
      RouteTableIds: [ !Ref RouteTable ]
      VpnGatewayId: !Ref Vgw

  CustomerGateway:
    Type: AWS::EC2::CustomerGateway
    Properties:
      Type: ipsec.1
      BgpAsn: 65000            # Required field even for static routing; value is unused.
      IpAddress: !Ref OnPremStrongSwanEIP
      Tags:
        - { Key: Name, Value: EG334S-onprem-cgw }

  VpnConnection:
    Type: AWS::EC2::VPNConnection
    Properties:
      Type: ipsec.1
      StaticRoutesOnly: true
      CustomerGatewayId: !Ref CustomerGateway
      VpnGatewayId: !Ref Vgw
      Tags:
        - { Key: Name, Value: EG334S-vpn1-connection }

  # Tell AWS which on-prem prefix is reachable over the tunnel.
  VpnStaticRoute:
    Type: AWS::EC2::VPNConnectionRoute
    Properties:
      DestinationCidrBlock: !Ref OnPremCidr
      VpnConnectionId: !Ref VpnConnection

  # ---- VPN #2 to Azure ----
  # This is a staged dependency: first deploy the Azure VPN gateway to obtain
  # its public IP, then update this stack with CreateAzureVpn=true. AWS assigns
  # the two tunnel outside IPs and PSKs; one pair is then supplied back to the
  # Azure Bicep connection deployment.
  AzureCustomerGateway:
    Type: AWS::EC2::CustomerGateway
    Condition: ShouldCreateAzureVpn
    Properties:
      Type: ipsec.1
      BgpAsn: 65000
      IpAddress: !Ref AzureGatewayPublicIp
      Tags:
        - { Key: Name, Value: eg334s-azure-cgw }

  AzureVpnConnection:
    Type: AWS::EC2::VPNConnection
    Condition: ShouldCreateAzureVpn
    Properties:
      Type: ipsec.1
      StaticRoutesOnly: true
      CustomerGatewayId: !Ref AzureCustomerGateway
      VpnGatewayId: !Ref Vgw
      LocalIpv4NetworkCidr: !Ref AzureCidr
      RemoteIpv4NetworkCidr: 10.1.0.0/16
      Tags:
        - { Key: Name, Value: eg334s-vpn2 }

  AzureVpnStaticRoute:
    Type: AWS::EC2::VPNConnectionRoute
    Condition: ShouldCreateAzureVpn
    Properties:
      DestinationCidrBlock: !Ref AzureCidr
      VpnConnectionId: !Ref AzureVpnConnection

  # ---- AWS-public test server (what you ping FROM on this side) ----
  PublicServerSg:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: AWS-public test server
      VpcId: !Ref Vpc2
      SecurityGroupIngress:
        - { IpProtocol: icmp, FromPort: -1, ToPort: -1, CidrIp: !Ref OnPremCidr, Description: Ping from on-prem }
        - { IpProtocol: -1, CidrIp: 10.1.0.0/16, Description: Local VPC }
        - { IpProtocol: tcp, FromPort: 22, ToPort: 22, CidrIp: !Ref AdminCidr, Description: SSH }
        - { IpProtocol: tcp, FromPort: 80, ToPort: 80, CidrIp: !Ref OnPremCidr, Description: HTTP test from on-prem }
        - { IpProtocol: icmp, FromPort: -1, ToPort: -1, CidrIp: !Ref AzureCidr, Description: Ping from Azure }
        - { IpProtocol: tcp, FromPort: 80, ToPort: 80, CidrIp: !Ref AzureCidr, Description: HTTP test from Azure }
      Tags:
        - { Key: Name, Value: EG334S-awspublic-server-sg }

  PublicServer:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: '{{resolve:ssm:/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2}}'
      KeyName: !Ref KeyPairName
      SubnetId: !Ref PublicSubnet
      SecurityGroupIds: [ !Ref PublicServerSg ]
      # The report and the demo both rely on an HTTP response from this host to
      # prove the tunnel carries a real application workload, not just ICMP.
      # Without this UserData the template creates a bare instance with nothing
      # listening on port 80, and a fresh deploy of this stack cannot reproduce
      # the documented result. Added 25 Jul 2026 after a rebuild exposed the gap.
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          set -euxo pipefail
          yum update -y
          yum install -y httpd
          systemctl enable --now httpd
          cat > /var/www/html/index.html <<'HTML'
          <!doctype html><meta charset="utf-8">
          <title>EG334S . AWS public cloud (hub)</title>
          <body style="font:16px/1.6 system-ui;margin:40px">
          <h1>EG334S &middot; AWS public cloud</h1>
          <p>You reached this page across an IPSec site-to-site VPN.</p>
          <ul>
            <li>Region: eu-west-1 (Ireland)</li>
            <li>VPC CIDR: 10.1.0.0/16</li>
            <li>Role: hub. One virtual private gateway terminates both tunnels.</li>
          </ul>
          <p id="h"></p>
          </body>
          HTML
          echo "<p>Host: $(hostname) &middot; private IP: $(hostname -I | awk '{print $1}')</p>" \
            >> /var/www/html/index.html
      Tags:
        - { Key: Name, Value: EG334S-awspublic-server }
        # Project tag is activated as an AWS cost allocation tag, so tagging
        # here is what makes per-project cost reporting possible at all.
        - { Key: Project, Value: EG334S }

  # ---- Logging, auditing and alerting ----
  AwsPublicVpcFlowLog:
    Type: AWS::EC2::FlowLog
    Properties:
      ResourceId: !Ref Vpc2
      ResourceType: VPC
      TrafficType: ALL
      LogDestinationType: s3
      LogDestination: !Sub 'arn:${AWS::Partition}:s3:::${CentralLogBucketName}'
      MaxAggregationInterval: 600
      Tags:
        - { Key: Name, Value: EG334S-awspublic-vpc-flow-log }
        - { Key: Project, Value: EG334S }

  AuditTrail:
    Type: AWS::CloudTrail::Trail
    Properties:
      TrailName: EG334S-audit-trail
      S3BucketName: !Ref CentralLogBucketName
      IsLogging: true
      IsMultiRegionTrail: true
      IncludeGlobalServiceEvents: true
      EnableLogFileValidation: true
      EventSelectors:
        - ReadWriteType: All
          IncludeManagementEvents: true
      Tags:
        - { Key: Project, Value: EG334S }

  MonitoringTopic:
    Type: AWS::SNS::Topic
    Properties:
      TopicName: EG334S-monitoring-alerts
      Tags:
        - { Key: Project, Value: EG334S }

  MonitoringEmailSubscription:
    Type: AWS::SNS::Subscription
    Condition: HasMonitoringEmail
    Properties:
      Protocol: email
      Endpoint: !Ref MonitoringEmail
      TopicArn: !Ref MonitoringTopic

  Vpn1TunnelDownAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: HasVpn1TunnelIps
    Properties:
      AlarmName: EG334S-VPN1-TunnelDown
      AlarmDescription: Alarm when neither AWS tunnel for VPN #1 is up.
      ActionsEnabled: true
      AlarmActions: [!Ref MonitoringTopic]
      ComparisonOperator: LessThanThreshold
      EvaluationPeriods: 1
      Threshold: 1
      TreatMissingData: breaching
      Metrics:
        - Id: t1
          MetricStat:
            Metric:
              Namespace: AWS/VPN
              MetricName: TunnelState
              Dimensions:
                - { Name: VpnId, Value: !Ref VpnConnection }
                - { Name: TunnelIpAddress, Value: !Ref Vpn1Tunnel1OutsideIp }
            Period: 300
            Stat: Maximum
          ReturnData: false
        - Id: t2
          MetricStat:
            Metric:
              Namespace: AWS/VPN
              MetricName: TunnelState
              Dimensions:
                - { Name: VpnId, Value: !Ref VpnConnection }
                - { Name: TunnelIpAddress, Value: !Ref Vpn1Tunnel2OutsideIp }
            Period: 300
            Stat: Maximum
          ReturnData: false
        - Id: anyup
          Expression: MAX([t1,t2])
          Label: Any tunnel up
          ReturnData: true

  Vpn2TunnelDownAlarm:
    Type: AWS::CloudWatch::Alarm
    Condition: HasVpn2TunnelIps
    Properties:
      AlarmName: EG334S-VPN2-TunnelDown
      AlarmDescription: Alarm when neither AWS tunnel for VPN #2 is up.
      ActionsEnabled: true
      AlarmActions: [!Ref MonitoringTopic]
      ComparisonOperator: LessThanThreshold
      EvaluationPeriods: 1
      Threshold: 1
      TreatMissingData: breaching
      Metrics:
        - Id: t1
          MetricStat:
            Metric:
              Namespace: AWS/VPN
              MetricName: TunnelState
              Dimensions:
                - { Name: VpnId, Value: !Ref AzureVpnConnection }
                - { Name: TunnelIpAddress, Value: !Ref Vpn2Tunnel1OutsideIp }
            Period: 300
            Stat: Maximum
          ReturnData: false
        - Id: t2
          MetricStat:
            Metric:
              Namespace: AWS/VPN
              MetricName: TunnelState
              Dimensions:
                - { Name: VpnId, Value: !Ref AzureVpnConnection }
                - { Name: TunnelIpAddress, Value: !Ref Vpn2Tunnel2OutsideIp }
            Period: 300
            Stat: Maximum
          ReturnData: false
        - Id: anyup
          Expression: MAX([t1,t2])
          Label: Any tunnel up
          ReturnData: true

# ---------------------------------------------------------------------------
# Outputs
# ---------------------------------------------------------------------------
Outputs:
  VpnConnectionId:
    Description: >
      Download the tunnel config from here:
      aws ec2 describe-vpn-connections --vpn-connection-ids <this> --region eu-west-1
      You need the 2 tunnel OutsideIpAddress values + PreSharedKeys for strongSwan.
    Value: !Ref VpnConnection

  PublicServerPrivateIp:
    Description: Private IP of the AWS-public test server - ping this FROM the on-prem server.
    Value: !GetAtt PublicServer.PrivateIp

  VgwId:
    Value: !Ref Vgw

  Vpc2Id:
    Value: !Ref Vpc2

  PublicSubnetId:
    Description: First Ireland public subnet; pass to hub-alb-eu-west-1.yaml.
    Value: !Ref PublicSubnet

  RouteTableId:
    Description: Ireland public route table; pass to hub-alb-eu-west-1.yaml.
    Value: !Ref RouteTable

  PublicServerInstanceId:
    Description: Existing hub workload; pass to hub-alb-eu-west-1.yaml and hub-recovery-eu-west-1.yaml.
    Value: !Ref PublicServer

  PublicServerSecurityGroupId:
    Description: Hub workload security group; pass to the ALB and recovery templates.
    Value: !Ref PublicServerSg

  AzureVpnConnectionId:
    Condition: ShouldCreateAzureVpn
    Description: >
      Query this connection after creation to obtain its two outside tunnel
      IPs and PSKs, then finish the Azure connection and enable its alarm.
    Value: !Ref AzureVpnConnection

  MonitoringTopicArn:
    Value: !Ref MonitoringTopic

  AuditTrailArn:
    Value: !GetAtt AuditTrail.Arn

  AwsPublicVpcFlowLogId:
    Value: !Ref AwsPublicVpcFlowLog
