Пример #1
0
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
Пример #2
0
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
Пример #3
0
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
Пример #4
0
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
Пример #5
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
Пример #6
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
Пример #7
0
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
Пример #8
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
Пример #9
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
Пример #10
0
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
Пример #11
0
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
Пример #12
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
Пример #13
0
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
Пример #14
0
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
Пример #15
0
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)
Пример #16
0
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
Пример #17
0
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
Пример #18
0
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