{
    "Metadata": {
        "README": {
            "Fn::Join": [
                "\n",
                [
                    "",
                    "AWS Cloudformation Template for deploying IPSec VPN Servers on AWS EC2,",
                    "based on the work of Lin Song <linsongui@gmail.com> : https://github.com/hwdsl2/setup-ipsec-vpn",
                    "The latest version of this template can be found at : https://github.com/hwdsl2/setup-ipsec-vpn/aws",
                    "",
                    "Copyright (C) 2020-2023 Scott X. L. <wtanglef@pm.me>",
                    "",
                    "This work is licensed under the Creative Commons Attribution-ShareAlike 3.0",
                    "Unported License: http://creativecommons.org/licenses/by-sa/3.0/",
                    "",
                    "Attribution required: Please include my name in any derivative and let me",
                    "know how you have improved it!",
                    ""
                ]
            ]
        }
    },
    "AWSTemplateFormatVersion": "2010-09-09",
    "Mappings": {
        "OS": {
            "Ubuntu2004": {
                "HelperInstallationCommands": "export DEBIAN_FRONTEND=noninteractive\napt-get -yq update\napt-get -yq install python3-pip zip awscli\npython3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz\n"
            },
            "Ubuntu2204": {
                "HelperInstallationCommands": "export DEBIAN_FRONTEND=noninteractive\napt-get -yq update\napt-get -yq install python3-pip zip awscli\npython3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz\n"
            },
            "Debian10": {
                "HelperInstallationCommands": "export DEBIAN_FRONTEND=noninteractive\napt-get -yq update\napt-get -yq install python3-pip zip\npython3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz\n"
            },
            "Debian11": {
                "HelperInstallationCommands": "export DEBIAN_FRONTEND=noninteractive\napt-get -yq update\napt-get -yq install python3-pip zip\npython3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz\n"
            },
            "Debian12": {
                "HelperInstallationCommands": "export DEBIAN_FRONTEND=noninteractive\nrm -rf /usr/lib/python3.*/EXTERNALLY-MANAGED\napt-get -yq update\napt-get -yq install python3-pip zip\npython3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz\n"
            },
            "CentOS7": {
                "HelperInstallationCommands": "yum -y install python3 wget zip awscli\npython3 -m pip install https://s3.amazonaws.com/cloudformation-examples/aws-cfn-bootstrap-py3-latest.tar.gz\n"
            },
            "AmazonLinux2": {
                "HelperInstallationCommands": "export PATH=\"$PATH:/opt/aws/bin\"\n"
            }
        }
    },
    "Resources": {
        "IAMInstanceProfile": {
            "Type": "AWS::IAM::InstanceProfile",
            "Properties": {
                "InstanceProfileName": {
                    "Ref": "KeyPair"
                },
                "Path": "/setup-ipsec-vpn/",
                "Roles": [
                    {
                        "Ref": "S3ExecutionRole"
                    }
                ]
            },
            "DependsOn": [
                "S3ExecutionRole",
                "KeyPair"
            ]
        },
        "Ikev2S3Bucket": {
            "Type": "AWS::S3::Bucket",
            "DeletionPolicy": "Retain",
            "Properties": {
                "PublicAccessBlockConfiguration": {
                    "BlockPublicAcls": false,
                    "BlockPublicPolicy": false,
                    "IgnorePublicAcls": false,
                    "RestrictPublicBuckets": false
                },
                "LifecycleConfiguration": {
                    "Rules": [
                        {
                            "Id": "DeletionAfterOneDay",
                            "Status": "Enabled",
                            "ExpirationInDays": 1
                        }
                    ]
                },
                "BucketName": {
                    "Fn::GetAtt": [
                        "KeyPairDisplayFunctionInfo",
                        "Combination"
                    ]
                }
            },
            "Metadata": {},
            "DependsOn": [
                "KeyPair"
            ]
        },
        "OpenBucketPolicy": {
            "Type": "AWS::S3::BucketPolicy",
            "Properties": {
                "Bucket": {
                    "Ref": "Ikev2S3Bucket"
                },
                "PolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": "*",
                            "Action": "s3:GetObject",
                            "Resource": {
                                "Fn::Join": [
                                    "",
                                    [
                                        "arn:aws:s3:::",
                                        {
                                            "Ref": "Ikev2S3Bucket"
                                        },
                                        "/*"
                                    ]
                                ]
                            }
                        }
                    ]
                }
            }
        },
        "VpnVpc": {
            "Type": "AWS::EC2::VPC",
            "Properties": {
                "CidrBlock": "10.0.0.0/24"
            },
            "Metadata": {}
        },
        "VpnSubnet": {
            "Type": "AWS::EC2::Subnet",
            "Properties": {
                "VpcId": {
                    "Ref": "VpnVpc"
                },
                "CidrBlock": "10.0.0.0/24",
                "MapPublicIpOnLaunch": true,
                "AvailabilityZone": {
                    "Fn::Select": [
                        "0",
                        {
                            "Fn::GetAZs": ""
                        }
                    ]
                }
            },
            "Metadata": {},
            "DependsOn": [
                "VpnVpc",
                "VpcInternetGateway"
            ]
        },
        "VpnRouteTable": {
            "Type": "AWS::EC2::RouteTable",
            "Properties": {
                "VpcId": {
                    "Ref": "VpnVpc"
                }
            },
            "Metadata": {},
            "DependsOn": [
                "VpnSubnet"
            ]
        },
        "PublicInternetRoute": {
            "Type": "AWS::EC2::Route",
            "Properties": {
                "DestinationCidrBlock": "0.0.0.0/0",
                "RouteTableId": {
                    "Ref": "VpnRouteTable"
                },
                "GatewayId": {
                    "Ref": "VpcInternetGateway"
                }
            },
            "Metadata": {},
            "DependsOn": [
                "VpnRouteTable",
                "VpcInternetGateway",
                "InternetGatewayAttachment"
            ]
        },
        "VpnInstance": {
            "Type": "AWS::EC2::Instance",
            "CreationPolicy": {
                "ResourceSignal": {
                    "Timeout": "PT15M"
                }
            },
            "Properties": {
                "IamInstanceProfile": {
                    "Ref": "IAMInstanceProfile"
                },
                "UserData": {
                    "Fn::Base64": {
                        "Fn::Join": [
                            "",
                            [
                                "#!/bin/bash -xe\n",
                                "trap 'cfn-signal -e 1 ",
                                " --stack ",
                                {
                                    "Ref": "AWS::StackName"
                                },
                                " --resource VpnInstance ",
                                " --region ",
                                {
                                    "Ref": "AWS::Region"
                                },
                                "' ERR\n",
                                "sleep 60\n",
                                {
                                    "Fn::FindInMap": [
                                        "OS",
                                        {
                                            "Ref": "OS"
                                        },
                                        "HelperInstallationCommands"
                                    ]
                                },
                                "export VPN_IPSEC_PSK='",
                                {
                                    "Ref": "VpnIpsecPsk"
                                },
                                "'\n",
                                "export VPN_USER='",
                                {
                                    "Ref": "VpnUser"
                                },
                                "'\n",
                                "export VPN_PASSWORD='",
                                {
                                    "Ref": "VpnPassword"
                                },
                                "'\n",
                                "wget -t 3 -T 30 -nv -O vpn.sh https://github.com/hwdsl2/setup-ipsec-vpn/raw/master/vpnsetup.sh\n",
                                "sh vpn.sh\n",
                                "mkdir /root/profiles\n",
                                "cp /root/vpnclient* /root/profiles\n",
                                "cd /root/ && zip -er --password '",
                                {
                                    "Ref": "VpnPassword"
                                },
                                "' profiles.zip ./profiles\n",
                                "aws s3 cp /root/profiles.zip s3://",
                                {
                                    "Ref": "Ikev2S3Bucket"
                                },
                                "/\n",
                                "cfn-signal -e 0 ",
                                " --stack ",
                                {
                                    "Ref": "AWS::StackName"
                                },
                                " --resource VpnInstance ",
                                " --region ",
                                {
                                    "Ref": "AWS::Region"
                                },
                                "\n"
                            ]
                        ]
                    }
                },
                "SecurityGroupIds": [
                    {
                        "Fn::GetAtt": [
                            "VpnSecurityGroup",
                            "GroupId"
                        ]
                    }
                ],
                "SubnetId": {
                    "Ref": "VpnSubnet"
                },
                "AvailabilityZone": {
                    "Fn::Select": [
                        "0",
                        {
                            "Fn::GetAZs": ""
                        }
                    ]
                },
                "InstanceType": {
                    "Ref": "InstanceType"
                },
                "KeyName": {
                    "Ref": "KeyPair"
                },
                "ImageId": {
                    "Fn::GetAtt": [
                        "AMIInfo",
                        "AMIId"
                    ]
                }
            },
            "Metadata": {},
            "DependsOn": [
                "VpnRouteTable",
                "KeyPair",
                "AMIInfoFunction",
                "VpnSecurityGroup",
                "Ikev2S3Bucket",
                "IAMInstanceProfile"
            ]
        },
        "KeyPair": {
            "Type": "AWS::EC2::KeyPair",
            "Properties": {
                "KeyName": {
                    "Fn::Join": [
                        "-",
                        [
                            "setup-ipsec-vpn",
                            {
                                "Ref": "AWS::StackName"
                            }
                        ]
                    ]
                }
            }
        },
        "VpnSecurityGroup": {
            "Type": "AWS::EC2::SecurityGroup",
            "Properties": {
                "GroupDescription": "The VPN Security Group, allowing ingress UDP traffic at port 4500 and 500.",
                "GroupName": "VpnSecurityGroup",
                "VpcId": {
                    "Ref": "VpnVpc"
                },
                "SecurityGroupIngress": [
                    {
                        "CidrIp": "0.0.0.0/0",
                        "IpProtocol": "tcp",
                        "FromPort": 22,
                        "ToPort": 22
                    },
                    {
                        "CidrIp": "0.0.0.0/0",
                        "IpProtocol": "udp",
                        "FromPort": 500,
                        "ToPort": 500
                    },
                    {
                        "CidrIp": "0.0.0.0/0",
                        "IpProtocol": "udp",
                        "FromPort": 4500,
                        "ToPort": 4500
                    }
                ],
                "SecurityGroupEgress": [
                    {
                        "CidrIp": "0.0.0.0/0",
                        "IpProtocol": -1
                    }
                ]
            },
            "Metadata": {}
        },
        "VpcInternetGateway": {
            "Type": "AWS::EC2::InternetGateway",
            "Properties": {},
            "Metadata": {},
            "DependsOn": [
                "VpnVpc"
            ]
        },
        "SubnetRouteTableAssociation": {
            "Type": "AWS::EC2::SubnetRouteTableAssociation",
            "Properties": {
                "RouteTableId": {
                    "Ref": "VpnRouteTable"
                },
                "SubnetId": {
                    "Ref": "VpnSubnet"
                }
            },
            "Metadata": {}
        },
        "KeyPairDisplayFunction": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Handler": "index.handler",
                "Runtime": "python3.8",
                "Role": {
                    "Fn::GetAtt": [
                        "LambdaExecutionRole",
                        "Arn"
                    ]
                },
                "Code": {
                    "ZipFile": {
                        "Fn::Join": [
                            "\n",
                            [
                                "import boto3",
                                "import cfnresponse",
                                "import string",
                                "import random",
                                "import traceback",
                                "'''",
                                "This python program should be embedded into its designated cloudformation",
                                "template as the inline code of one of the lambda functions.",
                                "Its function is to create a random combination of 20 characters for the naming of the Ikev2S3Bucket, and",
                                "to retrieve the private key material for display under the Outputs tab.",
                                "'''",
                                "def handler(event, context):",
                                "   try:",
                                "       if event['RequestType'] == 'Delete':",
                                "           cfnresponse.send(event, context, cfnresponse.SUCCESS, {})",
                                "       elif event['RequestType'] == 'Create':",
                                "           rCombination = 'setup-ipsec-vpn-' + ''.join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(20)).lower()",
                                "           region = event['ResourceProperties']['Region']",
                                "           ssm = boto3.client('ssm',region)",
                                "           response = ssm.get_parameter(",
                                {
                                    "Fn::Join": [
                                        "",
                                        [
                                            "               Name='/ec2/keypair/",
                                            {
                                                "Fn::GetAtt": [
                                                    "KeyPair",
                                                    "KeyPairId"
                                                ]
                                            },
                                            "',"
                                        ]
                                    ]
                                },
                                "               WithDecryption=True",
                                "           )",
                                "           keyMaterial = response['Parameter']['Value']",
                                "           cfnresponse.send(event, context, cfnresponse.SUCCESS, {'KeyMaterial':keyMaterial, 'Combination':rCombination}, 'KeyPairDisplayFunctionInfo')",
                                "   except Exception as e:",
                                "       cfnresponse.send(event, context, cfnresponse.FAILED, {'ErrorMsg':traceback.format_exc()})"
                            ]
                        ]
                    }
                },
                "Timeout": 30
            },
            "Metadata": {},
            "DependsOn": [
                "LambdaExecutionRole",
                "KeyPair"
            ]
        },
        "KeyPairDisplayFunctionInfo": {
            "Type": "Custom::KeyPairDisplayFunctionInfo",
            "Properties": {
                "Region": {
                    "Ref": "AWS::Region"
                },
                "ServiceToken": {
                    "Fn::GetAtt": [
                        "KeyPairDisplayFunction",
                        "Arn"
                    ]
                }
            },
            "Metadata": {},
            "DependsOn": [
                "KeyPairDisplayFunction",
                "KeyPair"
            ]
        },
        "AMIInfo": {
            "Type": "Custom::AMIInfo",
            "Properties": {
                "Region": {
                    "Ref": "AWS::Region"
                },
                "ServiceToken": {
                    "Fn::GetAtt": [
                        "AMIInfoFunction",
                        "Arn"
                    ]
                },
                "Distribution": {
                    "Ref": "OS"
                }
            },
            "Metadata": {},
            "DependsOn": [
                "AMIInfoFunction"
            ]
        },
        "AMIInfoFunction": {
            "Type": "AWS::Lambda::Function",
            "Properties": {
                "Handler": "index.handler",
                "Runtime": "python3.8",
                "Role": {
                    "Fn::GetAtt": [
                        "LambdaExecutionRole",
                        "Arn"
                    ]
                },
                "Code": {
                    "ZipFile": {
                        "Fn::Join": [
                            "\n",
                            [
                                "import boto3",
                                "import cfnresponse",
                                "import traceback",
                                "'''",
                                "This python script should be embeded into its designated cloudformation template.",
                                "Its function is to sort out the correct AMI image to use for each of the distribution options available.",
                                "'''",
                                "def creation_date(e):",
                                "   return e['CreationDate']",
                                "",
                                "def handler(event, context):",
                                "   try:",
                                "       regionName = event['ResourceProperties']['Region']",
                                "       distribution = event['ResourceProperties']['Distribution']",
                                "       ec2 = boto3.client('ec2',regionName)",
                                "       AMIName = {",
                                "           'Ubuntu2004': 'ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*',",
                                "           'Ubuntu2204': 'ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*',",
                                "           'Debian10': 'debian-10-amd64-*',",
                                "           'Debian11': 'debian-11-amd64-*',",
                                "           'Debian12': 'debian-12-amd64-*',",
                                "           'CentOS7': 'CentOS Linux 7 x86_64 - *',",
                                "           'AmazonLinux2': 'amzn2-ami-hvm-*.*-x86_64-gp2',",
                                "       }[distribution]",
                                "       response = ec2.describe_images(Filters=[{'Name':'name', 'Values':[AMIName]}], Owners=['099720109477', '136693071363', '125523088429', 'amazon'])",
                                "       images = response['Images']",
                                "       images.sort(key=creation_date,reverse=True)",
                                "       AMIId = images[0]['ImageId']",
                                "       cfnresponse.send(event, context, cfnresponse.SUCCESS, {'AMIId':AMIId}, 'AMIInfo')",
                                "   except Exception:",
                                "       cfnresponse.send(event, context, cfnresponse.FAILED, {'ErrorMsg':traceback.format_exc()})"
                            ]
                        ]
                    }
                },
                "Timeout": 30
            },
            "Metadata": {},
            "DependsOn": [
                "LambdaExecutionRole"
            ]
        },
        "LambdaExecutionRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": "lambda.amazonaws.com"
                            },
                            "Action": [
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "Path": "/",
                "Policies": [
                    {
                        "PolicyName": "root",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": "*",
                                    "Resource": "*"
                                }
                            ]
                        }
                    }
                ]
            },
            "Metadata": {}
        },
        "S3ExecutionRole": {
            "Type": "AWS::IAM::Role",
            "Properties": {
                "AssumeRolePolicyDocument": {
                    "Version": "2012-10-17",
                    "Statement": [
                        {
                            "Effect": "Allow",
                            "Principal": {
                                "Service": [
                                    "ec2.amazonaws.com"
                                ]
                            },
                            "Action": [
                                "sts:AssumeRole"
                            ]
                        }
                    ]
                },
                "Path": "/",
                "Policies": [
                    {
                        "PolicyName": "s3-bucket-specific-policy",
                        "PolicyDocument": {
                            "Version": "2012-10-17",
                            "Statement": [
                                {
                                    "Effect": "Allow",
                                    "Action": "s3:PutObject",
                                    "Resource": [
                                        {
                                            "Fn::Join": [
                                                "",
                                                [
                                                    {
                                                        "Fn::GetAtt": [
                                                            "Ikev2S3Bucket",
                                                            "Arn"
                                                        ]
                                                    },
                                                    "/*"
                                                ]
                                            ]
                                        }
                                    ]
                                }
                            ]
                        }
                    }
                ]
            },
            "Metadata": {}
        },
        "InternetGatewayAttachment": {
            "Type": "AWS::EC2::VPCGatewayAttachment",
            "Properties": {
                "InternetGatewayId": {
                    "Ref": "VpcInternetGateway"
                },
                "VpcId": {
                    "Ref": "VpnVpc"
                }
            },
            "Metadata": {}
        }
    },
    "Parameters": {
        "VpnUser": {
            "Type": "String",
            "Description": "Your VPN username"
        },
        "VpnIpsecPsk": {
            "Type": "String",
            "Description": "Your VPN IPsec PSK (pre-shared key)"
        },
        "VpnPassword": {
            "Type": "String",
            "Description": "Your VPN password"
        },
        "OS": {
            "Type": "String",
            "Description": "The OS of your VPN server. Default: Ubuntu 22.04",
            "Default": "Ubuntu2204",
            "AllowedValues": [
                "Ubuntu2004",
                "Ubuntu2204",
                "Debian10",
                "Debian11",
                "Debian12",
                "CentOS7",
                "AmazonLinux2"
            ]
        },
        "InstanceType": {
            "Type": "String",
            "Description": "The instance type of your VPN server. Using t2.micro or t3.micro may qualify for the AWS Free Tier.",
            "AllowedValues": [
                "t2.micro",
                "t3.nano",
                "m5.large",
                "t3.micro",
                "t3.small",
                "t2.nano",
                "t2.small",
                "t3a.nano",
                "t3a.micro",
                "t3a.small",
                "m5a.large",
                "t1.micro"
            ],
            "Default": "t2.micro"
        }
    },
    "Outputs": {
        "1VPNAddress": {
            "Description": "This is the public IP of your newly-launched VPN server.",
            "Value": {
                "Fn::GetAtt": [
                    "VpnInstance",
                    "PublicIp"
                ]
            }
        },
        "2VPNUsername": {
            "Description": "Your VPN username",
            "Value": {
                "Ref": "VpnUser"
            }
        },
        "3VPNPassword": {
            "Description": "Your VPN password",
            "Value": {
                "Ref": "VpnPassword"
            }
        },
        "4VPNKey": {
            "Description": "Your VPN IPsec PSK (pre-shared key)",
            "Value": {
                "Ref": "VpnIpsecPsk"
            }
        },
        "5EC2PrivateKeyId": {
            "Description": "The ID of the key pair created. For more information regarding how to retrieve the private key for authentication, please refer to: https://github.com/hwdsl2/setup-ipsec-vpn/blob/master/aws/README.md#faqs",
            "Value": {
                "Fn::GetAtt": [
                    "KeyPair",
                    "KeyPairId"
                ]
            }
        },
        "6EC2PrivateKeyMaterial": {
            "Description": "The content of your private key for accessing the VPN server via SSH. Save it as a file for use when connecting.",
            "Value": {
                "Fn::GetAtt": [
                    "KeyPairDisplayFunctionInfo",
                    "KeyMaterial"
                ]
            }
        },
        "7NextStep": {
            "Description": "Learn how to configure VPN clients.",
            "Value": "https://github.com/hwdsl2/setup-ipsec-vpn#next-steps"
        },
        "8WarningForDebianUsers": {
            "Description": "Please be noted that due to Debian images on AWS EC2 using cloud kernels, you are unable to use IPSec/L2TP mode if your server is running Debian. For more information, please refer to the link to the left.",
            "Value": "https://github.com/hwdsl2/setup-ipsec-vpn/blob/master/docs/clients.md#debian-kernel"
        },
        "9RetrieveYourIkev2Credentials": {
            "Description": "Please use the following link to download your IKEv2 connection credentials. The password to the ZIP file that stores the credentials, is the same password used to connect to your VPN server. The download link for the credentials will expire in ONE day.",
            "Value": {
                "Fn::Join": [
                    "",
                    [
                        "https://",
                        {
                            "Fn::GetAtt": [
                                "Ikev2S3Bucket",
                                "RegionalDomainName"
                            ]
                        },
                        "/profiles.zip"
                    ]
                ]
            }
        }
    }
}
