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..85fa930 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\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\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") diff --git a/template.yaml b/template.yaml index ec0b730..7d5cb85 100644 --- a/template.yaml +++ b/template.yaml @@ -27,6 +27,16 @@ 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 + Default: true + AllowedValues: + - true + - false Resources: @@ -37,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: @@ -61,6 +85,9 @@ Resources: Value: Ref: EnvironmentName TransportProtocol: udp + SplitTunnel: !Ref SplitTunel + SecurityGroupIds: + - !Ref ClientVPNSecurityGroup Type: AWS::EC2::ClientVpnEndpoint ClientVpnTargetNetworkAssociation: