Description: This template deploys restrict-iam-roles-to-access-aws-resources solution.
Parameters:
  EnvironmentName:
    Description: An environment name that is prefixed to resource names
    Type: String
    Default: Client-VPN-VPC
  VpcCIDR:
    Description: Please enter the IP range (CIDR notation) for this VPC
    Type: String
    Default: 10.192.0.0/16
  PublicSubnet1CIDR:
    Description: >-
      Please enter the IP range (CIDR notation) for the public subnet in the
      first Availability Zone
    Type: String
    Default: 10.192.10.0/24
  PublicSubnet2CIDR:
    Description: >-
      Please enter the IP range (CIDR notation) for the public subnet in the
      second Availability Zone
    Type: String
    Default: 10.192.11.0/24
  PrivateSubnet1CIDR:
    Description: >-
      Please enter the IP range (CIDR notation) for the private subnet in the
      first Availability Zone
    Type: String
    Default: 10.192.20.0/24
  PrivateSubnet2CIDR:
    Description: >-
      Please enter the IP range (CIDR notation) for the private subnet in the
      second Availability Zone
    Type: String
    Default: 10.192.21.0/24
  ServerCertificateArn:
    Description: >-
      The ARN of the server certificate. The server certificate must be
      provisioned in AWS Certificate Manager (ACM).
    Type: String
  LambdaProvisionedConcurrency:
    Description: >-
      Provisioned concurrency for the Lambda function to enable it to scale without fluctuations in latency.
    Type: Number
    Default: 1
    MinValue: 1
  ClientVPNLogGroupLogGroupName:
    Description: The name of the CloudWatch Logs log group to use.
    Type: String
    Default: ClientVPNLogGroupLogGroup
  ClientVPNLogGroupLogGroupRetentionInDays:
    Description: >-
      The number of days to retain the log events in the specified log group.
      Possible values are: 1, 3, 5, 7, 14, 30, 60, 90, 120, 150, 180, 365, 400,
      545, 731, 1827, and 3653.
    Type: Number
    Default: 7
  ClientVPNCidrBlock:
    Description: >-
      The IP address range, in CIDR notation, from which client IP addresses are
      allocated.
    Type: String
    Default: 10.0.100.0/22
  ClientVPNPort:
    Description: VPN port that the endpoint will use.
    Type: Number
    Default: 443
  AuthenticationOption:
    Type: String
    AllowedValues:
      - directory-service-authentication
      - federated-authentication
    Description: >-
      Information about the authentication method to be used to authenticate clients.
  AuthenticationOptionResourceIdentifier:
    Type: String
    Description: >-
      For Active Directory authentication: the ID of the AWS Directory Services
      directory to use. For Federated authentication: The ARN of SAML provider.
  CountryCodes:
    Type: String
    Default: US
    Description: >-
      String of comma separated country codes. Example: US,GB,DE.
      Country code has to be alpha-2 country ISO codes of the ISO 3166 international standard.
Conditions:
  AuthenticationOptionAwsAD: !Equals
    - !Ref AuthenticationOption
    - directory-service-authentication
  AuthenticationOptionSAML: !Equals
    - !Ref AuthenticationOption
    - federated-authentication

Resources:
  VPC:
    Type: 'AWS::EC2::VPC'
    Properties:
      CidrBlock: !Ref VpcCIDR
      EnableDnsSupport: true
      EnableDnsHostnames: true
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName
  InternetGateway:
    Type: 'AWS::EC2::InternetGateway'
    Properties:
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName
  InternetGatewayAttachment:
    Type: 'AWS::EC2::VPCGatewayAttachment'
    Properties:
      InternetGatewayId: !Ref InternetGateway
      VpcId: !Ref VPC
  PublicSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 0
        - !GetAZs ''
      CidrBlock: !Ref PublicSubnet1CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName} Public Subnet (AZ1)'
  PublicSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 1
        - !GetAZs ''
      CidrBlock: !Ref PublicSubnet2CIDR
      MapPublicIpOnLaunch: true
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName} Public Subnet (AZ2)'
  PrivateSubnet1:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 0
        - !GetAZs ''
      CidrBlock: !Ref PrivateSubnet1CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName} Private Subnet (AZ1)'
  PrivateSubnet2:
    Type: 'AWS::EC2::Subnet'
    Properties:
      VpcId: !Ref VPC
      AvailabilityZone: !Select
        - 1
        - !GetAZs ''
      CidrBlock: !Ref PrivateSubnet2CIDR
      MapPublicIpOnLaunch: false
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName} Private Subnet (AZ2)'
  NatGateway1EIP:
    Type: 'AWS::EC2::EIP'
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc
  NatGateway2EIP:
    Type: 'AWS::EC2::EIP'
    DependsOn: InternetGatewayAttachment
    Properties:
      Domain: vpc
  NatGateway1:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt
        - NatGateway1EIP
        - AllocationId
      SubnetId: !Ref PublicSubnet1
  NatGateway2:
    Type: 'AWS::EC2::NatGateway'
    Properties:
      AllocationId: !GetAtt
        - NatGateway2EIP
        - AllocationId
      SubnetId: !Ref PublicSubnet2
  PublicRouteTable:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName} Public Routes'
  DefaultPublicRoute:
    Type: 'AWS::EC2::Route'
    DependsOn: InternetGatewayAttachment
    Properties:
      RouteTableId: !Ref PublicRouteTable
      DestinationCidrBlock: 0.0.0.0/0
      GatewayId: !Ref InternetGateway
  PublicSubnet1RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet1
  PublicSubnet2RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PublicRouteTable
      SubnetId: !Ref PublicSubnet2
  PrivateRouteTable1:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName} Private Routes (AZ1)'
  DefaultPrivateRoute1:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway1
  PrivateSubnet1RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PrivateRouteTable1
      SubnetId: !Ref PrivateSubnet1
  PrivateRouteTable2:
    Type: 'AWS::EC2::RouteTable'
    Properties:
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Sub '${EnvironmentName} Private Routes (AZ2)'
  DefaultPrivateRoute2:
    Type: 'AWS::EC2::Route'
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      DestinationCidrBlock: 0.0.0.0/0
      NatGatewayId: !Ref NatGateway2
  PrivateSubnet2RouteTableAssociation:
    Type: 'AWS::EC2::SubnetRouteTableAssociation'
    Properties:
      RouteTableId: !Ref PrivateRouteTable2
      SubnetId: !Ref PrivateSubnet2
  ClientVPNLogGroup:
    Type: 'AWS::Logs::LogGroup'
    Properties:
      LogGroupName: !Ref ClientVPNLogGroupLogGroupName
      RetentionInDays: !Ref ClientVPNLogGroupLogGroupRetentionInDays
  ClientVPNSecurityGroup:
    Type: 'AWS::EC2::SecurityGroup'
    Properties:
      GroupDescription: Default SecurityGroup for Client VPN
      GroupName: DefaultSGClientVPN
      SecurityGroupEgress:
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: 0.0.0.0/0
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 0
          ToPort: 65535
          CidrIp: 0.0.0.0/0
      VpcId: !Ref VPC
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName
  ClientVpnEndpoint:
    Type: 'AWS::EC2::ClientVpnEndpoint'
    Properties:
      AuthenticationOptions:
        - !If
          - AuthenticationOptionAwsAD
          - Type: directory-service-authentication
            ActiveDirectory:
              DirectoryId: !Ref AuthenticationOptionResourceIdentifier
          - !If
            - AuthenticationOptionSAML
            - Type: federated-authentication
              FederatedAuthentication:
                SAMLProviderArn: !Ref AuthenticationOptionResourceIdentifier
            - !Ref 'AWS::NoValue'
      ClientCidrBlock: !Ref ClientVPNCidrBlock
      ClientConnectOptions:
        Enabled: true
        LambdaFunctionArn: !Ref LambdaFunctionVersion
      ConnectionLogOptions:
        CloudwatchLogGroup: !Ref ClientVPNLogGroupLogGroupName
        Enabled: true
      Description: ClientVPN
      SecurityGroupIds:
        - !Ref ClientVPNSecurityGroup
      ServerCertificateArn: !Ref ServerCertificateArn
      SplitTunnel: false
      TagSpecifications:
        - ResourceType: client-vpn-endpoint
          Tags:
            - Key: Name
              Value: !Ref EnvironmentName
      TransportProtocol: udp
      VpcId: !Ref VPC
      VpnPort: !Ref ClientVPNPort
  ClientVpnTargetNetworkAssociation1:
    Type: 'AWS::EC2::ClientVpnTargetNetworkAssociation'
    Properties:
      ClientVpnEndpointId: !Ref ClientVpnEndpoint
      SubnetId: !Ref PrivateSubnet1
  ClientVpnTargetNetworkAssociation2:
    Type: 'AWS::EC2::ClientVpnTargetNetworkAssociation'
    Properties:
      ClientVpnEndpointId: !Ref ClientVpnEndpoint
      SubnetId: !Ref PrivateSubnet2
  ClientVpnRouteOutbound1:
    DependsOn: ClientVpnTargetNetworkAssociation1
    Type: 'AWS::EC2::ClientVpnRoute'
    Properties:
      ClientVpnEndpointId: !Ref ClientVpnEndpoint
      Description: Outbound to 0.0.0.0/0
      DestinationCidrBlock: 0.0.0.0/0
      TargetVpcSubnetId: !Ref PrivateSubnet1
  ClientVpnRouteOutbound2:
    DependsOn: ClientVpnTargetNetworkAssociation2
    Type: 'AWS::EC2::ClientVpnRoute'
    Properties:
      ClientVpnEndpointId: !Ref ClientVpnEndpoint
      Description: Outbound to 0.0.0.0/0
      DestinationCidrBlock: 0.0.0.0/0
      TargetVpcSubnetId: !Ref PrivateSubnet2
  ClientVpnAuthorizationRule:
    Type: 'AWS::EC2::ClientVpnAuthorizationRule'
    Properties:
      ClientVpnEndpointId: !Ref ClientVpnEndpoint
      AuthorizeAllGroups: true
      TargetNetworkCidr: 0.0.0.0/0
      Description: DefaultAuthRule
  LambdaIAMPolicy:
    Type: 'AWS::IAM::Policy'
    Properties:
      Roles:
        - !Ref LambdaIAMRole
      PolicyName: ApiServiceIAMPolicy
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - logs:CreateLogGroup
              - logs:CreateLogStream
              - logs:PutLogEvents
            Resource: "arn:aws:logs:*:*:*"
  LambdaIAMRole:
    Type: 'AWS::IAM::Role'
    Properties:
      RoleName: ClientVPNAWSLambdaVPCAccessExecutionRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaVPCAccessExecutionRole
      AssumeRolePolicyDocument: |-
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Action": "sts:AssumeRole",
              "Principal": {
                "Service": "lambda.amazonaws.com"
              },
              "Effect": "Allow",
              "Sid": ""
            }
          ]
        }
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName
  LambdaFunctionVersion:
    Type: 'AWS::Lambda::Version'
    Properties:
      FunctionName: !Ref LambdaFunction
      ProvisionedConcurrencyConfig:
        ProvisionedConcurrentExecutions: !Ref LambdaProvisionedConcurrency
  LambdaFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      VpcConfig:
        SecurityGroupIds:
          - !GetAtt VPC.DefaultSecurityGroup
        SubnetIds:
          - !Ref PrivateSubnet1
          - !Ref PrivateSubnet2
      FunctionName: AWSClientVPN-GeoIpHandler
      Environment:
        Variables:
          ENDPOINT_DNS: !Sub '${ApiGatewayRestApi}.execute-api.${AWS::Region}.amazonaws.com'
          ENDPOINT: prod
      Code:
        ZipFile: |
          import os
          import http.client


          cloud_front_url = os.getenv("ENDPOINT_DNS")
          endpoint = os.getenv("ENDPOINT")
          success_status_codes = [200]


          def build_response(allow, status):
              return {
                  "allow": allow,
                  "error-msg-on-failed-posture-compliance": "Error establishing connection. Please contact your administrator.",
                  "posture-compliance-statuses": [status],
                  "schema-version": "v1"
              }


          def handler(event, context):
              ip = event['public-ip']

              conn = http.client.HTTPSConnection(cloud_front_url)
              conn.request("GET", f'/{endpoint}', headers={'X-Forwarded-For': ip})
              r1 = conn.getresponse()
              conn.close()

              status_code = r1.status

              if status_code in success_status_codes:
                  print("User's IP is based from an allowed country. Allowing the connection to VPN.")
                  return build_response(True, 'compliant')

              print("User's IP is NOT based from an allowed country. Blocking the connection to VPN.")
              return build_response(False, 'quarantined')
      Handler: index.handler
      Role: !GetAtt
        - LambdaIAMRole
        - Arn
      Runtime: python3.7
      Timeout: 30
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName
    DependsOn:
      - LambdaIAMPolicy
  NatGwIPSet:
    Type: 'AWS::WAFv2::IPSet'
    Properties:
      Description: VPC NAT GW IPSet
      Name: NatGwIPSet
      Scope: REGIONAL
      IPAddressVersion: IPV4
      Addresses:
        - !Sub '${NatGateway1EIP}/32'
        - !Sub '${NatGateway2EIP}/32'
  WebACL:
    Type: 'AWS::WAFv2::WebACL'
    Properties:
      Name: ClientVPNWebACL
      Scope: REGIONAL
      Description: Client VPN WebACL
      DefaultAction:
        Block: {}
      VisibilityConfig:
        SampledRequestsEnabled: true
        CloudWatchMetricsEnabled: true
        MetricName: WebACLMetric
      Rules:
        - Name: ByIP
          Priority: 0
          Action:
            Allow: {}
          Statement:
            IPSetReferenceStatement:
              Arn: !GetAtt NatGwIPSet.Arn
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: ByIPMatchStatement
        - Name: GEO
          Priority: 1
          Action:
            Allow: {}
          VisibilityConfig:
            SampledRequestsEnabled: true
            CloudWatchMetricsEnabled: true
            MetricName: RuleWithGeoMatchStatement
          Statement:
            GeoMatchStatement:
              CountryCodes: !Split [",", !Ref CountryCodes]
              ForwardedIPConfig:
                FallbackBehavior: NO_MATCH
                HeaderName: X-Forwarded-For
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName
  WebACLAssociation:
    Type: 'AWS::WAFv2::WebACLAssociation'
    Properties:
      ResourceArn: !Sub >-
        arn:aws:apigateway:${AWS::Region}::/restapis/${ApiGatewayRestApi}/stages/prod
      WebACLArn: !GetAtt
        - WebACL
        - Arn
  ApiGatewayRestApi:
    Type: 'AWS::ApiGateway::RestApi'
    Properties:
      Name: ClientVPN
      Tags:
        - Key: Name
          Value: !Ref EnvironmentName
  ApiGatewayMethod:
    Type: 'AWS::ApiGateway::Method'
    Properties:
      HttpMethod: GET
      ResourceId: !GetAtt
        - ApiGatewayRestApi
        - RootResourceId
      RestApiId: !Ref ApiGatewayRestApi
      AuthorizationType: NONE
      Integration:
        Type: MOCK
        IntegrationResponses:
          - StatusCode: 200
        RequestTemplates:
          application/json: |
            { statusCode: 200 }
      MethodResponses:
        - StatusCode: 200
  ApiGatewayDeployment:
    Type: 'AWS::ApiGateway::Deployment'
    Properties:
      RestApiId: !Ref ApiGatewayRestApi
      StageName: prod
    DependsOn:
      - ApiGatewayMethod
  IAMRoleClientVpnDenyIfNotNatIP:
    Type: 'AWS::IAM::ManagedPolicy'
    Properties:
      Description: >-
        Policy denies all request unless request originates from Client VPN NAT
        IP
      Path: /
      ManagedPolicyName: IAMRoleClientVpnDenyIfNotNatIP
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Deny
            Action: '*'
            Resource: '*'
            Condition:
              NotIpAddress:
                'aws:SourceIp':
                  - !Ref NatGateway1EIP
                  - !Ref NatGateway2EIP
              Bool:
                'aws:ViaAWSService': "false"
Outputs:
  IAMRoleClientVpnDenyIfNotNatIP:
    Description: IAM deny policy name
    Value: IAMRoleClientVpnDenyIfNotNatIP
  ClientVPNConsoleURL:
    Description: Client VPN Console URL
    Value: !Sub >-
      https://${AWS::Region}.console.aws.amazon.com/vpc/home?region=${AWS::Region}#ClientVPNEndpoints:sort=clientVpnEndpointId
