From de49946b5990525e1bdacbbc1e958360469905b6 Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Wed, 8 Apr 2020 17:56:16 -0700 Subject: [PATCH 01/13] Tests fail --- bless/aws_lambda/bless_lambda.py | 6 +- bless/aws_lambda/bless_lambda_lyft_host.py | 295 +++++++++++++++++++++ 2 files changed, 298 insertions(+), 3 deletions(-) create mode 100644 bless/aws_lambda/bless_lambda_lyft_host.py diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index f04e894a..f9c2f561 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -3,11 +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 +from bless.aws_lambda.bless_lambda_lyft_host import lambda_handler def lambda_handler(*args, **kwargs): """ - Wrapper around lambda_handler_user for backwards compatibility + Wrapper to redirect to Lyft version of bless_lambda_host """ - return lambda_handler_user(*args, **kwargs) + return lambda_handler(*args, **kwargs) diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py new file mode 100644 index 00000000..c7f4ede4 --- /dev/null +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -0,0 +1,295 @@ +""" +.. module: bless.aws_lambda.bless_lambda + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import base64 +import logging +import time + +import boto3 +import botocore +import os +import kmsauth +from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION, \ + CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ + BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, CERTIFICATE_TYPE_OPTION, \ + KMSAUTH_KEY_ID_OPTION, KMSAUTH_CONTEXT_OPTION, CROSS_ACCOUNT_ROLE_ARN_OPTION +from bless.request.bless_request import BlessUserSchema, BlessHostSchema +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 + +logger = logging.getLogger() + +REGIONS = { + 'iad': 'us-east-1', + 'sfo': 'us-west-1' +} + + +def get_role_name_from_request(request): + if request.onebox_name: + return 'onebox-production-iad' + else: + return '{}-{}-{}'.format( + request.service_name, + request.service_instance, + request.service_region) + + +def get_role_name(instance_id, cross_account_role_arn, aws_region='us-east-1'): + sts_client = boto3.client('sts') + assumed_role_object = sts_client.assume_role( + RoleArn=cross_account_role_arn, + RoleSessionName='AssumedRoleSession' + ) + credentials = assumed_role_object['Credentials'] + ec2_resource = boto3.resource( + 'ec2', + region_name=aws_region, + api_version='2016-11-15', + aws_access_key_id=credentials['AccessKeyId'], + aws_secret_access_key=credentials['SecretAccessKey'], + aws_session_token=credentials['SessionToken'] + ) + + instance = ec2_resource.Instance(instance_id) + try: + role = instance.iam_instance_profile['Arn'].split('/')[1] + except botocore.exceptions.ClientError: + logger.exception('Could not find instance {0}.'.format(instance_id)) + role = None + except IndexError: + logger.error( + 'Could not find the role associated with {0}.'.format(instance_id) + ) + role = None + except Exception: + logger.exception( + 'Failed to lookup role for instance id {0}.'.format(instance_id) + ) + role = None + return role + + +def validate_instance_id(instance_id, request, cross_account_role_arn): + aws_region = REGIONS.get(request.service_region, 'us-east-1') + role = get_role_name(instance_id, cross_account_role_arn, aws_region) + try: + role_split = role.split('-') + role_service_name = role_split[0] + role_service_instance = role_split[1] + role_service_region = role_split[2] + except IndexError: + logger.error( + 'Role is not a valid format {0}.'.format(role) + ) + return False + if (role_service_name in request.service_name and + role_service_instance == request.service_instance and + role_service_region == request.service_region): + return True + else: + return False + + +def get_hostnames(service_name, service_instance, service_region, instance_id, + availability_zone, onebox_name, is_canary): + cluster_name = '{0}-{1}-{2}'.format( + service_name, service_instance, service_region) + az_split = availability_zone.split('-') + az_shortened = az_split[2][-1] # last letter of 3rd block of az + + hostname_prefixes = [] + if instance_id: + # strip 'i' in 'i-12345' + instance_id_stripped = instance_id.split('-')[1] + hostname_prefixes.append(instance_id) + hostname_prefixes.append(instance_id_stripped) + hostname_prefixes.append(cluster_name) + hostname_prefixes.append(service_name) + hostname_prefixes.append('{service_name}-{az_letter}'.format( + service_name=service_name, + az_letter=az_shortened)) + hostname_prefixes.append('{service_name}-{service_region}'.format( + service_name=service_name, + service_region=service_region)) + hostname_prefixes.append('{service_name}-{service_instance}'.format( + service_name=service_name, + service_instance=service_instance)) + if is_canary: + hostname_prefixes.append('{service_name}-canary'.format( + service_name=service_name)) + hostname_prefixes.append('{cluster_name}-canary'.format( + cluster_name=cluster_name)) + if onebox_name: + hostname_prefixes.append('{onebox_name}.onebox'.format( + onebox_name=onebox_name)) + + hostname_suffixes = ['.lyft.net', '.ln'] + if service_name == 'gateway': + hostname_suffixes.append('lyft.com') + hostnames = [] + for prefix in hostname_prefixes: + for suffix in hostname_suffixes: + hostnames.append('{prefix}{suffix}'.format( + prefix=prefix, suffix=suffix)) + + return hostnames + + +def get_certificate_type(certificate_type_option): + if certificate_type_option == 'user': + return 1 + elif certificate_type_option == 'host': + return 2 + else: + raise ValueError('Invalid certificate type option: {}'.format(certificate_type_option)) + + +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 certificate_type: Type of certificate to be generated + :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) + + certificate_type = get_certificate_type(config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_TYPE_OPTION)) + 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_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) + cross_account_role_arn = config.get(BLESS_OPTIONS_SECTION, CROSS_ACCOUNT_ROLE_ARN_OPTION) + ca_private_key_file = config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) + password_ciphertext_b64 = config.getpassword() + kmsauth_key_id = config.get(BLESS_CA_SECTION, KMSAUTH_KEY_ID_OPTION) + kmsauth_context = config.get(BLESS_CA_SECTION, KMSAUTH_CONTEXT_OPTION) + + # 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 + if certificate_type == SSHCertificateType.HOST: + schema = BlessHostSchema(strict=True) + else: + schema = BlessUserSchema(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 + + # Authenticate the host with KMS, if key is setup + if kmsauth_key_id: + if request.kmsauth_token: + validator = kmsauth.KMSTokenValidator( + kmsauth_key_id, + kmsauth_key_id, + kmsauth_context, + region + ) + # decrypt_token will raise a TokenValidationError if token doesn't match + role_name = get_role_name_from_request(request) + validator.decrypt_token('2/service/{}'.format(role_name), request.kmsauth_token) + else: + raise ValueError('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, certificate_type, + request.public_key_to_sign) + if certificate_type == SSHCertificateType.USER: + cert_builder.add_valid_principal(request.remote_username) + # 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) + elif certificate_type == SSHCertificateType.HOST: + if not validate_instance_id(request.instance_id, request, cross_account_role_arn): + request.instance_id = None + remote_hostnames = get_hostnames(request.service_name, + request.service_instance, + request.service_region, + request.instance_id, + request.instance_availability_zone, + request.onebox_name, + request.is_canary) + for remote_hostname in remote_hostnames: + cert_builder.add_valid_principal(remote_hostname) + 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))) + else: + raise ValueError("Unknown certificate type") + + cert_builder.set_valid_before(valid_before) + cert_builder.set_valid_after(valid_after) + + cert_builder.set_key_id(key_id) + cert = cert_builder.get_cert_file() + + if certificate_type == SSHCertificateType.HOST: + remote_name = ', '.join(remote_hostnames) + bastion_ip = None + else: + remote_name = request.remote_username + bastion_ip = request.bastion_ip + + logger.info( + 'Issued a cert to bastion_ip[{}] for the remote_username of [{}] with the key_id[{}] and ' + 'valid_from[{}])'.format( + bastion_ip, remote_name, key_id, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) + return cert From d3630df05d60c378708aeaf36023f40f5f235cc7 Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Wed, 8 Apr 2020 18:50:25 -0700 Subject: [PATCH 02/13] Tests pass --- bless/aws_lambda/bless_lambda.py | 4 ++-- bless/aws_lambda/bless_lambda_lyft_host.py | 11 ++++++----- bless/config/bless_config.py | 8 ++++++++ requirements.txt | 2 +- tests/aws_lambda/test_bless_lambda_user.py | 8 -------- 5 files changed, 17 insertions(+), 16 deletions(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index f9c2f561..98ee00a5 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -3,11 +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_lyft_host import lambda_handler +from bless.aws_lambda.bless_lambda_lyft_host import lambda_lyft_host_handler def lambda_handler(*args, **kwargs): """ Wrapper to redirect to Lyft version of bless_lambda_host """ - return lambda_handler(*args, **kwargs) + return lambda_lyft_host_handler(*args, **kwargs) diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py index c7f4ede4..2991acab 100644 --- a/bless/aws_lambda/bless_lambda_lyft_host.py +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -15,7 +15,8 @@ CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, CERTIFICATE_TYPE_OPTION, \ KMSAUTH_KEY_ID_OPTION, KMSAUTH_CONTEXT_OPTION, CROSS_ACCOUNT_ROLE_ARN_OPTION -from bless.request.bless_request import BlessUserSchema, BlessHostSchema +from bless.request.bless_request_host import BlessHostSchema +from bless.request.bless_request_user import BlessUserSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType @@ -87,9 +88,9 @@ def validate_instance_id(instance_id, request, cross_account_role_arn): 'Role is not a valid format {0}.'.format(role) ) return False - if (role_service_name in request.service_name and - role_service_instance == request.service_instance and - role_service_region == request.service_region): + if (role_service_name in request.service_name + and role_service_instance == request.service_instance + and role_service_region == request.service_region): return True else: return False @@ -149,7 +150,7 @@ def get_certificate_type(certificate_type_option): raise ValueError('Invalid certificate type option: {}'.format(certificate_type_option)) -def lambda_handler(event, context=None, ca_private_key_password=None, +def lambda_lyft_host_handler(event, context=None, ca_private_key_password=None, entropy_check=True, config_file=os.path.join(os.path.dirname(__file__), 'bless_deploy.cfg')): """ diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index e5cf4a90..957a86a6 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -13,6 +13,7 @@ BLESS_OPTIONS_SECTION = 'Bless Options' CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'certificate_validity_before_seconds' CERTIFICATE_VALIDITY_AFTER_SEC_OPTION = 'certificate_validity_after_seconds' +CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION = 'certificate_validity_seconds' CERTIFICATE_VALIDITY_SEC_DEFAULT = 60 * 2 SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'server_certificate_validity_before_seconds' SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_DEFAULT = 120 @@ -31,6 +32,9 @@ TEST_USER_OPTION = 'test_user' TEST_USER_DEFAULT = None +CERTIFICATE_TYPE_OPTION = 'certificate_type' +CERTIFICATE_TYPE_DEFAULT = 'user' + CERTIFICATE_EXTENSIONS_OPTION = 'certificate_extensions' # These are the the ssh-keygen default extensions: CERTIFICATE_EXTENSIONS_DEFAULT = 'permit-X11-forwarding,' \ @@ -49,6 +53,8 @@ CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT = None REGION_PASSWORD_OPTION_SUFFIX = '_password' +CROSS_ACCOUNT_ROLE_ARN_OPTION = 'cross_account_role_arn' +CROSS_ACCOUNT_ROLE_ARN_DEFAULT = None KMSAUTH_SECTION = 'KMS Auth' KMSAUTH_USEKMSAUTH_OPTION = 'use_kmsauth' @@ -56,6 +62,8 @@ KMSAUTH_KEY_ID_OPTION = 'kmsauth_key_id' KMSAUTH_KEY_ID_DEFAULT = '' +KMSAUTH_CONTEXT_OPTION = 'kmsauth_context' +KMSAUTH_CONTEXT_DEFAULT = None KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION = 'kmsauth_remote_usernames_allowed' KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION_DEFAULT = None diff --git a/requirements.txt b/requirements.txt index 6ff53c89..c57a7995 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ asn1crypto==0.24.0 boto3==1.9.151 botocore==1.12.151 cffi==1.12.3 -cryptography==2.6.1 +cryptography==2.9.0 docutils==0.14 ipaddress==1.0.22 jmespath==0.9.4 diff --git a/tests/aws_lambda/test_bless_lambda_user.py b/tests/aws_lambda/test_bless_lambda_user.py index 40dce14f..71376edb 100644 --- a/tests/aws_lambda/test_bless_lambda_user.py +++ b/tests/aws_lambda/test_bless_lambda_user.py @@ -163,14 +163,6 @@ class Context(object): } -def test_basic_local_request_with_wrapper(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) - assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') - - def test_basic_local_request(): output = lambda_handler_user(VALID_TEST_REQUEST, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, From 4891a4f07423ce29c3943c48a2989d07c06d7ac5 Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Thu, 9 Apr 2020 08:40:02 -0700 Subject: [PATCH 03/13] lint --- bless/aws_lambda/bless_lambda_lyft_host.py | 10 ++++++---- bless/config/bless_config.py | 17 +++++++++-------- requirements.txt | 1 + 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py index 2991acab..ab7069d2 100644 --- a/bless/aws_lambda/bless_lambda_lyft_host.py +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -150,9 +150,12 @@ def get_certificate_type(certificate_type_option): raise ValueError('Invalid certificate type option: {}'.format(certificate_type_option)) -def lambda_lyft_host_handler(event, context=None, ca_private_key_password=None, - entropy_check=True, - config_file=os.path.join(os.path.dirname(__file__), 'bless_deploy.cfg')): +def lambda_lyft_host_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. @@ -162,7 +165,6 @@ def lambda_lyft_host_handler(event, context=None, ca_private_key_password=None, 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 certificate_type: Type of certificate to be generated :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. """ diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 957a86a6..a1b09405 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -10,10 +10,18 @@ import zlib import bz2 +# TODO Migrate bless_lambda_lyft_host to stop using these items below, which are migrated from an old version +CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION = 'certificate_validity_seconds' +CROSS_ACCOUNT_ROLE_ARN_OPTION = 'cross_account_role_arn' +CROSS_ACCOUNT_ROLE_ARN_DEFAULT = None +KMSAUTH_CONTEXT_OPTION = 'kmsauth_context' +KMSAUTH_CONTEXT_DEFAULT = None +CERTIFICATE_TYPE_OPTION = 'certificate_type' +CERTIFICATE_TYPE_DEFAULT = 'user' + BLESS_OPTIONS_SECTION = 'Bless Options' CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'certificate_validity_before_seconds' CERTIFICATE_VALIDITY_AFTER_SEC_OPTION = 'certificate_validity_after_seconds' -CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION = 'certificate_validity_seconds' CERTIFICATE_VALIDITY_SEC_DEFAULT = 60 * 2 SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'server_certificate_validity_before_seconds' SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_DEFAULT = 120 @@ -32,9 +40,6 @@ TEST_USER_OPTION = 'test_user' TEST_USER_DEFAULT = None -CERTIFICATE_TYPE_OPTION = 'certificate_type' -CERTIFICATE_TYPE_DEFAULT = 'user' - CERTIFICATE_EXTENSIONS_OPTION = 'certificate_extensions' # These are the the ssh-keygen default extensions: CERTIFICATE_EXTENSIONS_DEFAULT = 'permit-X11-forwarding,' \ @@ -53,8 +58,6 @@ CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT = None REGION_PASSWORD_OPTION_SUFFIX = '_password' -CROSS_ACCOUNT_ROLE_ARN_OPTION = 'cross_account_role_arn' -CROSS_ACCOUNT_ROLE_ARN_DEFAULT = None KMSAUTH_SECTION = 'KMS Auth' KMSAUTH_USEKMSAUTH_OPTION = 'use_kmsauth' @@ -62,8 +65,6 @@ KMSAUTH_KEY_ID_OPTION = 'kmsauth_key_id' KMSAUTH_KEY_ID_DEFAULT = '' -KMSAUTH_CONTEXT_OPTION = 'kmsauth_context' -KMSAUTH_CONTEXT_DEFAULT = None KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION = 'kmsauth_remote_usernames_allowed' KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION_DEFAULT = None diff --git a/requirements.txt b/requirements.txt index c57a7995..7411bcb1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,6 +3,7 @@ asn1crypto==0.24.0 boto3==1.9.151 botocore==1.12.151 cffi==1.12.3 +# Prevents fatal crashes when building on Mac cryptography==2.9.0 docutils==0.14 ipaddress==1.0.22 From 1d6579acceb8c8a9e6c3e84a142d0f45bdf8a9ee Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Thu, 9 Apr 2020 21:59:18 -0700 Subject: [PATCH 04/13] config --- bless/aws_lambda/bless_lambda_lyft_host.py | 14 ++-- bless/config/bless_config.py | 9 --- bless/config/bless_lyft_config.py | 74 ++++++++++++++++++++++ 3 files changed, 82 insertions(+), 15 deletions(-) create mode 100644 bless/config/bless_lyft_config.py diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py index ab7069d2..03297122 100644 --- a/bless/aws_lambda/bless_lambda_lyft_host.py +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -11,10 +11,13 @@ import botocore import os import kmsauth -from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION, \ - CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ - BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, CERTIFICATE_TYPE_OPTION, \ - KMSAUTH_KEY_ID_OPTION, KMSAUTH_CONTEXT_OPTION, CROSS_ACCOUNT_ROLE_ARN_OPTION +from bless.config.bless_config import BLESS_OPTIONS_SECTION, \ + ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ + BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, \ + KMSAUTH_KEY_ID_OPTION +from bless.config.bless_lyft_config import CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, CERTIFICATE_TYPE_OPTION,\ + KMSAUTH_CONTEXT_OPTION, CROSS_ACCOUNT_ROLE_ARN_OPTION +from bless.config.bless_lyft_config import LyftBlessConfig from bless.request.bless_request_host import BlessHostSchema from bless.request.bless_request_user import BlessUserSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ @@ -172,8 +175,7 @@ def lambda_lyft_host_handler( region = os.environ['AWS_REGION'] # Load the deployment config values - config = BlessConfig(region, - config_file=config_file) + config = LyftBlessConfig(region, config_file=config_file) certificate_type = get_certificate_type(config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_TYPE_OPTION)) logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION) diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index a1b09405..e5cf4a90 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -10,15 +10,6 @@ import zlib import bz2 -# TODO Migrate bless_lambda_lyft_host to stop using these items below, which are migrated from an old version -CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION = 'certificate_validity_seconds' -CROSS_ACCOUNT_ROLE_ARN_OPTION = 'cross_account_role_arn' -CROSS_ACCOUNT_ROLE_ARN_DEFAULT = None -KMSAUTH_CONTEXT_OPTION = 'kmsauth_context' -KMSAUTH_CONTEXT_DEFAULT = None -CERTIFICATE_TYPE_OPTION = 'certificate_type' -CERTIFICATE_TYPE_DEFAULT = 'user' - BLESS_OPTIONS_SECTION = 'Bless Options' CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'certificate_validity_before_seconds' CERTIFICATE_VALIDITY_AFTER_SEC_OPTION = 'certificate_validity_after_seconds' diff --git a/bless/config/bless_lyft_config.py b/bless/config/bless_lyft_config.py new file mode 100644 index 00000000..fe4037ef --- /dev/null +++ b/bless/config/bless_lyft_config.py @@ -0,0 +1,74 @@ +""" +.. module: bless.config.bless_config + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import configparser + +from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION + +CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION = 'certificate_validity_seconds' +CERTIFICATE_VALIDITY_SEC_DEFAULT = 60 * 2 + +ENTROPY_MINIMUM_BITS_OPTION = 'entropy_minimum_bits' +ENTROPY_MINIMUM_BITS_DEFAULT = 2048 + +RANDOM_SEED_BYTES_OPTION = 'random_seed_bytes' +RANDOM_SEED_BYTES_DEFAULT = 256 + +CROSS_ACCOUNT_ROLE_ARN_OPTION = 'cross_account_role_arn' +CROSS_ACCOUNT_ROLE_ARN_DEFAULT = None + +LOGGING_LEVEL_OPTION = 'logging_level' +LOGGING_LEVEL_DEFAULT = 'INFO' + +CERTIFICATE_TYPE_OPTION = 'certificate_type' +CERTIFICATE_TYPE_DEFAULT = 'user' + +BLESS_CA_SECTION = 'Bless CA' +CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' +KMS_KEY_ID_OPTION = 'kms_key_id' + +KMSAUTH_KEY_ID_OPTION = 'kmsauth_key_id' +KMSAUTH_KEY_ID_DEFAULT = None + +KMSAUTH_CONTEXT_OPTION = 'kmsauth_context' +KMSAUTH_CONTEXT_DEFAULT = None + +REGION_PASSWORD_OPTION_SUFFIX = '_password' + + +class LyftBlessConfig(BlessConfig): + def __init__(self, aws_region, config_file): + """ + Parses the BLESS config file, and provides some reasonable default values if they are + absent from the config file. + The [Bless Options] section is entirely optional, and has defaults. + The [Bless CA] section is required. + :param aws_region: The AWS Region BLESS is deployed to. + :param config_file: Path to the connfig file. + """ + self.aws_region = aws_region + defaults = {CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION: CERTIFICATE_VALIDITY_SEC_DEFAULT, + ENTROPY_MINIMUM_BITS_OPTION: ENTROPY_MINIMUM_BITS_DEFAULT, + RANDOM_SEED_BYTES_OPTION: RANDOM_SEED_BYTES_DEFAULT, + CROSS_ACCOUNT_ROLE_ARN_OPTION: CROSS_ACCOUNT_ROLE_ARN_DEFAULT, + LOGGING_LEVEL_OPTION: LOGGING_LEVEL_DEFAULT, + CERTIFICATE_TYPE_OPTION: CERTIFICATE_TYPE_DEFAULT, + KMSAUTH_KEY_ID_OPTION: KMSAUTH_KEY_ID_DEFAULT, + KMSAUTH_CONTEXT_OPTION: KMSAUTH_CONTEXT_DEFAULT} + configparser.RawConfigParser.__init__(self, defaults=defaults) + self.read(config_file) + + if not self.has_section(BLESS_OPTIONS_SECTION): + self.add_section(BLESS_OPTIONS_SECTION) + + if not self.has_option(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX): + raise ValueError("No Region Specific Password Provided.") + + def getpassword(self): + """ + Returns the correct encrypted password based off of the aws_region. + :return: A Base64 encoded KMS CiphertextBlob. + """ + return self.get(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX) From 17540f50397da109c4dfbabde27d5f72bd9b08b0 Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Fri, 10 Apr 2020 10:21:22 -0700 Subject: [PATCH 05/13] fix config a bit --- bless/config/bless_lyft_config.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bless/config/bless_lyft_config.py b/bless/config/bless_lyft_config.py index fe4037ef..43335d20 100644 --- a/bless/config/bless_lyft_config.py +++ b/bless/config/bless_lyft_config.py @@ -5,7 +5,7 @@ """ import configparser -from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION +from bless.config.bless_config import BLESS_OPTIONS_SECTION CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION = 'certificate_validity_seconds' CERTIFICATE_VALIDITY_SEC_DEFAULT = 60 * 2 @@ -38,7 +38,7 @@ REGION_PASSWORD_OPTION_SUFFIX = '_password' -class LyftBlessConfig(BlessConfig): +class LyftBlessConfig(configparser.RawConfigParser, object): def __init__(self, aws_region, config_file): """ Parses the BLESS config file, and provides some reasonable default values if they are From 094902858c0fb464515579a20b982615db1df75a Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Fri, 10 Apr 2020 13:42:52 -0700 Subject: [PATCH 06/13] Alter config file --- bless/aws_lambda/bless_lambda_lyft_host.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py index 03297122..50bd0c48 100644 --- a/bless/aws_lambda/bless_lambda_lyft_host.py +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -11,6 +11,8 @@ import botocore import os import kmsauth + +from bless.aws_lambda.bless_lambda_common import setup_lambda_cache from bless.config.bless_config import BLESS_OPTIONS_SECTION, \ ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, \ @@ -158,7 +160,7 @@ def lambda_lyft_host_handler( context=None, ca_private_key_password=None, entropy_check=True, - config_file=os.path.join(os.path.dirname(__file__), 'bless_deploy.cfg')): + config_file=None): """ This is the function that will be called when the lambda function starts. :param event: Dictionary of the json request. @@ -175,6 +177,7 @@ def lambda_lyft_host_handler( region = os.environ['AWS_REGION'] # Load the deployment config values + config_file = os.path.join(os.getcwd(), 'bless_deploy.cfg') config = LyftBlessConfig(region, config_file=config_file) certificate_type = get_certificate_type(config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_TYPE_OPTION)) From 4f0315104a071174e385fd448d00eeb293d37871 Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Fri, 10 Apr 2020 13:44:05 -0700 Subject: [PATCH 07/13] Debugging info --- bless/config/bless_lyft_config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bless/config/bless_lyft_config.py b/bless/config/bless_lyft_config.py index 43335d20..f1ae4e53 100644 --- a/bless/config/bless_lyft_config.py +++ b/bless/config/bless_lyft_config.py @@ -63,6 +63,9 @@ def __init__(self, aws_region, config_file): if not self.has_section(BLESS_OPTIONS_SECTION): self.add_section(BLESS_OPTIONS_SECTION) + if not self.has_section(BLESS_CA_SECTION): + raise ValueError("Can't read config file at: " + config_file) + if not self.has_option(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX): raise ValueError("No Region Specific Password Provided.") From ff6a0cfb2039234ad35611cc0e2028fc6454b001 Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Fri, 10 Apr 2020 14:52:35 -0700 Subject: [PATCH 08/13] don't use __file__ --- bless/aws_lambda/bless_lambda_lyft_host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py index 50bd0c48..6a143ade 100644 --- a/bless/aws_lambda/bless_lambda_lyft_host.py +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -199,7 +199,7 @@ def lambda_lyft_host_handler( kmsauth_context = config.get(BLESS_CA_SECTION, KMSAUTH_CONTEXT_OPTION) # read the private key .pem - with open(os.path.join(os.path.dirname(__file__), ca_private_key_file), 'r') as f: + with open(os.path.join(os.getcwd(), ca_private_key_file), 'r') as f: ca_private_key = f.read() # decrypt ca private key password From f962ec88274d0522e076fb65f552cfd796574990 Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Fri, 10 Apr 2020 15:58:05 -0700 Subject: [PATCH 09/13] Lyft specific schemas and requests for hosts --- bless/aws_lambda/bless_lambda_lyft_host.py | 4 +- bless/request/bless_request_lyft_host.py | 101 +++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) create mode 100644 bless/request/bless_request_lyft_host.py diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py index 6a143ade..27a51863 100644 --- a/bless/aws_lambda/bless_lambda_lyft_host.py +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -20,7 +20,7 @@ from bless.config.bless_lyft_config import CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, CERTIFICATE_TYPE_OPTION,\ KMSAUTH_CONTEXT_OPTION, CROSS_ACCOUNT_ROLE_ARN_OPTION from bless.config.bless_lyft_config import LyftBlessConfig -from bless.request.bless_request_host import BlessHostSchema +from bless.request.bless_request_lyft_host import BlessLyftHostSchema from bless.request.bless_request_user import BlessUserSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority @@ -227,7 +227,7 @@ def lambda_lyft_host_handler( # Process cert request if certificate_type == SSHCertificateType.HOST: - schema = BlessHostSchema(strict=True) + schema = BlessLyftHostSchema(strict=True) else: schema = BlessUserSchema(strict=True) request = schema.load(event).data diff --git a/bless/request/bless_request_lyft_host.py b/bless/request/bless_request_lyft_host.py new file mode 100644 index 00000000..382bf602 --- /dev/null +++ b/bless/request/bless_request_lyft_host.py @@ -0,0 +1,101 @@ +""" +.. module: bless.request.bless_request + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import ipaddress +import re +from marshmallow import Schema, fields, post_load, ValidationError + +# man 8 useradd +USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') +HOSTNAME_PATTERN = re.compile('[a-z0-9_.-]+') + + +def validate_ip(ip): + try: + ipaddress.ip_address(ip) + except ValueError: + raise ValidationError('Invalid IP address.') + + +def validate_user(user): + if len(user) > 32: + raise ValidationError('Username is too long') + if USERNAME_PATTERN.match(user) is None: + raise ValidationError('Username contains invalid characters') + + +def validate_host(hostname): + if len(hostname) > 64: + raise ValidationError('Hostname is too long') + if HOSTNAME_PATTERN.match(hostname) is None: + raise ValidationError('Hostname contains invalid characters') + + +class BlessLyftSchema(Schema): + public_key_to_sign = fields.Str() + + @post_load + def make_bless_request(self, data): + return BlessLyftRequest(**data) + + +class BlessLyftHostSchema(BlessLyftSchema): + service_name = fields.Str() + service_instance = fields.Str() + service_region = fields.Str() + kmsauth_token = fields.Str() + instance_id = fields.Str() + instance_availability_zone = fields.Str() + onebox_name = fields.Str(allow_none=True) + is_canary = fields.Bool() + + @post_load + def make_bless_request(self, data): + return BlessLyftHostRequest(**data) + + +class BlessLyftRequest: + def __init__(self, public_key_to_sign): + """ + A BlessRequest must have the following key value pairs to be valid. + :param public_key_to_sign: The id_rsa.pub that will be used in the SSH request. This is + enforced in the issued certificate. + """ + + self.public_key_to_sign = public_key_to_sign + + def __eq__(self, other): + return self.__dict__ == other.__dict__ + + +class BlessLyftHostRequest(BlessLyftRequest): + def __init__(self, public_key_to_sign, service_name, service_instance, + service_region, kmsauth_token, instance_id, + instance_availability_zone, onebox_name=None, is_canary=False): + """ + A BlessRequest must have the following key value pairs to be valid. + :param public_key_to_sign: The id_rsa.pub that will be used in the SSH request. This is + enforced in the issued certificate. + :param service_name: The service name. This is used to generate hostnames and verify kmsauth. + :param service_instance: The service instance name. This is used to generate hostnames + and verify kmsauth. (e.g. staging, production) + :param service_region: The service region name. This is used to generate hostnames + and verify kmsauth. (e.g. iad, sfo) + :param kmsauth_token: KMS auth token to authenticate the host + :param instance_id: The instance id of the host + :param instance_availability_zone: The availability zone of the host + :param onebox_name: The name of the onebox (or None if it is not a onebox) + :param is_canary: Whether the instance is a canary instance + """ + + self.public_key_to_sign = public_key_to_sign + self.service_name = service_name + self.service_instance = service_instance + self.service_region = service_region + self.kmsauth_token = kmsauth_token + self.instance_id = instance_id + self.instance_availability_zone = instance_availability_zone + self.onebox_name = onebox_name + self.is_canary = is_canary \ No newline at end of file From 50c5027be17d6fe5df097a6d0f93234120c17f4b Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Mon, 13 Apr 2020 08:47:12 -0700 Subject: [PATCH 10/13] strings vs bytes --- bless/aws_lambda/bless_lambda_lyft_host.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py index 27a51863..d84bd673 100644 --- a/bless/aws_lambda/bless_lambda_lyft_host.py +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -200,7 +200,7 @@ def lambda_lyft_host_handler( # read the private key .pem with open(os.path.join(os.getcwd(), ca_private_key_file), 'r') as f: - ca_private_key = f.read() + ca_private_key = bytes(f.read(), 'ascii') # decrypt ca private key password if ca_private_key_password is None: From f314d045c6c7fb2fd9e37e0db85fd9b392be004e Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Mon, 13 Apr 2020 10:42:42 -0700 Subject: [PATCH 11/13] Added tests --- bless/aws_lambda/bless_lambda_lyft_host.py | 8 ++-- bless/config/bless_lyft_config.py | 2 +- bless/request/bless_request_lyft_host.py | 10 +--- tests/aws_lambda/lyft-full.cfg | 16 +++++++ .../aws_lambda/test_bless_lambda_lyft_host.py | 46 +++++++++++++++++++ tests/config/full-lyft.cfg | 16 +++++++ tests/config/test_bless_lyft_config.py | 23 ++++++++++ tests/request/test_bless_request_lyft_host.py | 35 ++++++++++++++ 8 files changed, 142 insertions(+), 14 deletions(-) create mode 100644 tests/aws_lambda/lyft-full.cfg create mode 100644 tests/aws_lambda/test_bless_lambda_lyft_host.py create mode 100644 tests/config/full-lyft.cfg create mode 100644 tests/config/test_bless_lyft_config.py create mode 100644 tests/request/test_bless_request_lyft_host.py diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py index d84bd673..91d11b51 100644 --- a/bless/aws_lambda/bless_lambda_lyft_host.py +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -12,14 +12,13 @@ import os import kmsauth -from bless.aws_lambda.bless_lambda_common import setup_lambda_cache from bless.config.bless_config import BLESS_OPTIONS_SECTION, \ ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, \ KMSAUTH_KEY_ID_OPTION from bless.config.bless_lyft_config import CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, CERTIFICATE_TYPE_OPTION,\ KMSAUTH_CONTEXT_OPTION, CROSS_ACCOUNT_ROLE_ARN_OPTION -from bless.config.bless_lyft_config import LyftBlessConfig +from bless.config.bless_lyft_config import BlessLyftConfig from bless.request.bless_request_lyft_host import BlessLyftHostSchema from bless.request.bless_request_user import BlessUserSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ @@ -177,8 +176,9 @@ def lambda_lyft_host_handler( region = os.environ['AWS_REGION'] # Load the deployment config values - config_file = os.path.join(os.getcwd(), 'bless_deploy.cfg') - config = LyftBlessConfig(region, config_file=config_file) + if config_file is None: + config_file = os.path.join(os.getcwd(), 'bless_deploy.cfg') + config = BlessLyftConfig(region, config_file=config_file) certificate_type = get_certificate_type(config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_TYPE_OPTION)) logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION) diff --git a/bless/config/bless_lyft_config.py b/bless/config/bless_lyft_config.py index f1ae4e53..6076366b 100644 --- a/bless/config/bless_lyft_config.py +++ b/bless/config/bless_lyft_config.py @@ -38,7 +38,7 @@ REGION_PASSWORD_OPTION_SUFFIX = '_password' -class LyftBlessConfig(configparser.RawConfigParser, object): +class BlessLyftConfig(configparser.RawConfigParser, object): def __init__(self, aws_region, config_file): """ Parses the BLESS config file, and provides some reasonable default values if they are diff --git a/bless/request/bless_request_lyft_host.py b/bless/request/bless_request_lyft_host.py index 382bf602..32cc1803 100644 --- a/bless/request/bless_request_lyft_host.py +++ b/bless/request/bless_request_lyft_host.py @@ -8,7 +8,6 @@ from marshmallow import Schema, fields, post_load, ValidationError # man 8 useradd -USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') HOSTNAME_PATTERN = re.compile('[a-z0-9_.-]+') @@ -19,13 +18,6 @@ def validate_ip(ip): raise ValidationError('Invalid IP address.') -def validate_user(user): - if len(user) > 32: - raise ValidationError('Username is too long') - if USERNAME_PATTERN.match(user) is None: - raise ValidationError('Username contains invalid characters') - - def validate_host(hostname): if len(hostname) > 64: raise ValidationError('Hostname is too long') @@ -98,4 +90,4 @@ def __init__(self, public_key_to_sign, service_name, service_instance, self.instance_id = instance_id self.instance_availability_zone = instance_availability_zone self.onebox_name = onebox_name - self.is_canary = is_canary \ No newline at end of file + self.is_canary = is_canary diff --git a/tests/aws_lambda/lyft-full.cfg b/tests/aws_lambda/lyft-full.cfg new file mode 100644 index 00000000..47aec53f --- /dev/null +++ b/tests/aws_lambda/lyft-full.cfg @@ -0,0 +1,16 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_seconds = 604800 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +logging_level = DEBUG +certificate_type = host +cross_account_role_arn = arn:aws:iam:1234567:role/something + +[Bless CA] +us-east-1_password = pretend_password +us-west-2_password = another_pretend_password +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem +ca_private_key_compression = zlib +kmsauth_key_id = alias/authnz-iad, alias/authnz-sfo +kmsauth_context = secret-context diff --git a/tests/aws_lambda/test_bless_lambda_lyft_host.py b/tests/aws_lambda/test_bless_lambda_lyft_host.py new file mode 100644 index 00000000..330e643d --- /dev/null +++ b/tests/aws_lambda/test_bless_lambda_lyft_host.py @@ -0,0 +1,46 @@ +import os +import pytest + +from bless.aws_lambda.bless_lambda_lyft_host import lambda_lyft_host_handler +from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD + + +class Context(object): + aws_request_id = 'bogus aws_request_id' + invoked_function_arn = 'bogus invoked_function_arn' + + +VALID_TEST_REQUEST = { + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "hostnames": "thisthat.com", + 'service_name': 'testy', + 'service_instance': 'testing-staging-iad-03ab45f10397e780a.lyft.net', + 'service_region': 'us-east-1', + 'kmsauth_token': 'AABB', + 'instance_id': 'testing-staging-iad-03ab45f10397e780a', + 'instance_availability_zone': 'us-east-1' +} + +VALID_TEST_REQUEST_MULTIPLE_HOSTS = { + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "hostnames": "thisthat.com,thatthis.com", +} + +INVALID_TEST_REQUEST = { + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "hostname": "thisthat.com", # Wrong key name +} + +os.environ['AWS_REGION'] = 'us-west-2' + + +def test_basic_local_request(mocker): + mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") + # Below involves assuming a role on AWS + mocker.patch('bless.aws_lambda.bless_lambda_lyft_host.validate_instance_id', return_value=True) + output = lambda_lyft_host_handler(VALID_TEST_REQUEST, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'lyft-full.cfg')) + + assert output diff --git a/tests/config/full-lyft.cfg b/tests/config/full-lyft.cfg new file mode 100644 index 00000000..b9a14fc9 --- /dev/null +++ b/tests/config/full-lyft.cfg @@ -0,0 +1,16 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_seconds = 604800 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +logging_level = DEBUG +certificate_type = host +cross_account_role_arn = arn:aws:iam:1234567:role/something + +[Bless CA] +us-east-1_password = pretend_password +us-west-2_password = another_pretend_password +ca_private_key_file = secrets +ca_private_key_compression = zlib +kmsauth_key_id = secret-sauce +kmsauth_context = secret-context diff --git a/tests/config/test_bless_lyft_config.py b/tests/config/test_bless_lyft_config.py new file mode 100644 index 00000000..fdd24382 --- /dev/null +++ b/tests/config/test_bless_lyft_config.py @@ -0,0 +1,23 @@ +import os + +import pytest + +from bless.config.bless_lyft_config import BlessLyftConfig + + +def test_empty_config(): + with pytest.raises(ValueError): + BlessLyftConfig('us-west-2', config_file='') + + +def test_config_no_password(): + with pytest.raises(ValueError) as e: + BlessLyftConfig('bogus-region', + config_file=os.path.join(os.path.dirname(__file__), 'full.cfg')) + assert 'No Region Specific Password Provided.' == str(e.value) + + +def test_getpassword(): + config = BlessLyftConfig('us-east-1', config_file=(os.path.join(os.path.dirname(__file__), 'full-lyft.cfg'))) + + assert config.getpassword() == 'pretend_password' diff --git a/tests/request/test_bless_request_lyft_host.py b/tests/request/test_bless_request_lyft_host.py new file mode 100644 index 00000000..b2efb9e9 --- /dev/null +++ b/tests/request/test_bless_request_lyft_host.py @@ -0,0 +1,35 @@ +import pytest +from bless.request.bless_request_lyft_host import BlessLyftHostSchema, validate_host +from bless.request.bless_request_host import HOSTNAME_VALIDATION_OPTIONS +from marshmallow import ValidationError + + +@pytest.mark.parametrize("test_input", [ + 'thisthat', + 'this.that', + '10.1.1.1' +]) +def test_validate_hostnames(test_input): + validate_host(test_input) + + +@pytest.mark.parametrize("test_input", [ + '%&&&&', + '', + 'carzylongkdjfldksjfkldsfjlkdsjflkdsjflkdsjfkldjfkldjfdlkjfdkljdflkjslkdfjkldfjldk', +]) +def test_invalid_host(test_input): + with pytest.raises(ValidationError) as e: + validate_host(test_input) + + +@pytest.mark.parametrize("test_input", [ + 'this..that', + 'this!that.com', + 'this,that' +]) +def test_invalid_hostnames_with_disabled(test_input): + validate_host(test_input) + + + From 181a4061df12ce5a899ed06030a944d7f53f20d0 Mon Sep 17 00:00:00 2001 From: Lucy Cunningham Date: Wed, 15 Apr 2020 08:55:52 -0700 Subject: [PATCH 12/13] Ensure kms client is initialized --- bless/aws_lambda/bless_lambda_lyft_host.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py index 91d11b51..1bf33df1 100644 --- a/bless/aws_lambda/bless_lambda_lyft_host.py +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -219,6 +219,8 @@ def lambda_lyft_host_handler( 'System entropy was {}, which is lower than the entropy_' 'minimum {}. Using KMS to seed /dev/urandom'.format( entropy, entropy_minimum_bits)) + if not kms_client: + kms_client = boto3.client('kms', region_name=region) response = kms_client.generate_random( NumberOfBytes=random_seed_bytes) random_seed = response['Plaintext'] From 8a8c07152560761d0248ac472335a0676292d4ab Mon Sep 17 00:00:00 2001 From: Dean Liu Date: Tue, 21 Apr 2020 12:18:07 -0700 Subject: [PATCH 13/13] Add logging --- bless/aws_lambda/bless_lambda_lyft_host.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/bless/aws_lambda/bless_lambda_lyft_host.py b/bless/aws_lambda/bless_lambda_lyft_host.py index 1bf33df1..594e7aa8 100644 --- a/bless/aws_lambda/bless_lambda_lyft_host.py +++ b/bless/aws_lambda/bless_lambda_lyft_host.py @@ -64,16 +64,27 @@ def get_role_name(instance_id, cross_account_role_arn, aws_region='us-east-1'): try: role = instance.iam_instance_profile['Arn'].split('/')[1] except botocore.exceptions.ClientError: - logger.exception('Could not find instance {0}.'.format(instance_id)) + logger.exception( + 'Could not find instance {0}. ' + 'role: {1}, region: {2}'.format(instance_id, + cross_account_role_arn, + aws_region) + ) role = None except IndexError: logger.error( - 'Could not find the role associated with {0}.'.format(instance_id) + 'Could not find the role associated with {0}. ' + 'role: {1}, region: {2}'.format(instance_id, + cross_account_role_arn, + aws_region) ) role = None except Exception: logger.exception( - 'Failed to lookup role for instance id {0}.'.format(instance_id) + 'Failed to lookup role for instance id {0}. ' + 'role: {1}, region: {2}'.format(instance_id, + cross_account_role_arn, + aws_region) ) role = None return role