From 559529d655066cf09ff0a1fce076488bb681fd7d Mon Sep 17 00:00:00 2001 From: Yurii Bohdan Date: Tue, 18 Feb 2025 10:12:49 +0200 Subject: [PATCH 1/3] fix minor bugs; add imporvements --- LICENSE | 23 +++++++++++++++++++++++ client-vpn.py | 23 +++++++++++++++++++---- get-vpn-config.py | 29 +++++++++++++++++++---------- 3 files changed, 61 insertions(+), 14 deletions(-) diff --git a/LICENSE b/LICENSE index e69de29..2d15cb7 100644 --- a/LICENSE +++ b/LICENSE @@ -0,0 +1,23 @@ +MIT License + + +Copyright (c) 2019 Gus Vine +Copyright (c) 2025 Yurii Bohdan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/client-vpn.py b/client-vpn.py index d2ef60d..d9db550 100755 --- a/client-vpn.py +++ b/client-vpn.py @@ -11,6 +11,8 @@ def load_byte_file(filename): return open(filename, "rb").read() def import_certificate(cert,key,ca): + if args.region: + boto3.setup_default_session(region_name=args.region) acm = boto3.client('acm') response = acm.import_certificate( Certificate=load_byte_file(cert), @@ -20,6 +22,8 @@ def import_certificate(cert,key,ca): return response['CertificateArn'] def tag_certificate(arn,tags=[]): + if args.region: + boto3.setup_default_session(region_name=args.region) acm = boto3.client('acm') response = acm.add_tags_to_certificate( CertificateArn=arn, @@ -48,6 +52,9 @@ def tag_certificate(arn,tags=[]): parser.add_argument("--cidr", help="CIDR the vpn will give to the clients", action="store", required=False) +parser.add_argument("--region", help="set the region for the vpn", + action="store", required=False) + args = parser.parse_args() if not args.client_cn: @@ -57,12 +64,16 @@ def tag_certificate(arn,tags=[]): log.setLevel(logging.DEBUG) log.debug('set log level to verbose') -docker_exists = subprocess.call(['which', 'docker']) - -if docker_exists != 0: +docker_exists = subprocess.run(['which', 'docker'], capture_output=True) +if docker_exists.stdout == b'': log.error("docker command does not exist in your path. Please start or install docker to use this script!") exit(1) +docker_running = subprocess.run(['docker', 'info'], capture_output=True) +if docker_running.returncode != 0: + log.error("docker is not running. Please start docker to use this script!") + exit(1) + docker_run = ["docker", "run", "-it", "--rm"] docker_run.append(f"-e EASYRSA_REQ_CN={args.server_cn}") docker_run.append(f"-e EASYRSA_CLIENT_CN={args.client_cn}") @@ -108,6 +119,9 @@ def tag_certificate(arn,tags=[]): with open('template.yaml', 'r') as file: template = file.read() +if args.region: + log.info(f"Setting region to {args.region}") + boto3.setup_default_session(region_name=args.region) cloudformation = boto3.client('cloudformation') stack_name = f"{args.name}-vpn" @@ -128,8 +142,9 @@ def tag_certificate(arn,tags=[]): waiter_config = {'Delay': 5,'MaxAttempts': 720} try: waiter.wait(StackName=stack_name, WaiterConfig=waiter_config) -except botocore.exceptions.WaiterError as ex: +except exceptions.WaiterError as ex: log.error(f"failed to create stack {stack_name}", exc_info=ex) + exit(1) log.info(f"Cloudformation stack {stack_name} has successfully completed") log.info("Run ./get-vpn-config.py to download your client config file") diff --git a/get-vpn-config.py b/get-vpn-config.py index 2f1d494..4b3b42e 100755 --- a/get-vpn-config.py +++ b/get-vpn-config.py @@ -1,7 +1,7 @@ #!/usr/bin/env python import argparse -import subprocess import boto3 +import glob import logging import os import random @@ -27,6 +27,7 @@ def randomString(stringLength=10): action="store", required=False) args = parser.parse_args() +print("DEBUG: args.region: ", args.region) if not args.region: try: @@ -34,14 +35,13 @@ def randomString(stringLength=10): except KeyError as ex: log.error('Set the aws region using --region or environment variable AWS_REGION') exit(1) - if args.verbose: log.setLevel(logging.DEBUG) log.debug('set log level to verbose') -client = boto3.client('ec2') +client = boto3.client('ec2', region_name=args.region) -server_name = f"{args.name}-ClientVpn" +server_name = f"{args.name}" server = client.describe_client_vpn_endpoints( MaxResults=5, Filters=[ @@ -64,14 +64,23 @@ def randomString(stringLength=10): config = vpn_config['ClientConfiguration'] config = re.sub(rf"{id}.*",rf"{randomString()}.{id}.prod.clientvpn.{args.region}.amazonaws.com 443",config) -config = config + f"\n\ncert /path/client1.domain.tld.crt" -config = config + f"\nkey /path/client1.domain.tld.key\n" -config_file = f"output/{id}.ovpn" +client_cert_file = glob.glob(f'output/*{args.name}*.crt')[0] +with open(client_cert_file, 'r') as file: + client_cert_file_content = file.read() +regex = re.compile(r'-----BEGIN CERTIFICATE-----.*', re.S) +client_cert = regex.search(client_cert_file_content).group(0) +config = config + f"\n{client_cert}\n\n" + +client_key_file = glob.glob(f'output/*{args.name}*.key')[0] +with open(client_key_file, 'r') as file: + client_key_file_content = file.read() +config = config + f"\n{client_key_file_content}\n\n" -file = open(config_file, 'w') -file.write(config) -file.close() + +config_file = f"output/{id}.ovpn" +with open(config_file, 'w') as file: + file.write(config) log.info(f"Created config file {config_file}") log.info("Please copy the config along with the client certificate a key to a secure location in your computer") From ead1cb14495076af0377d996e49fe7e848014a26 Mon Sep 17 00:00:00 2001 From: Yurii Bohdan Date: Tue, 18 Feb 2025 16:07:15 +0200 Subject: [PATCH 2/3] feat: split traffic by default --- get-vpn-config.py | 4 ++-- template.yaml | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/get-vpn-config.py b/get-vpn-config.py index 4b3b42e..85fa930 100755 --- a/get-vpn-config.py +++ b/get-vpn-config.py @@ -70,12 +70,12 @@ def randomString(stringLength=10): client_cert_file_content = file.read() regex = re.compile(r'-----BEGIN CERTIFICATE-----.*', re.S) client_cert = regex.search(client_cert_file_content).group(0) -config = config + f"\n{client_cert}\n\n" +config = config + f"\n\n{client_cert}\n\n" client_key_file = glob.glob(f'output/*{args.name}*.key')[0] with open(client_key_file, 'r') as file: client_key_file_content = file.read() -config = config + f"\n{client_key_file_content}\n\n" +config = config + f"\n\n{client_key_file_content}\n\n" config_file = f"output/{id}.ovpn" diff --git a/template.yaml b/template.yaml index ec0b730..06a60fc 100644 --- a/template.yaml +++ b/template.yaml @@ -27,6 +27,13 @@ Parameters: AssociationSubnetId: Description: The subnet to assciate the VPN endpoint with Type: AWS::EC2::Subnet::Id + SplitTunel: + Description: Enable split tunel + Type: String + Default: true + AllowedValues: + - true + - false Resources: @@ -61,6 +68,7 @@ Resources: Value: Ref: EnvironmentName TransportProtocol: udp + SplitTunnel: !Ref SplitTunel Type: AWS::EC2::ClientVpnEndpoint ClientVpnTargetNetworkAssociation: From a9ec3becaabc59a2f27efea1181dd9915be08b24 Mon Sep 17 00:00:00 2001 From: Yurii Bohdan Date: Wed, 26 Feb 2025 13:29:51 +0200 Subject: [PATCH 3/3] create own SG --- template.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/template.yaml b/template.yaml index 06a60fc..7d5cb85 100644 --- a/template.yaml +++ b/template.yaml @@ -27,6 +27,9 @@ Parameters: AssociationSubnetId: Description: The subnet to assciate the VPN endpoint with Type: AWS::EC2::Subnet::Id + AssociationVPCId: + Description: The VPC to associate the VPN endpoint with + Type: AWS::EC2::VPC::Id SplitTunel: Description: Enable split tunel Type: String @@ -44,6 +47,20 @@ Resources: RetentionInDays: 30 Type: AWS::Logs::LogGroup + ClientVPNSecurityGroup: + Type: AWS::EC2::SecurityGroup + Properties: + Name: ClientVPNSecurityGroup + GroupDescription: Client VPN Security Group + VpcId: !Ref AssociationVPCId + SecurityGroupIngress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + Description: Allow all traffic + SecurityGroupEgress: + - IpProtocol: -1 + CidrIp: 0.0.0.0/0 + ClientVpnEndpoint: Properties: Description: @@ -69,6 +86,8 @@ Resources: Ref: EnvironmentName TransportProtocol: udp SplitTunnel: !Ref SplitTunel + SecurityGroupIds: + - !Ref ClientVPNSecurityGroup Type: AWS::EC2::ClientVpnEndpoint ClientVpnTargetNetworkAssociation: