AWSTemplateFormatVersion: 2010-09-09 Description: Cloudformation template Metadata: AWS::CloudFormation::Interface: ParameterGroups: - Label: default: "ACM Configuration" Parameters: - ACMarn - Label: default: "Network Configuration" Parameters: - VpcID - PublicSubnet - PrivateSubnet1 - PrivateSubnet2 - WorkstationIP - Label: default: "S3 Bucket details" Parameters: - S3BucketName - KeyObject - CertObject ParameterLabels: ACMarn: default: "ACM arn value" S3BucketName: default: "Enter the S3 bucket name." VPCID: default: "Which VPC should this be deployed to?" PublicSubnet: default: "Enter the public subnet ID" PrivateSubnet1: default: "Enter the private subnet ID (1st subnet)" PrivateSubnet2: default: "Enter the private subnet ID (2nd subnet)" Parameters: WorkstationIP: Type: String Description: IP Address of your workstation/laptop (Add /32 to your actual IP address). AllowedPattern: (\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/32 ConstraintDescription: Must be a valid IP address of the form x.x.x.x/32. ACMarn: Description: Please give the ARN of your ACM certificate. Type: String VpcID: Type: AWS::EC2::VPC::Id Description: Enter a valid VPC ID where your DocumentDB cluster resides. PublicSubnet: Type: AWS::EC2::Subnet::Id Description: Enter the public subnet ID from your VPC. PrivateSubnet1: Type: AWS::EC2::Subnet::Id Description: Enter the first private subnet ID from your VPC. PrivateSubnet2: Type: AWS::EC2::Subnet::Id Description: Enter the second private subnet ID from your VPC. S3BucketName: Type: String Description: The S3 bucket where you have uploaded the cert and key. KeyObject: Type: String Description: Enter the S3 object location for the private Key. CertObject: Type: String Description: Enter the S3 object location for the certificate. Outputs: S3BucketName: Description: The S3 bucket where your configuration file is downloaded. Value: !Ref S3BucketName Resources: VPNSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: Allow http to client host VpcId: Ref: VpcID SecurityGroupIngress: - IpProtocol: -1 FromPort: 0 ToPort: 65535 CidrIp: !Ref WorkstationIP SecurityGroupEgress: - IpProtocol: -1 FromPort: 0 ToPort: 65535 CidrIp: 0.0.0.0/0 AWSLambdaExecutionRole: Type: AWS::IAM::Role Properties: AssumeRolePolicyDocument: Statement: - Action: - sts:AssumeRole Effect: Allow Principal: Service: - lambda.amazonaws.com Version: '2012-10-17' Path: "/" Policies: - PolicyDocument: Statement: - Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Effect: Allow Resource: arn:aws:logs:*:*:* Version: '2012-10-17' PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-CW - PolicyDocument: Statement: - Action: - s3:PutObject - s3:DeleteObject - s3:GetObject - s3:List* Effect: Allow Resource: - !Sub arn:aws:s3:::${S3BucketName}/* - !Sub arn:aws:s3:::${S3BucketName} Version: '2012-10-17' PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-S3 - PolicyDocument: Statement: - Action: - ec2:DeleteClientVpnEndpoint - ec2:ModifyClientVpnEndpoint - ec2:AssociateClientVpnTargetNetwork - ec2:DisassociateClientVpnTargetNetwork - ec2:ApplySecurityGroupsToClientVpnTargetNetwork - ec2:AuthorizeClientVpnIngress - ec2:CreateClientVpnRoute - ec2:DeleteClientVpnRoute - ec2:RevokeClientVpnIngress - ec2:ExportClientVpnClientConfiguration Effect: Allow Resource: - arn:aws:ec2:*:*:client-vpn-endpoint/* Condition: StringEquals: "ec2:ResourceTag/Purpose": "DocumentDB-Private-Connectivity" Version: '2012-10-17' PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-EC2 - PolicyDocument: Statement: - Action: - acm:* Effect: Allow Resource: - !Ref ACMarn Version: '2012-10-17' PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-acm RoleName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambdaExecutionRole S3CustomResource: DependsOn: myRoute Type: Custom::S3CustomResource Properties: ServiceToken: !GetAtt AWSLambdaFunction.Arn the_bucket: !Ref S3BucketName the_clientVPN_endpointID: !Ref myClientVpnEndpoint the_acm_certificate: !Ref ACMarn the_cert_object: !Ref CertObject the_key_object: !Ref KeyObject AWSLambdaFunction: DependsOn: myRoute Type: "AWS::Lambda::Function" Properties: Description: "Export the client configuration and certificate and put it in S3 bucket for download and use!" FunctionName: !Sub '${AWS::StackName}-${AWS::Region}-lambda' Handler: index.handler Role: !GetAtt AWSLambdaExecutionRole.Arn Timeout: 360 Runtime: python3.9 Code: ZipFile: | import boto3 import json import cfnresponse from io import BytesIO def handler(event, context): # Init ... the_event = event['RequestType'] print("The event is: ", str(the_event)) response_data = {} s3 = boto3.client('s3') clientVPN = boto3.client('ec2') try: if the_event in ('Create', 'Update'): # Retrieve parameters the_bucket = event['ResourceProperties']['the_bucket'] the_clientVPN_endpointID = event['ResourceProperties']['the_clientVPN_endpointID'] the_acm_certificate = event['ResourceProperties']['the_acm_certificate'] the_cert_object = event['ResourceProperties']['the_cert_object'] the_key_object = event['ResourceProperties']['the_key_object'] clientVPN = clientVPN.export_client_vpn_client_configuration(ClientVpnEndpointId=the_clientVPN_endpointID, DryRun=False) result = json.dumps(clientVPN, indent=2) load = json.loads(result) file = open('/tmp/clientconfig', 'w') file.write(load['ClientConfiguration']) file.write("\n") file.close() file = open('/tmp/certificate', 'wb') file.write("".encode()) file.write("\n".encode()) file.write("-----BEGIN CERTIFICATE-----".encode()) file.write("\n".encode()) session = boto3.Session() s3_client = session.client("s3") f = BytesIO() s3_client.download_fileobj(the_bucket, the_cert_object, f) sub1 = "-----BEGIN CERTIFICATE-----" sub2 = "-----END CERTIFICATE-----" # getting index of substrings idx1 = f.getvalue().decode().index(sub1) idx2 = f.getvalue().decode().index(sub2) # length of substring 1 is added to # get string from next character res = f.getvalue().decode()[idx1 + len(sub1) + 1: idx2] file.write(res.encode()) file.write("-----END CERTIFICATE-----".encode()) file.write("\n".encode()) file.write("".encode()) file.write("\n".encode()) file.close() file = open('/tmp/privatekey','wb') file.write("".encode()) file.write("\n".encode()) session = boto3.Session() s3_client = session.client("s3") f = BytesIO() s3_client.download_fileobj(the_bucket, the_key_object, f) print(f.getvalue()) file.write(f.getvalue()) file.write("\n".encode()) file.write("".encode()) file.close() # Read in the file with open('/tmp/clientconfig', 'r') as file : filedata = file.read() # Replace the target string filedata = filedata.replace('remote ', 'remote append.') # Write the file out again with open('/tmp/clientconfig', 'w') as file: file.write(filedata) filenames = ['/tmp/clientconfig', '/tmp/certificate', '/tmp/privatekey'] with open('/tmp/clientVPN.ovpn', 'wb') as outfile: for fname in filenames: with open(fname) as infile: outfile.write(infile.read().encode()) filename = '/tmp/clientVPN.ovpn' file_obj = open(filename, 'rb') s3_upload = s3.put_object( Bucket=the_bucket, Key="Client.ovpn", Body=file_obj) cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) elif the_event == 'Delete': # Everything OK... send the signal back print("Operation successful!") cfnresponse.send(event, context, cfnresponse.SUCCESS, response_data) except Exception as e: print("Operation failed...") print(str(e)) response_data['Data'] = str(e) cfnresponse.send(event, context, cfnresponse.FAILED, response_data) myClientVpnEndpoint: Type: AWS::EC2::ClientVpnEndpoint Properties: AuthenticationOptions: - Type: "certificate-authentication" MutualAuthentication: ClientRootCertificateChainArn: !Ref ACMarn ClientCidrBlock: "10.244.0.0/22" ConnectionLogOptions: Enabled: false Description: "VPN Local Access to Amazon DocumentDB" ServerCertificateArn: !Ref ACMarn VpcId: !Ref VpcID SecurityGroupIds: - !Ref VPNSecurityGroup TagSpecifications: - ResourceType: "client-vpn-endpoint" Tags: - Key: "Purpose" Value: "DocumentDB-Private-Connectivity" TransportProtocol: "udp" PublicSubnetAssociation: DependsOn: myClientVpnEndpoint Type: "AWS::EC2::ClientVpnTargetNetworkAssociation" Properties: ClientVpnEndpointId: Ref: myClientVpnEndpoint SubnetId: Ref: PublicSubnet myPrivateSubnetAssociation1: DependsOn: myClientVpnEndpoint Type: "AWS::EC2::ClientVpnTargetNetworkAssociation" Properties: ClientVpnEndpointId: Ref: myClientVpnEndpoint SubnetId: Ref: PrivateSubnet1 myPrivateSubnetAssociation2: DependsOn: myClientVpnEndpoint Type: "AWS::EC2::ClientVpnTargetNetworkAssociation" Properties: ClientVpnEndpointId: Ref: myClientVpnEndpoint SubnetId: Ref: PrivateSubnet2 myAuthRule: DependsOn: myClientVpnEndpoint Type: "AWS::EC2::ClientVpnAuthorizationRule" Properties: ClientVpnEndpointId: Ref: myClientVpnEndpoint AuthorizeAllGroups: true TargetNetworkCidr: "0.0.0.0/0" Description: "myAuthRule" myRoute: DependsOn: PublicSubnetAssociation Type: "AWS::EC2::ClientVpnRoute" Properties: ClientVpnEndpointId: Ref: myClientVpnEndpoint TargetVpcSubnetId: Ref: PublicSubnet DestinationCidrBlock: "0.0.0.0/0" Description: "myRoute" myPrivRoute1: DependsOn: myPrivateSubnetAssociation1 Type: "AWS::EC2::ClientVpnRoute" Properties: ClientVpnEndpointId: Ref: myClientVpnEndpoint TargetVpcSubnetId: Ref: PrivateSubnet1 DestinationCidrBlock: "0.0.0.0/0" Description: "myRoute" myPrivRoute2: DependsOn: myPrivateSubnetAssociation2 Type: "AWS::EC2::ClientVpnRoute" Properties: ClientVpnEndpointId: Ref: myClientVpnEndpoint TargetVpcSubnetId: Ref: PrivateSubnet2 DestinationCidrBlock: "0.0.0.0/0" Description: "myRoute"