def association_is_ready(association_id): """ Use the AWS API to check if the association_id is ready to be used :param association_id: The newly created association_id :return: True if the association_id is ready """ state = State() vpn_endpoint_id = state.get('vpn_endpoint_id') session = boto3.Session(profile_name=state.get('profile'), region_name='us-east-1') ec2_client = session.client('ec2') try: response = ec2_client.describe_client_vpn_endpoints( ClientVpnEndpointIds=[vpn_endpoint_id], ) except Exception as e: print('Failed to describe the client VPN state: %s' % e) return False status = response['ClientVpnEndpoints'][0]['Status']['Code'] if status == 'available': print('AWS Client VPN %s is ready to use!' % vpn_endpoint_id) return True args = (vpn_endpoint_id, status) print('AWS Client VPN %s has status %s' % args) return False
def connect_to_vpn_server(openvpn_filename): state = State() params = OPENVPN_PARAMS[:] params.append('--config %s' % openvpn_filename) openvpn_executable = which('openvpn')[0] cmd = [openvpn_executable] cmd.extend(params) cmd = shlex.split(' '.join(cmd)) process = subprocess.Popen(cmd, close_fds=True) print('OpenVPN client started in process %s' % process.pid) print('VPN connection log is at %s' % OPENVPN_LOG_FILE) state.append('openvpn_pid', process.pid) time.sleep(5) print('\nLast five lines from connection log:') log_lines = tail(open(OPENVPN_LOG_FILE), 5) log_lines = ' '.join(log_lines) print(log_lines) return True
def validate(options): """ :param options: Options passed as command line arguments by the user :return: """ if not is_root(): print('This command requires root privileges on your system in order' ' to run the OpenVPN client in the background.') return False openvpn_executables = which('openvpn') if not openvpn_executables: print('This command requires `openvpn` to be installed in your' ' system.') return False state = State() if not state.dump(): print('The state file is empty. Call `create` first.') return False openvpn_config_file = state.get('openvpn_config_file') if openvpn_config_file is None: print('The `create` command did not save the `openvpn_config_file`' ' attribute to the state file.\n' '\n' 'Try to re-generate the VPN connection by running `purge` and' ' `create`.') return False return True
def status(options): state = State() openvpn_pid = state.get('openvpn_pid') if openvpn_pid is None: print( 'The VPN connection was never initiated. Call the `connect` sub-command' ) return 1 try: p = psutil.Process(openvpn_pid) except psutil.NoSuchProcess: print('The OpenVPN process died! Check the %s log file' % OPENVPN_LOG_FILE) return 1 if p.name() != 'openvpn': print('The OpenVPN process died! Check the %s log file' % OPENVPN_LOG_FILE) return 1 print('The VPN connection is alive') return 0
def purge(options): """ Remove all the AWS resources :param options: Options passed as command line arguments by the user :return: Return code """ state = State() if not state.dump(): print('The state file is empty. Call `create` first.') return 1 # # The opposite of create_aws_resources() # overall_success = True purge_steps = [ delete_client_vpn_endpoint, delete_acm_certs, delete_easy_rsa_install, ] for purge_step in purge_steps: success = purge_step() if not success: overall_success = False if overall_success: state.force({}) return 0
def get_cidr_block(options): """ This is the CIDR block for the VPN clients. We'll be the only ones connecting to this VPN so a /30 is more than enough, but it is very important for us to choose a CIDR block that: * Doesn't overlap with the client's local network (usually 192.168.0.0/24 or 10.0.0.0/16) * Doesn't overlap with any of the CIDR blocks defined in the target account, blocks defined in VPC peerings, etc. Ideally it should be a CIDR block that is adjacent to the VPC CIDR. For example if the VPC has 10.0.0.0/24 we should choose 10.0.1.0/30 to benefit from potential security groups which are allowing access to 10.0.0.0/16. :param options: Options passed as command line arguments by the user :return: True if we were able to find a CIDR block for the VPN client """ state = State() state.append('cidr_block', '10.2.0.0/16') # TODO: Choose a /30 or /29 CIDR block! Larger has more changes of collision print('Using CIDR block %s' % state.get('cidr_block')) return True
def connect(options): """ Connect to the VPN server :param options: Options passed as command line arguments by the user :return: Return code """ if not validate(options): return 1 state = State() openvpn_config_file = state.get('openvpn_config_file') openvpn_config_file = customize_openvpn_config(openvpn_config_file) openvpn_filename = write_config_file(openvpn_config_file) try: connect_to_vpn_server(openvpn_filename) except Exception as e: print('Unexpected exception while connecting to VPN server: %s' % e) os.remove(openvpn_filename) return 1 else: os.remove(openvpn_filename) return 0
def create(options): """ Create the VPN server in the VPC :param options: Options passed as command line arguments by the user :return: Return code :see: https://github.com/aws-quickstart/quickstart-biotech-blueprint/blob/f2e1e76dc8cbc30fd938dd78f0ea5c029c03a9d4/scripts/clientvpnendpoint-customlambdaresource.py#L40 """ state = State() # # Initial checks to increase the chances of success during AWS resource # creation # success = perform_initial_checks(options) if not success: return 1 msg = 'Creating VPN server in AWS account ID %s using %s' args = (state.get('account_id'), state.get('user_arn')) print(msg % args) # # Create the SSL certificates # success = create_ssl_certs(options) if not success: return 1 # # Create the AWS resources. Leave this step to the end in order to reduce # the number of resources to remove if something fails # success = create_aws_resources(options) if not success: return 1 success = wait_for_vpn_creation(options) if not success: return 1 success = download_openvpn_config(options) if not success: return 1 print('\nAWS Client VPN created! Connect using:') print('') print(' sudo ./vpc-vpn-pivot connect') print('') return 0
def add_certs(openvpn_config_file): cert_fmt = '\n\n<cert>\n%s\n</cert>\n' key_fmt = '\n\n<key>\n%s\n</key>\n' state = State() openvpn_config_file += cert_fmt % read_file( state.get('client_crt').encode('utf-8')) openvpn_config_file += key_fmt % read_file( state.get('client_key').encode('utf-8')) return openvpn_config_file
def download_openvpn_config(options): """ Downloads the OpenVPN config file from the Client VPN service and saves it to the state file. :param options: Options passed as command line arguments by the user :return: True if the config was saved to the state """ state = State() session = boto3.Session(profile_name=state.get('profile'), region_name='us-east-1') ec2_client = session.client('ec2') try: response = ec2_client.export_client_vpn_client_configuration( ClientVpnEndpointId=state.get('vpn_endpoint_id') ) except Exception as e: print('Failed to download the client VPN configuration: %s' % e) return False openvpn_config_file = response['ClientConfiguration'] state.append('openvpn_config_file', openvpn_config_file) print('Saved OpenVPN configuration to state') return True
def disconnect(options): """ Disconnect from the VPN, leaving all AWS resources intact. :param options: Options passed as command line arguments by the user :return: Return code """ state = State() if not state.dump(): print('The state file is empty. Call `create` first.') return 1 if not is_root(): print('You need root privileges to kill the openvpn process.') return 1 openvpn_pid = state.get('openvpn_pid') if openvpn_pid is None: print('The VPN connection was never initiated.') return 1 os.kill(openvpn_pid, signal.SIGINT) print('Ctrl+C sent to the OpenVPN client process') state.remove('openvpn_pid') return 0
def get_dns_servers(options): """ Get the DNS servers for the VPN connection. If the target VPC has a custom set of DNS servers (most likely internal or route53 servers) use those. They will allow us to better map the internal network. If there are no custom DNS servers set in the VPC just use: * 1.1.1.1 * 8.8.8.8 :param options: Options passed as command line arguments by the user :return: True if we were able to get the DNS servers for the VPN """ state = State() # TODO: Implement custom DNS according to remote config state.append('dns_server_list', DEFAULT_DNS_SERVERS) print('Using DNS servers: %s' % ', '.join(state.get('dns_server_list'))) return True
def wait_for_vpn_creation(options): """ The client VPN creation might take a few minutes to be created, this method will wait until all resources are ready. :param options: Options passed as command line arguments by the user :return: True if the VPN was successfully created and all resources are ready to be used. """ state = State() association_id = state.get('association_id') print('Waiting for association... (this might take a while)') for _ in range(120): if association_is_ready(association_id): return True time.sleep(5) print('Timeout waiting for association to be ready. The VPN might still' ' be usable, wait a few minutes and try to connect to it using the' ' `connect` sub-command.') return False
def delete_acm_certs(): """ Delete ACM certificates created during `create` :return: True if all certs were removed """ state = State() session = boto3.Session(profile_name=state.get('profile'), region_name='us-east-1') acm_client = session.client('acm') server_arn = state.get('server_cert_acm_arn') client_arn = state.get('client_cert_acm_arn') server_arn_success = True client_arn_success = True if server_arn is not None: try: acm_client.delete_certificate(CertificateArn=server_arn) except Exception as e: args = (server_arn, e) print('Failed to remove ACM server certificate with ARN %s: %s' % args) server_arn_success = False else: print('Removed ACM server certificate with ARN %s' % server_arn) state.remove('server_cert_acm_arn') if client_arn is not None: try: acm_client.delete_certificate(CertificateArn=client_arn) except Exception as e: args = (server_arn, e) print('Failed to remove ACM client certificate with ARN %s: %s' % args) client_arn_success = False else: print('Removed ACM client certificate with ARN %s' % server_arn) state.remove('client_cert_acm_arn') return server_arn_success and client_arn_success
def delete_client_vpn_endpoint(): """ Delete all resources created during Client VPN `create` :return: True if all certs were removed """ state = State() session = boto3.Session(profile_name=state.get('profile'), region_name='us-east-1') ec2_client = session.client('ec2') security_group_id = state.get('security_group_id') vpn_endpoint_id = state.get('vpn_endpoint_id') subnet_cidr_block = state.get('subnet_cidr_block') association_id = state.get('association_id') security_group_success = True client_vpn_endpoint_success = True client_vpn_target_network_success = True client_vpn_ingress_success = True if vpn_endpoint_id is None or subnet_cidr_block is None: print('There is no VPN ingress to revoke') else: try: ec2_client.revoke_client_vpn_ingress( ClientVpnEndpointId=vpn_endpoint_id, TargetNetworkCidr=subnet_cidr_block, RevokeAllGroups=True, ) except Exception as e: print('Failed to delete client VPN ingress: %s' % e) client_vpn_ingress_success = False else: print('Successfully removed client VPN ingress') if association_id is None: print('There is no VPN association ID to delete') else: try: ec2_client.disassociate_client_vpn_target_network( ClientVpnEndpointId=vpn_endpoint_id, AssociationId=association_id) except Exception as e: args = (association_id, e) print('Failed to delete client VPN association with ID %s: %s' % args) client_vpn_target_network_success = False else: print('Successfully removed client VPN association with ID %s' % association_id) state.remove('association_id') if vpn_endpoint_id is None: print('There is no client VPN endpoint to delete') else: try: ec2_client.delete_client_vpn_endpoint( ClientVpnEndpointId=vpn_endpoint_id) except Exception as e: args = (vpn_endpoint_id, e) print('Failed to delete client VPN endpoint with ID %s: %s' % args) client_vpn_endpoint_success = False else: print('Successfully removed client VPN endpoint with ID %s' % vpn_endpoint_id) state.remove('vpn_endpoint_id') if security_group_id is None: print('There is no security group to remove') else: try: ec2_client.delete_security_group(GroupId=security_group_id) except Exception as e: args = (security_group_id, e) print('Failed to delete resource with ARN %s: %s' % args) security_group_success = False else: print('Successfully removed resource with ARN %s' % security_group_id) state.remove('security_group_id') return (security_group_success and client_vpn_endpoint_success and client_vpn_target_network_success and client_vpn_ingress_success)
def create_client_vpn_endpoint(options): """ Create client VPN endpoint aws ec2 create-client-vpn-endpoint ... :param options: Options passed as command line arguments by the user :return: True if all the SSL certs were successfully created """ state = State() session = boto3.Session(profile_name=state.get('profile'), region_name='us-east-1') ec2_client = session.client('ec2') # # aws ec2 create-client-vpn-endpoint # try: response = ec2_client.create_client_vpn_endpoint( ClientCidrBlock=state.get('cidr_block'), ServerCertificateArn=state.get('server_cert_acm_arn'), AuthenticationOptions=[ {'Type': 'certificate-authentication', 'MutualAuthentication': { 'ClientRootCertificateChainArn': state.get('client_cert_acm_arn') }} ], ConnectionLogOptions={ 'Enabled': False, }, DnsServers=state.get('dns_server_list'), TransportProtocol='udp', # Only route some traffic to the VPN, internet traffic will # still go out using the workstation regular default route SplitTunnel=True ) except Exception as e: print('Failed to create client VPN endpoint: %s' % e) return False else: vpn_endpoint_id = response['ClientVpnEndpointId'] state.append('vpn_endpoint_id', vpn_endpoint_id) # # aws ec2 associate-client-vpn-target-network # try: response = ec2_client.associate_client_vpn_target_network( ClientVpnEndpointId=vpn_endpoint_id, SubnetId=state.get('subnet_id') ) except Exception as e: print('Failed to create client vpn association: %s' % e) return False else: association_id = response['AssociationId'] state.append('association_id', association_id) # # aws ec2 authorize-client-vpn-ingress # try: response = ec2_client.authorize_client_vpn_ingress( ClientVpnEndpointId=vpn_endpoint_id, TargetNetworkCidr=state.get('subnet_cidr_block'), AuthorizeAllGroups=True, Description='Client VPN ingress #1', ) except Exception as e: print('Failed to create ingress authorization for vpn client: %s' % e) return False else: # TODO: How do I get the authorization ID to remove it later? pass # # aws ec2 create-security-group # try: response = ec2_client.create_security_group( Description='Security group for client VPN', GroupName='client_vpn_%s' % int(time.time()), VpcId=state.get('vpc_id'), ) state.append('security_group_id', response['GroupId']) ec2_client.authorize_security_group_ingress( GroupId=state.get('security_group_id'), IpPermissions=[ {'IpProtocol': 'tcp', 'FromPort': 0, 'ToPort': 65535, 'IpRanges': [{'CidrIp': '0.0.0.0/0'}]}, {'IpProtocol': 'udp', 'FromPort': 0, 'ToPort': 65535, 'IpRanges': [{'CidrIp': '0.0.0.0/0'}]} ] ) except Exception as e: print('Failed to create security group for client vpn network: %s' % e) return False # # aws ec2 apply-security-groups-to-client-vpn-target-network # try: response = ec2_client.apply_security_groups_to_client_vpn_target_network( ClientVpnEndpointId=vpn_endpoint_id, VpcId=state.get('vpc_id'), SecurityGroupIds=[ state.get('security_group_id'), ], ) except Exception as e: print('Failed to apply security group to client vpn network: %s' % e) return False else: # TODO: How do I get the authorization ID to remove it later? pass return True
def perform_initial_checks(options): """ Perform initial checks on the user-controlled parameters :param options: Options passed as command line arguments by the user :return: True if all the inputs look good """ state = State() # # Check if there is a state and require the user to use --force in order to # remove it # if state.dump() and not options.force: print('The state file at %s is not empty.\n' '\n' 'This is most likely because the `purge` sub-command was not run' ' and the target AWS account could still have resources associated' ' with a previous call to `connect`.\n' '\n' 'Use the `purge` sub-command to remove all the remote resources' ' or `connect --force` to ignore this situation and run the connect' ' process anyways.' % STATE_FILE) return False if not is_valid_subnet_id(options.subnet_id): print('%s does not have a valid Subnet ID format' % options.subnet_id) return False # # Check if the profile is valid # try: session = boto3.Session(profile_name=options.profile, region_name='us-east-1') except Exception: print('%s is not a valid profile defined in ~/.aws/credentials' % options.profile) return False sts_client = session.client('sts') try: response = sts_client.get_caller_identity() except Exception as e: msg = ('The profile has invalid credentials.' ' Call to get_caller_identity() failed with error: %s') print(msg % e) return False account_id = response['Account'] arn = response['Arn'] # # Check if the specified Subnet ID exists in the target AWS account # ec2_client = session.client('ec2') try: subnets = ec2_client.describe_subnets(SubnetIds=[options.subnet_id]) except ClientError as e: if e.response['Error']['Code'] == 'InvalidSubnetID.NotFound': # # Show the error # msg = 'The specified Subnet ID (%s) does not exist in AWS account %s' args = (options.subnet_id, account_id) print(msg % args) # # Get the user a list of all the VPC IDs # print('') print('The following is a list of existing subnets:') print('') response = ec2_client.describe_subnets() for subnet_data in response['Subnets']: args = (subnet_data['SubnetId'], subnet_data['CidrBlock'], subnet_data['VpcId']) msg = ' - %s (%s , %s)' print(msg % args) else: print('Failed to call ec2.describe_subnets: %s' % e) return False # # We want to get the VPC ID for this subnet and store it # vpc_id = subnets['Subnets'][0]['VpcId'] subnet_cidr_block = subnets['Subnets'][0]['CidrBlock'] args = (options.subnet_id, subnet_cidr_block, vpc_id) print('%s has IP address CIDR %s and is in %s' % args) # # The first thing we want to do in the connect() is to save the profile # passed as parameter to the state. We do this in order to spare the user # the need of specifying the same parameter for all the sub-commands # state.append('profile', options.profile) state.append('account_id', account_id) state.append('user_arn', arn) state.append('vpc_id', vpc_id) state.append('subnet_id', options.subnet_id) state.append('subnet_cidr_block', subnet_cidr_block) return True
def create_acm_certs(options): """ Create the ACM resources :param options: Options passed as command line arguments by the user :return: True if all the resources were successfully created """ session = boto3.Session(profile_name=options.profile, region_name='us-east-1') acm_client = session.client('acm') state = State() try: response = acm_client.import_certificate( Certificate=read_file_b(state.get('server_crt')), PrivateKey=read_file_b(state.get('server_key')), CertificateChain=read_file_b(state.get('ca_crt')), ) except Exception as e: print('Failed to import server certificate: %s' % e) return False else: state.append('server_cert_acm_arn', response['CertificateArn']) try: response = acm_client.import_certificate( Certificate=read_file_b(state.get('client_crt')), PrivateKey=read_file_b(state.get('client_key')), CertificateChain=read_file_b(state.get('ca_crt')), ) except Exception as e: print('Failed to import client certificate: %s' % e) return False else: state.append('client_cert_acm_arn', response['CertificateArn']) print('Successfully created certificates in ACM') return True