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: