Exemplo n.º 1
0
def validate_remote_usernames_against_iam_groups(config: BlessConfig, request: BlessUserRequest):
    requested_remotes = request.remote_usernames.split(',')
    if config.getboolean(bless_config.BLESS_OPTIONS_SECTION,
                         bless_config.REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION):
        iam = boto3.client('iam')
        user_groups = iam.list_groups_for_user(UserName=request.bastion_user)

        iam_group_with_full_access = config.get(bless_config.BLESS_OPTIONS_SECTION,
                                                bless_config.FULL_ACCESS_IAM_GROUP_NAME_OPTION)

        # if full access iam group is set then check if the user belongs to the group
        if iam_group_with_full_access != bless_config.FULL_ACCESS_IAM_GROUP_NAME_DEFAULT:
            for group in user_groups['Groups']:
                if group['GroupName'] == iam_group_with_full_access:
                    return None

        group_name_template = config.get(bless_config.BLESS_OPTIONS_SECTION,
                                         bless_config.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('ValidationError',
                                      'user {} is not in the {} iam group'.format(
                                          request.bastion_user,
                                          required_group_name))

    return None
Exemplo n.º 2
0
    def __init__(self, ca_private_key_password=None, config_file=None):
        """

        :param ca_private_key_password: For local testing, if the password is provided, skip the KMS
        decrypt.
        :param config_file: The config file to load the SSH CA private key from, and additional settings.
        """
        # AWS Region determines configs related to KMS
        if 'AWS_REGION' in os.environ:
            self.region = os.environ['AWS_REGION']
        else:
            self.region = 'us-west-2'

            # Load the deployment config values
        self.config = BlessConfig(self.region, config_file=config_file)

        password_ciphertext_b64 = self.config.getpassword()

        # decrypt ca private key password
        if ca_private_key_password is None:
            kms_client = boto3.client('kms', region_name=self.region)
            try:
                ca_password = kms_client.decrypt(
                    CiphertextBlob=base64.b64decode(password_ciphertext_b64))
                self.ca_private_key_password = ca_password['Plaintext']
            except ClientError as e:
                self.ca_private_key_password_error = str(e)
        else:
            self.ca_private_key_password = ca_private_key_password
Exemplo n.º 3
0
def test_config_no_password():
    with pytest.raises(ValueError) as e:
        BlessConfig('bogus-region',
                    config_file=os.path.join(os.path.dirname(__file__), 'full.cfg'))
    assert 'No Region Specific And No Default Password Provided.' == e.value.message

    config = BlessConfig('bogus-region',
                config_file=os.path.join(os.path.dirname(__file__), 'full-with-default.cfg'))
    assert '<INSERT_DEFAULT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>' == config.getpassword()
Exemplo n.º 4
0
def test_config_no_password():
    with pytest.raises(ValueError) as e:
        BlessConfig(
            "bogus-region",
            config_file=os.path.join(os.path.dirname(__file__), "full.cfg"),
        )
    assert "No Region Specific And No Default Password Provided." == str(
        e.value)

    config = BlessConfig(
        "bogus-region",
        config_file=os.path.join(os.path.dirname(__file__),
                                 "full-with-default.cfg"),
    )
    assert ("<INSERT_DEFAULT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>"
            == config.getpassword())
Exemplo n.º 5
0
    def __init__(self, ca_private_key_password=None,
                 config_file=None):
        """

        :param ca_private_key_password: For local testing, if the password is provided, skip the KMS
        decrypt.
        :param config_file: The config file to load the SSH CA private key from, and additional settings.
        """
        # AWS Region determines configs related to KMS
        if 'AWS_REGION' in os.environ:
            self.region = os.environ['AWS_REGION']
        else:
            self.region = 'us-west-2'

            # Load the deployment config values
        self.config = BlessConfig(self.region, config_file=config_file)

        password_ciphertext_b64 = self.config.getpassword()

        # decrypt ca private key password
        if ca_private_key_password is None:
            kms_client = boto3.client('kms', region_name=self.region)
            try:
                ca_password = kms_client.decrypt(
                    CiphertextBlob=base64.b64decode(password_ciphertext_b64))
                self.ca_private_key_password = ca_password['Plaintext']
            except ClientError as e:
                self.ca_private_key_password_error = str(e)
        else:
            self.ca_private_key_password = ca_private_key_password
Exemplo n.º 6
0
def test_zlib_compression_env_with_uncompressed_key(monkeypatch):
    extra_environment_variables = {
        'bless_ca_default_password':
        '******',
        'bless_ca_ca_private_key_compression':
        'zlib',
        'bless_ca_ca_private_key':
        base64.b64encode(b'<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>'),
    }

    for k, v in extra_environment_variables.items():
        monkeypatch.setenv(k, v)

    # Create an empty config, everything is set in the environment
    config = BlessConfig('us-east-1', config_file='')

    with pytest.raises(zlib.error) as e:
        config.getprivatekey()
Exemplo n.º 7
0
def test_none_compression_env_key(monkeypatch):
    extra_environment_variables = {
        'bless_ca_default_password':
        '******',
        'bless_ca_ca_private_key_compression':
        'none',
        'bless_ca_ca_private_key':
        str(base64.b64encode(b'<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>'),
            encoding='ascii')
    }

    for k, v in extra_environment_variables.items():
        monkeypatch.setenv(k, v)

    # Create an empty config, everything is set in the environment
    config = BlessConfig('us-east-1', config_file='')

    assert b'<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>' == config.getprivatekey(
    )
Exemplo n.º 8
0
def test_configs(config, region, expected_cert_valid, expected_entropy_min,
                 expected_rand_seed, expected_log_level, expected_password,
                 expected_username_validation, expected_key_compression):
    config = BlessConfig(region, config_file=config)
    assert expected_cert_valid == config.getint(
        BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
    assert expected_cert_valid == config.getint(
        BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)

    assert expected_entropy_min == config.getint(BLESS_OPTIONS_SECTION,
                                                 ENTROPY_MINIMUM_BITS_OPTION)
    assert expected_rand_seed == config.getint(BLESS_OPTIONS_SECTION,
                                               RANDOM_SEED_BYTES_OPTION)
    assert expected_log_level == config.get(BLESS_OPTIONS_SECTION,
                                            LOGGING_LEVEL_OPTION)
    assert expected_password == config.getpassword()
    assert expected_username_validation == config.get(
        BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION)
    assert expected_key_compression == config.get(
        BLESS_CA_SECTION, CA_PRIVATE_KEY_COMPRESSION_OPTION)
Exemplo n.º 9
0
def test_wrong_compression_env_key(monkeypatch):
    extra_environment_variables = {
        'bless_ca_default_password':
        '******',
        'bless_ca_ca_private_key_compression':
        'lzh',
        'bless_ca_ca_private_key':
        str(base64.b64encode(b'<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>'),
            encoding='ascii')
    }

    for k, v in extra_environment_variables.items():
        monkeypatch.setenv(k, v)

    # Create an empty config, everything is set in the environment
    config = BlessConfig('us-east-1', config_file='')

    with pytest.raises(ValueError) as e:
        config.getprivatekey()

    assert "Compression lzh is not supported." == str(e.value)
Exemplo n.º 10
0
def test_none_compression_env_key(monkeypatch):
    extra_environment_variables = {
        "bless_ca_default_password":
        "******",
        "bless_ca_ca_private_key_compression":
        "none",
        "bless_ca_ca_private_key":
        str(
            base64.b64encode(b"<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>"),
            encoding="ascii",
        ),
    }

    for k, v in extra_environment_variables.items():
        monkeypatch.setenv(k, v)

    # Create an empty config, everything is set in the environment
    config = BlessConfig("us-east-1", config_file="")

    assert b"<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>" == config.getprivatekey(
    )
Exemplo n.º 11
0
def test_configs(config, region, expected_cert_valid, expected_entropy_min, expected_rand_seed,
                 expected_log_level, expected_password):
    config = BlessConfig(region, config_file=config)
    assert expected_cert_valid == config.getint(BLESS_OPTIONS_SECTION,
                                                CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
    assert expected_cert_valid == config.getint(BLESS_OPTIONS_SECTION,
                                                CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)

    assert expected_entropy_min == config.getint(BLESS_OPTIONS_SECTION,
                                                 ENTROPY_MINIMUM_BITS_OPTION)
    assert expected_rand_seed == config.getint(BLESS_OPTIONS_SECTION,
                                               RANDOM_SEED_BYTES_OPTION)
    assert expected_log_level == config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION)
    assert expected_password == config.getpassword()
Exemplo n.º 12
0
def test_configs(config, region, expected_cert_valid, expected_entropy_min, expected_rand_seed,
                 expected_log_level, expected_password, expected_username_validation):
    config = BlessConfig(region, config_file=config)
    assert expected_cert_valid == config.getint(BLESS_OPTIONS_SECTION,
                                                CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
    assert expected_cert_valid == config.getint(BLESS_OPTIONS_SECTION,
                                                CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)

    assert expected_entropy_min == config.getint(BLESS_OPTIONS_SECTION,
                                                 ENTROPY_MINIMUM_BITS_OPTION)
    assert expected_rand_seed == config.getint(BLESS_OPTIONS_SECTION,
                                               RANDOM_SEED_BYTES_OPTION)
    assert expected_log_level == config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION)
    assert expected_password == config.getpassword()
    assert expected_username_validation == config.get(BLESS_OPTIONS_SECTION,
                                                      USERNAME_VALIDATION_OPTION)
Exemplo n.º 13
0
def test_kms_config_opts(monkeypatch):
    # Default option
    config = BlessConfig("us-east-1",
                         config_file=os.path.join(os.path.dirname(__file__),
                                                  'full.cfg'))
    assert config.getboolean(KMSAUTH_SECTION,
                             KMSAUTH_USEKMSAUTH_OPTION) is False

    # Config file value
    config = BlessConfig("us-east-1",
                         config_file=os.path.join(os.path.dirname(__file__),
                                                  'full-with-kmsauth.cfg'))
    assert config.getboolean(KMSAUTH_SECTION,
                             KMSAUTH_USEKMSAUTH_OPTION) is True
    assert config.getboolean(
        KMSAUTH_SECTION,
        VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION) is False
Exemplo n.º 14
0
def test_empty_config():
    with pytest.raises(ValueError):
        BlessConfig('us-west-2', config_file='')
Exemplo n.º 15
0
def test_empty_config():
    with pytest.raises(ValueError):
        BlessConfig("us-west-2", config_file="")
Exemplo n.º 16
0
def test_configs(config, region, expected_cert_valid, expected_entropy_min,
                 expected_rand_seed, expected_host_cert_before_valid,
                 expected_host_cert_after_valid, expected_log_level,
                 expected_password, expected_username_validation,
                 expected_hostname_validation, expected_iam_groups_validation,
                 expected_iam_groups_validation_format,
                 expected_key_compression):
    config = BlessConfig(region, config_file=config)
    assert expected_cert_valid == config.getint(
        BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
    assert expected_cert_valid == config.getint(
        BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)
    assert expected_entropy_min == config.getint(BLESS_OPTIONS_SECTION,
                                                 ENTROPY_MINIMUM_BITS_OPTION)
    assert expected_rand_seed == config.getint(BLESS_OPTIONS_SECTION,
                                               RANDOM_SEED_BYTES_OPTION)
    assert expected_host_cert_before_valid == config.getint(
        BLESS_OPTIONS_SECTION, SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
    assert expected_host_cert_after_valid == config.getint(
        BLESS_OPTIONS_SECTION, SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)
    assert expected_log_level == config.get(BLESS_OPTIONS_SECTION,
                                            LOGGING_LEVEL_OPTION)
    assert expected_password == config.getpassword()
    assert expected_username_validation == config.get(
        BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION)
    assert expected_hostname_validation == config.get(
        BLESS_OPTIONS_SECTION, HOSTNAME_VALIDATION_OPTION)
    assert expected_iam_groups_validation == config.get(
        BLESS_OPTIONS_SECTION, REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION)
    assert expected_iam_groups_validation_format == config.get(
        BLESS_OPTIONS_SECTION, IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION)
    assert expected_key_compression == config.get(
        BLESS_CA_SECTION, CA_PRIVATE_KEY_COMPRESSION_OPTION)
Exemplo n.º 17
0
def test_config_environment_override(monkeypatch):
    extra_environment_variables = {
        "bless_options_certificate_validity_after_seconds":
        "1",
        "bless_options_certificate_validity_before_seconds":
        "1",
        "bless_options_server_certificate_validity_after_seconds":
        "1",
        "bless_options_server_certificate_validity_before_seconds":
        "1",
        "bless_options_hostname_validation":
        "disabled",
        "bless_options_entropy_minimum_bits":
        "2",
        "bless_options_random_seed_bytes":
        "3",
        "bless_options_logging_level":
        "DEBUG",
        "bless_options_certificate_extensions":
        "permit-X11-forwarding",
        "bless_options_username_validation":
        "debian",
        "bless_options_remote_usernames_validation":
        "useradd",
        "bless_ca_us_east_1_password":
        "******",
        "bless_ca_default_password":
        "******",
        "bless_ca_ca_private_key_file":
        "<INSERT_YOUR_ENCRYPTED_PEM_FILE_NAME>",
        "bless_ca_ca_private_key":
        str(
            base64.b64encode(b"<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>"),
            encoding="ascii",
        ),
        "kms_auth_use_kmsauth":
        "True",
        "kms_auth_kmsauth_key_id":
        "<INSERT_ARN>",
        "kms_auth_kmsauth_serviceid":
        "bless-test",
    }

    for k, v in extra_environment_variables.items():
        monkeypatch.setenv(k, v)

    # Create an empty config, everything is set in the environment
    config = BlessConfig("us-east-1", config_file="")

    assert 1 == config.getint(BLESS_OPTIONS_SECTION,
                              CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)
    assert 1 == config.getint(BLESS_OPTIONS_SECTION,
                              CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
    assert 1 == config.getint(BLESS_OPTIONS_SECTION,
                              SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
    assert 1 == config.getint(BLESS_OPTIONS_SECTION,
                              SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)
    assert 2 == config.getint(BLESS_OPTIONS_SECTION,
                              ENTROPY_MINIMUM_BITS_OPTION)
    assert 3 == config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION)
    assert "DEBUG" == config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION)
    assert "permit-X11-forwarding" == config.get(
        BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION)
    assert "debian" == config.get(BLESS_OPTIONS_SECTION,
                                  USERNAME_VALIDATION_OPTION)
    assert "disabled" == config.get(BLESS_OPTIONS_SECTION,
                                    HOSTNAME_VALIDATION_OPTION)
    assert "useradd" == config.get(BLESS_OPTIONS_SECTION,
                                   REMOTE_USERNAMES_VALIDATION_OPTION)

    assert ("<INSERT_US-EAST-1_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>"
            == config.getpassword())
    assert "<INSERT_YOUR_ENCRYPTED_PEM_FILE_NAME>" == config.get(
        BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION)
    assert b"<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>" == config.getprivatekey(
    )

    assert config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION)
    assert "<INSERT_ARN>" == config.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION)
    assert "bless-test" == config.get(KMSAUTH_SECTION,
                                      KMSAUTH_SERVICE_ID_OPTION)

    config.aws_region = "invalid"
    assert ("<INSERT_DEFAULT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>"
            == config.getpassword())
Exemplo n.º 18
0
def lambda_handler(event,
                   context=None,
                   ca_private_key_password=None,
                   entropy_check=True,
                   config_file=os.path.join(os.path.dirname(__file__),
                                            'bless_deploy.cfg')):
    """
    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.
    """
    # AWS Region determines configs related to KMS
    region = os.environ['AWS_REGION']

    # Load the deployment config values
    config = BlessConfig(region, config_file=config_file)

    logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION)
    numeric_level = getattr(logging, logging_level.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError('Invalid log level: {}'.format(logging_level))

    logger = logging.getLogger()
    logger.setLevel(numeric_level)

    certificate_validity_window_seconds = config.getint(
        BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION)
    entropy_minimum_bits = config.getint(BLESS_OPTIONS_SECTION,
                                         ENTROPY_MINIMUM_BITS_OPTION)
    random_seed_bytes = config.getint(BLESS_OPTIONS_SECTION,
                                      RANDOM_SEED_BYTES_OPTION)
    ca_private_key_file = config.get(BLESS_CA_SECTION,
                                     CA_PRIVATE_KEY_FILE_OPTION)
    password_ciphertext_b64 = config.getpassword()

    # read the private key .pem
    with open(os.path.join(os.path.dirname(__file__), ca_private_key_file),
              'r') as f:
        ca_private_key = f.read()

    # decrypt ca private key password
    if ca_private_key_password is None:
        kms_client = boto3.client('kms', region_name=region)
        ca_password = kms_client.decrypt(
            CiphertextBlob=base64.b64decode(password_ciphertext_b64))
        ca_private_key_password = ca_password['Plaintext']

    # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired
    if entropy_check:
        with open('/proc/sys/kernel/random/entropy_avail', 'r') as f:
            entropy = int(f.read())
            logger.debug(entropy)
            if entropy < entropy_minimum_bits:
                logger.info(
                    'System entropy was {}, which is lower than the entropy_'
                    'minimum {}.  Using KMS to seed /dev/urandom'.format(
                        entropy, entropy_minimum_bits))
                response = kms_client.generate_random(
                    NumberOfBytes=random_seed_bytes)
                random_seed = response['Plaintext']
                with open('/dev/urandom', 'w') as urandom:
                    urandom.write(random_seed)

    # Process cert request
    schema = BlessSchema(strict=True)
    request = schema.load(event).data

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

    # 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)
    cert_builder.add_valid_principal(request.remote_username)
    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[{}] 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_address(request.bastion_ip)
    cert_builder.set_key_id(key_id)
    cert = cert_builder.get_cert_file()

    logger.info(
        'Issued a cert to bastion_ip[{}] for the remote_username of [{}] with the key_id[{}] and '
        'valid_from[{}])'.format(
            request.bastion_ip, request.remote_username, key_id,
            time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after))))
    return cert
Exemplo n.º 19
0
def lambda_handler(event, context=None, ca_private_key_password=None,
                   entropy_check=True,
                   config_file=os.path.join(os.path.dirname(__file__), 'bless_deploy.cfg')):
    """
    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.
    """
    # AWS Region determines configs related to KMS
    region = os.environ['AWS_REGION']

    # Load the deployment config values
    config = BlessConfig(region,
                         config_file=config_file)

    logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION)
    numeric_level = getattr(logging, logging_level.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError('Invalid log level: {}'.format(logging_level))

    logger = logging.getLogger()
    logger.setLevel(numeric_level)

    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)
    entropy_minimum_bits = config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION)
    random_seed_bytes = config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION)
    ca_private_key = config.getprivatekey()
    password_ciphertext_b64 = config.getpassword()
    certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION)

    # Process cert request
    schema = BlessSchema(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)

    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))

    # decrypt ca private key password
    if ca_private_key_password is None:
        kms_client = boto3.client('kms', region_name=region)
        try:
            ca_password = kms_client.decrypt(
                CiphertextBlob=base64.b64decode(password_ciphertext_b64))
            ca_private_key_password = ca_password['Plaintext']
        except ClientError as e:
            return error_response('ClientError', str(e))

    # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired
    if entropy_check:
        with open('/proc/sys/kernel/random/entropy_avail', 'r') as f:
            entropy = int(f.read())
            logger.debug(entropy)
            if entropy < entropy_minimum_bits:
                logger.info(
                    'System entropy was {}, which is lower than the entropy_'
                    'minimum {}.  Using KMS to seed /dev/urandom'.format(
                        entropy, entropy_minimum_bits))
                response = kms_client.generate_random(
                    NumberOfBytes=random_seed_bytes)
                random_seed = response['Plaintext']
                with open('/dev/urandom', 'w') as urandom:
                    urandom.write(random_seed)

    # 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.get(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))
            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)
Exemplo n.º 20
0
def lambda_handler(event, context=None, ca_private_key_password=None, okta_api_token=None,
                   entropy_check=True,
                   config_file=os.path.join(os.path.dirname(__file__), 'bless_deploy.cfg')):
    """
    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.
    """
    # AWS Region determines configs related to KMS
    region = os.environ['AWS_REGION']

    # Load the deployment config values
    config = BlessConfig(region, config_file=config_file)

    logger = logging.getLogger(__name__)

    logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION)
    numeric_level = getattr(logging, logging_level.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError('Invalid log level: {}'.format(logging_level))
    logger.setLevel(numeric_level)

    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)
    certificate_breakglass_validity_before_seconds = config.getint(
        BLESS_OPTIONS_SECTION,
        CERTIFICATE_BREAKGLASS_BEFORE_SEC_OPTION
    )
    certificate_breakglass_validity_after_seconds = config.getint(
        BLESS_OPTIONS_SECTION,
        CERTIFICATE_BREAKGLASS_AFTER_SEC_OPTION
    )

    breakglass_user = config.get(BLESS_OPTIONS_SECTION, BREAKGLASS_USER_OPTION)

    entropy_minimum_bits = config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION)
    random_seed_bytes = config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION)
    ca_private_key_file = config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION)

    password_ciphertext_b64 = config.getpassword()

    certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION)

    encrypted_okta_api_token = config.get(OKTA_OPTIONS_SECTION, ENCRYPTED_OKTA_API_TOKEN_OPTION)
    okta_base_url = config.get(OKTA_OPTIONS_SECTION, OKTA_BASE_URL_OPTION)
    okta_allowed_groups = config.get(OKTA_OPTIONS_SECTION, OKTA_ALLOWED_GROUPS_OPTION)
    if okta_allowed_groups:
        okta_allowed_groups = set(okta_allowed_groups.split(','))
        logger.debug("Allowed okta groups: {}".format(okta_allowed_groups))

    # Process cert request
    schema = BlessSchema(strict=True)
    try:
        request = schema.load(event).data
    except ValidationError as e:
        return error_response('InputValidationError', str(e))

    logger.info('Bless lambda invoked by okta user: {}@{}, bastion user: {}, kmsauth_token: {}]'.format(
        request.okta_user,
        request.bastion_user_ip,
        request.bastion_user,
        request.kmsauth_token)
    )

    # read the private key .pem
    with open(os.path.join(os.path.dirname(__file__), ca_private_key_file), 'r') as f:
        ca_private_key = f.read()

    kms_client = boto3.client('kms', region_name=region)

    # decrypt ca private key password
    if ca_private_key_password is None:
        try:
            ca_password = kms_client.decrypt(
                CiphertextBlob=base64.b64decode(password_ciphertext_b64))
            ca_private_key_password = ca_password['Plaintext']
        except ClientError as e:
            return error_response('ClientError', str(e))

    # decrypt okta api token
    if okta_api_token is None:
        try:
            decrypted_okta_api_token = kms_client.decrypt(
                CiphertextBlob=base64.b64decode(encrypted_okta_api_token))
            okta_api_token = decrypted_okta_api_token['Plaintext']
        except ClientError as e:
            return error_response('ClientError', str(e))

    '''
    Fetch user context from Okta
    * Username
    * Groups
    * SSH key
    '''

    # add extra property to Okta user profile schema
    OktaUserProfile.types['public_key'] = str

    okta_users_client = okta.UsersClient(okta_base_url, okta_api_token)

    user = okta_users_client.get_user(request.okta_user)
    groups_info = okta_users_client.get_user_groups(request.okta_user)

    groups = set()
    for group_info in groups_info:
        # Encode unicode response from okta json api as a byte string to ease comparisons
        groups.add(group_info.profile.name.encode('utf-8'))

    logger.debug('User groups: {}'.format(groups))

    allowed_groups = okta_allowed_groups
    logger.debug('Whitelist groups: {}'.format(allowed_groups))

    logger.debug('Matched groups: {}'.format(groups.intersection(allowed_groups)))

    # If the len is greater than 0, breakglass will be false.
    breakglass = not len(groups.intersection(allowed_groups))

    public_key_to_sign = user.profile.public_key
    if public_key_to_sign.startswith(VALID_SSH_RSA_PUBLIC_KEY_HEADER):
        pass
    else:
        raise ValidationError('Invalid SSH Public Key.')

    # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired
    if entropy_check:
        with open('/proc/sys/kernel/random/entropy_avail', 'r') as f:
            entropy = int(f.read())
            logger.debug(entropy)
            if entropy < entropy_minimum_bits:
                logger.info(
                    'System entropy was {}, which is lower than the entropy_'
                    'minimum {}.  Using KMS to seed /dev/urandom'.format(
                        entropy, entropy_minimum_bits))
                response = kms_client.generate_random(
                    NumberOfBytes=random_seed_bytes)
                random_seed = response['Plaintext']
                with open('/dev/urandom', 'w') as urandom:
                    urandom.write(random_seed)

    # 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:  # Normal user flow
        bypass_time_validity_check = False
        valid_before = current_time + certificate_validity_after_seconds
        valid_after = current_time - certificate_validity_before_seconds

    # Authenticate the user with KMS, if key is setup
    if config.get(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION):
        if request.kmsauth_token:
            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.remote_usernames.split(',')[0]),
                    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,
                                               public_key_to_sign)
    # Handle breakglass certificate modifications
    if breakglass:
        logger.critical('Breakglass access for okta user: {}@{}, bastion user: {}'.format(
            request.okta_user,
            request.bastion_user_ip,
            request.bastion_user)
        )
        valid_before = current_time + certificate_breakglass_validity_before_seconds
        valid_after = current_time - certificate_breakglass_validity_after_seconds
        cert_builder.add_valid_principal(breakglass_user)

    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 = 'AWS Request ID: {}, User: {}, Source IP: {}, Command: {}, ' \
             'Fingerprint: {}, Bless CA: {}, Expires: {}'\
        .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 the remote_usernames of [{}] with the 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)
Exemplo n.º 21
0
def test_config_environment_override(monkeypatch):
    extra_environment_variables = {
        'bless_options_certificate_validity_after_seconds': '1',
        'bless_options_certificate_validity_before_seconds': '1',
        'bless_options_entropy_minimum_bits': '2',
        'bless_options_random_seed_bytes': '3',
        'bless_options_logging_level': 'DEBUG',
        'bless_options_certificate_extensions': 'permit-X11-forwarding',

        'bless_ca_us_east_1_password': '******',
        'bless_ca_default_password': '******',
        'bless_ca_ca_private_key_file': '<INSERT_YOUR_ENCRYPTED_PEM_FILE_NAME>',
        'bless_ca_ca_private_key': base64.b64encode('<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>'),

        'kms_auth_use_kmsauth': 'True',
        'kms_auth_kmsauth_key_id': '<INSERT_ARN>',
        'kms_auth_kmsauth_serviceid': 'bless-test',
    }

    for k,v in extra_environment_variables.items():
        monkeypatch.setenv(k, v)

    # Create an empty config, everything is set in the environment
    config = BlessConfig('us-east-1', config_file='')

    assert 1 == config.getint(BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)
    assert 1 == config.getint(BLESS_OPTIONS_SECTION, CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
    assert 2 == config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION)
    assert 3 == config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION)
    assert 'DEBUG' == config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION)
    assert 'permit-X11-forwarding' == config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION)

    assert '<INSERT_US-EAST-1_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>' == config.getpassword()
    assert '<INSERT_YOUR_ENCRYPTED_PEM_FILE_NAME>' == config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION)
    assert '<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>' == config.getprivatekey()

    assert config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION)
    assert '<INSERT_ARN>' == config.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION)
    assert 'bless-test' == config.get(KMSAUTH_SECTION, KMSAUTH_SERVICE_ID_OPTION)

    config.aws_region = 'invalid'
    assert '<INSERT_DEFAULT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>' == config.getpassword()
Exemplo n.º 22
0
def test_config_no_password():
    with pytest.raises(ValueError) as e:
        BlessConfig('bogus-region',
                    config_file=os.path.join(os.path.dirname(__file__),
                                             'full.cfg'))
    assert 'No Region Specific Password Provided.' == e.value.message
Exemplo n.º 23
0
def lambda_handler(event,
                   context=None,
                   ca_private_key_password=None,
                   entropy_check=True,
                   config_file=os.path.join(os.path.dirname(__file__),
                                            'bless_deploy.cfg')):
    """
    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.
    """
    # AWS Region determines configs related to KMS
    region = os.environ['AWS_REGION']

    # Load the deployment config values
    config = BlessConfig(region, config_file=config_file)

    logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION)
    numeric_level = getattr(logging, logging_level.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError('Invalid log level: {}'.format(logging_level))

    logger = logging.getLogger()
    logger.setLevel(numeric_level)

    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)
    entropy_minimum_bits = config.getint(BLESS_OPTIONS_SECTION,
                                         ENTROPY_MINIMUM_BITS_OPTION)
    random_seed_bytes = config.getint(BLESS_OPTIONS_SECTION,
                                      RANDOM_SEED_BYTES_OPTION)
    ca_private_key = config.getprivatekey()
    password_ciphertext_b64 = config.getpassword()
    certificate_extensions = config.get(BLESS_OPTIONS_SECTION,
                                        CERTIFICATE_EXTENSIONS_OPTION)

    # Process cert request
    schema = BlessSchema(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)

    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))

    # decrypt ca private key password
    if ca_private_key_password is None:
        kms_client = boto3.client('kms', region_name=region)
        try:
            ca_password = kms_client.decrypt(
                CiphertextBlob=base64.b64decode(password_ciphertext_b64))
            ca_private_key_password = ca_password['Plaintext']
        except ClientError as e:
            return error_response('ClientError', str(e))

    # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired
    if entropy_check:
        with open('/proc/sys/kernel/random/entropy_avail', 'r') as f:
            entropy = int(f.read())
            logger.debug(entropy)
            if entropy < entropy_minimum_bits:
                logger.info(
                    'System entropy was {}, which is lower than the entropy_'
                    'minimum {}.  Using KMS to seed /dev/urandom'.format(
                        entropy, entropy_minimum_bits))
                response = kms_client.generate_random(
                    NumberOfBytes=random_seed_bytes)
                random_seed = response['Plaintext']
                with open('/dev/urandom', 'w') as urandom:
                    urandom.write(random_seed)

    # 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.get(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION):
        if request.kmsauth_token:
            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.remote_usernames),
                    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)
Exemplo n.º 24
0
def lambda_handler(event, context=None, ca_private_key_password=None,
                   entropy_check=True,
                   config_file=os.path.join(os.path.dirname(__file__), 'bless_deploy.cfg')):
    """
    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.
    """
    # AWS Region determines configs related to KMS
    region = os.environ['AWS_REGION']

    # Load the deployment config values
    config = BlessConfig(region,
                         config_file=config_file)

    logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION)
    numeric_level = getattr(logging, logging_level.upper(), None)
    if not isinstance(numeric_level, int):
        raise ValueError('Invalid log level: {}'.format(logging_level))

    logger = logging.getLogger()
    logger.setLevel(numeric_level)

    certificate_validity_window_seconds = config.getint(BLESS_OPTIONS_SECTION,
                                                        CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION)
    entropy_minimum_bits = config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION)
    random_seed_bytes = config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION)
    ca_private_key_file = config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION)
    password_ciphertext_b64 = config.getpassword()

    # read the private key .pem
    with open(os.path.join(os.path.dirname(__file__), ca_private_key_file), 'r') as f:
        ca_private_key = f.read()

    # decrypt ca private key password
    if ca_private_key_password is None:
        kms_client = boto3.client('kms', region_name=region)
        ca_password = kms_client.decrypt(
            CiphertextBlob=base64.b64decode(password_ciphertext_b64))
        ca_private_key_password = ca_password['Plaintext']

    # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired
    if entropy_check:
        with open('/proc/sys/kernel/random/entropy_avail', 'r') as f:
            entropy = int(f.read())
            logger.debug(entropy)
            if entropy < entropy_minimum_bits:
                logger.info(
                    'System entropy was {}, which is lower than the entropy_'
                    'minimum {}.  Using KMS to seed /dev/urandom'.format(
                        entropy, entropy_minimum_bits))
                response = kms_client.generate_random(
                    NumberOfBytes=random_seed_bytes)
                random_seed = response['Plaintext']
                with open('/dev/urandom', 'w') as urandom:
                    urandom.write(random_seed)

    # Process cert request
    schema = BlessSchema(strict=True)
    request = schema.load(event).data

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

    # 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)
    cert_builder.add_valid_principal(request.remote_username)
    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[{}] 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_address(request.bastion_ip)
    cert_builder.set_key_id(key_id)
    cert = cert_builder.get_cert_file()

    logger.info(
        'Issued a cert to bastion_ip[{}] for the remote_username of [{}] with the key_id[{}] and '
        'valid_from[{}])'.format(
            request.bastion_ip, request.remote_username, key_id,
            time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after))))
    return cert