Skip to content

Commit

Permalink
Merge pull request Netflix#94 from russell-lewis/lambda-host-split
Browse files Browse the repository at this point in the history
In addition to bless_lambda.lambda_handler, you can now use bless_lambda_user.lambda_handler_user for user cert requests and bless_lambda_host.lambda_handler_host for host cert requests.  Please note that as implemented, anyone who can call the host lambda can obtain host certs for any hostname.
  • Loading branch information
russell-lewis authored May 22, 2019
2 parents 36fc01b + 3d8b0c9 commit c03b8d1
Show file tree
Hide file tree
Showing 20 changed files with 702 additions and 306 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ publish:
rm -rf ./publish/bless_lambda/
mkdir -p ./publish/bless_lambda
cp -r ./bless ./publish/bless_lambda/
mv ./publish/bless_lambda/bless/aws_lambda/* ./publish/bless_lambda/
cp ./publish/bless_lambda/bless/aws_lambda/bless* ./publish/bless_lambda/
cp -r ./aws_lambda_libs/. ./publish/bless_lambda/
if [ -d ./lambda_configs/ ]; then cp -r ./lambda_configs/. ./publish/bless_lambda/; fi
cd ./publish/bless_lambda && zip -FSr ../bless_lambda.zip .
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ The .zip must contain your lambda code and configurations at the top level of th
Makefile includes a publish target to package up everything into a deploy-able .zip if they are in
the expected locations. You will need to setup your own Python 3.7 lambda to deploy the .zip to.

Previously the AWS Lambda Handler needed to be set to `bless_lambda.lambda_handler`, and this would generate a user
cert. `bless_lambda.lambda_handler` still works for user certs. `bless_lambda_user.lambda_handler_user` is a handler
that can also be used to issue user certificates.

A new handler `bless_lambda_host.lambda_handler_host` has been created to allow for the creation of host SSH certs.

All three handlers exist in the published .zip.

### Compiling BLESS Lambda Dependencies
To deploy code as a Lambda Function, you need to package up all of the dependencies. You will need to
compile and include your dependencies before you can publish a working AWS Lambda.
Expand Down
235 changes: 4 additions & 231 deletions bless/aws_lambda/bless_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,238 +3,11 @@
:copyright: (c) 2016 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""
from bless.aws_lambda.bless_lambda_user import lambda_handler_user

import logging

import boto3
import os
import time
from bless.cache.bless_lambda_cache import BlessLambdaCache

from bless.config.bless_config import BLESS_OPTIONS_SECTION, \
CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \
CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \
ENTROPY_MINIMUM_BITS_OPTION, \
RANDOM_SEED_BYTES_OPTION, \
LOGGING_LEVEL_OPTION, \
USERNAME_VALIDATION_OPTION, \
KMSAUTH_SECTION, \
KMSAUTH_USEKMSAUTH_OPTION, \
KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION, \
VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION, \
KMSAUTH_SERVICE_ID_OPTION, \
TEST_USER_OPTION, \
CERTIFICATE_EXTENSIONS_OPTION, \
REMOTE_USERNAMES_VALIDATION_OPTION, \
IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION, \
REMOTE_USERNAMES_BLACKLIST_OPTION
from bless.request.bless_request import BlessSchema
from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \
get_ssh_certificate_authority
from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType
from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder
from kmsauth import KMSTokenValidator, TokenValidationError
from marshmallow.exceptions import ValidationError

global_bless_cache = None


def lambda_handler(event, context=None, ca_private_key_password=None, entropy_check=True, config_file=None):
def lambda_handler(*args, **kwargs):
"""
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.
Wrapper around lambda_handler_user for backwards compatibility
"""
# For testing, ignore the static bless_cache, otherwise fill the cache one time.
global global_bless_cache
if ca_private_key_password is not None or config_file is not None:
bless_cache = BlessLambdaCache(ca_private_key_password, config_file)
elif global_bless_cache is None:
global_bless_cache = BlessLambdaCache(config_file=os.path.join(os.path.dirname(__file__), 'bless_deploy.cfg'))
bless_cache = global_bless_cache
else:
bless_cache = global_bless_cache

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

# Load the deployment config values
config = bless_cache.config

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()
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)
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:
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))
kms_client = boto3.client('kms', region_name=bless_cache.region)
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.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)


def success_response(cert):
return {
'certificate': cert
}


def error_response(error_type, error_message):
return {
'errorType': error_type,
'errorMessage': error_message
}
return lambda_handler_user(*args, **kwargs)
75 changes: 75 additions & 0 deletions bless/aws_lambda/bless_lambda_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
"""
.. module: bless.aws_lambda.bless_lambda_common
:copyright: (c) 2016 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
"""
import logging
import os

import boto3
from bless.cache.bless_lambda_cache import BlessLambdaCache
from bless.config.bless_config import BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION, ENTROPY_MINIMUM_BITS_OPTION, \
RANDOM_SEED_BYTES_OPTION

global_bless_cache = None


def success_response(cert):
return {
'certificate': cert
}


def error_response(error_type, error_message):
return {
'errorType': error_type,
'errorMessage': error_message
}


def set_logger(config):
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)
return logger


def check_entropy(config, logger):
"""
Check the entropy pool and seed it with KMS if desired
"""
region = os.environ['AWS_REGION']
kms_client = boto3.client('kms', region_name=region)
entropy_minimum_bits = config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION)
random_seed_bytes = config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION)

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)


def setup_lambda_cache(ca_private_key_password, config_file):
# For testing, ignore the static bless_cache, otherwise fill the cache one time.
global global_bless_cache
if ca_private_key_password is not None or config_file is not None:
bless_cache = BlessLambdaCache(ca_private_key_password, config_file)
elif global_bless_cache is None:
global_bless_cache = BlessLambdaCache(config_file=os.path.join(os.getcwd(), 'bless_deploy.cfg'))
bless_cache = global_bless_cache
else:
bless_cache = global_bless_cache
return bless_cache
Loading

0 comments on commit c03b8d1

Please sign in to comment.