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"