Ejemplo n.º 1
0
def lambda_handler_user(event,
                        context=None,
                        ca_private_key_password=None,
                        entropy_check=True,
                        config_file=None):
    """
    This is the function that will be called when the lambda function starts.
    :param event: Dictionary of the json request.
    :param context: AWS LambdaContext Object
    http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html
    :param ca_private_key_password: For local testing, if the password is provided, skip the KMS
    decrypt.
    :param entropy_check: For local testing, if set to false, it will skip checking entropy and
    won't try to fetch additional random from KMS.
    :param config_file: The config file to load the SSH CA private key from, and additional settings.
    :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file.
    """
    bless_cache = setup_lambda_cache(ca_private_key_password, config_file)

    # AWS Region determines configs related to KMS
    region = bless_cache.region

    # Load the deployment config values
    config = bless_cache.config

    logger = set_logger(config)

    certificate_validity_before_seconds = config.getint(
        BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
    certificate_validity_after_seconds = config.getint(
        BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)
    ca_private_key = config.getprivatekey()
    certificate_extensions = config.get(BLESS_OPTIONS_SECTION,
                                        CERTIFICATE_EXTENSIONS_OPTION)

    # Process cert request
    schema = BlessUserSchema(strict=True)
    schema.context[USERNAME_VALIDATION_OPTION] = config.get(
        BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION)
    schema.context[REMOTE_USERNAMES_VALIDATION_OPTION] = config.get(
        BLESS_OPTIONS_SECTION, REMOTE_USERNAMES_VALIDATION_OPTION)
    schema.context[REMOTE_USERNAMES_BLACKLIST_OPTION] = config.get(
        BLESS_OPTIONS_SECTION, REMOTE_USERNAMES_BLACKLIST_OPTION)

    try:
        request = schema.load(event).data
    except ValidationError as e:
        return error_response('InputValidationError', str(e))

    logger.info(
        'Bless lambda invoked by [user: {0}, bastion_ips:{1}, public_key: {2}, kmsauth_token:{3}]'
        .format(request.bastion_user, request.bastion_user_ip,
                request.public_key_to_sign, request.kmsauth_token))

    # Make sure we have the ca private key password
    if bless_cache.ca_private_key_password is None:
        return error_response('ClientError',
                              bless_cache.ca_private_key_password_error)
    else:
        ca_private_key_password = bless_cache.ca_private_key_password

    # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired
    if entropy_check:
        check_entropy(config, logger)

    # cert values determined only by lambda and its configs
    current_time = int(time.time())
    test_user = config.get(BLESS_OPTIONS_SECTION, TEST_USER_OPTION)
    if test_user and (request.bastion_user == test_user
                      or request.remote_usernames == test_user):
        # This is a test call, the lambda will issue an invalid
        # certificate where valid_before < valid_after
        valid_before = current_time
        valid_after = current_time + 1
        bypass_time_validity_check = True
    else:
        valid_before = current_time + certificate_validity_after_seconds
        valid_after = current_time - certificate_validity_before_seconds
        bypass_time_validity_check = False

    # Authenticate the user with KMS, if key is setup
    if config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION):
        if request.kmsauth_token:
            # Allow bless to sign the cert for a different remote user than the name of the user who signed it
            allowed_remotes = config.get(
                KMSAUTH_SECTION, KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION)
            if allowed_remotes:
                allowed_users = allowed_remotes.split(',')
                requested_remotes = request.remote_usernames.split(',')
                if allowed_users != ['*'] and not all(
                    [u in allowed_users for u in requested_remotes]):
                    return error_response(
                        'KMSAuthValidationError',
                        'unallowed remote_usernames [{}]'.format(
                            request.remote_usernames))

                # Check if the user is in the required IAM groups
                if config.getboolean(
                        KMSAUTH_SECTION,
                        VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION):
                    iam = boto3.client('iam')
                    user_groups = iam.list_groups_for_user(
                        UserName=request.bastion_user)

                    group_name_template = config.get(
                        KMSAUTH_SECTION,
                        IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION)
                    for requested_remote in requested_remotes:
                        required_group_name = group_name_template.format(
                            requested_remote)

                        user_is_in_group = any(
                            group for group in user_groups['Groups']
                            if group['GroupName'] == required_group_name)

                        if not user_is_in_group:
                            return error_response(
                                'KMSAuthValidationError',
                                'user {} is not in the {} iam group'.format(
                                    request.bastion_user, required_group_name))

            elif request.remote_usernames != request.bastion_user:
                return error_response(
                    'KMSAuthValidationError',
                    'remote_usernames must be the same as bastion_user')
            try:
                validator = KMSTokenValidator(
                    None, config.getkmsauthkeyids(),
                    config.get(KMSAUTH_SECTION, KMSAUTH_SERVICE_ID_OPTION),
                    region)
                # decrypt_token will raise a TokenValidationError if token doesn't match
                validator.decrypt_token(
                    "2/user/{}".format(request.bastion_user),
                    request.kmsauth_token)
            except TokenValidationError as e:
                return error_response('KMSAuthValidationError', str(e))
        else:
            return error_response('InputValidationError',
                                  'Invalid request, missing kmsauth token')

    # Build the cert
    ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password)
    cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.USER,
                                               request.public_key_to_sign)
    for username in request.remote_usernames.split(','):
        cert_builder.add_valid_principal(username)

    cert_builder.set_valid_before(valid_before)
    cert_builder.set_valid_after(valid_after)

    if certificate_extensions:
        for e in certificate_extensions.split(','):
            if e:
                cert_builder.add_extension(e)
    else:
        cert_builder.clear_extensions()

    # cert_builder is needed to obtain the SSH public key's fingerprint
    key_id = 'request[{}] for[{}] from[{}] command[{}] ssh_key[{}]  ca[{}] valid_to[{}]'.format(
        context.aws_request_id, request.bastion_user, request.bastion_user_ip,
        request.command, cert_builder.ssh_public_key.fingerprint,
        context.invoked_function_arn,
        time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before)))
    cert_builder.set_critical_option_source_addresses(request.bastion_ips)
    cert_builder.set_key_id(key_id)
    cert = cert_builder.get_cert_file(bypass_time_validity_check)

    logger.info(
        'Issued a cert to bastion_ips[{}] for remote_usernames[{}] with key_id[{}] and '
        'valid_from[{}])'.format(
            request.bastion_ips, request.remote_usernames, key_id,
            time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after))))
    return success_response(cert)
Ejemplo n.º 2
0
def lambda_handler_host(
    event,
    context=None,
    ca_private_key_password=None,
    entropy_check=True,
    config_file=None,
):
    """
    This is the function that will be called when the lambda function starts.
    :param event: Dictionary of the json request.
    :param context: AWS LambdaContext Object
    http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html
    :param ca_private_key_password: For local testing, if the password is provided, skip the KMS
    decrypt.
    :param entropy_check: For local testing, if set to false, it will skip checking entropy and
    won't try to fetch additional random from KMS.
    :param config_file: The config file to load the SSH CA private key from, and additional settings.
    :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file.
    """
    bless_cache = setup_lambda_cache(ca_private_key_password, config_file)

    # Load the deployment config values
    config = bless_cache.config

    logger = set_logger(config)

    certificate_validity_before_seconds = config.getint(
        BLESS_OPTIONS_SECTION, SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
    certificate_validity_after_seconds = config.getint(
        BLESS_OPTIONS_SECTION, SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)

    ca_private_key = config.getprivatekey()

    # Process cert request
    schema = BlessHostSchema(strict=True)
    schema.context[HOSTNAME_VALIDATION_OPTION] = config.get(
        BLESS_OPTIONS_SECTION, HOSTNAME_VALIDATION_OPTION)

    try:
        request = schema.load(event).data
    except ValidationError as e:
        return error_response("InputValidationError", str(e))

    # todo: You'll want to bring your own hostnames validation.
    logger.info(
        "Bless lambda invoked by [public_key: {}] for hostnames[{}]".format(
            request.public_key_to_sign, request.hostnames))

    # Make sure we have the ca private key password
    if bless_cache.ca_private_key_password is None:
        return error_response("ClientError",
                              bless_cache.ca_private_key_password_error)
    else:
        ca_private_key_password = bless_cache.ca_private_key_password

    # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired
    if entropy_check:
        check_entropy(config, logger)

    # cert values determined only by lambda and its configs
    current_time = int(time.time())
    valid_before = current_time + certificate_validity_after_seconds
    valid_after = current_time - certificate_validity_before_seconds

    # Build the cert
    ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password)
    cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.HOST,
                                               request.public_key_to_sign)

    for hostname in request.hostnames.split(","):
        cert_builder.add_valid_principal(hostname)

    cert_builder.set_valid_before(valid_before)
    cert_builder.set_valid_after(valid_after)

    # cert_builder is needed to obtain the SSH public key's fingerprint
    key_id = "request[{}] ssh_key[{}] ca[{}] valid_to[{}]".format(
        context.aws_request_id,
        cert_builder.ssh_public_key.fingerprint,
        context.invoked_function_arn,
        time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before)),
    )

    cert_builder.set_key_id(key_id)
    cert = cert_builder.get_cert_file()

    logger.info("Issued a server cert to hostnames[{}] with key_id[{}] and "
                "valid_from[{}])".format(
                    request.hostnames,
                    key_id,
                    time.strftime("%Y/%m/%d %H:%M:%S",
                                  time.gmtime(valid_after)),
                ))
    return success_response(cert)