From 72713072b1be2e7c7b53099cca6153092898ab1f Mon Sep 17 00:00:00 2001 From: Matthew Aynalem Date: Thu, 22 Dec 2016 14:18:23 -0800 Subject: [PATCH 01/68] fixes #30 : add coveralls for test coverage reporting --- .coveragerc | 5 +++++ .travis.yml | 7 ++++++- Makefile | 4 +++- README.md | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..a2029cd --- /dev/null +++ b/.coveragerc @@ -0,0 +1,5 @@ +[report] +include = + bless/*.py +omit = + bless/__about__.py diff --git a/.travis.yml b/.travis.yml index 82618eb..1509dd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,12 +9,17 @@ matrix: - python: "2.7" install: + - pip install coveralls - make develop before_script: script: - - make test + - make lint + - make coverage + +after_success: + - coveralls notifications: email: diff --git a/Makefile b/Makefile index e963470..2d0428c 100644 --- a/Makefile +++ b/Makefile @@ -24,8 +24,10 @@ lint: @echo "" coverage: - coverage run --branch --source=bless -m py.test tests + @echo "--> Running Python tests with coverage" + coverage run --branch --source=bless -m py.test tests || exit 1 coverage html + @echo "" publish: rm -rf ./publish/bless_lambda/ diff --git a/README.md b/README.md index 540b137..df18496 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![alt text](bless_logo.png "BLESS") # BLESS - Bastion's Lambda Ephemeral SSH Service -[![Build Status](https://travis-ci.org/Netflix/bless.svg?branch=master)](https://travis-ci.org/Netflix/bless) [![Join the chat at https://gitter.im/Netflix/bless](https://badges.gitter.im/Netflix/bless.svg)](https://gitter.im/Netflix/bless?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://travis-ci.org/Netflix/bless.svg?branch=master)](https://travis-ci.org/Netflix/bless) [![Test coverage](https://coveralls.io/repos/github/Netflix/bless/badge.svg?branch=master)](https://coveralls.io/github/Netflix/bless) [![Join the chat at https://gitter.im/Netflix/bless](https://badges.gitter.im/Netflix/bless.svg)](https://gitter.im/Netflix/bless?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) BLESS is an SSH Certificate Authority that runs as a AWS Lambda function and is used to sign ssh public keys. From 69adbb75b3274d2187d8e4e994a26f66c4b21959 Mon Sep 17 00:00:00 2001 From: Chris Steipp Date: Fri, 3 Feb 2017 16:09:45 -0800 Subject: [PATCH 02/68] Squashed Lyft Changes --- .gitignore | 2 - bless/aws_lambda/bless_lambda.py | 89 ++++++++++++++++++++----- bless/config/bless_config.py | 36 +++++++++- bless/config/bless_deploy_example.cfg | 17 ++++- bless/request/bless_request.py | 20 +++--- bless_client/bless_client.py | 12 ++-- setup.py | 7 +- tests/aws_lambda/bless-test-kmsauth.cfg | 33 +++++++++ tests/aws_lambda/bless-test.cfg | 2 +- tests/aws_lambda/test_bless_lambda.py | 55 ++++++++++++++- tests/config/full.cfg | 5 +- tests/config/test_bless_config.py | 7 +- tests/request/test_bless_request.py | 20 ++++-- 13 files changed, 255 insertions(+), 50 deletions(-) create mode 100644 tests/aws_lambda/bless-test-kmsauth.cfg diff --git a/.gitignore b/.gitignore index fa9bb50..ec32c5a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,3 @@ htmlcov/ libs/ publish/ venv/ -aws_lambda_libs/ -lambda_configs/ diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index e0a3f5f..7fb34f2 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -9,9 +9,15 @@ import boto3 import os +from kmsauth import KMSTokenValidator, TokenValidationError +from botocore.exceptions import ClientError +from marshmallow.exceptions import ValidationError 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_VALIDITY_BEFORE_SEC_OPTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ + ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ + BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, KMSAUTH_SECTION, \ + KMSAUTH_USEKMSAUTH_OPTION, KMSAUTH_SERVICE_ID_OPTION, TEST_USER_OPTION + from bless.request.bless_request import BlessSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority @@ -49,13 +55,31 @@ def lambda_handler(event, context=None, ca_private_key_password=None, logger = logging.getLogger() logger.setLevel(numeric_level) - certificate_validity_window_seconds = config.getint(BLESS_OPTIONS_SECTION, - CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION) + 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_file = config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) password_ciphertext_b64 = config.getpassword() + # Process cert request + schema = BlessSchema(strict=True) + try: + request = schema.load(event).data + except ValidationError as e: + return { + 'errorType': 'InputValidationError', + 'errorMessage': 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)) + # 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() @@ -63,9 +87,15 @@ def lambda_handler(event, context=None, ca_private_key_password=None, # 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'] + try: + ca_password = kms_client.decrypt( + CiphertextBlob=base64.b64decode(password_ciphertext_b64)) + ca_private_key_password = ca_password['Plaintext'] + except ClientError as e: + return { + 'errorType': 'ClientError', + 'errorMessage': str(e) + } # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired if entropy_check: @@ -83,14 +113,41 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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 + test_user = config.get(BLESS_OPTIONS_SECTION, TEST_USER_OPTION) + if (test_user and (request.bastion_user == test_user or + request.remote_username == 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 + else: + 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_username), + request.kmsauth_token + ) + except TokenValidationError as e: + return { + 'errorType': 'KMSAuthValidationError', + 'errorMessage': str(e) + } + else: + raise ValueError('Invalid request, missing kmsauth token') # Build the cert ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) @@ -105,13 +162,13 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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_critical_option_source_address('{},{}'.format(request.bastion_user_ip, request.bastion_ips)) 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 ' + 'Issued a cert to bastion_ips[{}] for the remote_username of [{}] with the key_id[{}] and ' 'valid_from[{}])'.format( - request.bastion_ip, request.remote_username, key_id, + request.bastion_ips, request.remote_username, key_id, time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) return cert diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 1208606..83021e1 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -6,7 +6,8 @@ import ConfigParser BLESS_OPTIONS_SECTION = 'Bless Options' -CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION = 'certificate_validity_seconds' +CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'certificate_validity_before_seconds' +CERTIFICATE_VALIDITY_AFTER_SEC_OPTION = 'certificate_validity_after_seconds' CERTIFICATE_VALIDITY_SEC_DEFAULT = 60 * 2 ENTROPY_MINIMUM_BITS_OPTION = 'entropy_minimum_bits' @@ -18,12 +19,25 @@ LOGGING_LEVEL_OPTION = 'logging_level' LOGGING_LEVEL_DEFAULT = 'INFO' +TEST_USER_OPTION = 'test_user' +TEST_USER_DEFAULT = None + BLESS_CA_SECTION = 'Bless CA' CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' KMS_KEY_ID_OPTION = 'kms_key_id' REGION_PASSWORD_OPTION_SUFFIX = '_password' +KMSAUTH_SECTION = 'KMS Auth' +KMSAUTH_USEKMSAUTH_OPTION = 'use_kmsauth' +KMSAUTH_USEKMSAUTH_DEFAULT = False + +KMSAUTH_KEY_ID_OPTION = 'kmsauth_key_id' +KMSAUTH_KEY_ID_DEFAULT = '' + +KMSAUTH_SERVICE_ID_OPTION = 'kmsauth_serviceid' +KMSAUTH_SERVICE_ID_DEFAULT = None + class BlessConfig(ConfigParser.RawConfigParser): def __init__(self, aws_region, config_file): @@ -38,16 +52,24 @@ def __init__(self, aws_region, config_file): :param config_file: Path to the connfig file. """ self.aws_region = aws_region - defaults = {CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION: CERTIFICATE_VALIDITY_SEC_DEFAULT, + defaults = {CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION: CERTIFICATE_VALIDITY_SEC_DEFAULT, + CERTIFICATE_VALIDITY_AFTER_SEC_OPTION: CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_OPTION: ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_OPTION: RANDOM_SEED_BYTES_DEFAULT, - LOGGING_LEVEL_OPTION: LOGGING_LEVEL_DEFAULT} + LOGGING_LEVEL_OPTION: LOGGING_LEVEL_DEFAULT, + TEST_USER_OPTION: TEST_USER_DEFAULT, + KMSAUTH_SERVICE_ID_OPTION: KMSAUTH_SERVICE_ID_DEFAULT, + KMSAUTH_KEY_ID_OPTION: KMSAUTH_KEY_ID_DEFAULT, + KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_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_section(KMSAUTH_SECTION): + self.add_section(KMSAUTH_SECTION) + if not self.has_option(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX): raise ValueError("No Region Specific Password Provided.") @@ -57,3 +79,11 @@ def getpassword(self): :return: A Base64 encoded KMS CiphertextBlob. """ return self.get(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX) + + def getkmsauthkeyids(self): + """ + Returns a list of kmsauth keys used for validation (so a key generated + in one region can validate in another). + :return: A list of kmsauth key id's + """ + return map(str.strip, self.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION).split(',')) diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index 17d1397..69d174c 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -1,7 +1,8 @@ # This section and its options are optional [Bless Options] # Number of seconds +/- the issued time for the certificate to be valid -certificate_validity_window_seconds = 120 +certificate_validity_after_seconds = 120 +certificate_validity_before_seconds = 120 # Minimum number of bits in the system entropy pool before requiring an additional seeding step entropy_minimum_bits = 2048 # Number of bytes of random to fetch from KMS to seed /dev/urandom @@ -18,4 +19,16 @@ kms_key_id = us-east-1_password = us-west-2_password = # Specify the file name of your SSH CA's Private Key in PEM format. -ca_private_key_file = \ No newline at end of file +ca_private_key_file = + +# This section is optional +[KMS Auth] +# Enable kmsauth, to ensure the certificate's username matches the AWS user +# use_kmsauth = True + +# One or multiple KMS keys, setup for kmsauth (see github.com/lyft/python-kmsauth) +# kmsauth_key_id = arn:aws:kms:us-east-1:000000012345:key/eeff5544-6677-8899-9988-aaaabbbbcccc + +# If using kmsauth, you need to set the kmsauth service name. Users need to set the 'to' +# context to this same service name when they create a kmsauth token. +# kmsauth_serviceid = bless-production diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index 203682a..9b8eeda 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -11,9 +11,10 @@ USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') -def validate_ip(ip): +def validate_ips(ips): try: - ipaddress.ip_address(ip) + for ip in ips.split(','): + ipaddress.ip_network(ip, strict=True) except ValueError: raise ValidationError('Invalid IP address.') @@ -26,12 +27,13 @@ def validate_user(user): class BlessSchema(Schema): - bastion_ip = fields.Str(validate=validate_ip) + bastion_ips = fields.Str(validate=validate_ips) bastion_user = fields.Str(validate=validate_user) - bastion_user_ip = fields.Str(validate=validate_ip) + bastion_user_ip = fields.Str(validate=validate_ips) command = fields.Str() public_key_to_sign = fields.Str() remote_username = fields.Str(validate=validate_user) + kmsauth_token = fields.Str() @post_load def make_bless_request(self, data): @@ -39,11 +41,11 @@ def make_bless_request(self, data): class BlessRequest: - def __init__(self, bastion_ip, bastion_user, bastion_user_ip, command, public_key_to_sign, - remote_username): + def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_key_to_sign, + remote_username, kmsauth_token=None): """ A BlessRequest must have the following key value pairs to be valid. - :param bastion_ip: The source IP where the SSH connection will be initiated from. This is + :param bastion_ips: The source IPs where the SSH connection will be initiated from. This is enforced in the issued certificate. :param bastion_user: The user on the bastion, who is initiating the SSH request. :param bastion_user_ip: The IP of the user accessing the bastion. @@ -52,13 +54,15 @@ def __init__(self, bastion_ip, bastion_user, bastion_user_ip, command, public_ke enforced in the issued certificate. :param remote_username: The username on the remote server that will be used in the SSH request. This is enforced in the issued certificate. + :param kmsauth_token: An optional kms auth token to authenticate the user """ - self.bastion_ip = bastion_ip + self.bastion_ips = bastion_ips self.bastion_user = bastion_user self.bastion_user_ip = bastion_user_ip self.command = command self.public_key_to_sign = public_key_to_sign self.remote_username = remote_username + self.kmsauth_token = kmsauth_token def __eq__(self, other): return self.__dict__ == other.__dict__ diff --git a/bless_client/bless_client.py b/bless_client/bless_client.py index 65c3c28..c44d28d 100755 --- a/bless_client/bless_client.py +++ b/bless_client/bless_client.py @@ -30,7 +30,6 @@ "ssh will also try to load certificate information from the filename obtained by appending -cert.pub to identity filenames" e.g. the . """ -import base64 import json import stat import sys @@ -40,11 +39,11 @@ def main(argv): - if len(argv) != 9: + if len(argv) < 9 or len(argv) > 10: print ( 'Usage: bless_client.py region lambda_function_name bastion_user bastion_user_ip ' 'remote_username bastion_ip bastion_command ' - '') + ' [kmsauth token]') return -1 region, lambda_function_name, bastion_user, bastion_user_ip, remote_username, bastion_ip, \ @@ -54,11 +53,16 @@ def main(argv): public_key = f.read() payload = {'bastion_user': bastion_user, 'bastion_user_ip': bastion_user_ip, - 'remote_username': remote_username, 'bastion_ip': bastion_ip, + 'remote_username': remote_username, 'bastion_ips': bastion_ip, 'command': bastion_command, 'public_key_to_sign': public_key} + + if len(argv) == 10: + payload['kmsauth_token'] = argv[9] + payload_json = json.dumps(payload) print('Executing:') + print('payload_json is: \'{}\''.format(payload_json)) lambda_client = boto3.client('lambda', region_name=region) response = lambda_client.invoke(FunctionName=lambda_function_name, InvocationType='RequestResponse', LogType='None', diff --git a/setup.py b/setup.py index 5d4fedf..ff3c0d0 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ import os -from setuptools import setup +from setuptools import setup, find_packages ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__))) @@ -16,7 +16,7 @@ url=about["__uri__"], description=about["__summary__"], license=about["__license__"], - packages=[], + packages=find_packages(exclude=["test*"]), install_requires=[ 'boto3==1.3.1', 'botocore==1.4.37', @@ -32,7 +32,8 @@ 'pyasn1==0.1.9', 'pycparser==2.14', 'python-dateutil==2.5.3', - 'six==1.10.0' + 'six==1.10.0', + 'kmsauth==0.1.7' ], extras_require={ 'tests': [ diff --git a/tests/aws_lambda/bless-test-kmsauth.cfg b/tests/aws_lambda/bless-test-kmsauth.cfg new file mode 100644 index 0000000..5fb4e53 --- /dev/null +++ b/tests/aws_lambda/bless-test-kmsauth.cfg @@ -0,0 +1,33 @@ +[Bless CA] +ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +kms_key_id = alias/foo +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-test + +[KMS Auth] +use_kmsauth = True +kmsauth_key_id = alias/authnz-iad, alias/authnz-sfo +kmsauth_serviceid = kmsauth-prod + +# todo get from config, with some sane defaults +#[loggers] +#keys=root +# +#[handlers] +#keys=stream_handler +# +#[formatters] +#keys=formatter +# +#[logger_root] +#level=INFO +#handlers=stream_handler +# +#[handler_stream_handler] +#class=StreamHandler +#level=DEBUG +#formatter=formatter +#args=(sys.stderr,) +# +#[formatter_formatter] +#format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s diff --git a/tests/aws_lambda/bless-test.cfg b/tests/aws_lambda/bless-test.cfg index 4cef4e2..5bae8ad 100644 --- a/tests/aws_lambda/bless-test.cfg +++ b/tests/aws_lambda/bless-test.cfg @@ -2,4 +2,4 @@ ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem kms_key_id = alias/foo us-east-1_password = bogus-password-for-unit-test -us-west-2_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-tests diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 21763ba..bd81f41 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -4,6 +4,7 @@ from bless.aws_lambda.bless_lambda import lambda_handler from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ EXAMPLE_ED25519_PUBLIC_KEY +from kmsauth import TokenValidationError class Context(object): @@ -15,11 +16,30 @@ class Context(object): "remote_username": "user", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, "command": "ssh user@server", - "bastion_ip": "127.0.0.1", + "bastion_ips": "127.0.0.1", "bastion_user": "user", "bastion_user_ip": "127.0.0.1" } +INVALID_TEST_REQUEST = { + "remote_username": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "invalid_ip", + "bastion_user": "user", + "bastion_user_ip": "invalid_ip" +} + +VALID_TEST_REQUEST_KMSAUTH = { + "remote_username": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1", + "kmsauth_token": "validkmsauthtoken", +} + os.environ['AWS_REGION'] = 'us-west-2' @@ -31,6 +51,37 @@ def test_basic_local_request(): assert cert.startswith('ssh-rsa-cert-v01@openssh.com ') +def test_basic_local_unused_kmsauth_request(): + cert = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, 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 cert.startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_local_missing_kmsauth_request(): + with pytest.raises(ValueError): + cert = 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-kmsauth.cfg')) + +def test_invalid_kmsauth_request(): + output = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, 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-kmsauth.cfg')) + assert output['errorType'] == 'KMSAuthValidationError' + +def test_invalid_request(): + output = lambda_handler(INVALID_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['errorType'] == 'InputValidationError' + def test_local_request_key_not_found(): with pytest.raises(IOError): lambda_handler(VALID_TEST_REQUEST, context=Context, @@ -52,7 +103,7 @@ def test_local_request_invalid_pub_key(): "remote_username": "user", "public_key_to_sign": EXAMPLE_ED25519_PUBLIC_KEY, "command": "ssh user@server", - "bastion_ip": "127.0.0.1", + "bastion_ips": "127.0.0.1", "bastion_user": "user", "bastion_user_ip": "127.0.0.1" } diff --git a/tests/config/full.cfg b/tests/config/full.cfg index ab44d08..09b2fc1 100644 --- a/tests/config/full.cfg +++ b/tests/config/full.cfg @@ -1,6 +1,7 @@ [Bless Options] # The default values are sane, these are not. -certificate_validity_seconds = 1 +certificate_validity_after_seconds = 1 +certificate_validity_before_seconds = 1 entropy_minimum_bits = 2 random_seed_bytes = 3 logging_level = DEBUG @@ -9,4 +10,4 @@ logging_level = DEBUG kms_key_id = alias/foo us-east-1_password = us-west-2_password = -ca_private_key_file = \ No newline at end of file +ca_private_key_file = diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index db56260..5fc4da3 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -3,7 +3,8 @@ import pytest from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION, \ - CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ + CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ + ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, \ LOGGING_LEVEL_DEFAULT, LOGGING_LEVEL_OPTION @@ -38,7 +39,9 @@ def test_configs(config, region, expected_cert_valid, expected_entropy_min, expe expected_log_level, expected_password): config = BlessConfig(region, config_file=config) assert expected_cert_valid == config.getint(BLESS_OPTIONS_SECTION, - CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION) + 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) diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index 39eb5eb..5855bd4 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -1,12 +1,21 @@ import pytest -from bless.request.bless_request import validate_ip, validate_user +from bless.request.bless_request import validate_ips, validate_user from marshmallow import ValidationError -def test_validate_ip(): - validate_ip(u'127.0.0.1') +def test_validate_ips(): + validate_ips(u'127.0.0.1') with pytest.raises(ValidationError): - validate_ip(u'256.0.0.0') + validate_ips(u'256.0.0.0') + validate_ips(u'127.0.0.1,172.1.1.1') + with pytest.raises(ValidationError): + validate_ips(u'256.0.0.0,172.1.1.1') + + +def test_validate_ips_cidr(): + validate_ips(u'10.0.0.0/8,172.1.1.1') + with pytest.raises(ValidationError): + validate_ips(u'10.10.10.10/8') def test_validate_user_too_long(): @@ -26,6 +35,7 @@ def test_validate_user_contains_junk(test_input): validate_user(test_input) assert e.value.message == 'Username contains invalid characters' + @pytest.mark.parametrize("test_input", [ ('uservalid'), ('a32characterusernameyoumustok$'), @@ -33,4 +43,4 @@ def test_validate_user_contains_junk(test_input): ('abc123_-valid') ]) def test_validate_user(test_input): - validate_user(test_input) \ No newline at end of file + validate_user(test_input) From ba6dc07c7d697c7b004f04b5fc8f62a76a2c8087 Mon Sep 17 00:00:00 2001 From: Niels Laukens Date: Wed, 15 Feb 2017 14:22:53 +0100 Subject: [PATCH 03/68] Make the 'extensions' included in the cert configurable --- bless/aws_lambda/bless_lambda.py | 13 ++++++++++++- bless/config/bless_config.py | 3 +++ bless/config/bless_deploy_example.cfg | 2 ++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index e0a3f5f..f980597 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -11,7 +11,8 @@ import os 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 + BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, EXTENSIONS_OPTION +import ConfigParser from bless.request.bless_request import BlessSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority @@ -56,6 +57,11 @@ def lambda_handler(event, context=None, ca_private_key_password=None, ca_private_key_file = config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) password_ciphertext_b64 = config.getpassword() + try: + extensions = config.get(BLESS_OPTIONS_SECTION, EXTENSIONS_OPTION) + except ConfigParser.NoOptionError: + extensions = None + # 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() @@ -100,6 +106,11 @@ def lambda_handler(event, context=None, ca_private_key_password=None, cert_builder.set_valid_before(valid_before) cert_builder.set_valid_after(valid_after) + if extensions is not None: + cert_builder.clear_extensions() + for e in extensions.split(): + cert_builder.add_extension(e) + # 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, diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 1208606..2090b6d 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -18,6 +18,9 @@ LOGGING_LEVEL_OPTION = 'logging_level' LOGGING_LEVEL_DEFAULT = 'INFO' +EXTENSIONS_OPTION = 'extensions' +# Default for Extensions is None + BLESS_CA_SECTION = 'Bless CA' CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' KMS_KEY_ID_OPTION = 'kms_key_id' diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index 17d1397..d90a365 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -8,6 +8,8 @@ entropy_minimum_bits = 2048 random_seed_bytes = 256 # Set the logging level logging_level = INFO +# Optionally set more restrictive extensions. Not specifying this uses the default extension set +#extensions = permit-pty permit-user-rc permit-agent-forwarding # These values are all required to be modified for deployment [Bless CA] From 17e5336987779aa094abdc7a33312d03d7f63b40 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Fri, 24 Feb 2017 12:47:07 -0800 Subject: [PATCH 04/68] Moving pinned packages out of setup.py and into requirements.txt. --- .gitignore | 2 +- requirements.txt | 23 ++++++++++++++++++++++- setup.cfg | 2 +- setup.py | 34 +++++++++++----------------------- 4 files changed, 35 insertions(+), 26 deletions(-) diff --git a/.gitignore b/.gitignore index ec32c5a..9777179 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.pyc *.cache/ .idea/ -bless.egg-info/ +BLESS.egg-info/ htmlcov/ libs/ publish/ diff --git a/requirements.txt b/requirements.txt index ecf975e..48b6de7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,22 @@ --e . \ No newline at end of file +-e . +appdirs==1.4.1 +boto3==1.4.4 +botocore==1.5.16 +cffi==1.9.1 +cryptography==1.7.2 +docutils==0.13.1 +enum34==1.1.6 +futures==3.0.5 +idna==2.2 +ipaddress==1.0.18 +jmespath==0.9.1 +kmsauth==0.2.0 +marshmallow==2.13.0 +packaging==16.8 +pkg-resources==0.0.0 +pyasn1==0.2.2 +pycparser==2.17 +pyparsing==2.1.10 +python-dateutil==2.6.0 +s3transfer==0.1.10 +six==1.10.0 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index a7c505c..055b7e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ -[pytest] +[tool:pytest] python_files=test*.py addopts=--tb=native -p no:doctest norecursedirs=docs htmlcov .* {args} diff --git a/setup.py b/setup.py index ff3c0d0..0a3a3ae 100644 --- a/setup.py +++ b/setup.py @@ -18,32 +18,20 @@ license=about["__license__"], packages=find_packages(exclude=["test*"]), install_requires=[ - 'boto3==1.3.1', - 'botocore==1.4.37', - 'cffi==1.7.0', - 'cryptography==1.4', - 'docutils==0.12', - 'enum34==1.1.6', - 'futures==3.0.5', - 'idna==2.1', - 'ipaddress==1.0.16', - 'jmespath==0.9.0', - 'marshmallow==2.9.0', - 'pyasn1==0.1.9', - 'pycparser==2.14', - 'python-dateutil==2.5.3', - 'six==1.10.0', - 'kmsauth==0.1.7' + 'boto3', + 'botocore', + 'cffi', + 'cryptography', + 'ipaddress', + 'marshmallow', + 'kmsauth' ], extras_require={ 'tests': [ - 'coverage==4.1', - 'flake8==2.6.2', - 'mccabe==0.5.0', - 'pep8==1.7.0', - 'py==1.4.31', - 'pyflakes==1.2.3', - 'pytest==2.9.2' + 'coverage', + 'flake8', + 'pyflakes', + 'pytest' ] } ) From bc6b3bfa4d41080fc03263d879888e8188959812 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Fri, 24 Feb 2017 12:48:14 -0800 Subject: [PATCH 05/68] Adding a few changes after syncing with lyft's BLESS fork. Updated all refrences of bastion_ip to bastion_ips. Added some extra checks to the bless_request. --- bless/aws_lambda/bless_lambda.py | 2 +- bless/request/bless_request.py | 34 +++++-- .../certificates/ssh_certificate_builder.py | 2 +- bless_client/bless_client.py | 10 +- .../aws_lambda/bless-test-with-test-user.cfg | 8 ++ tests/aws_lambda/test_bless_lambda.py | 94 ++++++++++++++----- tests/ssh/test_ssh_certificate_rsa.py | 4 +- 7 files changed, 112 insertions(+), 42 deletions(-) create mode 100644 tests/aws_lambda/bless-test-with-test-user.cfg diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 7fb34f2..38f6515 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -162,7 +162,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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('{},{}'.format(request.bastion_user_ip, request.bastion_ips)) + cert_builder.set_critical_option_source_addresses('{}'.format(request.bastion_ips)) cert_builder.set_key_id(key_id) cert = cert_builder.get_cert_file() diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index 9b8eeda..132d106 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -3,12 +3,14 @@ :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 + +import ipaddress +from marshmallow import Schema, fields, post_load, ValidationError, validates_schema # man 8 useradd USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') +VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" def validate_ips(ips): @@ -26,14 +28,28 @@ def validate_user(user): raise ValidationError('Username contains invalid characters') +def validate_ssh_public_key(public_key): + if public_key.startswith(VALID_SSH_RSA_PUBLIC_KEY_HEADER): + pass + # todo other key types + else: + raise ValidationError('Invalid SSH Public Key.') + + class BlessSchema(Schema): - bastion_ips = fields.Str(validate=validate_ips) - bastion_user = fields.Str(validate=validate_user) - bastion_user_ip = fields.Str(validate=validate_ips) - command = fields.Str() - public_key_to_sign = fields.Str() - remote_username = fields.Str(validate=validate_user) - kmsauth_token = fields.Str() + bastion_ips = fields.Str(validate=validate_ips, required=True) + bastion_user = fields.Str(validate=validate_user, required=True) + bastion_user_ip = fields.Str(validate=validate_ips, required=True) + command = fields.Str(required=True) + public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True) + remote_username = fields.Str(validate=validate_user, required=True) + kmsauth_token = fields.Str(required=False) + + @validates_schema(pass_original=True) + def check_unknown_fields(self, data, original_data): + unknown = set(original_data) - set(self.fields) + if unknown: + raise ValidationError('Unknown field', unknown) @post_load def make_bless_request(self, data): diff --git a/bless/ssh/certificates/ssh_certificate_builder.py b/bless/ssh/certificates/ssh_certificate_builder.py index f01ad8a..9b80a19 100644 --- a/bless/ssh/certificates/ssh_certificate_builder.py +++ b/bless/ssh/certificates/ssh_certificate_builder.py @@ -141,7 +141,7 @@ def set_critical_option_force_command(self, command): else: raise ValueError("Provide a non-null string") - def set_critical_option_source_address(self, address): + def set_critical_option_source_addresses(self, address): """ Sets which IP address(es) this certificate can be used from for authentication. Addresses should be comma-separated and can be individual IPs or CIDR format (nn.nn.nn.nn/nn or diff --git a/bless_client/bless_client.py b/bless_client/bless_client.py index c44d28d..5c1fe7c 100755 --- a/bless_client/bless_client.py +++ b/bless_client/bless_client.py @@ -5,7 +5,7 @@ Usage: bless_client.py region lambda_function_name bastion_user bastion_user_ip remote_username - bastion_ip bastion_command + bastion_ips bastion_command region: AWS region where your lambda is deployed. @@ -18,7 +18,7 @@ remote_username: The username on the remote server that will be used in the SSH request. This is enforced in the issued certificate. - bastion_ip: The source IP where the SSH connection will be initiated from. This is + bastion_ips: The source IP(s) where the SSH connection will be initiated from. This is enforced in the issued certificate. bastion_command: Text information about the SSH request of the bastion_user. @@ -42,18 +42,18 @@ def main(argv): if len(argv) < 9 or len(argv) > 10: print ( 'Usage: bless_client.py region lambda_function_name bastion_user bastion_user_ip ' - 'remote_username bastion_ip bastion_command ' + 'remote_username bastion_ips bastion_command ' ' [kmsauth token]') return -1 - region, lambda_function_name, bastion_user, bastion_user_ip, remote_username, bastion_ip, \ + region, lambda_function_name, bastion_user, bastion_user_ip, remote_username, bastion_ips, \ bastion_command, public_key_filename, certificate_filename = argv with open(public_key_filename, 'r') as f: public_key = f.read() payload = {'bastion_user': bastion_user, 'bastion_user_ip': bastion_user_ip, - 'remote_username': remote_username, 'bastion_ips': bastion_ip, + 'remote_username': remote_username, 'bastion_ips': bastion_ips, 'command': bastion_command, 'public_key_to_sign': public_key} if len(argv) == 10: diff --git a/tests/aws_lambda/bless-test-with-test-user.cfg b/tests/aws_lambda/bless-test-with-test-user.cfg new file mode 100644 index 0000000..4f78eed --- /dev/null +++ b/tests/aws_lambda/bless-test-with-test-user.cfg @@ -0,0 +1,8 @@ +[Bless CA] +ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +kms_key_id = alias/foo +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-tests + +[Bless Options] +test_user=user \ No newline at end of file diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index bd81f41..003e38d 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -1,10 +1,10 @@ import os + import pytest from bless.aws_lambda.bless_lambda import lambda_handler from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ EXAMPLE_ED25519_PUBLIC_KEY -from kmsauth import TokenValidationError class Context(object): @@ -40,6 +40,33 @@ class Context(object): "kmsauth_token": "validkmsauthtoken", } +INVALID_TEST_REQUEST_KEY_TYPE = { + "remote_username": "user", + "public_key_to_sign": EXAMPLE_ED25519_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + +INVALID_TEST_REQUEST_EXTRA_FIELD = { + "remote_username": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1", + "bastion_ip": "127.0.0.1" # Note this is now an invalid field. +} + +INVALID_TEST_REQUEST_MISSING_FIELD = { + "remote_username": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + os.environ['AWS_REGION'] = 'us-west-2' @@ -61,27 +88,30 @@ def test_basic_local_unused_kmsauth_request(): def test_basic_local_missing_kmsauth_request(): with pytest.raises(ValueError): - cert = 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-kmsauth.cfg')) + 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-kmsauth.cfg')) + def test_invalid_kmsauth_request(): output = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, 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-kmsauth.cfg')) + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), + 'bless-test-kmsauth.cfg')) assert output['errorType'] == 'KMSAuthValidationError' + def test_invalid_request(): output = lambda_handler(INVALID_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')) + 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['errorType'] == 'InputValidationError' + def test_local_request_key_not_found(): with pytest.raises(IOError): lambda_handler(VALID_TEST_REQUEST, context=Context, @@ -99,16 +129,32 @@ def test_local_request_config_not_found(): def test_local_request_invalid_pub_key(): - invalid_key_request = { - "remote_username": "user", - "public_key_to_sign": EXAMPLE_ED25519_PUBLIC_KEY, - "command": "ssh user@server", - "bastion_ips": "127.0.0.1", - "bastion_user": "user", - "bastion_user_ip": "127.0.0.1" - } - with pytest.raises(TypeError): - lambda_handler(invalid_key_request, context=Context, + output = lambda_handler(INVALID_TEST_REQUEST_KEY_TYPE, 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['errorType'] == 'InputValidationError' + + +def test_local_request_extra_field(): + output = lambda_handler(INVALID_TEST_REQUEST_EXTRA_FIELD, 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['errorType'] == 'InputValidationError' + + +def test_local_request_missing_field(): + output = lambda_handler(INVALID_TEST_REQUEST_MISSING_FIELD, 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['errorType'] == 'InputValidationError' + + +def test_local_request_with_test_user(): + with pytest.raises(ValueError): + 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')) + config_file=os.path.join(os.path.dirname(__file__), 'bless-test-with-test-user.cfg')) diff --git a/tests/ssh/test_ssh_certificate_rsa.py b/tests/ssh/test_ssh_certificate_rsa.py index 2107666..ee34114 100644 --- a/tests/ssh/test_ssh_certificate_rsa.py +++ b/tests/ssh/test_ssh_certificate_rsa.py @@ -112,7 +112,7 @@ def test_bogus_critical_options(): cert_builder.set_critical_option_force_command('') with pytest.raises(ValueError): - cert_builder.set_critical_option_source_address('') + cert_builder.set_critical_option_source_addresses('') def test_rsa_user_cert_minimal(): @@ -202,7 +202,7 @@ def test_rsa_user_cert_critical_opt_source_address(): nonce=extract_nonce_from_cert(RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS)) cert_builder.set_key_id(RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS_KEY_ID) cert_builder.set_critical_option_force_command('/bin/ls') - cert_builder.set_critical_option_source_address('192.168.1.0/24') + cert_builder.set_critical_option_source_addresses('192.168.1.0/24') cert = cert_builder.get_cert_file() From 9962238970710d07c62e5749cf22e5b4c5bd7799 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Fri, 24 Feb 2017 12:56:02 -0800 Subject: [PATCH 06/68] Bumping version number to reflect the change in the BLESS request of bastion_ip to bastion_ips. Removing a package from requirements.txt that isn't needed. --- bless/__about__.py | 2 +- requirements.txt | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/bless/__about__.py b/bless/__about__.py index c82fa30..89972af 100644 --- a/bless/__about__.py +++ b/bless/__about__.py @@ -11,7 +11,7 @@ "sign SSH public keys.") __uri__ = "https://github.com/Netflix/bless" -__version__ = "0.1.1" +__version__ = "0.2.0" __author__ = "The BLESS developers" __email__ = "security@netflix.com" diff --git a/requirements.txt b/requirements.txt index 48b6de7..7c3db7b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,22 +1,28 @@ -e . -appdirs==1.4.1 boto3==1.4.4 -botocore==1.5.16 +botocore==1.5.17 cffi==1.9.1 +configparser==3.5.0 +coverage==4.3.4 cryptography==1.7.2 docutils==0.13.1 enum34==1.1.6 +flake8==3.3.0 futures==3.0.5 idna==2.2 ipaddress==1.0.18 jmespath==0.9.1 kmsauth==0.2.0 marshmallow==2.13.0 +mccabe==0.6.1 packaging==16.8 -pkg-resources==0.0.0 +py==1.4.32 pyasn1==0.2.2 +pycodestyle==2.3.1 pycparser==2.17 +pyflakes==1.5.0 pyparsing==2.1.10 +pytest==3.0.6 python-dateutil==2.6.0 s3transfer==0.1.10 six==1.10.0 \ No newline at end of file From 290801ba568484ac8893c1a2c706084376747f19 Mon Sep 17 00:00:00 2001 From: Vivian Ho Date: Fri, 24 Feb 2017 13:50:27 -0800 Subject: [PATCH 07/68] added a flag for bypassing validity check --- bless/aws_lambda/bless_lambda.py | 4 +++- bless/ssh/certificates/ssh_certificate_builder.py | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 7fb34f2..abbfa8e 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -122,9 +122,11 @@ def lambda_handler(event, context=None, ca_private_key_password=None, # 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): @@ -164,7 +166,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before))) cert_builder.set_critical_option_source_address('{},{}'.format(request.bastion_user_ip, request.bastion_ips)) cert_builder.set_key_id(key_id) - cert = cert_builder.get_cert_file() + cert = cert_builder.get_cert_file(bypass_time_validity_check) logger.info( 'Issued a cert to bastion_ips[{}] for the remote_username of [{}] with the key_id[{}] and ' diff --git a/bless/ssh/certificates/ssh_certificate_builder.py b/bless/ssh/certificates/ssh_certificate_builder.py index f01ad8a..a4ddca3 100644 --- a/bless/ssh/certificates/ssh_certificate_builder.py +++ b/bless/ssh/certificates/ssh_certificate_builder.py @@ -195,7 +195,7 @@ def add_extension(self, extension): self.extensions.add(extension) - def get_cert_file(self): + def get_cert_file(self, bypass_time_validity_check=False): """ Generate the SSH Certificate that can be written to id_rsa-cert.pub or similar file. @@ -206,7 +206,8 @@ def get_cert_file(self): """ file_contents = ( "{} {} {}" - ).format(self.cert_key_type, base64.b64encode(self._sign_cert()), + ).format(self.cert_key_type, + base64.b64encode(self._sign_cert(bypass_time_validity_check)), self.public_key_comment) return file_contents @@ -238,11 +239,12 @@ def _validate_cert_properties(self): if self.valid_after >= self.valid_before: raise ValueError("Impossible validity period") - def _sign_cert(self): + def _sign_cert(self, bypass_time_validity_check=False): if self.signed_cert is None: # build cert body self._initialize_unset_attributes() - self._validate_cert_properties() + if not bypass_time_validity_check: + self._validate_cert_properties() body_bytes = self._serialize_certificate_body() # sign the body From 5f264d5526ab1136c25902a1c5d6120b61223660 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Fri, 24 Feb 2017 14:32:08 -0800 Subject: [PATCH 08/68] Updating the "test_user" certificate test case. --- tests/aws_lambda/test_bless_lambda.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 003e38d..43b987a 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -153,8 +153,8 @@ def test_local_request_missing_field(): def test_local_request_with_test_user(): - with pytest.raises(ValueError): - 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-with-test-user.cfg')) + cert = 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-with-test-user.cfg')) + assert cert.startswith('ssh-rsa-cert-v01@openssh.com ') From 79c34630b666916cfb3cf00f1dc660df6f985253 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Tue, 28 Feb 2017 18:21:07 -0800 Subject: [PATCH 09/68] Updates after pulling in PR#33. Renamed extensions to certificate_extensions and explicitly set the defaults instead of needing ssh_certificate_builder.py to set them. --- bless/aws_lambda/bless_lambda.py | 12 +++++++----- bless/config/bless_config.py | 13 ++++++++++--- bless/config/bless_deploy_example.cfg | 4 ++-- .../bless-test-with-certificate-extensions.cfg | 8 ++++++++ tests/aws_lambda/test_bless_lambda.py | 9 +++++++++ tests/ssh/test_ssh_certificate_rsa.py | 4 +++- 6 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 tests/aws_lambda/bless-test-with-certificate-extensions.cfg diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index c614409..9fc23f3 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -17,7 +17,7 @@ CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, KMSAUTH_SECTION, \ - KMSAUTH_USEKMSAUTH_OPTION, KMSAUTH_SERVICE_ID_OPTION, TEST_USER_OPTION, EXTENSIONS_OPTION + KMSAUTH_USEKMSAUTH_OPTION, KMSAUTH_SERVICE_ID_OPTION, TEST_USER_OPTION, CERTIFICATE_EXTENSIONS_OPTION from bless.request.bless_request import BlessSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority @@ -63,7 +63,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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, EXTENSIONS_OPTION) + certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION) # Process cert request schema = BlessSchema(strict=True) @@ -160,10 +160,12 @@ def lambda_handler(event, context=None, ca_private_key_password=None, cert_builder.set_valid_before(valid_before) cert_builder.set_valid_after(valid_after) - if certificate_extensions is not None: + if certificate_extensions: + for e in certificate_extensions.split(','): + if e: + cert_builder.add_extension(e) + else: cert_builder.clear_extensions() - for e in certificate_extensions.split(): - cert_builder.add_extension(e) # cert_builder is needed to obtain the SSH public key's fingerprint key_id = 'request[{}] for[{}] from[{}] command[{}] ssh_key:[{}] ca:[{}] valid_to[{}]'.format( diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 6200939..ace6ea7 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -22,8 +22,13 @@ TEST_USER_OPTION = 'test_user' TEST_USER_DEFAULT = None -EXTENSIONS_OPTION = 'extensions' -# Default for Extensions is None +CERTIFICATE_EXTENSIONS_OPTION = 'certificate_extensions' +# These are the the ssh-keygen default extensions: +CERTIFICATE_EXTENSIONS_DEFAULT = 'permit-X11-forwarding,' \ + 'permit-agent-forwarding,' \ + 'permit-port-forwarding,' \ + 'permit-pty,' \ + 'permit-user-rc' BLESS_CA_SECTION = 'Bless CA' CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' @@ -63,7 +68,9 @@ def __init__(self, aws_region, config_file): TEST_USER_OPTION: TEST_USER_DEFAULT, KMSAUTH_SERVICE_ID_OPTION: KMSAUTH_SERVICE_ID_DEFAULT, KMSAUTH_KEY_ID_OPTION: KMSAUTH_KEY_ID_DEFAULT, - KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_DEFAULT} + KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_DEFAULT, + CERTIFICATE_EXTENSIONS_OPTION: CERTIFICATE_EXTENSIONS_DEFAULT + } ConfigParser.RawConfigParser.__init__(self, defaults=defaults) self.read(config_file) diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index 0b6e53e..229cd7e 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -9,8 +9,8 @@ entropy_minimum_bits = 2048 random_seed_bytes = 256 # Set the logging level logging_level = INFO -# Optionally set more restrictive extensions. Not specifying this uses the default extension set -#extensions = permit-pty permit-user-rc permit-agent-forwarding +# Comma separated list of the SSH Certificate extensions to include. Not specifying this uses the ssh-keygen defaults: +# certificate_extensions = permit-X11-forwarding,permit-agent-forwarding,permit-port-forwarding,permit-pty,permit-user-rc # These values are all required to be modified for deployment [Bless CA] diff --git a/tests/aws_lambda/bless-test-with-certificate-extensions.cfg b/tests/aws_lambda/bless-test-with-certificate-extensions.cfg new file mode 100644 index 0000000..dd7e2e8 --- /dev/null +++ b/tests/aws_lambda/bless-test-with-certificate-extensions.cfg @@ -0,0 +1,8 @@ +[Bless CA] +ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +kms_key_id = alias/foo +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-tests + +[Bless Options] +certificate_extensions = permit-pty,permit-user-rc \ No newline at end of file diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 43b987a..20e2f91 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -158,3 +158,12 @@ def test_local_request_with_test_user(): entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), 'bless-test-with-test-user.cfg')) assert cert.startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_local_request_with_custom_certificate_extensions(): + cert = 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-with-certificate-extensions.cfg')) + assert cert.startswith('ssh-rsa-cert-v01@openssh.com ') diff --git a/tests/ssh/test_ssh_certificate_rsa.py b/tests/ssh/test_ssh_certificate_rsa.py index ee34114..6c2d439 100644 --- a/tests/ssh/test_ssh_certificate_rsa.py +++ b/tests/ssh/test_ssh_certificate_rsa.py @@ -1,11 +1,12 @@ import base64 import pytest +from cryptography.hazmat.primitives.serialization import _ssh_read_next_string + from bless.ssh.certificate_authorities.rsa_certificate_authority import RSACertificateAuthority from bless.ssh.certificates.rsa_certificate_builder import RSACertificateBuilder from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType from bless.ssh.public_keys.rsa_public_key import RSAPublicKey -from cryptography.hazmat.primitives.serialization import _ssh_read_next_string from tests.ssh.vectors import RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ EXAMPLE_RSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION, RSA_USER_CERT_MINIMAL, \ RSA_USER_CERT_DEFAULTS, RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT, \ @@ -128,6 +129,7 @@ def test_default_extensions(): cert_builder.set_extensions_to_default() assert SSH_CERT_DEFAULT_EXTENSIONS == cert_builder._serialize_extensions() + def test_add_extensions(): extensions = {'permit-port-forwarding', 'permit-pty', 'permit-user-rc'} From 9f75ccff838ed451727ad127bc3656b919770a31 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Tue, 28 Feb 2017 19:31:25 -0800 Subject: [PATCH 10/68] Adding the coverage report to the travis bulid output. --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 1509dd8..11edb2c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,6 +20,7 @@ script: after_success: - coveralls + - coverage report notifications: email: From 909d9553d42675594c0c4bbebe51dc81ae4763d7 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 1 Mar 2017 13:47:03 -0800 Subject: [PATCH 11/68] Additional commit for BLESS v.0.2.0 which changes the format of BLESS Requests and the lambda responses. Changing BLESS requests from using remote_username to remote_usernames, a comma-separated list. remote_usernames can be used for SSH principals specified in an AuthorizedPrincipalsFile (see SSHD_CONFIG(5)). Aligning BLESS returns so that Lambda configuration errors raise exceptions, and request errors return a dictionary with either errorType and errorMessage or a certificate. Updating the sample BLESS client to deal with the new lambda return values. --- README.md | 4 +- bless/aws_lambda/bless_lambda.py | 67 +++++++---- bless/config/bless_config.py | 2 +- bless/request/bless_request.py | 25 ++-- .../certificates/ssh_certificate_builder.py | 2 +- bless_client/bless_client.py | 31 +++-- ...test-with-certificate-extensions-empty.cfg | 8 ++ ...bless-test-with-certificate-extensions.cfg | 3 +- .../aws_lambda/bless-test-with-test-user.cfg | 2 +- tests/aws_lambda/test_bless_lambda.py | 112 ++++++++++++------ tests/request/test_bless_request.py | 28 ++++- 11 files changed, 195 insertions(+), 89 deletions(-) create mode 100644 tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg diff --git a/README.md b/README.md index df18496..91d9af8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![alt text](bless_logo.png "BLESS") # BLESS - Bastion's Lambda Ephemeral SSH Service -[![Build Status](https://travis-ci.org/Netflix/bless.svg?branch=master)](https://travis-ci.org/Netflix/bless) [![Test coverage](https://coveralls.io/repos/github/Netflix/bless/badge.svg?branch=master)](https://coveralls.io/github/Netflix/bless) [![Join the chat at https://gitter.im/Netflix/bless](https://badges.gitter.im/Netflix/bless.svg)](https://gitter.im/Netflix/bless?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Build Status](https://travis-ci.org/Netflix/bless.svg?branch=master)](https://travis-ci.org/Netflix/bless) [![Test coverage](https://coveralls.io/repos/github/Netflix/bless/badge.svg?branch=master)](https://coveralls.io/github/Netflix/bless) [![Join the chat at https://gitter.im/Netflix/bless](https://badges.gitter.im/Netflix/bless.svg)](https://gitter.im/Netflix/bless?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![NetflixOSS Lifecycle](https://img.shields.io/osslifecycle/Netflix/bless.svg)]() BLESS is an SSH Certificate Authority that runs as a AWS Lambda function and is used to sign ssh public keys. @@ -146,7 +146,7 @@ random from kms (kms:GenerateRandom) and permissions for logging to CloudWatch L After you have [deployed BLESS](#deployment) you can run the sample [BLESS Client](bless_client/bless_client.py) from a system with access to the required [AWS Credentials](http://boto3.readthedocs.io/en/latest/guide/configuration.html). - (venv) $ ./bless_client.py region lambda_function_name bastion_user bastion_user_ip remote_username bastion_source_ip bastion_command + (venv) $ ./bless_client.py region lambda_function_name bastion_user bastion_user_ip remote_usernames bastion_source_ip bastion_command ## Verifying Certificates diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 9fc23f3..3d99b80 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -13,11 +13,20 @@ from kmsauth import KMSTokenValidator, TokenValidationError from marshmallow.exceptions import ValidationError -from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION, \ - CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ - ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ - BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, KMSAUTH_SECTION, \ - KMSAUTH_USEKMSAUTH_OPTION, KMSAUTH_SERVICE_ID_OPTION, TEST_USER_OPTION, CERTIFICATE_EXTENSIONS_OPTION +from bless.config.bless_config import BlessConfig, \ + BLESS_OPTIONS_SECTION, \ + CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ + CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ + ENTROPY_MINIMUM_BITS_OPTION, \ + RANDOM_SEED_BYTES_OPTION, \ + BLESS_CA_SECTION, \ + CA_PRIVATE_KEY_FILE_OPTION, \ + LOGGING_LEVEL_OPTION, \ + KMSAUTH_SECTION, \ + KMSAUTH_USEKMSAUTH_OPTION, \ + KMSAUTH_SERVICE_ID_OPTION, \ + TEST_USER_OPTION, \ + CERTIFICATE_EXTENSIONS_OPTION from bless.request.bless_request import BlessSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority @@ -56,9 +65,9 @@ def lambda_handler(event, context=None, ca_private_key_password=None, logger.setLevel(numeric_level) certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION, - CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) + CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) certificate_validity_after_seconds = config.getint(BLESS_OPTIONS_SECTION, - CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) + 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_file = config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) @@ -70,10 +79,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, try: request = schema.load(event).data except ValidationError as e: - return { - 'errorType': 'InputValidationError', - 'errorMessage': str(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, @@ -93,10 +99,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, CiphertextBlob=base64.b64decode(password_ciphertext_b64)) ca_private_key_password = ca_password['Plaintext'] except ClientError as e: - return { - 'errorType': 'ClientError', - 'errorMessage': str(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: @@ -141,22 +144,21 @@ def lambda_handler(event, context=None, ca_private_key_password=None, ) # decrypt_token will raise a TokenValidationError if token doesn't match validator.decrypt_token( - "2/user/{}".format(request.remote_username), + "2/user/{}".format(request.remote_usernames.split(',')[0]), request.kmsauth_token ) except TokenValidationError as e: - return { - 'errorType': 'KMSAuthValidationError', - 'errorMessage': str(e) - } + return error_response('KMSAuthValidationError', str(e)) else: - raise ValueError('Invalid request, missing kmsauth token') + 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) - cert_builder.add_valid_principal(request.remote_username) + 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) @@ -172,13 +174,26 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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('{}'.format(request.bastion_ips)) + 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_username of [{}] with the key_id[{}] and ' + 'Issued a cert to bastion_ips[{}] for the remote_usernames of [{}] with the key_id[{}] and ' 'valid_from[{}])'.format( - request.bastion_ips, request.remote_username, key_id, + request.bastion_ips, request.remote_usernames, key_id, time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) - return cert + 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 + } diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index ace6ea7..dd26372 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -94,6 +94,6 @@ def getkmsauthkeyids(self): """ Returns a list of kmsauth keys used for validation (so a key generated in one region can validate in another). - :return: A list of kmsauth key id's + :return: A list of kmsauth key ids """ return map(str.strip, self.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION).split(',')) diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index 132d106..16ccaaf 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -10,6 +10,9 @@ # man 8 useradd USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') +# It appears that most printable ascii is valid, excluding whitespace, #, and commas. +# There doesn't seem to be any practical size limits of a principal (> 4096B allowed). +PRINCIPAL_PATTERN = re.compile(r'[\d\w!"$%&\'()*+\-./:;<=>?@\[\\\]\^`{|}~]+\Z') VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" @@ -23,9 +26,15 @@ def validate_ips(ips): def validate_user(user): if len(user) > 32: - raise ValidationError('Username is too long') + raise ValidationError('Username is too long.') if USERNAME_PATTERN.match(user) is None: - raise ValidationError('Username contains invalid characters') + raise ValidationError('Username contains invalid characters.') + + +def validate_principals(principals): + for principal in principals.split(','): + if PRINCIPAL_PATTERN.match(principal) is None: + raise ValidationError('Principal contains invalid characters.') def validate_ssh_public_key(public_key): @@ -42,7 +51,7 @@ class BlessSchema(Schema): bastion_user_ip = fields.Str(validate=validate_ips, required=True) command = fields.Str(required=True) public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True) - remote_username = fields.Str(validate=validate_user, required=True) + remote_usernames = fields.Str(validate=validate_principals, required=True) kmsauth_token = fields.Str(required=False) @validates_schema(pass_original=True) @@ -58,7 +67,7 @@ def make_bless_request(self, data): class BlessRequest: def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_key_to_sign, - remote_username, kmsauth_token=None): + remote_usernames, kmsauth_token=None): """ A BlessRequest must have the following key value pairs to be valid. :param bastion_ips: The source IPs where the SSH connection will be initiated from. This is @@ -68,16 +77,16 @@ def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_k :param command: Text information about the SSH request of the user. :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 remote_username: The username on the remote server that will be used in the SSH - request. This is enforced in the issued certificate. - :param kmsauth_token: An optional kms auth token to authenticate the user + :param remote_usernames: Comma-separated list of username(s) or authorized principals on the remote + server that will be used in the SSH request. This is enforced in the issued certificate. + :param kmsauth_token: An optional kms auth token to authenticate the user. """ self.bastion_ips = bastion_ips self.bastion_user = bastion_user self.bastion_user_ip = bastion_user_ip self.command = command self.public_key_to_sign = public_key_to_sign - self.remote_username = remote_username + self.remote_usernames = remote_usernames self.kmsauth_token = kmsauth_token def __eq__(self, other): diff --git a/bless/ssh/certificates/ssh_certificate_builder.py b/bless/ssh/certificates/ssh_certificate_builder.py index b4faf17..cc1ffd7 100644 --- a/bless/ssh/certificates/ssh_certificate_builder.py +++ b/bless/ssh/certificates/ssh_certificate_builder.py @@ -110,7 +110,7 @@ def add_valid_principal(self, valid_principal): if valid_principal not in self.valid_principals: self.valid_principals.append(valid_principal) else: - raise ValueError("Principal already added.") + raise ValueError("Principal {} already added.".format(valid_principal)) else: raise ValueError("Provide a non-null string") diff --git a/bless_client/bless_client.py b/bless_client/bless_client.py index 5c1fe7c..7770d86 100755 --- a/bless_client/bless_client.py +++ b/bless_client/bless_client.py @@ -4,7 +4,7 @@ A sample client to invoke the BLESS Lambda function and save the signed SSH Certificate. Usage: - bless_client.py region lambda_function_name bastion_user bastion_user_ip remote_username + bless_client.py region lambda_function_name bastion_user bastion_user_ip remote_usernames bastion_ips bastion_command region: AWS region where your lambda is deployed. @@ -15,11 +15,12 @@ bastion_user_ip: The IP of the user accessing the bastion. - remote_username: The username on the remote server that will be used in the SSH - request. This is enforced in the issued certificate. + remote_usernames: Comma-separated list of username(s) or authorized principals on the remote + server that will be used in the SSH request. This is enforced in the issued certificate. - bastion_ips: The source IP(s) where the SSH connection will be initiated from. This is - enforced in the issued certificate. + bastion_ips: The source IP(s) where the SSH connection will be initiated from. + Addresses should be comma-separated and can be individual IPs or CIDR format (nn.nn.nn.nn/nn + or hhhh::hhhh/nn). This is enforced in the issued certificate. bastion_command: Text information about the SSH request of the bastion_user. @@ -42,18 +43,18 @@ def main(argv): if len(argv) < 9 or len(argv) > 10: print ( 'Usage: bless_client.py region lambda_function_name bastion_user bastion_user_ip ' - 'remote_username bastion_ips bastion_command ' + 'remote_usernames bastion_ips bastion_command ' ' [kmsauth token]') return -1 - region, lambda_function_name, bastion_user, bastion_user_ip, remote_username, bastion_ips, \ - bastion_command, public_key_filename, certificate_filename = argv + region, lambda_function_name, bastion_user, bastion_user_ip, remote_usernames, bastion_ips, \ + bastion_command, public_key_filename, certificate_filename = argv with open(public_key_filename, 'r') as f: - public_key = f.read() + public_key = f.read().strip() payload = {'bastion_user': bastion_user, 'bastion_user_ip': bastion_user_ip, - 'remote_username': remote_username, 'bastion_ips': bastion_ips, + 'remote_usernames': remote_usernames, 'bastion_ips': bastion_ips, 'command': bastion_command, 'public_key_to_sign': public_key} if len(argv) == 10: @@ -73,11 +74,17 @@ def main(argv): print ('Error creating cert.') return -1 - cert = response['Payload'].read() + payload = json.loads(response['Payload'].read()) + + if 'certificate' not in payload: + print payload + return -1 + + cert = payload['certificate'] with os.fdopen(os.open(certificate_filename, os.O_WRONLY | os.O_CREAT, 0o600), 'w') as cert_file: - cert_file.write(cert[1:len(cert) - 3]) + cert_file.write(cert) # If cert_file already existed with the incorrect permissions, fix them. file_status = os.stat(certificate_filename) diff --git a/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg b/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg new file mode 100644 index 0000000..112e104 --- /dev/null +++ b/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg @@ -0,0 +1,8 @@ +[Bless CA] +ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +kms_key_id = alias/foo +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-tests + +[Bless Options] +certificate_extensions = diff --git a/tests/aws_lambda/bless-test-with-certificate-extensions.cfg b/tests/aws_lambda/bless-test-with-certificate-extensions.cfg index dd7e2e8..e3549c8 100644 --- a/tests/aws_lambda/bless-test-with-certificate-extensions.cfg +++ b/tests/aws_lambda/bless-test-with-certificate-extensions.cfg @@ -5,4 +5,5 @@ us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-tests [Bless Options] -certificate_extensions = permit-pty,permit-user-rc \ No newline at end of file +# trailing comma in certificate_extensions shouldn't be there, but should be harmless +certificate_extensions = permit-pty,permit-user-rc, \ No newline at end of file diff --git a/tests/aws_lambda/bless-test-with-test-user.cfg b/tests/aws_lambda/bless-test-with-test-user.cfg index 4f78eed..6ff6350 100644 --- a/tests/aws_lambda/bless-test-with-test-user.cfg +++ b/tests/aws_lambda/bless-test-with-test-user.cfg @@ -5,4 +5,4 @@ us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-tests [Bless Options] -test_user=user \ No newline at end of file +test_user = user \ No newline at end of file diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 20e2f91..2b7f731 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -1,3 +1,4 @@ +import json import os import pytest @@ -13,7 +14,7 @@ class Context(object): VALID_TEST_REQUEST = { - "remote_username": "user", + "remote_usernames": "user", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, "command": "ssh user@server", "bastion_ips": "127.0.0.1", @@ -22,7 +23,7 @@ class Context(object): } INVALID_TEST_REQUEST = { - "remote_username": "user", + "remote_usernames": "user", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, "command": "ssh user@server", "bastion_ips": "invalid_ip", @@ -31,7 +32,7 @@ class Context(object): } VALID_TEST_REQUEST_KMSAUTH = { - "remote_username": "user", + "remote_usernames": "user", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, "command": "ssh user@server", "bastion_ips": "127.0.0.1", @@ -41,7 +42,7 @@ class Context(object): } INVALID_TEST_REQUEST_KEY_TYPE = { - "remote_username": "user", + "remote_usernames": "user", "public_key_to_sign": EXAMPLE_ED25519_PUBLIC_KEY, "command": "ssh user@server", "bastion_ips": "127.0.0.1", @@ -50,7 +51,7 @@ class Context(object): } INVALID_TEST_REQUEST_EXTRA_FIELD = { - "remote_username": "user", + "remote_usernames": "user", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, "command": "ssh user@server", "bastion_ips": "127.0.0.1", @@ -60,39 +61,57 @@ class Context(object): } INVALID_TEST_REQUEST_MISSING_FIELD = { - "remote_username": "user", + "remote_usernames": "user", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, "bastion_ips": "127.0.0.1", "bastion_user": "user", "bastion_user_ip": "127.0.0.1" } +VALID_TEST_REQUEST_MULTIPLE_PRINCIPALS = { + "remote_usernames": "user1,user2", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + +INVALID_TEST_REQUEST_MULTIPLE_PRINCIPALS = { + "remote_usernames": ",user#", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + os.environ['AWS_REGION'] = 'us-west-2' def test_basic_local_request(): - cert = 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 cert.startswith('ssh-rsa-cert-v01@openssh.com ') + 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_unused_kmsauth_request(): - cert = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, 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 cert.startswith('ssh-rsa-cert-v01@openssh.com ') + output = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, 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_missing_kmsauth_request(): - with pytest.raises(ValueError): - 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-kmsauth.cfg')) + 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-kmsauth.cfg')) + assert output['errorType'] == 'InputValidationError' def test_invalid_kmsauth_request(): @@ -153,17 +172,44 @@ def test_local_request_missing_field(): def test_local_request_with_test_user(): - cert = 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-with-test-user.cfg')) - assert cert.startswith('ssh-rsa-cert-v01@openssh.com ') + 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-with-test-user.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') def test_local_request_with_custom_certificate_extensions(): - cert = 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-with-certificate-extensions.cfg')) - assert cert.startswith('ssh-rsa-cert-v01@openssh.com ') + 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-with-certificate-extensions.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_local_request_with_empty_certificate_extensions(): + 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-with-certificate-extensions-empty.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_local_request_with_multiple_principals(): + output = lambda_handler(VALID_TEST_REQUEST_MULTIPLE_PRINCIPALS, 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_invalid_request_with_multiple_principals(): + output = lambda_handler(INVALID_TEST_REQUEST_MULTIPLE_PRINCIPALS, 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['errorType'] == 'InputValidationError' diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index 5855bd4..e09e66a 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -1,5 +1,5 @@ import pytest -from bless.request.bless_request import validate_ips, validate_user +from bless.request.bless_request import validate_ips, validate_user, validate_principals from marshmallow import ValidationError @@ -21,19 +21,20 @@ def test_validate_ips_cidr(): def test_validate_user_too_long(): with pytest.raises(ValidationError) as e: validate_user('a33characterusernameyoumustbenuts') - assert e.value.message == 'Username is too long' + assert e.value.message == 'Username is too long.' @pytest.mark.parametrize("test_input", [ ('user#invalid'), ('$userinvalid'), ('userinvali$d'), - ('userin&valid') + ('userin&valid'), + (' userinvalid') ]) def test_validate_user_contains_junk(test_input): with pytest.raises(ValidationError) as e: validate_user(test_input) - assert e.value.message == 'Username contains invalid characters' + assert e.value.message == 'Username contains invalid characters.' @pytest.mark.parametrize("test_input", [ @@ -44,3 +45,22 @@ def test_validate_user_contains_junk(test_input): ]) def test_validate_user(test_input): validate_user(test_input) + +@pytest.mark.parametrize("test_input", [ + ('uservalid'), + ('uservalid,uservalid2'), + ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~,uservalid2') +]) +def test_validate_multiple_principals(test_input): + validate_principals(test_input) + +@pytest.mark.parametrize("test_input", [ + ('user invalid'), + ('uservalid,us#erinvalid2'), + ('uservalid,,uservalid2'), + (' uservalid'), +]) +def test_validate_multiple_principals(test_input): + with pytest.raises(ValidationError) as e: + validate_principals(test_input) + assert e.value.message == 'Principal contains invalid characters.' From 4d2e67416a6aa3cc068524cfefacb9b04759e093 Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Thu, 9 Mar 2017 12:54:27 +0100 Subject: [PATCH 12/68] Add support to compile dependencies in container --- Makefile | 11 +++++++++++ README.md | 4 ++++ 2 files changed, 15 insertions(+) diff --git a/Makefile b/Makefile index 2d0428c..8977866 100644 --- a/Makefile +++ b/Makefile @@ -38,4 +38,15 @@ publish: cp -r ./lambda_configs/. ./publish/bless_lambda/ cd ./publish/bless_lambda && zip -r ../bless_lambda.zip . +compile: + yum install -y gcc libffi-devel openssl-devel python27-virtualenv + virtualenv /tmp/venv + /tmp/venv/bin/pip install --upgrade pip setuptools + /tmp/venv/bin/pip install -e . + cp -r /tmp/venv/lib/python2.7/site-packages/. ./aws_lambda_libs + +lambda-deps: + @echo "--> Compiling lambda dependencies" + docker run --rm -it -v ${CURDIR}:/src -w /src amazonlinux make compile + .PHONY: develop dev-docs clean test lint coverage publish diff --git a/README.md b/README.md index 91d9af8..a833740 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ all of the dependencies. AWS Lambda only supports Python 2.7 and BLESS depends [Cryptography](https://cryptography.io/en/latest/), which must be compiled. You will need to compile and include your dependencies before you can publish a working AWS Lambda. +You can use a docker container running amazon linux: +- Execute ```make lambda-deps``` and this will run a container and save all the dependencies in ./aws_lambda_libs + +Alternatively you can: - Deploy an [Amazon Linux AMI](http://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html) - SSH onto that instance - Copy BLESS' `setup.py` to the instance From c387868db567ac03df03499d355dbfdedc26dda9 Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Thu, 9 Mar 2017 17:58:13 +0100 Subject: [PATCH 13/68] Copy lib64 packages --- Makefile | 1 + 1 file changed, 1 insertion(+) diff --git a/Makefile b/Makefile index 8977866..1fabaf2 100644 --- a/Makefile +++ b/Makefile @@ -44,6 +44,7 @@ compile: /tmp/venv/bin/pip install --upgrade pip setuptools /tmp/venv/bin/pip install -e . cp -r /tmp/venv/lib/python2.7/site-packages/. ./aws_lambda_libs + cp -r /tmp/venv/lib64/python2.7/site-packages/. ./aws_lambda_libs lambda-deps: @echo "--> Compiling lambda dependencies" From 07c9866eb4365a4c7eb256bc4614e78296739829 Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Thu, 9 Mar 2017 22:00:27 +0100 Subject: [PATCH 14/68] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 91d9af8..8600786 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ def lambda_handler(event, context): - Manage your Private Keys .pem files and passwords outside of this repo. - Update your bless_deploy.cfg with your Private Key's filename and encrypted passwords. - Provide your desired ./lambda_configs/ca_key_name.pem prior to Publishing a new Lambda .zip +- Set the permissions of ./lambda_configs/ca_key_name.pem to 444. ### BLESS Config File - Refer to the the [Example BLESS Config File](bless/config/bless_deploy_example.cfg) and its From d8e879da2135c7ffdfec841f7092a490f85c1012 Mon Sep 17 00:00:00 2001 From: Chris Steipp Date: Wed, 15 Mar 2017 14:43:43 -0700 Subject: [PATCH 15/68] [HOTFIX] Fixes while merge testing --- bless/aws_lambda/bless_lambda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 3d99b80..b768467 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -121,7 +121,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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_username == test_user)): + 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 From 24f4abaa6f2369a490397512e934a2fe137cf196 Mon Sep 17 00:00:00 2001 From: Ben Bridts Date: Fri, 17 Mar 2017 13:52:46 +0100 Subject: [PATCH 16/68] Remove unused option 'kms_key_id' For decryption the key id is part of the ciphertext. --- bless/config/bless_config.py | 1 - bless/config/bless_deploy_example.cfg | 2 -- tests/aws_lambda/bless-test-broken.cfg | 1 - tests/aws_lambda/bless-test-kmsauth.cfg | 1 - .../aws_lambda/bless-test-with-certificate-extensions-empty.cfg | 1 - tests/aws_lambda/bless-test-with-certificate-extensions.cfg | 1 - tests/aws_lambda/bless-test-with-test-user.cfg | 1 - tests/aws_lambda/bless-test.cfg | 1 - tests/config/full.cfg | 1 - tests/config/minimal.cfg | 1 - 10 files changed, 11 deletions(-) diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index dd26372..3f8e5a8 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -32,7 +32,6 @@ BLESS_CA_SECTION = 'Bless CA' CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' -KMS_KEY_ID_OPTION = 'kms_key_id' REGION_PASSWORD_OPTION_SUFFIX = '_password' diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index 229cd7e..869fa16 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -14,8 +14,6 @@ logging_level = INFO # These values are all required to be modified for deployment [Bless CA] -# AWS KMS key alias used to encrypt your private key password -kms_key_id = # You must set an encrypted private key password for each AWS Region you deploy into # for each aws region specify a config option like '{}_password'.format(aws_region) us-east-1_password = diff --git a/tests/aws_lambda/bless-test-broken.cfg b/tests/aws_lambda/bless-test-broken.cfg index 606697c..f4de164 100644 --- a/tests/aws_lambda/bless-test-broken.cfg +++ b/tests/aws_lambda/bless-test-broken.cfg @@ -1,5 +1,4 @@ [Bless CA] ca_private_key_file = ../../tests/aws_lambda/not-found.pem -kms_key_id = alias/foo us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-test diff --git a/tests/aws_lambda/bless-test-kmsauth.cfg b/tests/aws_lambda/bless-test-kmsauth.cfg index 5fb4e53..ab207b6 100644 --- a/tests/aws_lambda/bless-test-kmsauth.cfg +++ b/tests/aws_lambda/bless-test-kmsauth.cfg @@ -1,6 +1,5 @@ [Bless CA] ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem -kms_key_id = alias/foo us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-test diff --git a/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg b/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg index 112e104..ec4916f 100644 --- a/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg +++ b/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg @@ -1,6 +1,5 @@ [Bless CA] ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem -kms_key_id = alias/foo us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-tests diff --git a/tests/aws_lambda/bless-test-with-certificate-extensions.cfg b/tests/aws_lambda/bless-test-with-certificate-extensions.cfg index e3549c8..6bed688 100644 --- a/tests/aws_lambda/bless-test-with-certificate-extensions.cfg +++ b/tests/aws_lambda/bless-test-with-certificate-extensions.cfg @@ -1,6 +1,5 @@ [Bless CA] ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem -kms_key_id = alias/foo us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-tests diff --git a/tests/aws_lambda/bless-test-with-test-user.cfg b/tests/aws_lambda/bless-test-with-test-user.cfg index 6ff6350..8fc454f 100644 --- a/tests/aws_lambda/bless-test-with-test-user.cfg +++ b/tests/aws_lambda/bless-test-with-test-user.cfg @@ -1,6 +1,5 @@ [Bless CA] ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem -kms_key_id = alias/foo us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-tests diff --git a/tests/aws_lambda/bless-test.cfg b/tests/aws_lambda/bless-test.cfg index 5bae8ad..ab24770 100644 --- a/tests/aws_lambda/bless-test.cfg +++ b/tests/aws_lambda/bless-test.cfg @@ -1,5 +1,4 @@ [Bless CA] ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem -kms_key_id = alias/foo us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-tests diff --git a/tests/config/full.cfg b/tests/config/full.cfg index 09b2fc1..e5fc965 100644 --- a/tests/config/full.cfg +++ b/tests/config/full.cfg @@ -7,7 +7,6 @@ random_seed_bytes = 3 logging_level = DEBUG [Bless CA] -kms_key_id = alias/foo us-east-1_password = us-west-2_password = ca_private_key_file = diff --git a/tests/config/minimal.cfg b/tests/config/minimal.cfg index b0ad399..91a1c73 100644 --- a/tests/config/minimal.cfg +++ b/tests/config/minimal.cfg @@ -1,5 +1,4 @@ [Bless CA] -kms_key_id = alias/foo us-west-2_password = us-west-2_kms_context = {'insert': 'your context for us-west-2'} ca_private_key_file = \ No newline at end of file From 62fe7fcd78c901957c1ceb79e410ca72e668864d Mon Sep 17 00:00:00 2001 From: Ben Bridts Date: Tue, 21 Mar 2017 18:12:31 +0100 Subject: [PATCH 17/68] Decouple configuration from deployment artifact (#45) Allow overriding settings with environment variables. Leveraging the environment variables in AWS Lambda makes it possible to include the bless_deploy.cfg in the same repo, without exposing secrets or to deploy the same zip with multiple configurations When deploying the same zip in mulitple regions, you can leave out the region_passwprd option and set the default_password option with environment variables. This allows you the change the same variable in every region * Add 'ca_private_key' option This extra option allows passing in the (encrypted) private key directly. When setting this with an environment variable it can be used to have one zip that can be deployed with different ca's. --- README.md | 2 + bless/aws_lambda/bless_lambda.py | 8 +--- bless/config/bless_config.py | 56 +++++++++++++++++++++++++-- bless/config/bless_deploy_example.cfg | 4 ++ tests/config/full-with-default.cfg | 14 +++++++ tests/config/test_bless_config.py | 55 +++++++++++++++++++++++++- 6 files changed, 127 insertions(+), 12 deletions(-) create mode 100644 tests/config/full-with-default.cfg diff --git a/README.md b/README.md index 50a9bac..2967b12 100644 --- a/README.md +++ b/README.md @@ -120,6 +120,8 @@ included documentation. - Manage your bless_deploy.cfg files outside of this repo. - Provide your desired ./lambda_configs/bless_deploy.cfg prior to Publishing a new Lambda .zip - The required [Bless CA] option values must be set for your environment. +- Every option can be changed in the environment. The environment variable name is contructed +as section_name_option_name (all lowercase, spaces replaced with underscores). ### Publish Lambda .zip - Provide your desired ./lambda_configs/ca_key_name.pem prior to Publishing diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index b768467..692a3a6 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -19,8 +19,6 @@ CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ ENTROPY_MINIMUM_BITS_OPTION, \ RANDOM_SEED_BYTES_OPTION, \ - BLESS_CA_SECTION, \ - CA_PRIVATE_KEY_FILE_OPTION, \ LOGGING_LEVEL_OPTION, \ KMSAUTH_SECTION, \ KMSAUTH_USEKMSAUTH_OPTION, \ @@ -70,7 +68,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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_file = config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) + ca_private_key = config.getprivatekey() password_ciphertext_b64 = config.getpassword() certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION) @@ -87,10 +85,6 @@ def lambda_handler(event, context=None, ca_private_key_password=None, request.public_key_to_sign, 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() - # decrypt ca private key password if ca_private_key_password is None: kms_client = boto3.client('kms', region_name=region) diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 3f8e5a8..2720cc9 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -4,6 +4,8 @@ :license: Apache, see LICENSE for more details. """ import ConfigParser +import base64 +import os BLESS_OPTIONS_SECTION = 'Bless Options' CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'certificate_validity_before_seconds' @@ -32,6 +34,7 @@ BLESS_CA_SECTION = 'Bless CA' CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' +CA_PRIVATE_KEY_OPTION = 'ca_private_key' REGION_PASSWORD_OPTION_SUFFIX = '_password' @@ -46,7 +49,7 @@ KMSAUTH_SERVICE_ID_DEFAULT = None -class BlessConfig(ConfigParser.RawConfigParser): +class BlessConfig(ConfigParser.RawConfigParser, object): def __init__(self, aws_region, config_file): """ Parses the BLESS config file, and provides some reasonable default values if they are @@ -80,14 +83,17 @@ def __init__(self, aws_region, config_file): self.add_section(KMSAUTH_SECTION) if not self.has_option(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX): - raise ValueError("No Region Specific Password Provided.") + if not self.has_option(BLESS_CA_SECTION, 'default' + REGION_PASSWORD_OPTION_SUFFIX): + raise ValueError("No Region Specific And No Default 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) + if self.has_option(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX): + return self.get(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX) + return self.get(BLESS_CA_SECTION, 'default' + REGION_PASSWORD_OPTION_SUFFIX) def getkmsauthkeyids(self): """ @@ -96,3 +102,47 @@ def getkmsauthkeyids(self): :return: A list of kmsauth key ids """ return map(str.strip, self.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION).split(',')) + + def getprivatekey(self): + if self.has_option(BLESS_CA_SECTION, CA_PRIVATE_KEY_OPTION): + return base64.b64decode(self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_OPTION)) + + ca_private_key_file = self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) + + # read the private key .pem + with open(os.path.join(os.path.dirname(__file__), ca_private_key_file), 'r') as f: + return f.read() + + def has_option(self, section, option): + """ + Checks if an option exists. + + This will search in both the environment variables and in the config file + :param section: The section to search in + :param option: The option to check + :return: True if it exists, False otherwise + """ + environment_key = self._environment_key(section, option) + if environment_key in os.environ: + return True + else: + return super(BlessConfig, self).has_option(section, option) + + def get(self, section, option): + """ + Gets a value from the configuration. + + Checks the environment before looking in the config file. + :param section: The config section to look in + :param option: The config option to look at + :return: The value of the config option + """ + environment_key = self._environment_key(section, option) + output = os.environ.get(environment_key, None) + if output is None: + output = super(BlessConfig, self).get(section, option) + return output + + @staticmethod + def _environment_key(section, option): + return (section.replace(' ', '_') + '_' + option).lower() diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index 869fa16..a01f65a 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -18,8 +18,12 @@ logging_level = INFO # for each aws region specify a config option like '{}_password'.format(aws_region) us-east-1_password = us-west-2_password = +# Or you can set a default password. Region specific password have precedence over the default +# default_password = # Specify the file name of your SSH CA's Private Key in PEM format. ca_private_key_file = +# Or specify the private key directly as a base64 encoded string. +# ca_private_key = # This section is optional [KMS Auth] diff --git a/tests/config/full-with-default.cfg b/tests/config/full-with-default.cfg new file mode 100644 index 0000000..6cd507d --- /dev/null +++ b/tests/config/full-with-default.cfg @@ -0,0 +1,14 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_after_seconds = 1 +certificate_validity_before_seconds = 1 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +logging_level = DEBUG + +[Bless CA] +kms_key_id = alias/foo +us-east-1_password = +us-west-2_password = +default_password = +ca_private_key_file = diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index 5fc4da3..7f2844b 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -1,3 +1,4 @@ +import base64 import os import pytest @@ -6,7 +7,9 @@ CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, \ - LOGGING_LEVEL_DEFAULT, LOGGING_LEVEL_OPTION + LOGGING_LEVEL_DEFAULT, LOGGING_LEVEL_OPTION, BLESS_CA_SECTION, \ + CA_PRIVATE_KEY_FILE_OPTION, KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION, KMSAUTH_KEY_ID_OPTION, \ + KMSAUTH_SERVICE_ID_OPTION, CERTIFICATE_EXTENSIONS_OPTION def test_empty_config(): @@ -18,7 +21,55 @@ 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 + 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 '' == config.getpassword() + + +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': '', + 'bless_ca_ca_private_key': base64.b64encode(''), + + 'kms_auth_use_kmsauth': 'True', + 'kms_auth_kmsauth_key_id': '', + '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 '' == config.getpassword() + assert '' == config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) + assert '' == config.getprivatekey() + + assert config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION) + assert '' == config.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION) + assert 'bless-test' == config.get(KMSAUTH_SECTION, KMSAUTH_SERVICE_ID_OPTION) + + config.aws_region = 'invalid' + assert '' == config.getpassword() @pytest.mark.parametrize( From 485663cdc4d0cf0b6892c430cf6e80eeffd31965 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Tue, 21 Mar 2017 10:27:20 -0700 Subject: [PATCH 18/68] Removing an unneeded reference to kms_key_id. (#47) --- tests/config/full-with-default.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/config/full-with-default.cfg b/tests/config/full-with-default.cfg index 6cd507d..bc19017 100644 --- a/tests/config/full-with-default.cfg +++ b/tests/config/full-with-default.cfg @@ -7,7 +7,6 @@ random_seed_bytes = 3 logging_level = DEBUG [Bless CA] -kms_key_id = alias/foo us-east-1_password = us-west-2_password = default_password = From 21a417bacb2a50fe7da2c8520f8a9489e3ffe9b8 Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Tue, 14 Mar 2017 22:07:25 +0100 Subject: [PATCH 19/68] Add support for debian username validations --- bless/request/bless_request.py | 26 +++++++++++++++++++ tests/request/test_bless_request.py | 39 +++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index 16ccaaf..facf6ca 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -10,11 +10,25 @@ # man 8 useradd USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') + +# debian +# On Debian, the only constraints are that usernames must neither start +# with a dash ('-') nor plus ('+') nor tilde ('~') nor contain a colon +# (':'), a comma (','), or a whitespace (space: ' ', end of line: '\n', +# tabulation: '\t', etc.). Note that using a slash ('/') may break the +# default algorithm for the definition of the user's home directory. +USERNAME_PATTERN_DEBIAN = re.compile('\A[^-+~][^:,\s]*\Z') + # It appears that most printable ascii is valid, excluding whitespace, #, and commas. # There doesn't seem to be any practical size limits of a principal (> 4096B allowed). PRINCIPAL_PATTERN = re.compile(r'[\d\w!"$%&\'()*+\-./:;<=>?@\[\\\]\^`{|}~]+\Z') VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" +USERNAME_VALIDATION_USERADD = 'useradd' +USERNAME_VALIDATION_DEBIAN = 'debian' + +username_validation = USERNAME_VALIDATION_USERADD + def validate_ips(ips): try: @@ -27,10 +41,22 @@ def validate_ips(ips): def validate_user(user): if len(user) > 32: raise ValidationError('Username is too long.') + if username_validation == USERNAME_VALIDATION_DEBIAN: + _validate_user_debian(user) + else: + _validate_user_useradd(user) + + +def _validate_user_useradd(user): if USERNAME_PATTERN.match(user) is None: raise ValidationError('Username contains invalid characters.') +def _validate_user_debian(user): + if USERNAME_PATTERN_DEBIAN.match(user) is None: + raise ValidationError('Username contains invalid characters.') + + def validate_principals(principals): for principal in principals.split(','): if PRINCIPAL_PATTERN.match(principal) is None: diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index e09e66a..367d23f 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -46,6 +46,45 @@ def test_validate_user_contains_junk(test_input): def test_validate_user(test_input): validate_user(test_input) + +def test_validate_user_debian_too_long(monkeypatch): + monkeypatch.setattr('bless.request.bless_request.username_validation', 'debian') + with pytest.raises(ValidationError) as e: + validate_user('a33characterusernameyoumustbenuts') + assert e.value.message == 'Username is too long.' + + +@pytest.mark.parametrize("test_input", [ + ('~userinvalid'), + ('-userinvalid'), + ('+userinvalid'), + ('user:invalid'), + ('user,invalid'), + ('user invalid'), + ('user\tinvalid'), + ('user\ninvalid'), +]) +def test_validate_user_debian_invalid(test_input, monkeypatch): + monkeypatch.setattr('bless.request.bless_request.username_validation', 'debian') + with pytest.raises(ValidationError) as e: + validate_user(test_input) + assert e.value.message == 'Username contains invalid characters.' + + +@pytest.mark.parametrize("test_input", [ + ('uservalid'), + ('a32characterusernameyoumustok$'), + ('_uservalid$'), + ('abc123_-valid'), + ('user~valid'), + ('user-valid'), + ('user+valid'), +]) +def test_validate_user_debian(test_input, monkeypatch): + monkeypatch.setattr('bless.request.bless_request.username_validation', 'debian') + validate_user(test_input) + + @pytest.mark.parametrize("test_input", [ ('uservalid'), ('uservalid,uservalid2'), From b87bbab75625eed180f2c2f64b2aa2dc9444a3b7 Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Tue, 14 Mar 2017 22:37:55 +0100 Subject: [PATCH 20/68] Add support for relaxed username validations --- bless/request/bless_request.py | 3 +++ tests/request/test_bless_request.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index facf6ca..9254f45 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -26,6 +26,7 @@ USERNAME_VALIDATION_USERADD = 'useradd' USERNAME_VALIDATION_DEBIAN = 'debian' +USERNAME_VALIDATION_RELAXED = 'relaxed' username_validation = USERNAME_VALIDATION_USERADD @@ -41,6 +42,8 @@ def validate_ips(ips): def validate_user(user): if len(user) > 32: raise ValidationError('Username is too long.') + if username_validation == USERNAME_VALIDATION_RELAXED: + return if username_validation == USERNAME_VALIDATION_DEBIAN: _validate_user_debian(user) else: diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index 367d23f..d8a2f3e 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -85,6 +85,35 @@ def test_validate_user_debian(test_input, monkeypatch): validate_user(test_input) +def test_validate_user_relaxed_too_long(monkeypatch): + monkeypatch.setattr('bless.request.bless_request.username_validation', 'relaxed') + with pytest.raises(ValidationError) as e: + validate_user('a33characterusernameyoumustbenuts') + assert e.value.message == 'Username is too long.' + + +@pytest.mark.parametrize("test_input", [ + ('uservalid'), + ('a32characterusernameyoumustok$'), + ('_uservalid$'), + ('abc123_-valid'), + ('user~valid'), + ('user-valid'), + ('user+valid'), + ('~uservalid'), + ('-uservalid'), + ('+uservalid'), + ('user:valid'), + ('user,valid'), + ('user valid'), + ('user\tvalid'), + ('user\nvalid'), +]) +def test_validate_user_relaxed(test_input, monkeypatch): + monkeypatch.setattr('bless.request.bless_request.username_validation', 'relaxed') + validate_user(test_input) + + @pytest.mark.parametrize("test_input", [ ('uservalid'), ('uservalid,uservalid2'), From 329e8dc304e16f98692c06747b54eae37acb2a16 Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Tue, 14 Mar 2017 23:00:30 +0100 Subject: [PATCH 21/68] Load username_validation configuration value --- bless/config/bless_config.py | 6 +++++- tests/config/full.cfg | 1 + tests/config/test_bless_config.py | 20 ++++++++++++++------ 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 2720cc9..8ca2a24 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -48,6 +48,9 @@ KMSAUTH_SERVICE_ID_OPTION = 'kmsauth_serviceid' KMSAUTH_SERVICE_ID_DEFAULT = None +USERNAME_VALIDATION_OPTION = 'username_validation' +USERNAME_VALIDATION_DEFAULT = 'useradd' + class BlessConfig(ConfigParser.RawConfigParser, object): def __init__(self, aws_region, config_file): @@ -71,7 +74,8 @@ def __init__(self, aws_region, config_file): KMSAUTH_SERVICE_ID_OPTION: KMSAUTH_SERVICE_ID_DEFAULT, KMSAUTH_KEY_ID_OPTION: KMSAUTH_KEY_ID_DEFAULT, KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_DEFAULT, - CERTIFICATE_EXTENSIONS_OPTION: CERTIFICATE_EXTENSIONS_DEFAULT + CERTIFICATE_EXTENSIONS_OPTION: CERTIFICATE_EXTENSIONS_DEFAULT, + USERNAME_VALIDATION_OPTION: USERNAME_VALIDATION_DEFAULT, } ConfigParser.RawConfigParser.__init__(self, defaults=defaults) self.read(config_file) diff --git a/tests/config/full.cfg b/tests/config/full.cfg index e5fc965..fa35f43 100644 --- a/tests/config/full.cfg +++ b/tests/config/full.cfg @@ -5,6 +5,7 @@ certificate_validity_before_seconds = 1 entropy_minimum_bits = 2 random_seed_bytes = 3 logging_level = DEBUG +username_validation = debian [Bless CA] us-east-1_password = diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index 7f2844b..51582cf 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -9,7 +9,7 @@ CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, \ LOGGING_LEVEL_DEFAULT, LOGGING_LEVEL_OPTION, BLESS_CA_SECTION, \ CA_PRIVATE_KEY_FILE_OPTION, KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION, KMSAUTH_KEY_ID_OPTION, \ - KMSAUTH_SERVICE_ID_OPTION, CERTIFICATE_EXTENSIONS_OPTION + KMSAUTH_SERVICE_ID_OPTION, CERTIFICATE_EXTENSIONS_OPTION, USERNAME_VALIDATION_OPTION, USERNAME_VALIDATION_DEFAULT def test_empty_config(): @@ -74,20 +74,26 @@ def test_config_environment_override(monkeypatch): @pytest.mark.parametrize( "config,region,expected_cert_valid,expected_entropy_min,expected_rand_seed,expected_log_level," - "expected_password", [ + "expected_password,expected_username_validation", [ ((os.path.join(os.path.dirname(__file__), 'minimal.cfg')), 'us-west-2', CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, LOGGING_LEVEL_DEFAULT, - ''), + '', + USERNAME_VALIDATION_DEFAULT + ), ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-west-2', 1, 2, 3, 'DEBUG', - ''), + '', + 'debian' + ), ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-east-1', 1, 2, 3, 'DEBUG', - '') + '', + 'debian' + ) ]) def test_configs(config, region, expected_cert_valid, expected_entropy_min, expected_rand_seed, - expected_log_level, expected_password): + 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) @@ -100,3 +106,5 @@ def test_configs(config, region, expected_cert_valid, expected_entropy_min, expe 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) From c050a48ba914045aaf22f6389b6c6584755fb6d4 Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Tue, 14 Mar 2017 23:39:10 +0100 Subject: [PATCH 22/68] Refactor username_validation configuration * Use enum type and raise exception for wrong value --- bless/request/bless_request.py | 17 +++++++++++------ tests/request/test_bless_request.py | 25 +++++++++++++++++++------ 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index 9254f45..1af4aa7 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -3,6 +3,7 @@ :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ +from enum import Enum import re import ipaddress @@ -24,11 +25,15 @@ PRINCIPAL_PATTERN = re.compile(r'[\d\w!"$%&\'()*+\-./:;<=>?@\[\\\]\^`{|}~]+\Z') VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" -USERNAME_VALIDATION_USERADD = 'useradd' -USERNAME_VALIDATION_DEBIAN = 'debian' -USERNAME_VALIDATION_RELAXED = 'relaxed' +USERNAME_VALIDATION_OPTIONS = Enum('UserNameValidationOptions', + 'useradd debian relaxed') -username_validation = USERNAME_VALIDATION_USERADD +username_validation = USERNAME_VALIDATION_OPTIONS.useradd + + +def set_username_validation(value): + global username_validation + username_validation = USERNAME_VALIDATION_OPTIONS[value] def validate_ips(ips): @@ -42,9 +47,9 @@ def validate_ips(ips): def validate_user(user): if len(user) > 32: raise ValidationError('Username is too long.') - if username_validation == USERNAME_VALIDATION_RELAXED: + if username_validation == USERNAME_VALIDATION_OPTIONS.relaxed: return - if username_validation == USERNAME_VALIDATION_DEBIAN: + if username_validation == USERNAME_VALIDATION_OPTIONS.debian: _validate_user_debian(user) else: _validate_user_useradd(user) diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index d8a2f3e..cc0ad54 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -1,5 +1,5 @@ import pytest -from bless.request.bless_request import validate_ips, validate_user, validate_principals +from bless.request.bless_request import validate_ips, validate_user, validate_principals, set_username_validation, USERNAME_VALIDATION_OPTIONS from marshmallow import ValidationError @@ -48,7 +48,7 @@ def test_validate_user(test_input): def test_validate_user_debian_too_long(monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', 'debian') + monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.debian) with pytest.raises(ValidationError) as e: validate_user('a33characterusernameyoumustbenuts') assert e.value.message == 'Username is too long.' @@ -65,7 +65,7 @@ def test_validate_user_debian_too_long(monkeypatch): ('user\ninvalid'), ]) def test_validate_user_debian_invalid(test_input, monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', 'debian') + monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.debian) with pytest.raises(ValidationError) as e: validate_user(test_input) assert e.value.message == 'Username contains invalid characters.' @@ -81,12 +81,12 @@ def test_validate_user_debian_invalid(test_input, monkeypatch): ('user+valid'), ]) def test_validate_user_debian(test_input, monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', 'debian') + monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.debian) validate_user(test_input) def test_validate_user_relaxed_too_long(monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', 'relaxed') + monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.relaxed) with pytest.raises(ValidationError) as e: validate_user('a33characterusernameyoumustbenuts') assert e.value.message == 'Username is too long.' @@ -110,7 +110,7 @@ def test_validate_user_relaxed_too_long(monkeypatch): ('user\nvalid'), ]) def test_validate_user_relaxed(test_input, monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', 'relaxed') + monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.relaxed) validate_user(test_input) @@ -132,3 +132,16 @@ def test_validate_multiple_principals(test_input): with pytest.raises(ValidationError) as e: validate_principals(test_input) assert e.value.message == 'Principal contains invalid characters.' + + +def test_set_username_validation_invalid(): + with pytest.raises(KeyError) as e: + set_username_validation('random') + +@pytest.mark.parametrize("test_input", [ + ('useradd'), + ('debian'), + ('relaxed'), +]) +def test_set_username_validation(test_input): + set_username_validation(test_input) From 9f3c7c16c387049ed5c671753fee79fced5aad19 Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Tue, 14 Mar 2017 23:41:36 +0100 Subject: [PATCH 23/68] Set username_validation when calling lambda --- bless/aws_lambda/bless_lambda.py | 5 ++++- .../bless-test-username-validation-relaxed.cfg | 7 +++++++ tests/aws_lambda/test_bless_lambda.py | 18 ++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 tests/aws_lambda/bless-test-username-validation-relaxed.cfg diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 692a3a6..120b161 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -20,12 +20,13 @@ ENTROPY_MINIMUM_BITS_OPTION, \ RANDOM_SEED_BYTES_OPTION, \ LOGGING_LEVEL_OPTION, \ + USERNAME_VALIDATION_OPTION, \ KMSAUTH_SECTION, \ KMSAUTH_USEKMSAUTH_OPTION, \ KMSAUTH_SERVICE_ID_OPTION, \ TEST_USER_OPTION, \ CERTIFICATE_EXTENSIONS_OPTION -from bless.request.bless_request import BlessSchema +from bless.request.bless_request import BlessSchema, set_username_validation from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType @@ -72,6 +73,8 @@ def lambda_handler(event, context=None, ca_private_key_password=None, password_ciphertext_b64 = config.getpassword() certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION) + set_username_validation(config.get(BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION)) + # Process cert request schema = BlessSchema(strict=True) try: diff --git a/tests/aws_lambda/bless-test-username-validation-relaxed.cfg b/tests/aws_lambda/bless-test-username-validation-relaxed.cfg new file mode 100644 index 0000000..e8ce06b --- /dev/null +++ b/tests/aws_lambda/bless-test-username-validation-relaxed.cfg @@ -0,0 +1,7 @@ +[Bless CA] +ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +kms_key_id = alias/foo +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-tests +[Bless Options] +username_validation = relaxed diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 2b7f731..c55bdbc 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -22,6 +22,15 @@ class Context(object): "bastion_user_ip": "127.0.0.1" } +VALID_TEST_REQUEST_USERNAME_VALIDATION_RELAXED = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "~+:,/relaxeduser", + "bastion_user_ip": "127.0.0.1" +} + INVALID_TEST_REQUEST = { "remote_usernames": "user", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, @@ -114,6 +123,15 @@ def test_basic_local_missing_kmsauth_request(): assert output['errorType'] == 'InputValidationError' +def test_basic_local_username_validation_relaxed(): + output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_RELAXED, 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-username-validation-relaxed.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + def test_invalid_kmsauth_request(): output = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, From c58b32812ac5e94424d2ddfc248fb815230b6cb8 Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Tue, 14 Mar 2017 23:58:31 +0100 Subject: [PATCH 24/68] Add support to disable username validation --- bless/request/bless_request.py | 4 +++- .../bless-test-username-validation-disabled.cfg | 7 +++++++ tests/aws_lambda/test_bless_lambda.py | 17 +++++++++++++++++ tests/request/test_bless_request.py | 11 +++++++++++ 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 tests/aws_lambda/bless-test-username-validation-disabled.cfg diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index 1af4aa7..1f8937b 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -26,7 +26,7 @@ VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" USERNAME_VALIDATION_OPTIONS = Enum('UserNameValidationOptions', - 'useradd debian relaxed') + 'useradd debian relaxed disabled') username_validation = USERNAME_VALIDATION_OPTIONS.useradd @@ -45,6 +45,8 @@ def validate_ips(ips): def validate_user(user): + if username_validation == USERNAME_VALIDATION_OPTIONS.disabled: + return if len(user) > 32: raise ValidationError('Username is too long.') if username_validation == USERNAME_VALIDATION_OPTIONS.relaxed: diff --git a/tests/aws_lambda/bless-test-username-validation-disabled.cfg b/tests/aws_lambda/bless-test-username-validation-disabled.cfg new file mode 100644 index 0000000..4ebcd64 --- /dev/null +++ b/tests/aws_lambda/bless-test-username-validation-disabled.cfg @@ -0,0 +1,7 @@ +[Bless CA] +ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +kms_key_id = alias/foo +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-tests +[Bless Options] +username_validation = disabled diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index c55bdbc..264f52b 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -31,6 +31,15 @@ class Context(object): "bastion_user_ip": "127.0.0.1" } +VALID_TEST_REQUEST_USERNAME_VALIDATION_DISABLED = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "a33characterusernameyoumustbenuts", + "bastion_user_ip": "127.0.0.1" +} + INVALID_TEST_REQUEST = { "remote_usernames": "user", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, @@ -129,6 +138,14 @@ def test_basic_local_username_validation_relaxed(): entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), 'bless-test-username-validation-relaxed.cfg')) + + +def test_basic_local_username_validation_disabled(): + output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_DISABLED, 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-username-validation-disabled.cfg')) assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index cc0ad54..302ff85 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -114,6 +114,15 @@ def test_validate_user_relaxed(test_input, monkeypatch): validate_user(test_input) +@pytest.mark.parametrize("test_input", [ + ('a33characterusernameyoumustbenuts'), + ('~:, \n\t@') +]) +def test_validate_user_disabled(test_input, monkeypatch): + monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.disabled) + validate_user(test_input) + + @pytest.mark.parametrize("test_input", [ ('uservalid'), ('uservalid,uservalid2'), @@ -138,10 +147,12 @@ def test_set_username_validation_invalid(): with pytest.raises(KeyError) as e: set_username_validation('random') + @pytest.mark.parametrize("test_input", [ ('useradd'), ('debian'), ('relaxed'), + ('disabled') ]) def test_set_username_validation(test_input): set_username_validation(test_input) From 6f91bb6552e8f08a535ff8f4ef073f084f7c773c Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Tue, 21 Mar 2017 09:39:40 +0100 Subject: [PATCH 25/68] Use schema context for username validation --- bless/aws_lambda/bless_lambda.py | 6 ++-- bless/request/bless_request.py | 16 +++++----- tests/aws_lambda/test_bless_lambda.py | 18 +++++++++++ tests/request/test_bless_request.py | 44 ++++++++------------------- 4 files changed, 40 insertions(+), 44 deletions(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 120b161..c41c655 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -26,7 +26,7 @@ KMSAUTH_SERVICE_ID_OPTION, \ TEST_USER_OPTION, \ CERTIFICATE_EXTENSIONS_OPTION -from bless.request.bless_request import BlessSchema, set_username_validation +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 @@ -73,10 +73,10 @@ def lambda_handler(event, context=None, ca_private_key_password=None, password_ciphertext_b64 = config.getpassword() certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION) - set_username_validation(config.get(BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION)) - # Process cert request schema = BlessSchema(strict=True) + schema.context['username_validation'] = config.get(BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION) + try: request = schema.load(event).data except ValidationError as e: diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index 1f8937b..cabc6db 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -28,13 +28,6 @@ USERNAME_VALIDATION_OPTIONS = Enum('UserNameValidationOptions', 'useradd debian relaxed disabled') -username_validation = USERNAME_VALIDATION_OPTIONS.useradd - - -def set_username_validation(value): - global username_validation - username_validation = USERNAME_VALIDATION_OPTIONS[value] - def validate_ips(ips): try: @@ -44,7 +37,7 @@ def validate_ips(ips): raise ValidationError('Invalid IP address.') -def validate_user(user): +def validate_user(user, username_validation): if username_validation == USERNAME_VALIDATION_OPTIONS.disabled: return if len(user) > 32: @@ -83,7 +76,7 @@ def validate_ssh_public_key(public_key): class BlessSchema(Schema): bastion_ips = fields.Str(validate=validate_ips, required=True) - bastion_user = fields.Str(validate=validate_user, required=True) + bastion_user = fields.Str(required=True) bastion_user_ip = fields.Str(validate=validate_ips, required=True) command = fields.Str(required=True) public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True) @@ -96,6 +89,11 @@ def check_unknown_fields(self, data, original_data): if unknown: raise ValidationError('Unknown field', unknown) + @validates_schema + def validate_user(self, data): + username_validation = USERNAME_VALIDATION_OPTIONS[self.context['username_validation']] + validate_user(data['bastion_user'], username_validation) + @post_load def make_bless_request(self, data): return BlessRequest(**data) diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 264f52b..af73c09 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -104,6 +104,15 @@ class Context(object): "bastion_user_ip": "127.0.0.1" } +INVALID_TEST_REQUEST_USERNAME_INVALID = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "~@.", + "bastion_user_ip": "127.0.0.1" +} + os.environ['AWS_REGION'] = 'us-west-2' @@ -149,6 +158,15 @@ def test_basic_local_username_validation_disabled(): assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') +def test_invalid_username_request(): + output = lambda_handler(INVALID_TEST_REQUEST_USERNAME_INVALID, 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['errorType'] == 'InputValidationError' + + def test_invalid_kmsauth_request(): output = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index 302ff85..31df99e 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -1,5 +1,5 @@ import pytest -from bless.request.bless_request import validate_ips, validate_user, validate_principals, set_username_validation, USERNAME_VALIDATION_OPTIONS +from bless.request.bless_request import validate_ips, validate_user, validate_principals, USERNAME_VALIDATION_OPTIONS from marshmallow import ValidationError @@ -20,7 +20,7 @@ def test_validate_ips_cidr(): def test_validate_user_too_long(): with pytest.raises(ValidationError) as e: - validate_user('a33characterusernameyoumustbenuts') + validate_user('a33characterusernameyoumustbenuts', USERNAME_VALIDATION_OPTIONS.useradd) assert e.value.message == 'Username is too long.' @@ -33,7 +33,7 @@ def test_validate_user_too_long(): ]) def test_validate_user_contains_junk(test_input): with pytest.raises(ValidationError) as e: - validate_user(test_input) + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.useradd) assert e.value.message == 'Username contains invalid characters.' @@ -44,13 +44,12 @@ def test_validate_user_contains_junk(test_input): ('abc123_-valid') ]) def test_validate_user(test_input): - validate_user(test_input) + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.useradd) -def test_validate_user_debian_too_long(monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.debian) +def test_validate_user_debian_too_long(): with pytest.raises(ValidationError) as e: - validate_user('a33characterusernameyoumustbenuts') + validate_user('a33characterusernameyoumustbenuts', USERNAME_VALIDATION_OPTIONS.debian) assert e.value.message == 'Username is too long.' @@ -65,9 +64,8 @@ def test_validate_user_debian_too_long(monkeypatch): ('user\ninvalid'), ]) def test_validate_user_debian_invalid(test_input, monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.debian) with pytest.raises(ValidationError) as e: - validate_user(test_input) + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.debian) assert e.value.message == 'Username contains invalid characters.' @@ -81,14 +79,12 @@ def test_validate_user_debian_invalid(test_input, monkeypatch): ('user+valid'), ]) def test_validate_user_debian(test_input, monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.debian) - validate_user(test_input) + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.debian) def test_validate_user_relaxed_too_long(monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.relaxed) with pytest.raises(ValidationError) as e: - validate_user('a33characterusernameyoumustbenuts') + validate_user('a33characterusernameyoumustbenuts', USERNAME_VALIDATION_OPTIONS.relaxed) assert e.value.message == 'Username is too long.' @@ -110,8 +106,7 @@ def test_validate_user_relaxed_too_long(monkeypatch): ('user\nvalid'), ]) def test_validate_user_relaxed(test_input, monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.relaxed) - validate_user(test_input) + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.relaxed) @pytest.mark.parametrize("test_input", [ @@ -119,8 +114,7 @@ def test_validate_user_relaxed(test_input, monkeypatch): ('~:, \n\t@') ]) def test_validate_user_disabled(test_input, monkeypatch): - monkeypatch.setattr('bless.request.bless_request.username_validation', USERNAME_VALIDATION_OPTIONS.disabled) - validate_user(test_input) + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.disabled) @pytest.mark.parametrize("test_input", [ @@ -131,6 +125,7 @@ def test_validate_user_disabled(test_input, monkeypatch): def test_validate_multiple_principals(test_input): validate_principals(test_input) + @pytest.mark.parametrize("test_input", [ ('user invalid'), ('uservalid,us#erinvalid2'), @@ -141,18 +136,3 @@ def test_validate_multiple_principals(test_input): with pytest.raises(ValidationError) as e: validate_principals(test_input) assert e.value.message == 'Principal contains invalid characters.' - - -def test_set_username_validation_invalid(): - with pytest.raises(KeyError) as e: - set_username_validation('random') - - -@pytest.mark.parametrize("test_input", [ - ('useradd'), - ('debian'), - ('relaxed'), - ('disabled') -]) -def test_set_username_validation(test_input): - set_username_validation(test_input) From 8e80230a0eeb7cedb98fed31aa9701dd0f960e8a Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Tue, 21 Mar 2017 20:31:05 +0100 Subject: [PATCH 26/68] Add test for username_validation environment value --- tests/config/test_bless_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index 51582cf..db2f095 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -36,6 +36,7 @@ def test_config_environment_override(monkeypatch): 'bless_options_random_seed_bytes': '3', 'bless_options_logging_level': 'DEBUG', 'bless_options_certificate_extensions': 'permit-X11-forwarding', + 'bless_options_username_validation': 'debian', 'bless_ca_us-east-1_password': '', 'bless_ca_default_password': '', @@ -59,6 +60,7 @@ def test_config_environment_override(monkeypatch): 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 '' == config.getpassword() assert '' == config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) From 43407376071f7c8f5f9306f33bab7ae661dc6980 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 22 Mar 2017 11:33:48 -0700 Subject: [PATCH 27/68] Enhancing PR#43 to include support for configurable remote_usernames validation in addition to bastion_user. Added an email and principal validation option. Updated bless_deploy_example with the new options. --- bless/aws_lambda/bless_lambda.py | 10 ++- bless/config/bless_config.py | 4 + bless/config/bless_deploy_example.cfg | 5 ++ bless/request/bless_request.py | 60 +++++++++---- setup.py | 1 + ...less-test-username-validation-disabled.cfg | 7 -- ...bless-test-username-validation-relaxed.cfg | 7 -- tests/aws_lambda/test_bless_lambda.py | 44 +++++++--- tests/config/test_bless_config.py | 40 ++++++--- tests/request/test_bless_request.py | 84 ++++++++++++------- 10 files changed, 171 insertions(+), 91 deletions(-) delete mode 100644 tests/aws_lambda/bless-test-username-validation-disabled.cfg delete mode 100644 tests/aws_lambda/bless-test-username-validation-relaxed.cfg diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index c41c655..9b259c2 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -25,7 +25,8 @@ KMSAUTH_USEKMSAUTH_OPTION, \ KMSAUTH_SERVICE_ID_OPTION, \ TEST_USER_OPTION, \ - CERTIFICATE_EXTENSIONS_OPTION + CERTIFICATE_EXTENSIONS_OPTION, \ + REMOTE_USERNAMES_VALIDATION_OPTION from bless.request.bless_request import BlessSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority @@ -75,7 +76,9 @@ def lambda_handler(event, context=None, ca_private_key_password=None, # Process cert request schema = BlessSchema(strict=True) - schema.context['username_validation'] = config.get(BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION) + 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 @@ -117,8 +120,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, # 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)): + 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 diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 8ca2a24..c0e537b 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -51,6 +51,9 @@ USERNAME_VALIDATION_OPTION = 'username_validation' USERNAME_VALIDATION_DEFAULT = 'useradd' +REMOTE_USERNAMES_VALIDATION_OPTION = 'remote_usernames_validation' +REMOTE_USERNAMES_VALIDATION_DEFAULT = 'principal' + class BlessConfig(ConfigParser.RawConfigParser, object): def __init__(self, aws_region, config_file): @@ -76,6 +79,7 @@ def __init__(self, aws_region, config_file): KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_DEFAULT, CERTIFICATE_EXTENSIONS_OPTION: CERTIFICATE_EXTENSIONS_DEFAULT, USERNAME_VALIDATION_OPTION: USERNAME_VALIDATION_DEFAULT, + REMOTE_USERNAMES_VALIDATION_OPTION: REMOTE_USERNAMES_VALIDATION_DEFAULT } ConfigParser.RawConfigParser.__init__(self, defaults=defaults) self.read(config_file) diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index a01f65a..4e0a16f 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -11,6 +11,11 @@ random_seed_bytes = 256 logging_level = INFO # Comma separated list of the SSH Certificate extensions to include. Not specifying this uses the ssh-keygen defaults: # certificate_extensions = permit-X11-forwarding,permit-agent-forwarding,permit-port-forwarding,permit-pty,permit-user-rc +# Username validation options are described in bless_request.py:USERNAME_VALIDATION_OPTIONS +# Configure how bastion_user names are validated. +# username_validation = useradd +# Configure how remote_usernames names are validated. +# remote_usernames_validation = principal # These values are all required to be modified for deployment [Bless CA] diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index cabc6db..ad182a0 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -3,11 +3,16 @@ :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ -from enum import Enum import re import ipaddress +from enum import Enum from marshmallow import Schema, fields, post_load, ValidationError, validates_schema +from marshmallow import validates +from marshmallow.validate import Email + +from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION, \ + USERNAME_VALIDATION_DEFAULT, REMOTE_USERNAMES_VALIDATION_DEFAULT # man 8 useradd USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') @@ -21,12 +26,16 @@ USERNAME_PATTERN_DEBIAN = re.compile('\A[^-+~][^:,\s]*\Z') # It appears that most printable ascii is valid, excluding whitespace, #, and commas. -# There doesn't seem to be any practical size limits of a principal (> 4096B allowed). +# There doesn't seem to be any practical size limits of an SSH Certificate Principal (> 4096B allowed). PRINCIPAL_PATTERN = re.compile(r'[\d\w!"$%&\'()*+\-./:;<=>?@\[\\\]\^`{|}~]+\Z') VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" USERNAME_VALIDATION_OPTIONS = Enum('UserNameValidationOptions', - 'useradd debian relaxed disabled') + 'useradd ' # Allowable usernames per 'man 8 useradd' + 'debian ' # Allowable usernames on debian systems. + 'email ' # username is a valid email address. + 'principal ' # SSH Certificate Principal. See 'man 5 sshd_con# fig'. + 'disabled') # no additional validation of the string. def validate_ips(ips): @@ -40,14 +49,18 @@ def validate_ips(ips): def validate_user(user, username_validation): if username_validation == USERNAME_VALIDATION_OPTIONS.disabled: return - if len(user) > 32: + elif username_validation == USERNAME_VALIDATION_OPTIONS.email: + Email('Invalid email address.').__call__(user) + elif username_validation == USERNAME_VALIDATION_OPTIONS.principal: + _validate_principal(user) + elif len(user) > 32: raise ValidationError('Username is too long.') - if username_validation == USERNAME_VALIDATION_OPTIONS.relaxed: - return - if username_validation == USERNAME_VALIDATION_OPTIONS.debian: + elif username_validation == USERNAME_VALIDATION_OPTIONS.useradd: + _validate_user_useradd(user) + elif username_validation == USERNAME_VALIDATION_OPTIONS.debian: _validate_user_debian(user) else: - _validate_user_useradd(user) + raise ValidationError('Invalid username validator.') def _validate_user_useradd(user): @@ -60,10 +73,9 @@ def _validate_user_debian(user): raise ValidationError('Username contains invalid characters.') -def validate_principals(principals): - for principal in principals.split(','): - if PRINCIPAL_PATTERN.match(principal) is None: - raise ValidationError('Principal contains invalid characters.') +def _validate_principal(principal): + if PRINCIPAL_PATTERN.match(principal) is None: + raise ValidationError('Principal contains invalid characters.') def validate_ssh_public_key(public_key): @@ -80,7 +92,7 @@ class BlessSchema(Schema): bastion_user_ip = fields.Str(validate=validate_ips, required=True) command = fields.Str(required=True) public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True) - remote_usernames = fields.Str(validate=validate_principals, required=True) + remote_usernames = fields.Str(required=True) kmsauth_token = fields.Str(required=False) @validates_schema(pass_original=True) @@ -89,15 +101,27 @@ def check_unknown_fields(self, data, original_data): if unknown: raise ValidationError('Unknown field', unknown) - @validates_schema - def validate_user(self, data): - username_validation = USERNAME_VALIDATION_OPTIONS[self.context['username_validation']] - validate_user(data['bastion_user'], username_validation) - @post_load def make_bless_request(self, data): return BlessRequest(**data) + @validates('bastion_user') + def validate_bastion_user(self, user): + if USERNAME_VALIDATION_OPTION in self.context: + username_validation = USERNAME_VALIDATION_OPTIONS[self.context[USERNAME_VALIDATION_OPTION]] + else: + username_validation = USERNAME_VALIDATION_OPTIONS[USERNAME_VALIDATION_DEFAULT] + validate_user(user, username_validation) + + @validates('remote_usernames') + def validate_remote_usernames(self, remote_usernames): + if REMOTE_USERNAMES_VALIDATION_OPTION in self.context: + username_validation = USERNAME_VALIDATION_OPTIONS[self.context[REMOTE_USERNAMES_VALIDATION_OPTION]] + else: + username_validation = USERNAME_VALIDATION_OPTIONS[REMOTE_USERNAMES_VALIDATION_DEFAULT] + for remote_username in remote_usernames.split(','): + validate_user(remote_username, username_validation) + class BlessRequest: def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_key_to_sign, diff --git a/setup.py b/setup.py index 0a3a3ae..b28ca95 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ 'botocore', 'cffi', 'cryptography', + 'enum34', 'ipaddress', 'marshmallow', 'kmsauth' diff --git a/tests/aws_lambda/bless-test-username-validation-disabled.cfg b/tests/aws_lambda/bless-test-username-validation-disabled.cfg deleted file mode 100644 index 4ebcd64..0000000 --- a/tests/aws_lambda/bless-test-username-validation-disabled.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[Bless CA] -ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem -kms_key_id = alias/foo -us-east-1_password = bogus-password-for-unit-test -us-west-2_password = bogus-password-for-unit-tests -[Bless Options] -username_validation = disabled diff --git a/tests/aws_lambda/bless-test-username-validation-relaxed.cfg b/tests/aws_lambda/bless-test-username-validation-relaxed.cfg deleted file mode 100644 index e8ce06b..0000000 --- a/tests/aws_lambda/bless-test-username-validation-relaxed.cfg +++ /dev/null @@ -1,7 +0,0 @@ -[Bless CA] -ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem -kms_key_id = alias/foo -us-east-1_password = bogus-password-for-unit-test -us-west-2_password = bogus-password-for-unit-tests -[Bless Options] -username_validation = relaxed diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index af73c09..6cf3113 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -1,4 +1,3 @@ -import json import os import pytest @@ -22,17 +21,17 @@ class Context(object): "bastion_user_ip": "127.0.0.1" } -VALID_TEST_REQUEST_USERNAME_VALIDATION_RELAXED = { - "remote_usernames": "user", +VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD = { + "remote_usernames": "user,anotheruser", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, "command": "ssh user@server", "bastion_ips": "127.0.0.1", - "bastion_user": "~+:,/relaxeduser", + "bastion_user": "someone@example.com", "bastion_user_ip": "127.0.0.1" } VALID_TEST_REQUEST_USERNAME_VALIDATION_DISABLED = { - "remote_usernames": "user", + "remote_usernames": "'~:, \n\t@'", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, "command": "ssh user@server", "bastion_ips": "127.0.0.1", @@ -141,20 +140,39 @@ def test_basic_local_missing_kmsauth_request(): assert output['errorType'] == 'InputValidationError' -def test_basic_local_username_validation_relaxed(): - output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_RELAXED, context=Context, +def test_basic_local_username_validation_disabled(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': '../../tests/aws_lambda/only-use-for-unit-tests.pem', + 'bless_options_username_validation': 'disabled', + 'bless_options_remote_usernames_validation': 'disabled', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_DISABLED, 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-username-validation-relaxed.cfg')) + config_file=os.path.join(os.path.dirname(__file__), '')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') -def test_basic_local_username_validation_disabled(): - output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_DISABLED, context=Context, +def test_basic_local_username_validation_email_remote_usernames_useradd(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': '../../tests/aws_lambda/only-use-for-unit-tests.pem', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, 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-username-validation-disabled.cfg')) + config_file=os.path.join(os.path.dirname(__file__), '')) assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index db2f095..06e0d29 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -3,13 +3,27 @@ import pytest -from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION, \ - CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ - ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \ - CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, \ - LOGGING_LEVEL_DEFAULT, LOGGING_LEVEL_OPTION, BLESS_CA_SECTION, \ - CA_PRIVATE_KEY_FILE_OPTION, KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION, KMSAUTH_KEY_ID_OPTION, \ - KMSAUTH_SERVICE_ID_OPTION, CERTIFICATE_EXTENSIONS_OPTION, USERNAME_VALIDATION_OPTION, USERNAME_VALIDATION_DEFAULT +from bless.config.bless_config import BlessConfig, \ + BLESS_OPTIONS_SECTION, \ + CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ + CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ + ENTROPY_MINIMUM_BITS_OPTION, \ + RANDOM_SEED_BYTES_OPTION, \ + CERTIFICATE_VALIDITY_SEC_DEFAULT, \ + ENTROPY_MINIMUM_BITS_DEFAULT, \ + RANDOM_SEED_BYTES_DEFAULT, \ + LOGGING_LEVEL_DEFAULT, \ + LOGGING_LEVEL_OPTION, \ + BLESS_CA_SECTION, \ + CA_PRIVATE_KEY_FILE_OPTION, \ + KMSAUTH_SECTION, \ + KMSAUTH_USEKMSAUTH_OPTION, \ + KMSAUTH_KEY_ID_OPTION, \ + KMSAUTH_SERVICE_ID_OPTION, \ + CERTIFICATE_EXTENSIONS_OPTION, \ + USERNAME_VALIDATION_OPTION, \ + USERNAME_VALIDATION_DEFAULT, \ + REMOTE_USERNAMES_VALIDATION_OPTION def test_empty_config(): @@ -24,7 +38,7 @@ def test_config_no_password(): 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')) + config_file=os.path.join(os.path.dirname(__file__), 'full-with-default.cfg')) assert '' == config.getpassword() @@ -37,6 +51,7 @@ def test_config_environment_override(monkeypatch): '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': '', @@ -48,7 +63,7 @@ def test_config_environment_override(monkeypatch): 'kms_auth_kmsauth_serviceid': 'bless-test', } - for k,v in extra_environment_variables.items(): + for k, v in extra_environment_variables.items(): monkeypatch.setenv(k, v) # Create an empty config, everything is set in the environment @@ -61,6 +76,7 @@ def test_config_environment_override(monkeypatch): 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 'useradd' == config.get(BLESS_OPTIONS_SECTION, REMOTE_USERNAMES_VALIDATION_OPTION) assert '' == config.getpassword() assert '' == config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) @@ -82,17 +98,17 @@ def test_config_environment_override(monkeypatch): LOGGING_LEVEL_DEFAULT, '', USERNAME_VALIDATION_DEFAULT - ), + ), ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-west-2', 1, 2, 3, 'DEBUG', '', 'debian' - ), + ), ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-east-1', 1, 2, 3, 'DEBUG', '', 'debian' - ) + ) ]) def test_configs(config, region, expected_cert_valid, expected_entropy_min, expected_rand_seed, expected_log_level, expected_password, expected_username_validation): diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index 31df99e..b6ff49a 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -1,7 +1,9 @@ import pytest -from bless.request.bless_request import validate_ips, validate_user, validate_principals, USERNAME_VALIDATION_OPTIONS from marshmallow import ValidationError +from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION +from bless.request.bless_request import validate_ips, validate_user, USERNAME_VALIDATION_OPTIONS, BlessSchema + def test_validate_ips(): validate_ips(u'127.0.0.1') @@ -63,7 +65,7 @@ def test_validate_user_debian_too_long(): ('user\tinvalid'), ('user\ninvalid'), ]) -def test_validate_user_debian_invalid(test_input, monkeypatch): +def test_validate_user_debian_invalid(test_input): with pytest.raises(ValidationError) as e: validate_user(test_input, USERNAME_VALIDATION_OPTIONS.debian) assert e.value.message == 'Username contains invalid characters.' @@ -78,52 +80,60 @@ def test_validate_user_debian_invalid(test_input, monkeypatch): ('user-valid'), ('user+valid'), ]) -def test_validate_user_debian(test_input, monkeypatch): +def test_validate_user_debian(test_input): validate_user(test_input, USERNAME_VALIDATION_OPTIONS.debian) -def test_validate_user_relaxed_too_long(monkeypatch): - with pytest.raises(ValidationError) as e: - validate_user('a33characterusernameyoumustbenuts', USERNAME_VALIDATION_OPTIONS.relaxed) - assert e.value.message == 'Username is too long.' - - @pytest.mark.parametrize("test_input", [ ('uservalid'), ('a32characterusernameyoumustok$'), - ('_uservalid$'), - ('abc123_-valid'), - ('user~valid'), - ('user-valid'), - ('user+valid'), - ('~uservalid'), - ('-uservalid'), - ('+uservalid'), - ('user:valid'), - ('user,valid'), - ('user valid'), - ('user\tvalid'), - ('user\nvalid'), + ('!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~') +]) +def test_validate_user_principal(test_input): + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.principal) + + +@pytest.mark.parametrize("test_input", [ + ('a33characterusernameyoumustbenuts@example.com'), + ('a@example.com'), + ('a+b@example.com') ]) -def test_validate_user_relaxed(test_input, monkeypatch): - validate_user(test_input, USERNAME_VALIDATION_OPTIONS.relaxed) +def test_validate_user_email(test_input): + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.email) + + +@pytest.mark.parametrize("test_input", [ + ('a33characterusernameyoumustbenuts@ex@mple.com'), + ('a@example'), +]) +def test_invalid_user_email(test_input): + with pytest.raises(ValidationError) as e: + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.email) + assert e.value.message == 'Invalid email address.' @pytest.mark.parametrize("test_input", [ ('a33characterusernameyoumustbenuts'), - ('~:, \n\t@') + ('~:, \n\t@'), + ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~,'), ]) -def test_validate_user_disabled(test_input, monkeypatch): +def test_validate_user_disabled(test_input): validate_user(test_input, USERNAME_VALIDATION_OPTIONS.disabled) @pytest.mark.parametrize("test_input", [ ('uservalid'), ('uservalid,uservalid2'), - ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~,uservalid2') + ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~,' + 'uservalid2') ]) def test_validate_multiple_principals(test_input): - validate_principals(test_input) + BlessSchema().validate_remote_usernames(test_input) + + schema = BlessSchema() + schema.context[USERNAME_VALIDATION_OPTION] = USERNAME_VALIDATION_OPTIONS.principal.name + schema.context[REMOTE_USERNAMES_VALIDATION_OPTION] = USERNAME_VALIDATION_OPTIONS.principal.name + schema.validate_remote_usernames(test_input) @pytest.mark.parametrize("test_input", [ @@ -131,8 +141,22 @@ def test_validate_multiple_principals(test_input): ('uservalid,us#erinvalid2'), ('uservalid,,uservalid2'), (' uservalid'), + ('user\ninvalid'), + ('~:, \n\t@') ]) -def test_validate_multiple_principals(test_input): +def test_invalid_multiple_principals(test_input): with pytest.raises(ValidationError) as e: - validate_principals(test_input) + BlessSchema().validate_remote_usernames(test_input) assert e.value.message == 'Principal contains invalid characters.' + + +def test_invalid_user_with_default_context_of_useradd(): + with pytest.raises(ValidationError) as e: + BlessSchema().validate_bastion_user('user#invalid') + assert e.value.message == 'Username contains invalid characters.' + + +def test_invalid_call_of_validate_user(): + with pytest.raises(ValidationError) as e: + validate_user('test', None) + assert e.value.message == 'Invalid username validator.' From 32e4f4b61078d6d7a51d4b710837b88059a4a846 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 22 Mar 2017 13:57:30 -0700 Subject: [PATCH 28/68] Fixing https://github.com/Netflix/bless/issues/48 and moving the expected ca private key path back to the top level of the published .zip. --- bless/config/bless_config.py | 2 +- tests/aws_lambda/bless-test-broken.cfg | 2 +- tests/aws_lambda/bless-test-kmsauth.cfg | 2 +- .../aws_lambda/bless-test-with-certificate-extensions-empty.cfg | 2 +- tests/aws_lambda/bless-test-with-certificate-extensions.cfg | 2 +- tests/aws_lambda/bless-test-with-test-user.cfg | 2 +- tests/aws_lambda/bless-test.cfg | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 2720cc9..d6c2ef0 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -110,7 +110,7 @@ def getprivatekey(self): ca_private_key_file = self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_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.path.dirname(__file__), os.pardir, os.pardir, ca_private_key_file), 'r') as f: return f.read() def has_option(self, section, option): diff --git a/tests/aws_lambda/bless-test-broken.cfg b/tests/aws_lambda/bless-test-broken.cfg index f4de164..7df734b 100644 --- a/tests/aws_lambda/bless-test-broken.cfg +++ b/tests/aws_lambda/bless-test-broken.cfg @@ -1,4 +1,4 @@ [Bless CA] -ca_private_key_file = ../../tests/aws_lambda/not-found.pem +ca_private_key_file = tests/aws_lambda/not-found.pem us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-test diff --git a/tests/aws_lambda/bless-test-kmsauth.cfg b/tests/aws_lambda/bless-test-kmsauth.cfg index ab207b6..34d0b25 100644 --- a/tests/aws_lambda/bless-test-kmsauth.cfg +++ b/tests/aws_lambda/bless-test-kmsauth.cfg @@ -1,5 +1,5 @@ [Bless CA] -ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-test diff --git a/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg b/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg index ec4916f..9d7ca41 100644 --- a/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg +++ b/tests/aws_lambda/bless-test-with-certificate-extensions-empty.cfg @@ -1,5 +1,5 @@ [Bless CA] -ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-tests diff --git a/tests/aws_lambda/bless-test-with-certificate-extensions.cfg b/tests/aws_lambda/bless-test-with-certificate-extensions.cfg index 6bed688..11d4031 100644 --- a/tests/aws_lambda/bless-test-with-certificate-extensions.cfg +++ b/tests/aws_lambda/bless-test-with-certificate-extensions.cfg @@ -1,5 +1,5 @@ [Bless CA] -ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-tests diff --git a/tests/aws_lambda/bless-test-with-test-user.cfg b/tests/aws_lambda/bless-test-with-test-user.cfg index 8fc454f..246ba14 100644 --- a/tests/aws_lambda/bless-test-with-test-user.cfg +++ b/tests/aws_lambda/bless-test-with-test-user.cfg @@ -1,5 +1,5 @@ [Bless CA] -ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-tests diff --git a/tests/aws_lambda/bless-test.cfg b/tests/aws_lambda/bless-test.cfg index ab24770..c4d6965 100644 --- a/tests/aws_lambda/bless-test.cfg +++ b/tests/aws_lambda/bless-test.cfg @@ -1,4 +1,4 @@ [Bless CA] -ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem us-east-1_password = bogus-password-for-unit-test us-west-2_password = bogus-password-for-unit-tests From 95693c44d31e2e7557d71ab1716f2ba7bb666a4d Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Wed, 22 Mar 2017 23:02:35 +0100 Subject: [PATCH 29/68] Replace non word characters in environment key --- bless/config/bless_config.py | 3 ++- tests/config/test_bless_config.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index d6c2ef0..95fbce7 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -6,6 +6,7 @@ import ConfigParser import base64 import os +import re BLESS_OPTIONS_SECTION = 'Bless Options' CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'certificate_validity_before_seconds' @@ -145,4 +146,4 @@ def get(self, section, option): @staticmethod def _environment_key(section, option): - return (section.replace(' ', '_') + '_' + option).lower() + return (re.sub('\W+', '_', section) + '_' + re.sub('\W+', '_', option)).lower() diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index 7f2844b..68a7dff 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -37,7 +37,7 @@ def test_config_environment_override(monkeypatch): 'bless_options_logging_level': 'DEBUG', 'bless_options_certificate_extensions': 'permit-X11-forwarding', - 'bless_ca_us-east-1_password': '', + 'bless_ca_us_east_1_password': '', 'bless_ca_default_password': '', 'bless_ca_ca_private_key_file': '', 'bless_ca_ca_private_key': base64.b64encode(''), From 1393a8420cd4e09de770dec2a00733d246394c22 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 22 Mar 2017 17:56:30 -0700 Subject: [PATCH 30/68] Revising the certifiace key_id to keep consistency in the key[value] format. Updated the lambda log message to match. --- bless/aws_lambda/bless_lambda.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 692a3a6..4199d92 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -164,7 +164,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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( + 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))) @@ -173,7 +173,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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 ' + '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)))) From c543416cf622dd0491a1f369bed80c597e9a8fe5 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 29 Mar 2017 15:40:31 -0700 Subject: [PATCH 31/68] Thanks to @Stype: Update the kmsauth token validation code to verify that all remote_usernames are checked against the offered kmsauth_token. --- bless/aws_lambda/bless_lambda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 4199d92..8a482ab 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -138,7 +138,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, ) # decrypt_token will raise a TokenValidationError if token doesn't match validator.decrypt_token( - "2/user/{}".format(request.remote_usernames.split(',')[0]), + "2/user/{}".format(request.remote_usernames), request.kmsauth_token ) except TokenValidationError as e: From d8f6d1eb7b1282f70e51c7c8edd8a753402b398d Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 5 Apr 2017 16:04:59 -0700 Subject: [PATCH 32/68] Fixing typos in readme. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2967b12..37d34cc 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ # BLESS - Bastion's Lambda Ephemeral SSH Service [![Build Status](https://travis-ci.org/Netflix/bless.svg?branch=master)](https://travis-ci.org/Netflix/bless) [![Test coverage](https://coveralls.io/repos/github/Netflix/bless/badge.svg?branch=master)](https://coveralls.io/github/Netflix/bless) [![Join the chat at https://gitter.im/Netflix/bless](https://badges.gitter.im/Netflix/bless.svg)](https://gitter.im/Netflix/bless?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![NetflixOSS Lifecycle](https://img.shields.io/osslifecycle/Netflix/bless.svg)]() -BLESS is an SSH Certificate Authority that runs as a AWS Lambda function and is used to sign ssh +BLESS is an SSH Certificate Authority that runs as an AWS Lambda function and is used to sign SSH public keys. -SSH Certificates are an excellent way to authorize users to access a particular ssh host, +SSH Certificates are an excellent way to authorize users to access a particular SSH host, as they can be restricted for a single use case, and can be short lived. Instead of managing the authorized_keys of a host, or controlling who has access to SSH Private Keys, hosts just need to be configured to trust an SSH CA. From d5a1c1fd4831b4c46c7d475f4231bc5fec846bbf Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Tue, 18 Apr 2017 19:22:14 -0700 Subject: [PATCH 33/68] Fixing test key paths after merging https://github.com/Netflix/bless/pull/43 . --- tests/aws_lambda/test_bless_lambda.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 6cf3113..ab9a131 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -143,7 +143,7 @@ def test_basic_local_missing_kmsauth_request(): def test_basic_local_username_validation_disabled(monkeypatch): extra_environment_variables = { 'bless_ca_default_password': '', - 'bless_ca_ca_private_key_file': '../../tests/aws_lambda/only-use-for-unit-tests.pem', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.pem', 'bless_options_username_validation': 'disabled', 'bless_options_remote_usernames_validation': 'disabled', } @@ -161,7 +161,7 @@ def test_basic_local_username_validation_disabled(monkeypatch): def test_basic_local_username_validation_email_remote_usernames_useradd(monkeypatch): extra_environment_variables = { 'bless_ca_default_password': '', - 'bless_ca_ca_private_key_file': '../../tests/aws_lambda/only-use-for-unit-tests.pem', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.pem', 'bless_options_username_validation': 'email', 'bless_options_remote_usernames_validation': 'useradd', } From 7cd151524e20c0895c2a2ea769bd6bdad8863be2 Mon Sep 17 00:00:00 2001 From: djcrabhat Date: Sat, 29 Apr 2017 18:33:37 -0700 Subject: [PATCH 34/68] base kmsauth token on bastion_user instead of remote_usernames --- bless/aws_lambda/bless_lambda.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 73350ec..0928a04 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -143,7 +143,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, ) # decrypt_token will raise a TokenValidationError if token doesn't match validator.decrypt_token( - "2/user/{}".format(request.remote_usernames), + "2/user/{}".format(request.bastion_user), request.kmsauth_token ) except TokenValidationError as e: From 9ad57e0ebbc8dc091816a7ee1feb935cd0a94c73 Mon Sep 17 00:00:00 2001 From: djcrabhat Date: Sat, 6 May 2017 20:30:16 -0700 Subject: [PATCH 35/68] enforce that bastion_user == remote_usernames by default. add config to allow different remote_usernames --- bless/aws_lambda/bless_lambda.py | 10 ++++++++++ bless/config/bless_config.py | 4 ++++ bless/config/bless_deploy_example.cfg | 5 +++++ 3 files changed, 19 insertions(+) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 0928a04..a73e229 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -23,6 +23,7 @@ USERNAME_VALIDATION_OPTION, \ KMSAUTH_SECTION, \ KMSAUTH_USEKMSAUTH_OPTION, \ + KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION, \ KMSAUTH_SERVICE_ID_OPTION, \ TEST_USER_OPTION, \ CERTIFICATE_EXTENSIONS_OPTION, \ @@ -134,6 +135,15 @@ def lambda_handler(event, context=None, ca_private_key_password=None, # 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 + if KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION: + allowed_users = KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION.split(",") + if allowed_users != '*' and request.remote_usernames not in allowed_users: + return error_response('KMSAuthValidationError', + 'invalid 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, diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index e8c9989..89a5dd8 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -46,6 +46,9 @@ KMSAUTH_KEY_ID_OPTION = 'kmsauth_key_id' KMSAUTH_KEY_ID_DEFAULT = '' +KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION = 'kmsauth_remote_usernames_allowed' +KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION_DEFAULT = None + KMSAUTH_SERVICE_ID_OPTION = 'kmsauth_serviceid' KMSAUTH_SERVICE_ID_DEFAULT = None @@ -77,6 +80,7 @@ def __init__(self, aws_region, config_file): TEST_USER_OPTION: TEST_USER_DEFAULT, KMSAUTH_SERVICE_ID_OPTION: KMSAUTH_SERVICE_ID_DEFAULT, KMSAUTH_KEY_ID_OPTION: KMSAUTH_KEY_ID_DEFAULT, + KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION: KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION_DEFAULT, KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_DEFAULT, CERTIFICATE_EXTENSIONS_OPTION: CERTIFICATE_EXTENSIONS_DEFAULT, USERNAME_VALIDATION_OPTION: USERNAME_VALIDATION_DEFAULT, diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index 4e0a16f..7d75356 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -41,3 +41,8 @@ ca_private_key_file = # If using kmsauth, you need to set the kmsauth service name. Users need to set the 'to' # context to this same service name when they create a kmsauth token. # kmsauth_serviceid = bless-production + +# By default, kmsauth requires that requested bastion_user must be the same as the requested remote_usernames. If you +# want Bless to sign a certificate for a different remote_usernames (like root, or a shared admin account), you must +# specify those allowed names here. * will allow signing for all remote_usernames +# kmsauth_remote_usernames_allowed = ubuntu,root,ec2-user,stufflikethat \ No newline at end of file From 3b268a6498dc42cd4b149d47165b815663e51c91 Mon Sep 17 00:00:00 2001 From: djcrabhat Date: Sat, 6 May 2017 20:56:09 -0700 Subject: [PATCH 36/68] add tests for allowing remote_usernames to differ --- bless/aws_lambda/bless_lambda.py | 9 +++-- .../bless-test-kmsauth-different-remote.cfg | 33 +++++++++++++++++ tests/aws_lambda/test_bless_lambda.py | 37 +++++++++++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 tests/aws_lambda/bless-test-kmsauth-different-remote.cfg diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index a73e229..e366689 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -136,11 +136,12 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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 - if KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION: - allowed_users = KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION.split(",") - if allowed_users != '*' and request.remote_usernames not in allowed_users: + allowed_remotes = config.get(KMSAUTH_SECTION, KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION) + if allowed_remotes: + allowed_users = allowed_remotes.split(",") + if allowed_users != ['*'] and request.remote_usernames not in allowed_users: return error_response('KMSAuthValidationError', - 'invalid remote_usernames [{}]'.format(request.remote_usernames)) + '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') diff --git a/tests/aws_lambda/bless-test-kmsauth-different-remote.cfg b/tests/aws_lambda/bless-test-kmsauth-different-remote.cfg new file mode 100644 index 0000000..757a9d3 --- /dev/null +++ b/tests/aws_lambda/bless-test-kmsauth-different-remote.cfg @@ -0,0 +1,33 @@ +[Bless CA] +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-test + +[KMS Auth] +use_kmsauth = True +kmsauth_key_id = alias/authnz-iad, alias/authnz-sfo +kmsauth_serviceid = kmsauth-prod +kmsauth_remote_usernames_allowed = ubuntu + +# todo get from config, with some sane defaults +#[loggers] +#keys=root +# +#[handlers] +#keys=stream_handler +# +#[formatters] +#keys=formatter +# +#[logger_root] +#level=INFO +#handlers=stream_handler +# +#[handler_stream_handler] +#class=StreamHandler +#level=DEBUG +#formatter=formatter +#args=(sys.stderr,) +# +#[formatter_formatter] +#format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index ab9a131..9762556 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -112,6 +112,27 @@ class Context(object): "bastion_user_ip": "127.0.0.1" } +INVALID_TEST_KMSAUTH_REQUEST_USERNAME_DOESNT_MATCH_REMOTE = { + "remote_usernames": "userb", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "usera", + "bastion_user_ip": "127.0.0.1", + "kmsauth_token": "validkmsauthtoken" +} + +VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER = { + "remote_usernames": "ubuntu", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "usera", + "bastion_user_ip": "127.0.0.1", + "kmsauth_token": "validkmsauthtoken" +} + + os.environ['AWS_REGION'] = 'us-west-2' @@ -284,3 +305,19 @@ def test_invalid_request_with_multiple_principals(): config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) assert output['errorType'] == 'InputValidationError' + +def test_invalid_request_with_mismatched_bastion_and_remote(): + output = lambda_handler(INVALID_TEST_KMSAUTH_REQUEST_USERNAME_DOESNT_MATCH_REMOTE, 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-kmsauth.cfg')) + assert output['errorType'] == 'KMSAuthValidationError' + +def test_valid_request_with_allowed_remote(): + output = lambda_handler(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, 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-kmsauth-different-remote.cfg')) + assert output['errorType'] == 'KMSAuthValidationError' From 5b452d1a34c30b25bdbf26e1a07ea3a9b59e59a7 Mon Sep 17 00:00:00 2001 From: djcrabhat Date: Sat, 6 May 2017 21:13:57 -0700 Subject: [PATCH 37/68] eek out some test coverage --- tests/aws_lambda/test_bless_lambda.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 9762556..0812a82 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -122,8 +122,8 @@ class Context(object): "kmsauth_token": "validkmsauthtoken" } -VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER = { - "remote_usernames": "ubuntu", +INVALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER = { + "remote_usernames": "root", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, "command": "ssh user@server", "bastion_ips": "127.0.0.1", @@ -132,7 +132,6 @@ class Context(object): "kmsauth_token": "validkmsauthtoken" } - os.environ['AWS_REGION'] = 'us-west-2' @@ -306,6 +305,7 @@ def test_invalid_request_with_multiple_principals(): 'bless-test.cfg')) assert output['errorType'] == 'InputValidationError' + def test_invalid_request_with_mismatched_bastion_and_remote(): output = lambda_handler(INVALID_TEST_KMSAUTH_REQUEST_USERNAME_DOESNT_MATCH_REMOTE, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, @@ -314,8 +314,9 @@ def test_invalid_request_with_mismatched_bastion_and_remote(): 'bless-test-kmsauth.cfg')) assert output['errorType'] == 'KMSAuthValidationError' -def test_valid_request_with_allowed_remote(): - output = lambda_handler(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, + +def test_invalid_request_with_unallowed_remote(): + output = lambda_handler(INVALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), From cadd803bae8136ef48f0bcfd3a359e2c68ac3972 Mon Sep 17 00:00:00 2001 From: djcrabhat Date: Sun, 7 May 2017 12:21:42 -0700 Subject: [PATCH 38/68] make sure all requested remote_usernames are allowed to be used added positive test mocking kmsauth sucessfully decrypting a token --- bless/aws_lambda/bless_lambda.py | 5 ++-- requirements.txt | 1 + .../bless-test-kmsauth-different-remote.cfg | 2 +- tests/aws_lambda/test_bless_lambda.py | 24 +++++++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index e366689..2a9f94a 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -138,8 +138,9 @@ def lambda_handler(event, context=None, ca_private_key_password=None, # 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(",") - if allowed_users != ['*'] and request.remote_usernames not in allowed_users: + 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: diff --git a/requirements.txt b/requirements.txt index 7c3db7b..853340a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,6 +23,7 @@ pycparser==2.17 pyflakes==1.5.0 pyparsing==2.1.10 pytest==3.0.6 +pytest-mock==1.6.0 python-dateutil==2.6.0 s3transfer==0.1.10 six==1.10.0 \ No newline at end of file diff --git a/tests/aws_lambda/bless-test-kmsauth-different-remote.cfg b/tests/aws_lambda/bless-test-kmsauth-different-remote.cfg index 757a9d3..28b2830 100644 --- a/tests/aws_lambda/bless-test-kmsauth-different-remote.cfg +++ b/tests/aws_lambda/bless-test-kmsauth-different-remote.cfg @@ -7,7 +7,7 @@ us-west-2_password = bogus-password-for-unit-test use_kmsauth = True kmsauth_key_id = alias/authnz-iad, alias/authnz-sfo kmsauth_serviceid = kmsauth-prod -kmsauth_remote_usernames_allowed = ubuntu +kmsauth_remote_usernames_allowed = ubuntu,alloweduser # todo get from config, with some sane defaults #[loggers] diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 0812a82..e6816e8 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -132,6 +132,16 @@ class Context(object): "kmsauth_token": "validkmsauthtoken" } +VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER = { + "remote_usernames": "alloweduser", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "usera", + "bastion_user_ip": "127.0.0.1", + "kmsauth_token": "validkmsauthtoken" +} + os.environ['AWS_REGION'] = 'us-west-2' @@ -307,6 +317,10 @@ def test_invalid_request_with_multiple_principals(): def test_invalid_request_with_mismatched_bastion_and_remote(): + ''' + Test default kmsauth behavior, that a bastion_user and remote_usernames must match + :return: + ''' output = lambda_handler(INVALID_TEST_KMSAUTH_REQUEST_USERNAME_DOESNT_MATCH_REMOTE, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, @@ -322,3 +336,13 @@ def test_invalid_request_with_unallowed_remote(): config_file=os.path.join(os.path.dirname(__file__), 'bless-test-kmsauth-different-remote.cfg')) assert output['errorType'] == 'KMSAuthValidationError' + + +def test_valid_request_with_allowed_remote(mocker): + mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") + output = lambda_handler(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, 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-kmsauth-different-remote.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') From f32b9a1f3ae0fc064db3c3775641d063049e0bbb Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 7 Jun 2017 19:45:43 -0700 Subject: [PATCH 39/68] Updating the SSH Certificate comment when no public key comment is set, so that it better matches the format used when logging. --- bless/ssh/certificates/ssh_certificate_builder.py | 4 ++-- tests/ssh/vectors.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bless/ssh/certificates/ssh_certificate_builder.py b/bless/ssh/certificates/ssh_certificate_builder.py index cc1ffd7..d862d44 100644 --- a/bless/ssh/certificates/ssh_certificate_builder.py +++ b/bless/ssh/certificates/ssh_certificate_builder.py @@ -232,8 +232,8 @@ def _initialize_unset_attributes(self): if not self.public_key_comment: self.public_key_comment = \ - 'Certificate type:{} principals:{} with the id:[{}]'.format( - self.cert_type, self.valid_principals, self.key_id) + 'Certificate type[{}] principals[{}] with the id[{}]'.format( + self.cert_type, ','.join(self.valid_principals), self.key_id) def _validate_cert_properties(self): if self.valid_after >= self.valid_before: diff --git a/tests/ssh/vectors.py b/tests/ssh/vectors.py index bb3c345..a81be0d 100644 --- a/tests/ssh/vectors.py +++ b/tests/ssh/vectors.py @@ -29,7 +29,7 @@ RSA_USER_CERT_DEFAULTS = 'ssh-rsa-cert-v01@openssh.com AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgjK2qHMgSOZdciBtG4HJT/cZ1Q2Rl5Sf8IBMNmrJTTuYAAAADAQABAAACAQDTuckaZVjP4tP+xUc5xbmBzX1bdPUA78Zi/J8AGAJHTOBU2hHv6532DwWwXYIIj0nhrYpSEpcOgYUkEv3iH5a+nC11QDiOgO7Fs5xXMLKgZP8AyGoWcEspjw7GxiBn6D29SaVPbSYGsyQBaxSGW+D2TMx3OXiec5MlCzVfiu0FnayvEl/1xs9KPbCQnh+BoavQSn7VnK6lNR/vanbtoaRebVKcn4b78koSiI6FegFH7vaLK5UpfRz2tw+SCFIByGfXeYkbrX23Z3t+dOj0JWxGzW/NA7tlRAmE9stYPlIDU9ldjXPm5YhMvCAUyG26+wC5Inr1/JvvDVYh9i6Z1m98O7fvHlcRuw9eP4/mGsJ3HOvbf/nb/UhdkwIwunPkIY/zYRqZ+YLPDV+mQe2D28P9QIANfoiAK4kueS0LPH0psT2jiz/qqzkaMa7WcUiKwCgrLUggyg/T4Dcd58YD2vXxWXump3GL2ykoOHFYVmgoh030F3jPVJEx5f+7rz5YcxRcRzxE9Cz7L126E6gsN3fifJX4t6VUHfubZd2/isNlE4vZRWQk9S3Ej/wB4k6fSlKy1yStlbXfnDrFnQ//AS+OhsKUyIklUhqpDQE31b21cBYCddndG5gxQD0Apigt6u9HDdBH8NJaPZ52JHB5CFN8h1bpTY3/J6sLVe/MmPLhXwAAAAAAAAAAAAAAAQAAAD5zc2gta2V5Z2VuIC1zIHRlc3QtcnNhLWNhIC1JICcnIHRlc3QtcnNhLXVzZXItYWxsLWRlZmF1bHRzLnB1YgAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAOI0JnxeJSdtUXtHu7x3MVVLdDN2iFCr8B8xCSGsdmJ+VdI2sBF0xc7l5/HZVyATgCBTt9dT7eG6zFCnQhPJUUB+uhUu9EeKwu3Pqw9mWnTXy/sa/R++yswFjU1Syxws+iBbiG9IG1BIqXY+eOuXaf+8kEFi877bjU4Nrl0U/KL2qpCJwhFh3vu5XBQ26ih/TcZVRaPyOTBVqOD0MNeVXs0JkVc/gd4mLHWgFvcIrTNmmKwR0HNSDPpYbLxol+3DbfaY9CE/sUetLA6OxO4EF38PQKPY+Ud2/yypc4uC/GT1VfEc4ALZVCZYC8Cut3hYb5ef3xdab7W4ikXIyU2vtPR/n1Ju/5nXVFX1Y0F5u0ShZkz8SI7/BLF2i4SIMiZhNTgqVj8mr2LpwSHB04m68d9GsPnD1mQQxlcfx7pbfOAjXmnTV3mQmn4TnW1KxKH0n7NJE1jvRy31n3Crs3aMJ6cuq6+gxbK4cg/X48Q16PnpuYzOiTtD5hEuu4RL4cg86Mi57DmchNwb1CsvxFJrueJB65J4efIDTqmuppDQVjkZOzH2URzRzGE2azdcQPgcV5aQQiZuiqeZ5xG9oEwB/2kNVu3hsM49ugJ9OBGHtlDoemC4YkQkJe0ejFJVc8uKOzGmXfjGcXKi21Aq5jGvEsDRb3DcuLEceOfXRpU/59OpAAACDwAAAAdzc2gtcnNhAAACANuCshq4p24CB7M+lRVyBSAQhkj0C/KQjM0RqtsLT1HaPGB3c401aus4s+UB0Igaf1FQ7XXSbPXza7xrvDXPSsUoa9ihcbtEGOXp/EoHvj6Ek9m3euoQJL2tujb8QMgwhQ19KOWteK6YxUC+vbdoBp5lWDnkpZ8WOPXAuzk6o2WlFnxL7LcjFewJcM5CQr5UWHST24j1jk5U4Zer4bZueKxCk0hamjGBRlY5MnxuYbuHmLz0gqTUgtYuA422iEvNrWPSHWUUBqkz29EUiq5MJG2GWb4h+svdN9lv8blL7BFwPd0sBAaJbR9A7voPs5ZK97YW1liYNljukeW6h4dMYNAhxCC+rYNxVI3+WSshlXZy5pF7pDb9Z9RkFW8vmpEAvlLrqJqKrTJDYZEX+0tSQvHsp/y2xwk871vkWTNA/T4LKVc3GwJboXkrZrJ+q5k3JU8o8JOIabRDfAj6DEy/xW1Lshqi3kciUhjEZYNEFyYiXB9AAz6RsUUoBDnRMjnK1XpsQ6gGsL9yX5hyUiN7g/1pHi+G+ZNj8ueV/e860YgEhxYcCzTI/Xrf3dpkbgjhVpcvk9VuKmMWtZ1Q1zfC2mh8/JSej5ewTZYRNBYAvqNG70u7ej5uuemgc7hTKzrrRFpYdcp1vTKe+K8vBCmQ7Audj/Wnw1fwBT5tAHRByIKd Test RSA User Key' RSA_USER_CERT_DEFAULTS_KEY_ID = 'ssh-keygen -s test-rsa-ca -I \'\' test-rsa-user-all-defaults.pub' # ssh-keygen -s test-rsa-ca -I "ssh-keygen -s test-rsa-ca -I '' test-rsa-user-all-defaults.pub" test-rsa-user-all-defaults.pub -RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT = 'ssh-rsa-cert-v01@openssh.com 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 Certificate type:1 principals:[] with the id:[ssh-keygen -s test-rsa-ca -I \'\' test-rsa-user-all-defaults.pub]' +RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT = 'ssh-rsa-cert-v01@openssh.com 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 Certificate type[1] principals[] with the id[ssh-keygen -s test-rsa-ca -I \'\' test-rsa-user-all-defaults.pub]' RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT_KEY_ID = 'ssh-keygen -s test-rsa-ca -I \'\' test-rsa-user-all-defaults.pub' # ssh-keygen -s test-rsa-ca -n user1,user2,other_user1,other_user2 -I "ssh-keygen -s test-rsa-ca -n user1,user2,other_user1,other_user2 -I '' test-rsa-user-many-principals.pub" test-rsa-user-many-principals.pub RSA_USER_CERT_MANY_PRINCIPALS = 'ssh-rsa-cert-v01@openssh.com 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 Test RSA User Key' From d2bee4589bfd421f933448586903b8330dbbe639 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 7 Jun 2017 19:49:39 -0700 Subject: [PATCH 40/68] Updating dependencies prior to release. --- requirements.txt | 20 ++++++++++++-------- setup.py | 3 ++- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 853340a..93f16c0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,21 +1,25 @@ -e . +asn1crypto==0.22.0 boto3==1.4.4 -botocore==1.5.17 -cffi==1.9.1 +botocore==1.5.63 +cffi==1.10.0 configparser==3.5.0 coverage==4.3.4 -cryptography==1.7.2 +cryptography==1.9 docutils==0.13.1 enum34==1.1.6 flake8==3.3.0 -futures==3.0.5 -idna==2.2 +funcsigs==1.0.2 +futures==3.1.1 +idna==2.5 ipaddress==1.0.18 -jmespath==0.9.1 +jmespath==0.9.3 kmsauth==0.2.0 -marshmallow==2.13.0 +marshmallow==2.13.5 mccabe==0.6.1 +mock==2.0.0 packaging==16.8 +pbr==3.0.1 py==1.4.32 pyasn1==0.2.2 pycodestyle==2.3.1 @@ -26,4 +30,4 @@ pytest==3.0.6 pytest-mock==1.6.0 python-dateutil==2.6.0 s3transfer==0.1.10 -six==1.10.0 \ No newline at end of file +six==1.10.0 diff --git a/setup.py b/setup.py index b28ca95..1999ec1 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,8 @@ 'coverage', 'flake8', 'pyflakes', - 'pytest' + 'pytest', + 'pytest-mock' ] } ) From fd1d8020b08acca4305051cef333204bd083ba2c Mon Sep 17 00:00:00 2001 From: Hugh Topping Date: Fri, 23 Jun 2017 10:12:46 +0100 Subject: [PATCH 41/68] Allows username validation against IAM groups This change gives the option to validate the remote username against the IAM groups containing the user invoking the lambda function. This is an optional feature which is used in conjunction with kmsauth. For example, if there were two groups of users, you could put your admins in the ssh-admin IAM group to allow them to generate certificates with a remote_username of 'admin'. Users with fewer permissions could be in the ssh-user group to allow them to generate certificates for the 'user' account. The group name is configurable, however they must all be in a consistent format, and must all contain the relevant remote_username once. --- .gitignore | 2 ++ bless/aws_lambda/bless_lambda.py | 25 ++++++++++++- bless/config/bless_config.py | 10 +++++- bless/config/bless_deploy_example.cfg | 12 ++++++- ...less-test-kmsauth-iam-group-validation.cfg | 35 +++++++++++++++++++ tests/aws_lambda/test_bless_lambda.py | 27 ++++++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 tests/aws_lambda/bless-test-kmsauth-iam-group-validation.cfg diff --git a/.gitignore b/.gitignore index 9777179..a0cf94a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ htmlcov/ libs/ publish/ venv/ +aws_lambda_libs/ +lambda_configs/ diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 2a9f94a..8831352 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -24,10 +24,12 @@ 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 + REMOTE_USERNAMES_VALIDATION_OPTION, \ + IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION from bless.request.bless_request import BlessSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority @@ -143,6 +145,27 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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.get(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') diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 89a5dd8..e77f394 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -58,6 +58,12 @@ REMOTE_USERNAMES_VALIDATION_OPTION = 'remote_usernames_validation' REMOTE_USERNAMES_VALIDATION_DEFAULT = 'principal' +VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION = 'kmsauth_validate_remote_usernames_against_iam_groups' +VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_DEFAULT = False + +IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION = 'kmsauth_iam_group_name_format' +IAM_GROUP_NAME_VALIDATION_FORMAT_DEFAULT = 'ssh-{}' + class BlessConfig(ConfigParser.RawConfigParser, object): def __init__(self, aws_region, config_file): @@ -84,7 +90,9 @@ def __init__(self, aws_region, config_file): KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_DEFAULT, CERTIFICATE_EXTENSIONS_OPTION: CERTIFICATE_EXTENSIONS_DEFAULT, USERNAME_VALIDATION_OPTION: USERNAME_VALIDATION_DEFAULT, - REMOTE_USERNAMES_VALIDATION_OPTION: REMOTE_USERNAMES_VALIDATION_DEFAULT + REMOTE_USERNAMES_VALIDATION_OPTION: REMOTE_USERNAMES_VALIDATION_DEFAULT, + VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION: VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_DEFAULT, + IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION: IAM_GROUP_NAME_VALIDATION_FORMAT_DEFAULT } ConfigParser.RawConfigParser.__init__(self, defaults=defaults) self.read(config_file) diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index 7d75356..c4767e2 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -45,4 +45,14 @@ ca_private_key_file = # By default, kmsauth requires that requested bastion_user must be the same as the requested remote_usernames. If you # want Bless to sign a certificate for a different remote_usernames (like root, or a shared admin account), you must # specify those allowed names here. * will allow signing for all remote_usernames -# kmsauth_remote_usernames_allowed = ubuntu,root,ec2-user,stufflikethat \ No newline at end of file +# kmsauth_remote_usernames_allowed = ubuntu,root,ec2-user,stufflikethat + +# If the kmsauth_remote_usernames_allowed option is set, kmsauth will allow certifiates for those usernames +# to be generated by any user who can invoke the lambda function. If you would like to ensure that users have to +# be in a an IAM group pertaining to the remote_username, enable this option. +# kmsauth_validate_remote_usernames_against_iam_groups = False + +# For use with the kmsauth_validate_remote_usernames_against_iam_groups option. By default the required format for +# the group name is "ssh-{}".format(remote_username), but that can be changed here. The groups must have a +# consistent naming scheme and must all contain the remote_username once. For example, ssh-ubuntu. +# kmsauth_iam_group_name_format = ssh-{} diff --git a/tests/aws_lambda/bless-test-kmsauth-iam-group-validation.cfg b/tests/aws_lambda/bless-test-kmsauth-iam-group-validation.cfg new file mode 100644 index 0000000..7f77cba --- /dev/null +++ b/tests/aws_lambda/bless-test-kmsauth-iam-group-validation.cfg @@ -0,0 +1,35 @@ +[Bless CA] +ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem +us-east-1_password = bogus-password-for-unit-test +us-west-2_password = bogus-password-for-unit-test + +[KMS Auth] +use_kmsauth = True +kmsauth_key_id = alias/authnz-iad, alias/authnz-sfo +kmsauth_serviceid = kmsauth-prod +kmsauth_remote_usernames_allowed = ubuntu,alloweduser +kmsauth_validate_remote_usernames_against_iam_groups = True +kmsauth_iam_group_name_format = ssh-{} + +# todo get from config, with some sane defaults +#[loggers] +#keys=root +# +#[handlers] +#keys=stream_handler +# +#[formatters] +#keys=formatter +# +#[logger_root] +#level=INFO +#handlers=stream_handler +# +#[handler_stream_handler] +#class=StreamHandler +#level=DEBUG +#formatter=formatter +#args=(sys.stderr,) +# +#[formatter_formatter] +#format=%(asctime)s %(name)-12s %(levelname)-8s %(message)s diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index e6816e8..7f46628 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -346,3 +346,30 @@ def test_valid_request_with_allowed_remote(mocker): config_file=os.path.join(os.path.dirname(__file__), 'bless-test-kmsauth-different-remote.cfg')) assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + +def test_valid_request_with_allowed_remote_and_allowed_iam_group(mocker): + mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") + clientmock = mocker.MagicMock() + clientmock.list_groups_for_user.return_value = {"Groups":[{"GroupName":"ssh-alloweduser"}]} + botomock = mocker.patch('boto3.client') + botomock.return_value = clientmock + output = lambda_handler(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, 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-kmsauth-iam-group-validation.cfg')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_invalid_request_with_allowed_remote_and_not_allowed_iam_group(mocker): + mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") + clientmock = mocker.MagicMock() + clientmock.list_groups_for_user.return_value = {"Groups": [{"GroupName": "ssh-notalloweduser"}]} + botomock = mocker.patch('boto3.client') + botomock.return_value = clientmock + output = lambda_handler(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, 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-kmsauth-iam-group-validation.cfg')) + assert output['errorType'] == 'KMSAuthValidationError' \ No newline at end of file From 3f37e179a5012ab3e288581188890d23c96a16d9 Mon Sep 17 00:00:00 2001 From: Vasilyev Viacheslav Date: Wed, 29 Nov 2017 18:44:20 +0200 Subject: [PATCH 42/68] Compressed CA private key support --- bless/config/bless_config.py | 33 ++++++- tests/aws_lambda/only-use-for-unit-tests.zlib | Bin 0 -> 2551 bytes tests/aws_lambda/test_bless_lambda.py | 58 ++++++++++++ tests/config/full-zlib.cfg | 14 +++ tests/config/full.cfg | 1 + tests/config/test_bless_config.py | 87 ++++++++++++++++-- 6 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 tests/aws_lambda/only-use-for-unit-tests.zlib create mode 100644 tests/config/full-zlib.cfg diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 89a5dd8..6e59b5d 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -7,6 +7,7 @@ import base64 import os import re +import zlib BLESS_OPTIONS_SECTION = 'Bless Options' CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'certificate_validity_before_seconds' @@ -36,6 +37,8 @@ BLESS_CA_SECTION = 'Bless CA' CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' CA_PRIVATE_KEY_OPTION = 'ca_private_key' +CA_PRIVATE_KEY_COMPRESSION_OPTION = 'ca_private_key_compression' +CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT = None REGION_PASSWORD_OPTION_SUFFIX = '_password' @@ -84,7 +87,8 @@ def __init__(self, aws_region, config_file): KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_DEFAULT, CERTIFICATE_EXTENSIONS_OPTION: CERTIFICATE_EXTENSIONS_DEFAULT, USERNAME_VALIDATION_OPTION: USERNAME_VALIDATION_DEFAULT, - REMOTE_USERNAMES_VALIDATION_OPTION: REMOTE_USERNAMES_VALIDATION_DEFAULT + REMOTE_USERNAMES_VALIDATION_OPTION: REMOTE_USERNAMES_VALIDATION_DEFAULT, + CA_PRIVATE_KEY_COMPRESSION_OPTION: CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT } ConfigParser.RawConfigParser.__init__(self, defaults=defaults) self.read(config_file) @@ -117,14 +121,23 @@ def getkmsauthkeyids(self): return map(str.strip, self.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION).split(',')) def getprivatekey(self): + compression = CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT + + if self.has_option(BLESS_CA_SECTION, CA_PRIVATE_KEY_COMPRESSION_OPTION): + compression = self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_COMPRESSION_OPTION) + compression = CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT if type(compression) == str and compression.lower() == "none" else compression + + if compression not in [CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT, 'zlib']: + raise ValueError("Compression {} for private key is not supported.".format(compression)) + if self.has_option(BLESS_CA_SECTION, CA_PRIVATE_KEY_OPTION): - return base64.b64decode(self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_OPTION)) + return self._decompress(base64.b64decode(self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_OPTION)), compression) ca_private_key_file = self.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) # read the private key .pem with open(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, ca_private_key_file), 'r') as f: - return f.read() + return self._decompress(f.read(), compression) def has_option(self, section, option): """ @@ -159,3 +172,17 @@ def get(self, section, option): @staticmethod def _environment_key(section, option): return (re.sub('\W+', '_', section) + '_' + re.sub('\W+', '_', option)).lower() + + @staticmethod + def _decompress(data, algorithm): + if algorithm is CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT: + return data + elif algorithm is 'zlib': + result = '' + try: + result = zlib.decompress(data) + except zlib.error: + raise ValueError("Wrong compression {} for private key.".format(algorithm)) + return result + else: + raise ValueError("Compression {} for private key is not supported.".format(algorithm)) diff --git a/tests/aws_lambda/only-use-for-unit-tests.zlib b/tests/aws_lambda/only-use-for-unit-tests.zlib new file mode 100644 index 0000000000000000000000000000000000000000..db65cf14e05d048848d5b1f714a201b5ca8f16c4 GIT binary patch literal 2551 zcmV2r_5bh~h}!wg+^m|=&Xe*T$y+*-0M>s!nAzyJOP zC(5e%kL$7j*sdC~zW9$OCjXoG7qGWE|JQ$(^1uJ!{}D~zO>AEj072CLRi|qH??0^Q z{fnUVzdXnPhbJ%`VF-#wB~%b7oFh?$mLv%mFo_~45&(X~cz#BeT}^pO&?S!hlQ=!^OxYXp;Qfle} za%4A+Izs=!PT3iX0pT2nwX7yrqV!=SwV7KG(3+n{iA`eY`K0b_jzp66Iu8?QP7AP)q>6JCk0bWEHnDb8TeFCMYxtRX-wO^gP zq43xV-{X3HP~H+4#R(JUBi5Xkg3va`e?6GDwwF%R{Egd>Iha9IS&Vvw( zI)!P%=gx0Z6b$uN%O{>)k)V20`9i(MB1ronJ$Yd!^P`xVSJ=*&>)Ft| z@#Ww40_8iL00%(6d(>RwL-}^98+;q+6XWUE927LWtp3mE8}$rwyr?~! z=OrC!c9Nlk3a8Vj1I6+HOrC13Y7!-`q>?z@a!}9;R2Xc=?o4%>%(2kxt~H;$TQ@=?Q&cY9tk01H3>GgPa{7L6P8=sJDlb9q)`pskzfewkvx$QmlBd=5 zno~{-2#qSccYrIW%)UW4&~Yu_&k5*M*;60uP2N6a zV48iJ;ZF+@I1|&yi=XaR=6e&xzVt=Q(^c9+LhcjI{qt8(*V8WA`>d*O`}e(42*o9p zN6aGw+I$iho9oSyaPmVK-7qT5(-BZDIAvME)*;E@%#*g_J=J_9TLZIi>&)|Pz*Sy zKk-p&6l_xQNF4Q?4bK?bKmE7}&}0uWNT?GI;A{iv;|8G8JwSW1_N(%pE7H5W`}j+smB6$^`^(R&burO|Q+=U;hnq2xVrUI=-Hhwpeu>EE z^|VdF7-GVLp++g4w@**ltF^mCXiqwlc+)Lwpv-xBpbMn*5A^RgoTyW&V=TKNd?`yo zqIku@oS439-x6JNakd5yS;912V0u=`I)W(&F|cWn#}{qGCpnzV2P-5IZ{rWrm3Bt2 z%v9;_TU%#Ibbs;t5HV^H*|Q+2OdfSWVM~G{LP3oSo({T9^0euGTUMX3L43^%6Pg^2 z);VRwL@9p|9vizxZ(>PvALl4}U_^@JXU=Q4k6H192+OSK(Y?xLVHq3BOZ_} z!?d?Lca~lk;X;wr&~l%Q6M$rd2pK1_CFl~9!9d}D^wDsZrT>{@H7Q4}ioHjOCiY!$ zN`%ZH&7W2p&V>lvCB%WRFt_$>-s{rfhfCDgLXS}EIp8!NA5$JQw`^k}Slf%*-)_|I z(LW*vX)Bcc`H-*yC6&%mw0^=(=k(i(64&=+6Zx%qK)o_=fsStXC;kmSEj26;#|?GB z8Ut*{$X-*+1lW$`c)J;Yqg!fgML3<~nY_G>-;4P3?gL$e=VJyDS zyukNM2q)l%xVkeBV=1?nyXf~%?ZY4tLgn6PaAnDF;<=Wow~>g4rMKvCG?&`E?dJVE zX>IZgz)4R~e=Ot1P96=*Gts)_J8U!jS|2_XtvzB$v##>gVHz&N!MWJVN_&xo)bcE@y)H4e9;QEJTH|F+QYl9xo)nO`C@PLJt6^ z9n@NJ!efdQS%e*0`#FmK`yH@yNH-=azJ1ah$uf$&3ysEnM2T?tGTXZ@!p0Ere_xDf N3jhCi^FPmkK^66%2=@R0 literal 0 HcmV?d00001 diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index e6816e8..e631374 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -206,6 +206,64 @@ def test_basic_local_username_validation_email_remote_usernames_useradd(monkeypa assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') +def test_basic_local_username_validation_email_remote_usernames_useradd_zlib_zlib(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.zlib', + 'bless_ca_ca_private_key_compression': 'zlib', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + +def test_basic_local_username_validation_email_remote_usernames_useradd_zlib_none(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.zlib', + 'bless_ca_ca_private_key_compression': 'none', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + with pytest.raises(TypeError) as e: + lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + + assert 'Unsupported CA Private Key Type' == e.value.message + +def test_basic_local_username_validation_email_remote_usernames_useradd_none_zlib(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_file': 'tests/aws_lambda/only-use-for-unit-tests.pem', + 'bless_ca_ca_private_key_compression': 'zlib', + 'bless_options_username_validation': 'email', + 'bless_options_remote_usernames_validation': 'useradd', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + with pytest.raises(ValueError) as e: + lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) + + assert 'Wrong compression zlib for private key.' == e.value.message + def test_invalid_username_request(): output = lambda_handler(INVALID_TEST_REQUEST_USERNAME_INVALID, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, diff --git a/tests/config/full-zlib.cfg b/tests/config/full-zlib.cfg new file mode 100644 index 0000000..68c96cc --- /dev/null +++ b/tests/config/full-zlib.cfg @@ -0,0 +1,14 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_after_seconds = 1 +certificate_validity_before_seconds = 1 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +logging_level = DEBUG +username_validation = debian + +[Bless CA] +us-east-1_password = +us-west-2_password = +ca_private_key = +ca_private_key_compression = zlib diff --git a/tests/config/full.cfg b/tests/config/full.cfg index fa35f43..b3d9f7f 100644 --- a/tests/config/full.cfg +++ b/tests/config/full.cfg @@ -11,3 +11,4 @@ username_validation = debian us-east-1_password = us-west-2_password = ca_private_key_file = +ca_private_key_compression = zlib diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index a7113c8..3f6775b 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -1,6 +1,6 @@ import base64 import os - +import zlib import pytest from bless.config.bless_config import BlessConfig, \ @@ -23,7 +23,9 @@ CERTIFICATE_EXTENSIONS_OPTION, \ USERNAME_VALIDATION_OPTION, \ USERNAME_VALIDATION_DEFAULT, \ - REMOTE_USERNAMES_VALIDATION_OPTION + REMOTE_USERNAMES_VALIDATION_OPTION, \ + CA_PRIVATE_KEY_COMPRESSION_OPTION, \ + CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT def test_empty_config(): @@ -41,6 +43,70 @@ def test_config_no_password(): config_file=os.path.join(os.path.dirname(__file__), 'full-with-default.cfg')) assert '' == config.getpassword() +def test_wrong_compression(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_compression': 'lzh', + } + + 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 for private key is not supported." == e.value.message + +def test_none_compression(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_compression': 'none', + 'bless_ca_ca_private_key': base64.b64encode(''), + } + + 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 '' == config.getprivatekey() + +def test_zlib_positive_compression(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_compression': 'zlib', + 'bless_ca_ca_private_key': base64.b64encode(zlib.compress('')), + } + + 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 '' == config.getprivatekey() + +def test_zlib_negative_compression(monkeypatch): + extra_environment_variables = { + 'bless_ca_default_password': '', + 'bless_ca_ca_private_key_compression': 'zlib', + 'bless_ca_ca_private_key': base64.b64encode(''), + } + + 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 "Wrong compression zlib for private key." == e.value.message def test_config_environment_override(monkeypatch): extra_environment_variables = { @@ -92,26 +158,29 @@ def test_config_environment_override(monkeypatch): @pytest.mark.parametrize( "config,region,expected_cert_valid,expected_entropy_min,expected_rand_seed,expected_log_level," - "expected_password,expected_username_validation", [ + "expected_password,expected_username_validation,expected_key_compression", [ ((os.path.join(os.path.dirname(__file__), 'minimal.cfg')), 'us-west-2', CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, LOGGING_LEVEL_DEFAULT, '', - USERNAME_VALIDATION_DEFAULT + USERNAME_VALIDATION_DEFAULT, + CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT ), - ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-west-2', + ((os.path.join(os.path.dirname(__file__), 'full-zlib.cfg')), 'us-west-2', 1, 2, 3, 'DEBUG', '', - 'debian' + 'debian', + 'zlib' ), ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-east-1', 1, 2, 3, 'DEBUG', '', - 'debian' + 'debian', + 'zlib' ) ]) def test_configs(config, region, expected_cert_valid, expected_entropy_min, expected_rand_seed, - expected_log_level, expected_password, expected_username_validation): + 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) @@ -126,3 +195,5 @@ def test_configs(config, region, expected_cert_valid, expected_entropy_min, expe 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) From ed5466869a47d882deb6f1bbab5ad973143305e5 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Fri, 13 Jul 2018 15:19:40 -0700 Subject: [PATCH 43/68] Fixing https://github.com/Netflix/bless/issues/72 thanks @Immortalin and @tuxinaut . --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1fabaf2..54ab4f7 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,6 @@ compile: lambda-deps: @echo "--> Compiling lambda dependencies" - docker run --rm -it -v ${CURDIR}:/src -w /src amazonlinux make compile + docker run --rm -it -v ${CURDIR}:/src -w /src amazonlinux:1 make compile .PHONY: develop dev-docs clean test lint coverage publish From cdde67a01a4737634e49d09003c399a0e5f71607 Mon Sep 17 00:00:00 2001 From: John Newbigin Date: Sun, 10 Jun 2018 19:37:22 +1000 Subject: [PATCH 44/68] Add support for loading ED25519 public keys --- bless/ssh/public_keys/ed25519_public_key.py | 63 +++++++++++++++++++ .../ssh/public_keys/ssh_public_key_factory.py | 4 ++ tests/ssh/test_ssh_public_key_ed25519.py | 29 +++++++++ tests/ssh/test_ssh_public_key_factory.py | 11 ++-- tests/ssh/vectors.py | 7 ++- 5 files changed, 109 insertions(+), 5 deletions(-) create mode 100644 bless/ssh/public_keys/ed25519_public_key.py create mode 100644 tests/ssh/test_ssh_public_key_ed25519.py diff --git a/bless/ssh/public_keys/ed25519_public_key.py b/bless/ssh/public_keys/ed25519_public_key.py new file mode 100644 index 0000000..9ca569a --- /dev/null +++ b/bless/ssh/public_keys/ed25519_public_key.py @@ -0,0 +1,63 @@ +""" +.. module: bless.ssh.public_keys.ed25519_public_key + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import base64 +import hashlib + +from bless.ssh.public_keys.ssh_public_key import SSHPublicKey, SSHPublicKeyType +from cryptography.hazmat.primitives import serialization + + +class ED25519PublicKey(SSHPublicKey): + def __init__(self, ssh_public_key): + """ + Extracts the useful ED25519 Public Key information from an SSH Public Key file. + :param ssh_public_key: SSH Public Key file contents. (i.e. 'ssh-ed25519 AAAAB3NzaC1yc2E..'). + """ + super(ED25519PublicKey, self).__init__() + + self.type = SSHPublicKeyType.ED25519 + + split_ssh_public_key = ssh_public_key.split(' ') + split_key_len = len(split_ssh_public_key) + + # is there a key comment at the end? + if split_key_len > 2: + self.key_comment = ' '.join(split_ssh_public_key[2:]) + else: + self.key_comment = '' + + # hazmat does not support ed25519 so we have out own loader based on serialization.load_ssh_public_key + + if split_key_len < 2: + raise ValueError( + 'Key is not in the proper format or contains extra data.') + + key_type = split_ssh_public_key[0] + key_body = split_ssh_public_key[1] + + if key_type != SSHPublicKeyType.ED25519: + raise TypeError("Public Key is not the correct type or format") + + try: + decoded_data = base64.b64decode(key_body) + except TypeError: + raise ValueError('Key is not in the proper format.') + + inner_key_type, rest = serialization._ssh_read_next_string(decoded_data) + + if inner_key_type != key_type: + raise ValueError( + 'Key header and key body contain different key type values.' + ) + + # ed25519 public key is a single string https://tools.ietf.org/html/rfc8032#section-5.1.5 + self.a, rest = serialization._ssh_read_next_string(rest) + + key_bytes = base64.b64decode(split_ssh_public_key[1]) + fingerprint = hashlib.md5(key_bytes).hexdigest() + + self.fingerprint = 'ED25519 ' + ':'.join( + fingerprint[i:i + 2] for i in range(0, len(fingerprint), 2)) diff --git a/bless/ssh/public_keys/ssh_public_key_factory.py b/bless/ssh/public_keys/ssh_public_key_factory.py index 066c4bb..e9837e7 100644 --- a/bless/ssh/public_keys/ssh_public_key_factory.py +++ b/bless/ssh/public_keys/ssh_public_key_factory.py @@ -4,6 +4,7 @@ :license: Apache, see LICENSE for more details. """ from bless.ssh.public_keys.rsa_public_key import RSAPublicKey +from bless.ssh.public_keys.ed25519_public_key import ED25519PublicKey from bless.ssh.public_keys.ssh_public_key import SSHPublicKeyType @@ -17,5 +18,8 @@ def get_ssh_public_key(ssh_public_key): rsa_public_key = RSAPublicKey(ssh_public_key) rsa_public_key.validate_for_signing() return rsa_public_key + elif ssh_public_key.startswith(SSHPublicKeyType.ED25519): + ed25519_public_key = ED25519PublicKey(ssh_public_key) + return ed25519_public_key else: raise TypeError("Unsupported Public Key Type") diff --git a/tests/ssh/test_ssh_public_key_ed25519.py b/tests/ssh/test_ssh_public_key_ed25519.py new file mode 100644 index 0000000..4a4e5d7 --- /dev/null +++ b/tests/ssh/test_ssh_public_key_ed25519.py @@ -0,0 +1,29 @@ +import pytest + +from bless.ssh.public_keys.ed25519_public_key import ED25519PublicKey +from tests.ssh.vectors import EXAMPLE_ED25519_PUBLIC_KEY, EXAMPLE_ED25519_PUBLIC_KEY_A, \ + EXAMPLE_ECDSA_PUBLIC_KEY, \ + EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION + + +def test_valid_key(): + pub_key = ED25519PublicKey(EXAMPLE_ED25519_PUBLIC_KEY) + assert 'Test ED25519 User Key' == pub_key.key_comment + assert EXAMPLE_ED25519_PUBLIC_KEY_A == pub_key.a + assert 'ED25519 fb:80:ca:21:7d:c8:9d:38:35:c0:f6:ba:fb:6d:82:e8' == pub_key.fingerprint + + +def test_valid_key_no_description(): + pub_key = ED25519PublicKey(EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION) + assert '' == pub_key.key_comment + assert EXAMPLE_ED25519_PUBLIC_KEY_A == pub_key.a + assert 'ED25519 fb:80:ca:21:7d:c8:9d:38:35:c0:f6:ba:fb:6d:82:e8' == pub_key.fingerprint + + +def test_invalid_keys(): + with pytest.raises(TypeError): + ED25519PublicKey(EXAMPLE_ECDSA_PUBLIC_KEY) + + with pytest.raises(ValueError): + ED25519PublicKey('bogus') + diff --git a/tests/ssh/test_ssh_public_key_factory.py b/tests/ssh/test_ssh_public_key_factory.py index 3485bbb..8175ef8 100644 --- a/tests/ssh/test_ssh_public_key_factory.py +++ b/tests/ssh/test_ssh_public_key_factory.py @@ -2,7 +2,8 @@ from bless.ssh.public_keys.ssh_public_key_factory import get_ssh_public_key from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, EXAMPLE_ED25519_PUBLIC_KEY, \ - EXAMPLE_ECDSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_N, EXAMPLE_RSA_PUBLIC_KEY_E + EXAMPLE_ECDSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_N, EXAMPLE_RSA_PUBLIC_KEY_E, \ + EXAMPLE_ED25519_PUBLIC_KEY_A def test_valid_rsa(): @@ -13,9 +14,11 @@ def test_valid_rsa(): assert 'RSA 57:3d:48:4c:65:90:30:8e:39:ba:d8:fa:d0:20:2e:6c' == pub_key.fingerprint -def test_unsupported_ed_25519(): - with pytest.raises(TypeError): - get_ssh_public_key(EXAMPLE_ED25519_PUBLIC_KEY) +def test_valid_ed25519(): + pub_key = get_ssh_public_key(EXAMPLE_ED25519_PUBLIC_KEY) + assert 'Test ED25519 User Key' == pub_key.key_comment + assert EXAMPLE_ED25519_PUBLIC_KEY_A == pub_key.a + assert 'ED25519 fb:80:ca:21:7d:c8:9d:38:35:c0:f6:ba:fb:6d:82:e8' == pub_key.fingerprint def test_invalid_key(): diff --git a/tests/ssh/vectors.py b/tests/ssh/vectors.py index a81be0d..35c3e93 100644 --- a/tests/ssh/vectors.py +++ b/tests/ssh/vectors.py @@ -14,6 +14,11 @@ EXAMPLE_ED25519_PUBLIC_KEY = u'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp Test ED25519 User Key' +EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION = u'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp' + +EXAMPLE_ED25519_PUBLIC_KEY_A = base64.b64decode( + 'bv5wBtPgQVI+zzuhLiQNhK1DQovTHAdapVJnZtgcgCk=') + EXAMPLE_ECDSA_PUBLIC_KEY = u'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMHnC16D7nbmdS7GtNsIoiRaG8yz1QLzv0IAKfAJ+NsnIxbQSvVa+/wWYmGgkIblPdK3ZrtbzZje61Xq08iDUyE= Test ECDSA User Key' EXAMPLE_RSA_PUBLIC_KEY_2048 = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9D7rmQAyeSrvKGS1mmBHY0uYjePaorbZHyL7HD2SLYROiuQpdZnNDHCdKZB5fWgKVdszYgZAdQmBOolLS+cRpowv8GXKot4QHi1EaKOb74PEFvLQpxCqlHPqYCArakUamwcj81m9dJeK7VpLLxJno092lC2r50RDVZfi/2X4GM8vkptASJUuLzMLdsrC1MmYnPeJ2vNLUtV3EQ3YX+XVaebB/N6ee/yhwb9JmlXqABTfUrX2L94fU/Ry+4ljHRwLb7DNnM4LfeT0pMviH9wa0TeqIA/A5xLEp83mjyO4+QBDreQDIaBCZSVLF32iH+FMlPeRKZd6oYq80j+iLUH33' @@ -45,4 +50,4 @@ 'AAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==') SSH_CERT_CUSTOM_EXTENSIONS = base64.b64decode( - 'AAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==') \ No newline at end of file + 'AAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==') From f1e2a30cd5fcf814bdb6875e0f8873cfe53ea3e7 Mon Sep 17 00:00:00 2001 From: John Newbigin Date: Sun, 10 Jun 2018 22:02:41 +1000 Subject: [PATCH 45/68] Add certificate builder and test ED25519 signed by RSA --- .../ed25519_certificate_builder.py | 39 +++++++++++++++++++ .../ssh_certificate_builder_factory.py | 4 ++ .../test_ssh_certificate_builder_factory.py | 11 ++++-- tests/ssh/test_ssh_certificate_rsa.py | 17 +++++++- tests/ssh/vectors.py | 4 ++ 5 files changed, 70 insertions(+), 5 deletions(-) create mode 100644 bless/ssh/certificates/ed25519_certificate_builder.py diff --git a/bless/ssh/certificates/ed25519_certificate_builder.py b/bless/ssh/certificates/ed25519_certificate_builder.py new file mode 100644 index 0000000..8293980 --- /dev/null +++ b/bless/ssh/certificates/ed25519_certificate_builder.py @@ -0,0 +1,39 @@ +""" +.. module: bless.ssh.certificates.ed25519_certificate_builder + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from bless.ssh.certificates.ssh_certificate_builder import \ + SSHCertificateBuilder, SSHCertifiedKeyType +from bless.ssh.protocol.ssh_protocol import pack_ssh_string + + +class ED25519CertificateBuilder(SSHCertificateBuilder): + def __init__(self, ca, cert_type, ssh_public_key_ed25519): + """ + Produces an SSH certificate for ED25519 public keys. + :param ca: The SSHCertificateAuthority that will sign the certificate. The + SSHCertificateAuthority type does not need to be the same type as the + SSHCertificateBuilder. + :param cert_type: The SSHCertificateType. Is this a User or Host certificate? Some of + the SSH Certificate fields do not apply or have a slightly different meaning depending on + the certificate type. + See http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys + :param ssh_public_key_ed25519: The ED25519PublicKey to issue a certificate for. + """ + super(ED25519CertificateBuilder, self).__init__(ca, cert_type) + self.cert_key_type = SSHCertifiedKeyType.ED25519 + self.ssh_public_key = ssh_public_key_ed25519 + self.public_key_comment = ssh_public_key_ed25519.key_comment + self.a = ssh_public_key_ed25519.a + + def _serialize_ssh_public_key(self): + """ + Serialize the Public Key into a string. This is not specified in + http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys + but https://tools.ietf.org/id/draft-ietf-curdle-ssh-ed25519-02.html + :return: The bytes that belong in the SSH Certificate between the nonce and the + certificate serial number. + """ + public_key = pack_ssh_string(self.a) + return public_key diff --git a/bless/ssh/certificates/ssh_certificate_builder_factory.py b/bless/ssh/certificates/ssh_certificate_builder_factory.py index e26d7b7..802c210 100644 --- a/bless/ssh/certificates/ssh_certificate_builder_factory.py +++ b/bless/ssh/certificates/ssh_certificate_builder_factory.py @@ -5,6 +5,8 @@ """ from bless.ssh.certificates.rsa_certificate_builder \ import RSACertificateBuilder +from bless.ssh.certificates.ed25519_certificate_builder \ + import ED25519CertificateBuilder from bless.ssh.public_keys.ssh_public_key import SSHPublicKeyType from bless.ssh.public_keys.ssh_public_key_factory import get_ssh_public_key @@ -23,5 +25,7 @@ def get_ssh_certificate_builder(ca, cert_type, public_key_to_sign): if ssh_public_key.type is SSHPublicKeyType.RSA: return RSACertificateBuilder(ca, cert_type, ssh_public_key) + elif ssh_public_key.type is SSHPublicKeyType.ED25519: + return ED25519CertificateBuilder(ca, cert_type, ssh_public_key) else: raise TypeError("Unsupported Public Key Type") diff --git a/tests/ssh/test_ssh_certificate_builder_factory.py b/tests/ssh/test_ssh_certificate_builder_factory.py index a9ce14b..e35c9f8 100644 --- a/tests/ssh/test_ssh_certificate_builder_factory.py +++ b/tests/ssh/test_ssh_certificate_builder_factory.py @@ -4,6 +4,7 @@ get_ssh_certificate_authority from bless.ssh.certificates.rsa_certificate_builder import RSACertificateBuilder, \ SSHCertifiedKeyType +from bless.ssh.certificates.ed25519_certificate_builder import ED25519CertificateBuilder from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder from tests.ssh.vectors import RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ @@ -18,10 +19,12 @@ def test_valid_rsa_request(): assert cert.startswith(SSHCertifiedKeyType.RSA) -def test_invalid_ed25519_request(): - with pytest.raises(TypeError): - ca = get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD) - get_ssh_certificate_builder(ca, SSHCertificateType.USER, EXAMPLE_ED25519_PUBLIC_KEY) +def test_valid_ed25519_request(): + ca = get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD) + cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.USER, EXAMPLE_ED25519_PUBLIC_KEY) + cert = cert_builder.get_cert_file() + assert isinstance(cert_builder, ED25519CertificateBuilder) + assert cert.startswith(SSHCertifiedKeyType.ED25519) def test_invalid_key_request(): diff --git a/tests/ssh/test_ssh_certificate_rsa.py b/tests/ssh/test_ssh_certificate_rsa.py index 6c2d439..eae09d1 100644 --- a/tests/ssh/test_ssh_certificate_rsa.py +++ b/tests/ssh/test_ssh_certificate_rsa.py @@ -5,8 +5,10 @@ from bless.ssh.certificate_authorities.rsa_certificate_authority import RSACertificateAuthority from bless.ssh.certificates.rsa_certificate_builder import RSACertificateBuilder +from bless.ssh.certificates.ed25519_certificate_builder import ED25519CertificateBuilder from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType from bless.ssh.public_keys.rsa_public_key import RSAPublicKey +from bless.ssh.public_keys.ed25519_public_key import ED25519PublicKey from tests.ssh.vectors import RSA_CA_PRIVATE_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ EXAMPLE_RSA_PUBLIC_KEY, EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION, RSA_USER_CERT_MINIMAL, \ RSA_USER_CERT_DEFAULTS, RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT, \ @@ -14,7 +16,8 @@ RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS, \ RSA_USER_CERT_FORCE_COMMAND_AND_SOURCE_ADDRESS_KEY_ID, RSA_HOST_CERT_MANY_PRINCIPALS_KEY_ID, \ RSA_USER_CERT_MANY_PRINCIPALS_KEY_ID, RSA_USER_CERT_DEFAULTS_NO_PUBLIC_KEY_COMMENT_KEY_ID, \ - RSA_USER_CERT_DEFAULTS_KEY_ID, SSH_CERT_DEFAULT_EXTENSIONS, SSH_CERT_CUSTOM_EXTENSIONS + RSA_USER_CERT_DEFAULTS_KEY_ID, SSH_CERT_DEFAULT_EXTENSIONS, SSH_CERT_CUSTOM_EXTENSIONS, \ + EXAMPLE_ED25519_PUBLIC_KEY, ED25519_USER_CERT_DEFAULTS, ED25519_USER_CERT_DEFAULTS_KEY_ID USER1 = 'user1' @@ -219,3 +222,15 @@ def test_nonce(): cert_builder2.set_nonce() assert cert_builder.nonce != cert_builder2.nonce + + +def test_ed25519_user_cert_defaults(): + ca = get_basic_rsa_ca() + pub_key = ED25519PublicKey(EXAMPLE_ED25519_PUBLIC_KEY) + cert_builder = ED25519CertificateBuilder(ca, SSHCertificateType.USER, pub_key) + cert_builder.set_nonce( + nonce=extract_nonce_from_cert(ED25519_USER_CERT_DEFAULTS)) + cert_builder.set_key_id(ED25519_USER_CERT_DEFAULTS_KEY_ID) + + cert = cert_builder.get_cert_file() + assert ED25519_USER_CERT_DEFAULTS == cert diff --git a/tests/ssh/vectors.py b/tests/ssh/vectors.py index 35c3e93..b87a16c 100644 --- a/tests/ssh/vectors.py +++ b/tests/ssh/vectors.py @@ -51,3 +51,7 @@ SSH_CERT_CUSTOM_EXTENSIONS = base64.b64decode( 'AAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAA==') + +# ssh-keygen -s test-rsa-ca -I "ssh-keygen -s test-rsa-ca -I '' test-ed25519-user-all-defaults.pub" test-ed25519-user-all-defaults.pub +ED25519_USER_CERT_DEFAULTS = 'ssh-ed25519-cert-v01@openssh.com AAAAIHNzaC1lZDI1NTE5LWNlcnQtdjAxQG9wZW5zc2guY29tAAAAINa9ZmyR9YBRNfC464IJE2AlDa0xU02tVbY37AlRr79/AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIApAAAAAAAAAAAAAAABAAAAQnNzaC1rZXlnZW4gLXMgdGVzdC1yc2EtY2EgLUkgJycgdGVzdC1lZDI1NTE5LXVzZXItYWxsLWRlZmF1bHRzLnB1YgAAAAAAAAAAAAAAAP//////////AAAAAAAAAIIAAAAVcGVybWl0LVgxMS1mb3J3YXJkaW5nAAAAAAAAABdwZXJtaXQtYWdlbnQtZm9yd2FyZGluZwAAAAAAAAAWcGVybWl0LXBvcnQtZm9yd2FyZGluZwAAAAAAAAAKcGVybWl0LXB0eQAAAAAAAAAOcGVybWl0LXVzZXItcmMAAAAAAAAAAAAAAhcAAAAHc3NoLXJzYQAAAAMBAAEAAAIBAOI0JnxeJSdtUXtHu7x3MVVLdDN2iFCr8B8xCSGsdmJ+VdI2sBF0xc7l5/HZVyATgCBTt9dT7eG6zFCnQhPJUUB+uhUu9EeKwu3Pqw9mWnTXy/sa/R++yswFjU1Syxws+iBbiG9IG1BIqXY+eOuXaf+8kEFi877bjU4Nrl0U/KL2qpCJwhFh3vu5XBQ26ih/TcZVRaPyOTBVqOD0MNeVXs0JkVc/gd4mLHWgFvcIrTNmmKwR0HNSDPpYbLxol+3DbfaY9CE/sUetLA6OxO4EF38PQKPY+Ud2/yypc4uC/GT1VfEc4ALZVCZYC8Cut3hYb5ef3xdab7W4ikXIyU2vtPR/n1Ju/5nXVFX1Y0F5u0ShZkz8SI7/BLF2i4SIMiZhNTgqVj8mr2LpwSHB04m68d9GsPnD1mQQxlcfx7pbfOAjXmnTV3mQmn4TnW1KxKH0n7NJE1jvRy31n3Crs3aMJ6cuq6+gxbK4cg/X48Q16PnpuYzOiTtD5hEuu4RL4cg86Mi57DmchNwb1CsvxFJrueJB65J4efIDTqmuppDQVjkZOzH2URzRzGE2azdcQPgcV5aQQiZuiqeZ5xG9oEwB/2kNVu3hsM49ugJ9OBGHtlDoemC4YkQkJe0ejFJVc8uKOzGmXfjGcXKi21Aq5jGvEsDRb3DcuLEceOfXRpU/59OpAAACDwAAAAdzc2gtcnNhAAACACnir9y/PhoKvLNvs4fMkS24mrHyGxc24nTxaKJO9OBGr4PY3Glg90hKaZvC/cLN0wSAeIYzigp/PGm1PNIM342NqxMkIx7yUKrEbcIQaommn8kfThMIcLJo9QOpDoMAxOsfAtZrtWQKMMYM6s8hZKrb3OT8k0le/x4S2GttHMb9z006nZgyTUI4cniq+ZxwcL4l5/wzaXamQUsod/JRysEBZdLF88A50iCiSC+4NQUqquPBs76UmTpU+Lv3OEvVBwMil7Hxkzv8kqit2Xal0Ou9n/+CC1G9l/dpTO1TgaNtNERQT3rQOhkjqwmgN3wbh1sRkc1+zSenWsNrHkFBnVTJPxLwXbFeAvstDWDxTvNCpygsTfz/ejUnKfqZ1rAWfNRhzrSIz1D1+/GQbOdtM3xq3r6CVjVxE0KTLsdR1hyOd5SmszubE1UAkKWF7NRyHcgma/9hkOXc6a/4ylBcOj0yUFnjVq7Jb6C33ba0Ra6LnopZWUS7lr02dt7aYG/Qhd8OtJx7R+XiRYRnfsuJH+L18UxM34xqj9qlMPA5p1nUB2ZnklKyueLhrp0/thuWsdKCv4w66A8rOthbDtLip6TYtKwDDMBupq5ROoRHYXb6nYthHJYCX1QuDIzmoBjTlkWa7aVohggQWezYhGGo0owulURxkFNZBNi1Lc/aRuDs Test ED25519 User Key' +ED25519_USER_CERT_DEFAULTS_KEY_ID = 'ssh-keygen -s test-rsa-ca -I \'\' test-ed25519-user-all-defaults.pub' From ba550217eee4dc5fa7822340529e6ed2c9faadc3 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Fri, 13 Jul 2018 17:37:00 -0700 Subject: [PATCH 46/68] =?UTF-8?q?Allowing=20BLESS=20lambda=20to=20accept?= =?UTF-8?q?=20ed25519=20keys,=20completing=20https://gith=E2=80=A6=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allowing BLESS lambda to accept ed25519 keys, completing https://github.com/Netflix/bless/pull/71 . Thanks @jnewbigin . --- bless/request/bless_request.py | 3 ++- tests/aws_lambda/test_bless_lambda.py | 21 +++++++++++++++++++-- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index ad182a0..7712562 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -29,6 +29,7 @@ # There doesn't seem to be any practical size limits of an SSH Certificate Principal (> 4096B allowed). PRINCIPAL_PATTERN = re.compile(r'[\d\w!"$%&\'()*+\-./:;<=>?@\[\\\]\^`{|}~]+\Z') VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" +VALID_SSH_ED25519_PUBLIC_KEY_HEADER = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5" USERNAME_VALIDATION_OPTIONS = Enum('UserNameValidationOptions', 'useradd ' # Allowable usernames per 'man 8 useradd' @@ -79,7 +80,7 @@ def _validate_principal(principal): def validate_ssh_public_key(public_key): - if public_key.startswith(VALID_SSH_RSA_PUBLIC_KEY_HEADER): + if public_key.startswith(VALID_SSH_RSA_PUBLIC_KEY_HEADER) or public_key.startswith(VALID_SSH_ED25519_PUBLIC_KEY_HEADER): pass # todo other key types else: diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index e6816e8..cc1b298 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -4,7 +4,7 @@ from bless.aws_lambda.bless_lambda import lambda_handler from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ - EXAMPLE_ED25519_PUBLIC_KEY + EXAMPLE_ED25519_PUBLIC_KEY, EXAMPLE_ECDSA_PUBLIC_KEY class Context(object): @@ -21,6 +21,15 @@ class Context(object): "bastion_user_ip": "127.0.0.1" } +VALID_TEST_REQUEST_ED2551 = { + "remote_usernames": "user", + "public_key_to_sign": EXAMPLE_ED25519_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD = { "remote_usernames": "user,anotheruser", "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, @@ -60,7 +69,7 @@ class Context(object): INVALID_TEST_REQUEST_KEY_TYPE = { "remote_usernames": "user", - "public_key_to_sign": EXAMPLE_ED25519_PUBLIC_KEY, + "public_key_to_sign": EXAMPLE_ECDSA_PUBLIC_KEY, "command": "ssh user@server", "bastion_ips": "127.0.0.1", "bastion_user": "user", @@ -153,6 +162,14 @@ def test_basic_local_request(): assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') +def test_basic_local_request_ed2551(): + output = lambda_handler(VALID_TEST_REQUEST_ED2551, 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-ed25519-cert-v01@openssh.com ') + + def test_basic_local_unused_kmsauth_request(): output = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, From cf26b72b0cdfaeb6e765597c43de8fddf6244746 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 25 Jul 2018 09:50:09 -0700 Subject: [PATCH 47/68] Moving BLESS to python 3.6. (#75) * Moving BLESS to python 3.6. You just need to rebuild, publish, and switch your lambda runtime from 2.7 to 3.6. * Moving TravisCI to Python3.6 as well. --- .gitignore | 1 + .travis.yml | 2 +- Makefile | 8 +-- README.md | 2 +- bless/__about__.py | 2 - bless/config/bless_config.py | 14 ++-- .../rsa_certificate_authority.py | 5 +- .../ssh_certificate_authority_factory.py | 6 +- .../certificates/ssh_certificate_builder.py | 8 +-- bless/ssh/protocol/ssh_protocol.py | 20 +++--- bless/ssh/public_keys/ed25519_public_key.py | 2 +- bless/ssh/public_keys/rsa_public_key.py | 2 +- bless_client/bless_client.py | 2 +- requirements.txt | 45 ++++--------- setup.py | 3 - tests/config/test_bless_config.py | 6 +- tests/request/test_bless_request.py | 28 ++++---- .../test_ssh_certificate_authority_factory.py | 6 +- tests/ssh/test_ssh_certificate_rsa.py | 7 +- tests/ssh/test_ssh_protocol.py | 67 ++++++++++--------- tests/ssh/vectors.py | 24 +++---- 21 files changed, 118 insertions(+), 142 deletions(-) diff --git a/.gitignore b/.gitignore index 9777179..edb1561 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ htmlcov/ libs/ publish/ venv/ +.pytest_cache/ diff --git a/.travis.yml b/.travis.yml index 11edb2c..6b8f3ce 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ addons: matrix: include: - - python: "2.7" + - python: "3.6" install: - pip install coveralls diff --git a/Makefile b/Makefile index 54ab4f7..61cdc28 100644 --- a/Makefile +++ b/Makefile @@ -39,12 +39,12 @@ publish: cd ./publish/bless_lambda && zip -r ../bless_lambda.zip . compile: - yum install -y gcc libffi-devel openssl-devel python27-virtualenv - virtualenv /tmp/venv + yum install -y gcc libffi-devel openssl-devel python36 python36-virtualenv + virtualenv-3.6 /tmp/venv /tmp/venv/bin/pip install --upgrade pip setuptools /tmp/venv/bin/pip install -e . - cp -r /tmp/venv/lib/python2.7/site-packages/. ./aws_lambda_libs - cp -r /tmp/venv/lib64/python2.7/site-packages/. ./aws_lambda_libs + cp -r /tmp/venv/lib/python3.6/site-packages/. ./aws_lambda_libs + cp -r /tmp/venv/lib64/python3.6/site-packages/. ./aws_lambda_libs lambda-deps: @echo "--> Compiling lambda dependencies" diff --git a/README.md b/README.md index 37d34cc..8edfa9c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Cd to the bless repo: Create a virtualenv if you haven't already: - $ virtualenv venv + $ python3.6 -m venv venv Activate the venv: diff --git a/bless/__about__.py b/bless/__about__.py index 89972af..98bf681 100644 --- a/bless/__about__.py +++ b/bless/__about__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division, print_function - __all__ = [ "__title__", "__summary__", "__uri__", "__version__", "__author__", "__email__", "__license__", "__copyright__", diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 89a5dd8..77dd41a 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -3,7 +3,7 @@ :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ -import ConfigParser +import configparser import base64 import os import re @@ -59,7 +59,7 @@ REMOTE_USERNAMES_VALIDATION_DEFAULT = 'principal' -class BlessConfig(ConfigParser.RawConfigParser, object): +class BlessConfig(configparser.RawConfigParser, object): def __init__(self, aws_region, config_file): """ Parses the BLESS config file, and provides some reasonable default values if they are @@ -86,7 +86,7 @@ def __init__(self, aws_region, config_file): USERNAME_VALIDATION_OPTION: USERNAME_VALIDATION_DEFAULT, REMOTE_USERNAMES_VALIDATION_OPTION: REMOTE_USERNAMES_VALIDATION_DEFAULT } - ConfigParser.RawConfigParser.__init__(self, defaults=defaults) + configparser.RawConfigParser.__init__(self, defaults=defaults) self.read(config_file) if not self.has_section(BLESS_OPTIONS_SECTION): @@ -114,7 +114,7 @@ def getkmsauthkeyids(self): in one region can validate in another). :return: A list of kmsauth key ids """ - return map(str.strip, self.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION).split(',')) + return list(map(str.strip, self.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION).split(','))) def getprivatekey(self): if self.has_option(BLESS_CA_SECTION, CA_PRIVATE_KEY_OPTION): @@ -124,7 +124,7 @@ def getprivatekey(self): # read the private key .pem with open(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, ca_private_key_file), 'r') as f: - return f.read() + return f.read().encode('ascii') def has_option(self, section, option): """ @@ -141,7 +141,7 @@ def has_option(self, section, option): else: return super(BlessConfig, self).has_option(section, option) - def get(self, section, option): + def get(self, section, option, **kwargs): """ Gets a value from the configuration. @@ -153,7 +153,7 @@ def get(self, section, option): environment_key = self._environment_key(section, option) output = os.environ.get(environment_key, None) if output is None: - output = super(BlessConfig, self).get(section, option) + output = super(BlessConfig, self).get(section, option, **kwargs) return output @staticmethod diff --git a/bless/ssh/certificate_authorities/rsa_certificate_authority.py b/bless/ssh/certificate_authorities/rsa_certificate_authority.py index 385c77f..55ef165 100644 --- a/bless/ssh/certificate_authorities/rsa_certificate_authority.py +++ b/bless/ssh/certificate_authorities/rsa_certificate_authority.py @@ -29,8 +29,6 @@ def __init__(self, pem_private_key, private_key_password=None): private_key_password, default_backend()) - self.signer = self.private_key.signer(padding.PKCS1v15(), - hashes.SHA1()) ca_pub_numbers = self.private_key.public_key().public_numbers() self.e = ca_pub_numbers.e @@ -55,7 +53,6 @@ def sign(self, body): signature key. :return: SSH RSA Signature. """ - self.signer.update(body) - signature = self.signer.finalize() + signature = self.private_key.sign(body, padding.PKCS1v15(), hashes.SHA1()) return self._serialize_signature(signature) diff --git a/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py b/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py index f080767..25aea40 100644 --- a/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py +++ b/bless/ssh/certificate_authorities/ssh_certificate_authority_factory.py @@ -12,12 +12,12 @@ def get_ssh_certificate_authority(private_key, password=None): """ Returns the proper SSHCertificateAuthority instance based off the private_key type. - :param private_key: SSH compatible Private Key (e.g., PEM or SSH Protocol 2 Private Key). + :param private_key: ASCII bytes of an SSH compatible Private Key (e.g., PEM or SSH Protocol 2 Private Key). It should be encrypted with a password, but that is not required. - :param password: Password to decrypt the Private Key, if it is encrypted. Which it should be. + :param password: ASCII bytes of the Password to decrypt the Private Key, if it is encrypted. Which it should be. :return: An SSHCertificateAuthority instance. """ - if private_key.startswith(SSHCertificateAuthorityPrivateKeyType.RSA): + if private_key.decode('ascii').startswith(SSHCertificateAuthorityPrivateKeyType.RSA): return RSACertificateAuthority(private_key, password) else: raise TypeError("Unsupported CA Private Key Type") diff --git a/bless/ssh/certificates/ssh_certificate_builder.py b/bless/ssh/certificates/ssh_certificate_builder.py index d862d44..8e4fb09 100644 --- a/bless/ssh/certificates/ssh_certificate_builder.py +++ b/bless/ssh/certificates/ssh_certificate_builder.py @@ -207,7 +207,7 @@ def get_cert_file(self, bypass_time_validity_check=False): file_contents = ( "{} {} {}" ).format(self.cert_key_type, - base64.b64encode(self._sign_cert(bypass_time_validity_check)), + str(base64.b64encode(self._sign_cert(bypass_time_validity_check)), encoding='ascii'), self.public_key_comment) return file_contents @@ -273,7 +273,7 @@ def _serialize_extensions(self): # sequence. Each named option may only appear once in a certificate. extensions_list = sorted(self.extensions) - serialized = '' + serialized = b'' # Format is a series of {extension name}{empty string} for extension in extensions_list: serialized += pack_ssh_string(extension) @@ -282,7 +282,7 @@ def _serialize_extensions(self): return serialized def _serialize_valid_principals(self): - serialized = '' + serialized = b'' for principal in self.valid_principals: serialized += pack_ssh_string(principal) @@ -292,7 +292,7 @@ def _serialize_valid_principals(self): def _serialize_critical_options(self): # Options must be lexically ordered by "name" if they appear in the # sequence. Each named option may only appear once in a certificate. - serialized = '' + serialized = b'' if self.critical_option_force_command is not None: serialized += pack_ssh_string('force-command') diff --git a/bless/ssh/protocol/ssh_protocol.py b/bless/ssh/protocol/ssh_protocol.py index 3cf853b..2fa37c9 100644 --- a/bless/ssh/protocol/ssh_protocol.py +++ b/bless/ssh/protocol/ssh_protocol.py @@ -44,13 +44,13 @@ def pack_ssh_string(string): :return: An SSH String stored as a unint32 representing the length of the input string, followed by that many bytes. """ - if isinstance(string, unicode): + if isinstance(string, str): string = string.encode('utf-8') str_len = len(string) if len(string) > 4294967295: - raise ValueError("String must be less than 2^32 bytes long") + raise ValueError("String must be less than 2^32 bytes long.") return struct.pack('>I{}s'.format(str_len), str_len, string) @@ -58,13 +58,13 @@ def pack_ssh_string(string): def pack_ssh_uint64(i): """ Packs a 64-bit unsigned integer. - :param i: integer or long. + :param i: integer :return: Eight bytes in the order of decreasing significance (network byte order). """ - if not isinstance(i, int) and not isinstance(i, long): - raise TypeError("Must be a int or long") + if not isinstance(i, int): + raise TypeError("Must be an int") elif i.bit_length() > 64: - raise ValueError("Must be a 64bit value") + raise ValueError("Must be a 64bit value.") return struct.pack('>Q', i) @@ -75,10 +75,10 @@ def pack_ssh_uint32(i): :param i: integer or long. :return: Four bytes in the order of decreasing significance (network byte order). """ - if not isinstance(i, int) and not isinstance(i, long): - raise TypeError("Must be a int or long") + if not isinstance(i, int): + raise TypeError("Must be an int") elif i.bit_length() > 32: - raise ValueError("Must be a 32bit value") + raise ValueError("Must be a 32bit value.") return struct.pack('>I', i) @@ -94,7 +94,7 @@ def _hex_characters_length(mpint): # how many bytes? num_bits = mpint.bit_length() - num_bytes = num_bits / 8 + num_bytes = num_bits // 8 # if there are remaining bits, we need an extra byte if num_bits % 8: diff --git a/bless/ssh/public_keys/ed25519_public_key.py b/bless/ssh/public_keys/ed25519_public_key.py index 9ca569a..7d054c6 100644 --- a/bless/ssh/public_keys/ed25519_public_key.py +++ b/bless/ssh/public_keys/ed25519_public_key.py @@ -48,7 +48,7 @@ def __init__(self, ssh_public_key): inner_key_type, rest = serialization._ssh_read_next_string(decoded_data) - if inner_key_type != key_type: + if inner_key_type != key_type.encode("utf-8"): raise ValueError( 'Key header and key body contain different key type values.' ) diff --git a/bless/ssh/public_keys/rsa_public_key.py b/bless/ssh/public_keys/rsa_public_key.py index 58a0e61..d1afcbd 100644 --- a/bless/ssh/public_keys/rsa_public_key.py +++ b/bless/ssh/public_keys/rsa_public_key.py @@ -57,7 +57,7 @@ def __init__(self, ssh_public_key): else: self.key_comment = '' - public_key = serialization.load_ssh_public_key(ssh_public_key, default_backend()) + public_key = serialization.load_ssh_public_key(ssh_public_key.encode('ascii'), default_backend()) ca_pub_numbers = public_key.public_numbers() if not isinstance(ca_pub_numbers, RSAPublicNumbers): raise TypeError("Public Key is not the correct type or format") diff --git a/bless_client/bless_client.py b/bless_client/bless_client.py index 7770d86..31e86ff 100755 --- a/bless_client/bless_client.py +++ b/bless_client/bless_client.py @@ -77,7 +77,7 @@ def main(argv): payload = json.loads(response['Payload'].read()) if 'certificate' not in payload: - print payload + print(payload) return -1 cert = payload['certificate'] diff --git a/requirements.txt b/requirements.txt index 93f16c0..e553eb2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,33 +1,16 @@ -e . -asn1crypto==0.22.0 -boto3==1.4.4 -botocore==1.5.63 -cffi==1.10.0 -configparser==3.5.0 -coverage==4.3.4 -cryptography==1.9 -docutils==0.13.1 -enum34==1.1.6 -flake8==3.3.0 -funcsigs==1.0.2 -futures==3.1.1 -idna==2.5 -ipaddress==1.0.18 +asn1crypto==0.24.0 +boto3==1.7.61 +botocore==1.10.61 +cffi==1.11.5 +cryptography==2.3 +docutils==0.14 +idna==2.7 +ipaddress==1.0.22 jmespath==0.9.3 -kmsauth==0.2.0 -marshmallow==2.13.5 -mccabe==0.6.1 -mock==2.0.0 -packaging==16.8 -pbr==3.0.1 -py==1.4.32 -pyasn1==0.2.2 -pycodestyle==2.3.1 -pycparser==2.17 -pyflakes==1.5.0 -pyparsing==2.1.10 -pytest==3.0.6 -pytest-mock==1.6.0 -python-dateutil==2.6.0 -s3transfer==0.1.10 -six==1.10.0 +kmsauth==0.3.0 +marshmallow==2.15.3 +pycparser==2.18 +python-dateutil==2.7.3 +s3transfer==0.1.13 +six==1.11.0 diff --git a/setup.py b/setup.py index 1999ec1..f63b48c 100644 --- a/setup.py +++ b/setup.py @@ -19,10 +19,7 @@ packages=find_packages(exclude=["test*"]), install_requires=[ 'boto3', - 'botocore', - 'cffi', 'cryptography', - 'enum34', 'ipaddress', 'marshmallow', 'kmsauth' diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index a7113c8..5a17a9a 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -35,7 +35,7 @@ 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 + 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')) @@ -56,7 +56,7 @@ def test_config_environment_override(monkeypatch): 'bless_ca_us_east_1_password': '', 'bless_ca_default_password': '', 'bless_ca_ca_private_key_file': '', - 'bless_ca_ca_private_key': base64.b64encode(''), + 'bless_ca_ca_private_key': str(base64.b64encode(b''), encoding='ascii'), 'kms_auth_use_kmsauth': 'True', 'kms_auth_kmsauth_key_id': '', @@ -80,7 +80,7 @@ def test_config_environment_override(monkeypatch): assert '' == config.getpassword() assert '' == config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION) - assert '' == config.getprivatekey() + assert b'' == config.getprivatekey() assert config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION) assert '' == config.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION) diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index b6ff49a..63b2ae8 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -6,24 +6,24 @@ def test_validate_ips(): - validate_ips(u'127.0.0.1') + validate_ips('127.0.0.1') with pytest.raises(ValidationError): - validate_ips(u'256.0.0.0') - validate_ips(u'127.0.0.1,172.1.1.1') + validate_ips('256.0.0.0') + validate_ips('127.0.0.1,172.1.1.1') with pytest.raises(ValidationError): - validate_ips(u'256.0.0.0,172.1.1.1') + validate_ips('256.0.0.0,172.1.1.1') def test_validate_ips_cidr(): - validate_ips(u'10.0.0.0/8,172.1.1.1') + validate_ips('10.0.0.0/8,172.1.1.1') with pytest.raises(ValidationError): - validate_ips(u'10.10.10.10/8') + validate_ips('10.10.10.10/8') def test_validate_user_too_long(): with pytest.raises(ValidationError) as e: validate_user('a33characterusernameyoumustbenuts', USERNAME_VALIDATION_OPTIONS.useradd) - assert e.value.message == 'Username is too long.' + assert str(e.value) == 'Username is too long.' @pytest.mark.parametrize("test_input", [ @@ -36,7 +36,7 @@ def test_validate_user_too_long(): def test_validate_user_contains_junk(test_input): with pytest.raises(ValidationError) as e: validate_user(test_input, USERNAME_VALIDATION_OPTIONS.useradd) - assert e.value.message == 'Username contains invalid characters.' + assert str(e.value) == 'Username contains invalid characters.' @pytest.mark.parametrize("test_input", [ @@ -52,7 +52,7 @@ def test_validate_user(test_input): def test_validate_user_debian_too_long(): with pytest.raises(ValidationError) as e: validate_user('a33characterusernameyoumustbenuts', USERNAME_VALIDATION_OPTIONS.debian) - assert e.value.message == 'Username is too long.' + assert str(e.value) == 'Username is too long.' @pytest.mark.parametrize("test_input", [ @@ -68,7 +68,7 @@ def test_validate_user_debian_too_long(): def test_validate_user_debian_invalid(test_input): with pytest.raises(ValidationError) as e: validate_user(test_input, USERNAME_VALIDATION_OPTIONS.debian) - assert e.value.message == 'Username contains invalid characters.' + assert str(e.value) == 'Username contains invalid characters.' @pytest.mark.parametrize("test_input", [ @@ -109,7 +109,7 @@ def test_validate_user_email(test_input): def test_invalid_user_email(test_input): with pytest.raises(ValidationError) as e: validate_user(test_input, USERNAME_VALIDATION_OPTIONS.email) - assert e.value.message == 'Invalid email address.' + assert str(e.value) == 'Invalid email address.' @pytest.mark.parametrize("test_input", [ @@ -147,16 +147,16 @@ def test_validate_multiple_principals(test_input): def test_invalid_multiple_principals(test_input): with pytest.raises(ValidationError) as e: BlessSchema().validate_remote_usernames(test_input) - assert e.value.message == 'Principal contains invalid characters.' + assert str(e.value) == 'Principal contains invalid characters.' def test_invalid_user_with_default_context_of_useradd(): with pytest.raises(ValidationError) as e: BlessSchema().validate_bastion_user('user#invalid') - assert e.value.message == 'Username contains invalid characters.' + assert str(e.value) == 'Username contains invalid characters.' def test_invalid_call_of_validate_user(): with pytest.raises(ValidationError) as e: validate_user('test', None) - assert e.value.message == 'Invalid username validator.' + assert str(e.value) == 'Invalid username validator.' diff --git a/tests/ssh/test_ssh_certificate_authority_factory.py b/tests/ssh/test_ssh_certificate_authority_factory.py index 9aad05c..bf7b6aa 100644 --- a/tests/ssh/test_ssh_certificate_authority_factory.py +++ b/tests/ssh/test_ssh_certificate_authority_factory.py @@ -29,14 +29,14 @@ def test_valid_key_missing_password(): def test_valid_key_invalid_password(): with pytest.raises(ValueError): - get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, 'bogus') + get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY, b'bogus') def test_valid_key_not_encrypted_invalid_pass(): with pytest.raises(TypeError): - get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED, 'bogus') + get_ssh_certificate_authority(RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED, b'bogus') def test_invalid_key(): with pytest.raises(TypeError): - get_ssh_certificate_authority('bogus') + get_ssh_certificate_authority(b'bogus') diff --git a/tests/ssh/test_ssh_certificate_rsa.py b/tests/ssh/test_ssh_certificate_rsa.py index eae09d1..41bcced 100644 --- a/tests/ssh/test_ssh_certificate_rsa.py +++ b/tests/ssh/test_ssh_certificate_rsa.py @@ -71,7 +71,7 @@ def test_serialize_no_principals(): cert = get_basic_cert_builder_rsa() assert list() == cert.valid_principals - assert '' == cert._serialize_valid_principals() + assert b'' == cert._serialize_valid_principals() def test_serialize_one_principal(): @@ -99,14 +99,14 @@ def test_no_extensions(): assert cert_builder.extensions is None cert_builder.clear_extensions() - assert '' == cert_builder._serialize_extensions() + assert b'' == cert_builder._serialize_extensions() def test_bogus_cert_validity_range(): cert_builder = get_basic_cert_builder_rsa() with pytest.raises(ValueError): + cert_builder.set_valid_before(99) cert_builder.set_valid_after(100) - cert_builder.set_valid_after(99) cert_builder._validate_cert_properties() @@ -142,7 +142,6 @@ def test_add_extensions(): for extension in extensions: cert_builder.add_extension(extension) - print base64.b64encode(cert_builder._serialize_extensions()) assert SSH_CERT_CUSTOM_EXTENSIONS == cert_builder._serialize_extensions() diff --git a/tests/ssh/test_ssh_protocol.py b/tests/ssh/test_ssh_protocol.py index 79530d9..84f92b1 100644 --- a/tests/ssh/test_ssh_protocol.py +++ b/tests/ssh/test_ssh_protocol.py @@ -1,65 +1,66 @@ +import binascii import pytest from bless.ssh.protocol.ssh_protocol import pack_ssh_mpint, _hex_characters_length, \ pack_ssh_uint32, pack_ssh_uint64, pack_ssh_string def test_strings(): - strings = {'': '00000000'.decode('hex'), u'abc': '00000003616263'.decode('hex'), - b'1234': '0000000431323334'.decode('hex'), '1234': '0000000431323334'.decode('hex')} + strings = {'': binascii.unhexlify('00000000'), 'abc': binascii.unhexlify('00000003616263'), + b'1234': binascii.unhexlify('0000000431323334'), '1234': binascii.unhexlify('0000000431323334')} - for known_input, known_answer in strings.iteritems(): + for known_input, known_answer in strings.items(): assert known_answer == pack_ssh_string(known_input) def test_mpint_known_answers(): # mipint values are from https://www.ietf.org/rfc/rfc4251.txt - mpints = {long(0): '00000000'.decode('hex'), - long(0x9a378f9b2e332a7): '0000000809a378f9b2e332a7'.decode('hex'), - long(0x80): '000000020080'.decode('hex'), long(-0x1234): '00000002edcc'.decode('hex'), - long(-0xdeadbeef): '00000005ff21524111'.decode('hex')} - for known_input, known_answer in mpints.iteritems(): + mpints = {int(0): binascii.unhexlify('00000000'), + int(0x9a378f9b2e332a7): binascii.unhexlify('0000000809a378f9b2e332a7'), + int(0x80): binascii.unhexlify('000000020080'), int(-0x1234): binascii.unhexlify('00000002edcc'), + int(-0xdeadbeef): binascii.unhexlify('00000005ff21524111')} + for known_input, known_answer in mpints.items(): assert known_answer == pack_ssh_mpint(known_input) def test_mpints(): - mpints = {long(-1): '00000001ff'.decode('hex'), long(1): '0000000101'.decode('hex'), - long(127): '000000017f'.decode('hex'), long(128): '000000020080'.decode('hex'), - long(-128): '0000000180'.decode('hex'), long(-129): '00000002ff7f'.decode('hex'), - long(255): '0000000200ff'.decode('hex'), long(256): '000000020100'.decode('hex'), - long(-256): '00000002ff00'.decode('hex'), long(-257): '00000002feff'.decode('hex')} - for known_input, known_answer in mpints.iteritems(): + mpints = {int(-1): binascii.unhexlify('00000001ff'), int(1): binascii.unhexlify('0000000101'), + int(127): binascii.unhexlify('000000017f'), int(128): binascii.unhexlify('000000020080'), + int(-128): binascii.unhexlify('0000000180'), int(-129): binascii.unhexlify('00000002ff7f'), + int(255): binascii.unhexlify('0000000200ff'), int(256): binascii.unhexlify('000000020100'), + int(-256): binascii.unhexlify('00000002ff00'), int(-257): binascii.unhexlify('00000002feff')} + for known_input, known_answer in mpints.items(): assert known_answer == pack_ssh_mpint(known_input) def test_hex_characters_length(): digits = {0: 0, 1: 2, 64: 2, 127: 2, 128: 4, 16384: 4, 32767: 4, 32768: 6, -1: 2, - long(-0x1234): 4, long(-0xdeadbeef): 10, -128: 2} - for known_input, known_answer in digits.iteritems(): + int(-0x1234): 4, int(-0xdeadbeef): 10, -128: 2} + for known_input, known_answer in digits.items(): assert known_answer == _hex_characters_length(known_input) def test_uint32(): - uint32s = {0x00: '00000000'.decode('hex'), 0x0a: '0000000a'.decode('hex'), - 0xab: '000000ab'.decode('hex'), 0xabcd: '0000abcd'.decode('hex'), - 0xabcdef: '00abcdef'.decode('hex'), 0xffffffff: 'ffffffff'.decode('hex'), - 0xf0f0f0f0: 'f0f0f0f0'.decode('hex'), 0x0f0f0f0f: '0f0f0f0f'.decode('hex')} + uint32s = {0x00: binascii.unhexlify('00000000'), 0x0a: binascii.unhexlify('0000000a'), + 0xab: binascii.unhexlify('000000ab'), 0xabcd: binascii.unhexlify('0000abcd'), + 0xabcdef: binascii.unhexlify('00abcdef'), 0xffffffff: binascii.unhexlify('ffffffff'), + 0xf0f0f0f0: binascii.unhexlify('f0f0f0f0'), 0x0f0f0f0f: binascii.unhexlify('0f0f0f0f')} - for known_input, known_answer in uint32s.iteritems(): + for known_input, known_answer in uint32s.items(): assert known_answer == pack_ssh_uint32(known_input) def test_uint64(): - uint64s = {0x00: '0000000000000000'.decode('hex'), 0x0a: '000000000000000a'.decode('hex'), - 0xab: '00000000000000ab'.decode('hex'), 0xabcd: '000000000000abcd'.decode('hex'), - 0xabcdef: '0000000000abcdef'.decode('hex'), - 0xffffffff: '00000000ffffffff'.decode('hex'), - 0xf0f0f0f0: '00000000f0f0f0f0'.decode('hex'), - 0x0f0f0f0f: '000000000f0f0f0f'.decode('hex'), - 0xf0f0f0f000000000: 'f0f0f0f000000000'.decode('hex'), - 0x0f0f0f0f00000000: '0f0f0f0f00000000'.decode('hex'), - 0xffffffffffffffff: 'ffffffffffffffff'.decode('hex')} - - for known_input, known_answer in uint64s.iteritems(): + uint64s = {0x00: binascii.unhexlify('0000000000000000'), 0x0a: binascii.unhexlify('000000000000000a'), + 0xab: binascii.unhexlify('00000000000000ab'), 0xabcd: binascii.unhexlify('000000000000abcd'), + 0xabcdef: binascii.unhexlify('0000000000abcdef'), + 0xffffffff: binascii.unhexlify('00000000ffffffff'), + 0xf0f0f0f0: binascii.unhexlify('00000000f0f0f0f0'), + 0x0f0f0f0f: binascii.unhexlify('000000000f0f0f0f'), + 0xf0f0f0f000000000: binascii.unhexlify('f0f0f0f000000000'), + 0x0f0f0f0f00000000: binascii.unhexlify('0f0f0f0f00000000'), + 0xffffffffffffffff: binascii.unhexlify('ffffffffffffffff')} + + for known_input, known_answer in uint64s.items(): assert known_answer == pack_ssh_uint64(known_input) @@ -76,7 +77,7 @@ def test_uint_too_long(): pack_ssh_uint64(0x1FFFFFFFFFFFFFFFF) with pytest.raises(ValueError): - pack_ssh_uint32(long(0x1FFFFFFFF)) + pack_ssh_uint32(int(0x1FFFFFFFF)) with pytest.raises(ValueError): pack_ssh_uint32(int(0x1FFFFFFFF)) diff --git a/tests/ssh/vectors.py b/tests/ssh/vectors.py index b87a16c..4479281 100644 --- a/tests/ssh/vectors.py +++ b/tests/ssh/vectors.py @@ -1,30 +1,30 @@ import base64 # These are for the test CA Private Key. Please don't use it for anything other than unit tests. Just don't. -RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED = '-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n' -RSA_CA_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,C534195782F2DD74B6218FFF4D3F7576\n\n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\n-----END RSA PRIVATE KEY-----\n' -RSA_CA_PRIVATE_KEY_PASSWORD = 'MdPaveLFOys0hMjL6AQfCdTa7VmzyLkpytuKWAe2RH9zU5KmkOI1P2gjFulGiWg' +RSA_CA_PRIVATE_KEY_NOT_ENCRYPTED = b'-----BEGIN RSA PRIVATE KEY-----\n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\n-----END RSA PRIVATE KEY-----\n' +RSA_CA_PRIVATE_KEY = b'-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: AES-128-CBC,C534195782F2DD74B6218FFF4D3F7576\n\n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\n-----END RSA PRIVATE KEY-----\n' +RSA_CA_PRIVATE_KEY_PASSWORD = b'MdPaveLFOys0hMjL6AQfCdTa7VmzyLkpytuKWAe2RH9zU5KmkOI1P2gjFulGiWg' RSA_CA_SSH_PUBLIC_KEY = base64.b64decode( 'AAAAB3NzaC1yc2EAAAADAQABAAACAQDiNCZ8XiUnbVF7R7u8dzFVS3QzdohQq/AfMQkhrHZiflXSNrARdMXO5efx2VcgE4AgU7fXU+3husxQp0ITyVFAfroVLvRHisLtz6sPZlp018v7Gv0fvsrMBY1NUsscLPogW4hvSBtQSKl2Pnjrl2n/vJBBYvO+241ODa5dFPyi9qqQicIRYd77uVwUNuoof03GVUWj8jkwVajg9DDXlV7NCZFXP4HeJix1oBb3CK0zZpisEdBzUgz6WGy8aJftw232mPQhP7FHrSwOjsTuBBd/D0Cj2PlHdv8sqXOLgvxk9VXxHOAC2VQmWAvArrd4WG+Xn98XWm+1uIpFyMlNr7T0f59Sbv+Z11RV9WNBebtEoWZM/EiO/wSxdouEiDImYTU4KlY/Jq9i6cEhwdOJuvHfRrD5w9ZkEMZXH8e6W3zgI15p01d5kJp+E51tSsSh9J+zSRNY70ct9Z9wq7N2jCenLquvoMWyuHIP1+PENej56bmMzok7Q+YRLruES+HIPOjIuew5nITcG9QrL8RSa7niQeuSeHnyA06prqaQ0FY5GTsx9lEc0cxhNms3XED4HFeWkEImboqnmecRvaBMAf9pDVbt4bDOPboCfTgRh7ZQ6HpguGJEJCXtHoxSVXPLijsxpl34xnFyottQKuYxrxLA0W9w3LixHHjn10aVP+fTqQ==') -EXAMPLE_RSA_PUBLIC_KEY = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTuckaZVjP4tP+xUc5xbmBzX1bdPUA78Zi/J8AGAJHTOBU2hHv6532DwWwXYIIj0nhrYpSEpcOgYUkEv3iH5a+nC11QDiOgO7Fs5xXMLKgZP8AyGoWcEspjw7GxiBn6D29SaVPbSYGsyQBaxSGW+D2TMx3OXiec5MlCzVfiu0FnayvEl/1xs9KPbCQnh+BoavQSn7VnK6lNR/vanbtoaRebVKcn4b78koSiI6FegFH7vaLK5UpfRz2tw+SCFIByGfXeYkbrX23Z3t+dOj0JWxGzW/NA7tlRAmE9stYPlIDU9ldjXPm5YhMvCAUyG26+wC5Inr1/JvvDVYh9i6Z1m98O7fvHlcRuw9eP4/mGsJ3HOvbf/nb/UhdkwIwunPkIY/zYRqZ+YLPDV+mQe2D28P9QIANfoiAK4kueS0LPH0psT2jiz/qqzkaMa7WcUiKwCgrLUggyg/T4Dcd58YD2vXxWXump3GL2ykoOHFYVmgoh030F3jPVJEx5f+7rz5YcxRcRzxE9Cz7L126E6gsN3fifJX4t6VUHfubZd2/isNlE4vZRWQk9S3Ej/wB4k6fSlKy1yStlbXfnDrFnQ//AS+OhsKUyIklUhqpDQE31b21cBYCddndG5gxQD0Apigt6u9HDdBH8NJaPZ52JHB5CFN8h1bpTY3/J6sLVe/MmPLhXw== Test RSA User Key' -EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTuckaZVjP4tP+xUc5xbmBzX1bdPUA78Zi/J8AGAJHTOBU2hHv6532DwWwXYIIj0nhrYpSEpcOgYUkEv3iH5a+nC11QDiOgO7Fs5xXMLKgZP8AyGoWcEspjw7GxiBn6D29SaVPbSYGsyQBaxSGW+D2TMx3OXiec5MlCzVfiu0FnayvEl/1xs9KPbCQnh+BoavQSn7VnK6lNR/vanbtoaRebVKcn4b78koSiI6FegFH7vaLK5UpfRz2tw+SCFIByGfXeYkbrX23Z3t+dOj0JWxGzW/NA7tlRAmE9stYPlIDU9ldjXPm5YhMvCAUyG26+wC5Inr1/JvvDVYh9i6Z1m98O7fvHlcRuw9eP4/mGsJ3HOvbf/nb/UhdkwIwunPkIY/zYRqZ+YLPDV+mQe2D28P9QIANfoiAK4kueS0LPH0psT2jiz/qqzkaMa7WcUiKwCgrLUggyg/T4Dcd58YD2vXxWXump3GL2ykoOHFYVmgoh030F3jPVJEx5f+7rz5YcxRcRzxE9Cz7L126E6gsN3fifJX4t6VUHfubZd2/isNlE4vZRWQk9S3Ej/wB4k6fSlKy1yStlbXfnDrFnQ//AS+OhsKUyIklUhqpDQE31b21cBYCddndG5gxQD0Apigt6u9HDdBH8NJaPZ52JHB5CFN8h1bpTY3/J6sLVe/MmPLhXw==' +EXAMPLE_RSA_PUBLIC_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTuckaZVjP4tP+xUc5xbmBzX1bdPUA78Zi/J8AGAJHTOBU2hHv6532DwWwXYIIj0nhrYpSEpcOgYUkEv3iH5a+nC11QDiOgO7Fs5xXMLKgZP8AyGoWcEspjw7GxiBn6D29SaVPbSYGsyQBaxSGW+D2TMx3OXiec5MlCzVfiu0FnayvEl/1xs9KPbCQnh+BoavQSn7VnK6lNR/vanbtoaRebVKcn4b78koSiI6FegFH7vaLK5UpfRz2tw+SCFIByGfXeYkbrX23Z3t+dOj0JWxGzW/NA7tlRAmE9stYPlIDU9ldjXPm5YhMvCAUyG26+wC5Inr1/JvvDVYh9i6Z1m98O7fvHlcRuw9eP4/mGsJ3HOvbf/nb/UhdkwIwunPkIY/zYRqZ+YLPDV+mQe2D28P9QIANfoiAK4kueS0LPH0psT2jiz/qqzkaMa7WcUiKwCgrLUggyg/T4Dcd58YD2vXxWXump3GL2ykoOHFYVmgoh030F3jPVJEx5f+7rz5YcxRcRzxE9Cz7L126E6gsN3fifJX4t6VUHfubZd2/isNlE4vZRWQk9S3Ej/wB4k6fSlKy1yStlbXfnDrFnQ//AS+OhsKUyIklUhqpDQE31b21cBYCddndG5gxQD0Apigt6u9HDdBH8NJaPZ52JHB5CFN8h1bpTY3/J6sLVe/MmPLhXw== Test RSA User Key' +EXAMPLE_RSA_PUBLIC_KEY_NO_DESCRIPTION = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDTuckaZVjP4tP+xUc5xbmBzX1bdPUA78Zi/J8AGAJHTOBU2hHv6532DwWwXYIIj0nhrYpSEpcOgYUkEv3iH5a+nC11QDiOgO7Fs5xXMLKgZP8AyGoWcEspjw7GxiBn6D29SaVPbSYGsyQBaxSGW+D2TMx3OXiec5MlCzVfiu0FnayvEl/1xs9KPbCQnh+BoavQSn7VnK6lNR/vanbtoaRebVKcn4b78koSiI6FegFH7vaLK5UpfRz2tw+SCFIByGfXeYkbrX23Z3t+dOj0JWxGzW/NA7tlRAmE9stYPlIDU9ldjXPm5YhMvCAUyG26+wC5Inr1/JvvDVYh9i6Z1m98O7fvHlcRuw9eP4/mGsJ3HOvbf/nb/UhdkwIwunPkIY/zYRqZ+YLPDV+mQe2D28P9QIANfoiAK4kueS0LPH0psT2jiz/qqzkaMa7WcUiKwCgrLUggyg/T4Dcd58YD2vXxWXump3GL2ykoOHFYVmgoh030F3jPVJEx5f+7rz5YcxRcRzxE9Cz7L126E6gsN3fifJX4t6VUHfubZd2/isNlE4vZRWQk9S3Ej/wB4k6fSlKy1yStlbXfnDrFnQ//AS+OhsKUyIklUhqpDQE31b21cBYCddndG5gxQD0Apigt6u9HDdBH8NJaPZ52JHB5CFN8h1bpTY3/J6sLVe/MmPLhXw==' EXAMPLE_RSA_PUBLIC_KEY_N = 863765597390437179936880505555316900679139439563534171378010191103037256891865752287401357532386053704846357061277355058425215781749953909539037751959779297844615696266967812249814741811813214887928783792221522790304297884601112319360723158242982362152538534517792264464776081835168055835189781482682493136923038734624107701903646705547410300883750998647287587929185720484066436341331195925984075699168429357216427056893851459700813345902685631135237388578414270244970778240740795928438846416944644092277106238587968690484934416451408183365968935178112856844073906509362367924503252063127972460845602238192159061426436817335330921795349155206805425009150162732786426778585802637172182106209312890241009071807202705546749148063984549775286112624905112511951652674845607083374421597899302609662133400077385016285752279857194248901093535059948349008431744427219342487822381449072585539524484989393533507171281966556907880034930485429159724230925010429126325954683461210886022933944242055541038221737797142318602403013636630990332566384663121261896469133144198125969293719738317514483451902518238751088148376136388153900264080474983146540911501501723872658100092602399884083442609325830811845748736526156589543373619454484831196371149151 EXAMPLE_RSA_PUBLIC_KEY_E = 65537 -EXAMPLE_ED25519_PUBLIC_KEY = u'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp Test ED25519 User Key' +EXAMPLE_ED25519_PUBLIC_KEY = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp Test ED25519 User Key' -EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION = u'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp' +EXAMPLE_ED25519_PUBLIC_KEY_NO_DESCRIPTION = 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIG7+cAbT4EFSPs87oS4kDYStQ0KL0xwHWqVSZ2bYHIAp' EXAMPLE_ED25519_PUBLIC_KEY_A = base64.b64decode( 'bv5wBtPgQVI+zzuhLiQNhK1DQovTHAdapVJnZtgcgCk=') -EXAMPLE_ECDSA_PUBLIC_KEY = u'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMHnC16D7nbmdS7GtNsIoiRaG8yz1QLzv0IAKfAJ+NsnIxbQSvVa+/wWYmGgkIblPdK3ZrtbzZje61Xq08iDUyE= Test ECDSA User Key' +EXAMPLE_ECDSA_PUBLIC_KEY = 'ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBMHnC16D7nbmdS7GtNsIoiRaG8yz1QLzv0IAKfAJ+NsnIxbQSvVa+/wWYmGgkIblPdK3ZrtbzZje61Xq08iDUyE= Test ECDSA User Key' -EXAMPLE_RSA_PUBLIC_KEY_2048 = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9D7rmQAyeSrvKGS1mmBHY0uYjePaorbZHyL7HD2SLYROiuQpdZnNDHCdKZB5fWgKVdszYgZAdQmBOolLS+cRpowv8GXKot4QHi1EaKOb74PEFvLQpxCqlHPqYCArakUamwcj81m9dJeK7VpLLxJno092lC2r50RDVZfi/2X4GM8vkptASJUuLzMLdsrC1MmYnPeJ2vNLUtV3EQ3YX+XVaebB/N6ee/yhwb9JmlXqABTfUrX2L94fU/Ry+4ljHRwLb7DNnM4LfeT0pMviH9wa0TeqIA/A5xLEp83mjyO4+QBDreQDIaBCZSVLF32iH+FMlPeRKZd6oYq80j+iLUH33' -EXAMPLE_RSA_PUBLIC_KEY_1024 = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCxc0Yw+5HMM2h1iPKGp4uoh9rNNbmfDgTmv9nCT9QRo4QgKzX/wrA4JSzpSSZLC2qIHADVESeWsOEi9Ohko7lgPGD+vAHFfNv/TNK8EFTzAbSoM/7eriO3wR5Y668Z0yEWhoCVnUWAHBWCBkQgHTxRYV/feXTirbI4ugp0v35d6w==' -EXAMPLE_RSA_PUBLIC_KEY_SMALLPRIME = u'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQI3LzCywCXa4DNeS4gzyDWKeLJqauP6CSLXWjxVLi2iIzroKx8YM1nJVHXfLFseDgfAZGaJhLBXxyDr5vh47U086SP0TFf6JowWofNOerTzotMRNhx9TH/vVu/IGCCPs9P0RVr2g04XcagyA7hjTc26e5jvIkDtczKAMeo/jHoSm2Ot9HA2b+KjZkiZGBIflzJ1uadkNnh+IBlMymJH7GAPbRF9pvbc/XlRT3czwG+AD6d+CHij5pd+91Y8pwpV1QiTxJo1moiea7d7mOmX5RQc6b+YC9CtThN92mzrWsq6wDLCawJZODHL2/hRnjmX6PlvuazfMZv5KA2dr7mh8Xnl' -EXAMPLE_RSA_PUBLIC_KEY_E3 = u'ssh-rsa AAAAB3NzaC1yc2EAAAABAwAAAQEAvQ+65kAMnkq7yhktZpgR2NLmI3j2qK22R8i+xw9ki2ETorkKXWZzQxwnSmQeX1oClXbM2IGQHUJgTqJS0vnEaaML/BlyqLeEB4tRGijm++DxBby0KcQqpRz6mAgK2pFGpsHI/NZvXSXiu1aSy8SZ6NPdpQtq+dEQ1WX4v9l+BjPL5KbQEiVLi8zC3bKwtTJmJz3idrzS1LVdxEN2F/l1Wnmwfzennv8ocG/SZpV6gAU31K19i/eH1P0cvuJYx0cC2+wzZzOC33k9KTL4h/cGtE3qiAPwOcSxKfN5o8juPkAQ63kAyGgQmUlSxd9oh/hTJT3kSmXeqGKvNI/oi1B99w==' +EXAMPLE_RSA_PUBLIC_KEY_2048 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC9D7rmQAyeSrvKGS1mmBHY0uYjePaorbZHyL7HD2SLYROiuQpdZnNDHCdKZB5fWgKVdszYgZAdQmBOolLS+cRpowv8GXKot4QHi1EaKOb74PEFvLQpxCqlHPqYCArakUamwcj81m9dJeK7VpLLxJno092lC2r50RDVZfi/2X4GM8vkptASJUuLzMLdsrC1MmYnPeJ2vNLUtV3EQ3YX+XVaebB/N6ee/yhwb9JmlXqABTfUrX2L94fU/Ry+4ljHRwLb7DNnM4LfeT0pMviH9wa0TeqIA/A5xLEp83mjyO4+QBDreQDIaBCZSVLF32iH+FMlPeRKZd6oYq80j+iLUH33' +EXAMPLE_RSA_PUBLIC_KEY_1024 = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQCxc0Yw+5HMM2h1iPKGp4uoh9rNNbmfDgTmv9nCT9QRo4QgKzX/wrA4JSzpSSZLC2qIHADVESeWsOEi9Ohko7lgPGD+vAHFfNv/TNK8EFTzAbSoM/7eriO3wR5Y668Z0yEWhoCVnUWAHBWCBkQgHTxRYV/feXTirbI4ugp0v35d6w==' +EXAMPLE_RSA_PUBLIC_KEY_SMALLPRIME = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQI3LzCywCXa4DNeS4gzyDWKeLJqauP6CSLXWjxVLi2iIzroKx8YM1nJVHXfLFseDgfAZGaJhLBXxyDr5vh47U086SP0TFf6JowWofNOerTzotMRNhx9TH/vVu/IGCCPs9P0RVr2g04XcagyA7hjTc26e5jvIkDtczKAMeo/jHoSm2Ot9HA2b+KjZkiZGBIflzJ1uadkNnh+IBlMymJH7GAPbRF9pvbc/XlRT3czwG+AD6d+CHij5pd+91Y8pwpV1QiTxJo1moiea7d7mOmX5RQc6b+YC9CtThN92mzrWsq6wDLCawJZODHL2/hRnjmX6PlvuazfMZv5KA2dr7mh8Xnl' +EXAMPLE_RSA_PUBLIC_KEY_E3 = 'ssh-rsa AAAAB3NzaC1yc2EAAAABAwAAAQEAvQ+65kAMnkq7yhktZpgR2NLmI3j2qK22R8i+xw9ki2ETorkKXWZzQxwnSmQeX1oClXbM2IGQHUJgTqJS0vnEaaML/BlyqLeEB4tRGijm++DxBby0KcQqpRz6mAgK2pFGpsHI/NZvXSXiu1aSy8SZ6NPdpQtq+dEQ1WX4v9l+BjPL5KbQEiVLi8zC3bKwtTJmJz3idrzS1LVdxEN2F/l1Wnmwfzennv8ocG/SZpV6gAU31K19i/eH1P0cvuJYx0cC2+wzZzOC33k9KTL4h/cGtE3qiAPwOcSxKfN5o8juPkAQ63kAyGgQmUlSxd9oh/hTJT3kSmXeqGKvNI/oi1B99w==' # ssh-keygen -s test-rsa-ca -I user@test -n username -V -5m:+5m test-rsa-user RSA_USER_CERT = 'ssh-rsa-cert-v01@openssh.com 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 Test RSA User Key' From 1e01e1d58d18ce1d196a8da24ed17fc01bdf123c Mon Sep 17 00:00:00 2001 From: Preston Date: Wed, 25 Jul 2018 11:44:49 -0700 Subject: [PATCH 48/68] bless_client.py: fix argv unpacking when using a kmsauth token (#63) --- bless_client/bless_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bless_client/bless_client.py b/bless_client/bless_client.py index 31e86ff..bb967d2 100755 --- a/bless_client/bless_client.py +++ b/bless_client/bless_client.py @@ -48,7 +48,7 @@ def main(argv): return -1 region, lambda_function_name, bastion_user, bastion_user_ip, remote_usernames, bastion_ips, \ - bastion_command, public_key_filename, certificate_filename = argv + bastion_command, public_key_filename, certificate_filename = argv[:9] with open(public_key_filename, 'r') as f: public_key = f.read().strip() From 467eaa80cc7db0527f5e841d985ca248c42ada4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Guerraz?= Date: Wed, 25 Jul 2018 19:52:26 +0100 Subject: [PATCH 49/68] Add the FileSync flag to the zip command (#76) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 61cdc28..ea81a76 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ publish: mv ./publish/bless_lambda/bless/aws_lambda/* ./publish/bless_lambda/ cp -r ./aws_lambda_libs/. ./publish/bless_lambda/ cp -r ./lambda_configs/. ./publish/bless_lambda/ - cd ./publish/bless_lambda && zip -r ../bless_lambda.zip . + cd ./publish/bless_lambda && zip -FSr ../bless_lambda.zip . compile: yum install -y gcc libffi-devel openssl-devel python36 python36-virtualenv From 5830630840369e1ea510d172b0b4422a57f0c5ad Mon Sep 17 00:00:00 2001 From: Adam McElwee Date: Wed, 25 Jul 2018 12:00:51 -0700 Subject: [PATCH 50/68] Make lambda_configs dir optional for publish make target (#69) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ea81a76..3b365d0 100644 --- a/Makefile +++ b/Makefile @@ -35,7 +35,7 @@ publish: cp -r ./bless ./publish/bless_lambda/ mv ./publish/bless_lambda/bless/aws_lambda/* ./publish/bless_lambda/ cp -r ./aws_lambda_libs/. ./publish/bless_lambda/ - cp -r ./lambda_configs/. ./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 . compile: From 87f9de4ded653f755c253d9a7b1e76253406f672 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Thu, 19 Jul 2018 13:27:43 -0700 Subject: [PATCH 51/68] Adding a blacklisted remote_usernames option. This would prevent particular SSH Authorized Principals from being included in a BLESS certificate. --- bless/aws_lambda/bless_lambda.py | 9 +++++--- bless/config/bless_config.py | 6 +++++- bless/config/bless_deploy_example.cfg | 2 ++ bless/request/bless_request.py | 17 +++++++++++---- tests/aws_lambda/test_bless_lambda.py | 27 +++++++++++++++++++++++- tests/request/test_bless_request.py | 30 ++++++++++++++++++++------- 6 files changed, 74 insertions(+), 17 deletions(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 8831352..008b809 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -29,7 +29,8 @@ TEST_USER_OPTION, \ CERTIFICATE_EXTENSIONS_OPTION, \ REMOTE_USERNAMES_VALIDATION_OPTION, \ - IAM_GROUP_NAME_VALIDATION_FORMAT_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 @@ -82,6 +83,8 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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 @@ -167,8 +170,8 @@ def lambda_handler(event, context=None, ca_private_key_password=None, required_group_name)) elif request.remote_usernames != request.bastion_user: - return error_response('KMSAuthValidationError', - 'remote_usernames must be the same as bastion_user') + return error_response('KMSAuthValidationError', + 'remote_usernames must be the same as bastion_user') try: validator = KMSTokenValidator( None, diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index a0cd258..d5e6961 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -64,6 +64,9 @@ IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION = 'kmsauth_iam_group_name_format' IAM_GROUP_NAME_VALIDATION_FORMAT_DEFAULT = 'ssh-{}' +REMOTE_USERNAMES_BLACKLIST_OPTION = 'remote_usernames_blacklist' +REMOTE_USERNAMES_BLACKLIST_DEFAULT = None + class BlessConfig(configparser.RawConfigParser, object): def __init__(self, aws_region, config_file): @@ -92,7 +95,8 @@ def __init__(self, aws_region, config_file): USERNAME_VALIDATION_OPTION: USERNAME_VALIDATION_DEFAULT, REMOTE_USERNAMES_VALIDATION_OPTION: REMOTE_USERNAMES_VALIDATION_DEFAULT, VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION: VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_DEFAULT, - IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION: IAM_GROUP_NAME_VALIDATION_FORMAT_DEFAULT + IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION: IAM_GROUP_NAME_VALIDATION_FORMAT_DEFAULT, + REMOTE_USERNAMES_BLACKLIST_OPTION: REMOTE_USERNAMES_BLACKLIST_DEFAULT } configparser.RawConfigParser.__init__(self, defaults=defaults) self.read(config_file) diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index c4767e2..4efdc0b 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -16,6 +16,8 @@ logging_level = INFO # username_validation = useradd # Configure how remote_usernames names are validated. # remote_usernames_validation = principal +# Configure a regex of blacklisted remote_usernames that will be rejected for any value of remote_usernames_validation. +# remote_usernames_blacklist = root|admin.* # These values are all required to be modified for deployment [Bless CA] diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index 7712562..e70951f 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -12,7 +12,8 @@ from marshmallow.validate import Email from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION, \ - USERNAME_VALIDATION_DEFAULT, REMOTE_USERNAMES_VALIDATION_DEFAULT + USERNAME_VALIDATION_DEFAULT, REMOTE_USERNAMES_VALIDATION_DEFAULT, REMOTE_USERNAMES_BLACKLIST_OPTION, \ + REMOTE_USERNAMES_BLACKLIST_DEFAULT # man 8 useradd USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') @@ -35,7 +36,7 @@ 'useradd ' # Allowable usernames per 'man 8 useradd' 'debian ' # Allowable usernames on debian systems. 'email ' # username is a valid email address. - 'principal ' # SSH Certificate Principal. See 'man 5 sshd_con# fig'. + 'principal ' # SSH Certificate Principal. See 'man 5 sshd_config'. 'disabled') # no additional validation of the string. @@ -47,7 +48,11 @@ def validate_ips(ips): raise ValidationError('Invalid IP address.') -def validate_user(user, username_validation): +def validate_user(user, username_validation, username_blacklist=None): + if username_blacklist: + if re.match(username_blacklist, user) is not None: + raise ValidationError('Username contains invalid characters.') + if username_validation == USERNAME_VALIDATION_OPTIONS.disabled: return elif username_validation == USERNAME_VALIDATION_OPTIONS.email: @@ -120,8 +125,12 @@ def validate_remote_usernames(self, remote_usernames): username_validation = USERNAME_VALIDATION_OPTIONS[self.context[REMOTE_USERNAMES_VALIDATION_OPTION]] else: username_validation = USERNAME_VALIDATION_OPTIONS[REMOTE_USERNAMES_VALIDATION_DEFAULT] + if REMOTE_USERNAMES_BLACKLIST_OPTION in self.context: + username_blacklist = self.context[REMOTE_USERNAMES_BLACKLIST_OPTION] + else: + username_blacklist = REMOTE_USERNAMES_BLACKLIST_DEFAULT for remote_username in remote_usernames.split(','): - validate_user(remote_username, username_validation) + validate_user(remote_username, username_validation, username_blacklist) class BlessRequest: diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 1b6f523..346d86d 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -151,6 +151,15 @@ class Context(object): "kmsauth_token": "validkmsauthtoken" } +INVALID_TEST_REQUEST_BLACKLISTED_REMOTE_USERNAME = { + "remote_usernames": "alloweduser,balrog", + "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, + "command": "ssh user@server", + "bastion_ips": "127.0.0.1", + "bastion_user": "user", + "bastion_user_ip": "127.0.0.1" +} + os.environ['AWS_REGION'] = 'us-west-2' @@ -364,6 +373,7 @@ def test_valid_request_with_allowed_remote(mocker): 'bless-test-kmsauth-different-remote.cfg')) assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + def test_valid_request_with_allowed_remote_and_allowed_iam_group(mocker): mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") clientmock = mocker.MagicMock() @@ -389,4 +399,19 @@ def test_invalid_request_with_allowed_remote_and_not_allowed_iam_group(mocker): entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), 'bless-test-kmsauth-iam-group-validation.cfg')) - assert output['errorType'] == 'KMSAuthValidationError' \ No newline at end of file + assert output['errorType'] == 'KMSAuthValidationError' + + +def test_basic_local_request_blacklisted(monkeypatch): + extra_environment_variables = { + 'bless_options_remote_usernames_blacklist': 'root|balrog', + } + + for k, v in extra_environment_variables.items(): + monkeypatch.setenv(k, v) + + output = lambda_handler(INVALID_TEST_REQUEST_BLACKLISTED_REMOTE_USERNAME, 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['errorType'] == 'InputValidationError' diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index 63b2ae8..197945d 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -1,8 +1,8 @@ import pytest -from marshmallow import ValidationError - -from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION +from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION, \ + REMOTE_USERNAMES_BLACKLIST_OPTION from bless.request.bless_request import validate_ips, validate_user, USERNAME_VALIDATION_OPTIONS, BlessSchema +from marshmallow import ValidationError def test_validate_ips(): @@ -63,12 +63,25 @@ def test_validate_user_debian_too_long(): ('user,invalid'), ('user invalid'), ('user\tinvalid'), - ('user\ninvalid'), + ('user\ninvalid') ]) def test_validate_user_debian_invalid(test_input): with pytest.raises(ValidationError) as e: validate_user(test_input, USERNAME_VALIDATION_OPTIONS.debian) - assert str(e.value) == 'Username contains invalid characters.' + assert str(e.value) == 'Username contains invalid characters.' + + +@pytest.mark.parametrize("test_input", [ + ('root'), + ("admin"), + ("administrator"), + ('balrog'), + ("teal'c") +]) +def test_validate_user_blacklist(test_input): + with pytest.raises(ValidationError) as e: + validate_user(test_input, USERNAME_VALIDATION_OPTIONS.principal, 'root|admin.*|balrog|.+\'.*') + assert str(e.value) == 'Username contains invalid characters.' @pytest.mark.parametrize("test_input", [ @@ -109,7 +122,7 @@ def test_validate_user_email(test_input): def test_invalid_user_email(test_input): with pytest.raises(ValidationError) as e: validate_user(test_input, USERNAME_VALIDATION_OPTIONS.email) - assert str(e.value) == 'Invalid email address.' + assert str(e.value) == 'Invalid email address.' @pytest.mark.parametrize("test_input", [ @@ -133,6 +146,7 @@ def test_validate_multiple_principals(test_input): schema = BlessSchema() schema.context[USERNAME_VALIDATION_OPTION] = USERNAME_VALIDATION_OPTIONS.principal.name schema.context[REMOTE_USERNAMES_VALIDATION_OPTION] = USERNAME_VALIDATION_OPTIONS.principal.name + schema.context[REMOTE_USERNAMES_BLACKLIST_OPTION] = 'balrog' schema.validate_remote_usernames(test_input) @@ -153,10 +167,10 @@ def test_invalid_multiple_principals(test_input): def test_invalid_user_with_default_context_of_useradd(): with pytest.raises(ValidationError) as e: BlessSchema().validate_bastion_user('user#invalid') - assert str(e.value) == 'Username contains invalid characters.' + assert str(e.value) == 'Username contains invalid characters.' def test_invalid_call_of_validate_user(): with pytest.raises(ValidationError) as e: validate_user('test', None) - assert str(e.value) == 'Invalid username validator.' + assert str(e.value) == 'Invalid username validator.' From a9ad2910016c6edca82a89f4e8f3a27bdb5b26a5 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Tue, 24 Jul 2018 17:26:51 -0700 Subject: [PATCH 52/68] Refactored BLESS to cache KMS decrypt results for the ca private key password. --- bless/aws_lambda/bless_lambda.py | 56 ++++++++++++++------------- bless/cache/__init__.py | 0 bless/cache/bless_lambda_cache.py | 44 +++++++++++++++++++++ tests/aws_lambda/test_bless_lambda.py | 2 - 4 files changed, 74 insertions(+), 28 deletions(-) create mode 100644 bless/cache/__init__.py create mode 100644 bless/cache/bless_lambda_cache.py diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 008b809..51a603a 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -3,18 +3,15 @@ :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ -import base64 + import logging -import os -import time import boto3 -from botocore.exceptions import ClientError -from kmsauth import KMSTokenValidator, TokenValidationError -from marshmallow.exceptions import ValidationError +import os +import time +from bless.cache.bless_lambda_cache import BlessLambdaCache -from bless.config.bless_config import BlessConfig, \ - BLESS_OPTIONS_SECTION, \ +from bless.config.bless_config import BLESS_OPTIONS_SECTION, \ CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ ENTROPY_MINIMUM_BITS_OPTION, \ @@ -36,11 +33,13 @@ 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=os.path.join(os.path.dirname(__file__), 'bless_deploy.cfg')): +def lambda_handler(event, context=None, ca_private_key_password=None, entropy_check=True, config_file=None): """ This is the function that will be called when the lambda function starts. :param event: Dictionary of the json request. @@ -49,16 +48,25 @@ def lambda_handler(event, context=None, ca_private_key_password=None, :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 + 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. """ + # 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 = os.environ['AWS_REGION'] + region = bless_cache.region # Load the deployment config values - config = BlessConfig(region, - config_file=config_file) + config = bless_cache.config logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION) numeric_level = getattr(logging, logging_level.upper(), None) @@ -75,7 +83,6 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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 @@ -97,15 +104,11 @@ def lambda_handler(event, context=None, ca_private_key_password=None, 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)) + # 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: @@ -117,6 +120,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, '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'] diff --git a/bless/cache/__init__.py b/bless/cache/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bless/cache/bless_lambda_cache.py b/bless/cache/bless_lambda_cache.py new file mode 100644 index 0000000..2082a38 --- /dev/null +++ b/bless/cache/bless_lambda_cache.py @@ -0,0 +1,44 @@ +import base64 +import os + +import boto3 +from bless.config.bless_config import BlessConfig +from botocore.exceptions import ClientError + + +class BlessLambdaCache: + region = None + config = None + ca_private_key_password = None + ca_private_key_password_error = None + + 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 diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda.py index 346d86d..f3da1e4 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda.py @@ -160,8 +160,6 @@ class Context(object): "bastion_user_ip": "127.0.0.1" } -os.environ['AWS_REGION'] = 'us-west-2' - def test_basic_local_request(): output = lambda_handler(VALID_TEST_REQUEST, context=Context, From 0b97ba218b6d3385cbb1403cedd014ebc1f3e712 Mon Sep 17 00:00:00 2001 From: Gonzalo Peci Date: Mon, 23 Jul 2018 19:05:08 +0200 Subject: [PATCH 53/68] Move development to pipenv --- .gitignore | 119 +++++++++++++++- .travis.yml | 6 +- Makefile | 12 +- Pipfile | 17 +++ Pipfile.lock | 355 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 12 +- requirements.txt | 16 --- setup.py | 13 +- 8 files changed, 501 insertions(+), 49 deletions(-) create mode 100644 Pipfile create mode 100644 Pipfile.lock delete mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore index 640646b..46859dd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,121 @@ -.coverage -*.pyc -*.cache/ +# Project Ignore .idea/ BLESS.egg-info/ -htmlcov/ libs/ publish/ -venv/ aws_lambda_libs/ lambda_configs/ + +# Created by https://www.gitignore.io/api/python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ .pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +### Python Patch ### +.venv/ + + +# End of https://www.gitignore.io/api/python diff --git a/.travis.yml b/.travis.yml index 6b8f3ce..78a983b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,7 @@ matrix: - python: "3.6" install: - - pip install coveralls + - pip install pipenv - make develop before_script: @@ -19,8 +19,8 @@ script: - make coverage after_success: - - coveralls - - coverage report + - pipenv run coveralls + - pipenv run coverage report notifications: email: diff --git a/Makefile b/Makefile index 3b365d0..2a534f8 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,12 @@ test: lint @echo "--> Running Python tests" - py.test tests || exit 1 + pipenv run py.test tests || exit 1 @echo "" develop: @echo "--> Installing dependencies" - pip install -r requirements.txt - pip install "file://`pwd`#egg=bless[tests]" + pipenv install + pipenv install --dev @echo "" dev-docs: @@ -20,13 +20,13 @@ clean: lint: @echo "--> Linting Python files" - PYFLAKES_NODOCTEST=1 flake8 bless + PYFLAKES_NODOCTEST=1 pipenv run flake8 bless @echo "" coverage: @echo "--> Running Python tests with coverage" - coverage run --branch --source=bless -m py.test tests || exit 1 - coverage html + pipenv run coverage run --branch --source=bless -m py.test tests || exit 1 + pipenv run coverage html @echo "" publish: diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..c78591a --- /dev/null +++ b/Pipfile @@ -0,0 +1,17 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +bless = {editable = true, path = "."} + +[dev-packages] +coverage = "*" +flake8 = "*" +pytest = "*" +pytest-mock = "*" +coveralls = "*" + +[requires] +python_version = "3.6" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..0fc906a --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,355 @@ +{ + "_meta": { + "hash": { + "sha256": "8f1729d3c4b29de39d788f74df627b05a6043de64493c4178f89e3a6683e1a9b" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.6" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "asn1crypto": { + "hashes": [ + "sha256:2f1adbb7546ed199e3c90ef23ec95c5cf3585bac7d11fb7eb562a3fe89c64e87", + "sha256:9d5c20441baf0cb60a4ac34cc447c6c189024b6b4c6cd7877034f4965c464e49" + ], + "version": "==0.24.0" + }, + "bless": { + "editable": true, + "path": "." + }, + "boto3": { + "hashes": [ + "sha256:3ec81575237a56a3cc31e963bb7ef8847752b41dd8343a8a758d466830215772", + "sha256:8f0f16ad7a6672b7748f2fa753f44007ada17ffb8767bb59dbd73f26ec6d265a" + ], + "version": "==1.7.66" + }, + "botocore": { + "hashes": [ + "sha256:cc09308c7923331a330d02df055a966bd28657cb7b68b00ea1f3264599df133b", + "sha256:e8e120af179aef5df247178c94627836239c1ffb22694087d48e3fc57af3062e" + ], + "version": "==1.10.66" + }, + "cffi": { + "hashes": [ + "sha256:151b7eefd035c56b2b2e1eb9963c90c6302dc15fbd8c1c0a83a163ff2c7d7743", + "sha256:1553d1e99f035ace1c0544050622b7bc963374a00c467edafac50ad7bd276aef", + "sha256:1b0493c091a1898f1136e3f4f991a784437fac3673780ff9de3bcf46c80b6b50", + "sha256:2ba8a45822b7aee805ab49abfe7eec16b90587f7f26df20c71dd89e45a97076f", + "sha256:3bb6bd7266598f318063e584378b8e27c67de998a43362e8fce664c54ee52d30", + "sha256:3c85641778460581c42924384f5e68076d724ceac0f267d66c757f7535069c93", + "sha256:3eb6434197633b7748cea30bf0ba9f66727cdce45117a712b29a443943733257", + "sha256:495c5c2d43bf6cebe0178eb3e88f9c4aa48d8934aa6e3cddb865c058da76756b", + "sha256:4c91af6e967c2015729d3e69c2e51d92f9898c330d6a851bf8f121236f3defd3", + "sha256:57b2533356cb2d8fac1555815929f7f5f14d68ac77b085d2326b571310f34f6e", + "sha256:770f3782b31f50b68627e22f91cb182c48c47c02eb405fd689472aa7b7aa16dc", + "sha256:79f9b6f7c46ae1f8ded75f68cf8ad50e5729ed4d590c74840471fc2823457d04", + "sha256:7a33145e04d44ce95bcd71e522b478d282ad0eafaf34fe1ec5bbd73e662f22b6", + "sha256:857959354ae3a6fa3da6651b966d13b0a8bed6bbc87a0de7b38a549db1d2a359", + "sha256:87f37fe5130574ff76c17cab61e7d2538a16f843bb7bca8ebbc4b12de3078596", + "sha256:95d5251e4b5ca00061f9d9f3d6fe537247e145a8524ae9fd30a2f8fbce993b5b", + "sha256:9d1d3e63a4afdc29bd76ce6aa9d58c771cd1599fbba8cf5057e7860b203710dd", + "sha256:a36c5c154f9d42ec176e6e620cb0dd275744aa1d804786a71ac37dc3661a5e95", + "sha256:a6a5cb8809091ec9ac03edde9304b3ad82ad4466333432b16d78ef40e0cce0d5", + "sha256:ae5e35a2c189d397b91034642cb0eab0e346f776ec2eb44a49a459e6615d6e2e", + "sha256:b0f7d4a3df8f06cf49f9f121bead236e328074de6449866515cea4907bbc63d6", + "sha256:b75110fb114fa366b29a027d0c9be3709579602ae111ff61674d28c93606acca", + "sha256:ba5e697569f84b13640c9e193170e89c13c6244c24400fc57e88724ef610cd31", + "sha256:be2a9b390f77fd7676d80bc3cdc4f8edb940d8c198ed2d8c0be1319018c778e1", + "sha256:ca1bd81f40adc59011f58159e4aa6445fc585a32bb8ac9badf7a2c1aa23822f2", + "sha256:d5d8555d9bfc3f02385c1c37e9f998e2011f0db4f90e250e5bc0c0a85a813085", + "sha256:e55e22ac0a30023426564b1059b035973ec82186ddddbac867078435801c7801", + "sha256:e90f17980e6ab0f3c2f3730e56d1fe9bcba1891eeea58966e89d352492cc74f4", + "sha256:ecbb7b01409e9b782df5ded849c178a0aa7c906cf8c5a67368047daab282b184", + "sha256:ed01918d545a38998bfa5902c7c00e0fee90e957ce036a4000a88e3fe2264917", + "sha256:edabd457cd23a02965166026fd9bfd196f4324fe6032e866d0f3bd0301cd486f", + "sha256:fdf1c1dc5bafc32bc5d08b054f94d659422b05aba244d6be4ddc1c72d9aa70fb" + ], + "version": "==1.11.5" + }, + "cryptography": { + "hashes": [ + "sha256:21af753934f2f6d1a10fe8f4c0a64315af209ef6adeaee63ca349797d747d687", + "sha256:27bb401a20a838d6d0ea380f08c6ead3ccd8c9d8a0232dc9adcc0e4994576a66", + "sha256:29720c4253263cff9aea64585adbbe85013ba647f6e98367efff9db2d7193ded", + "sha256:2a35b7570d8f247889784010aac8b384fd2e4a47b33e15c4a60b45a7c1944120", + "sha256:42c531a6a354407f42ee07fda5c2c0dc822cf6d52744949c182f2b295fbd4183", + "sha256:5eb86f03f9c4f0ac2336ac5431271072ddf7ecc76b338e26366732cfac58aa19", + "sha256:67f7f57eae8dede577f3f7775957f5bec93edd6bdb6ce597bb5b28e1bdf3d4fb", + "sha256:6ec84edcbc966ae460560a51a90046503ff0b5b66157a9efc61515c68059f6c8", + "sha256:7ba834564daef87557e7fcd35c3c3183a4147b0b3a57314e53317360b9b201b3", + "sha256:7d7f084cbe1fdb82be5a0545062b59b1ad3637bc5a48612ac2eb428ff31b31ea", + "sha256:82409f5150e529d699e5c33fa8fd85e965104db03bc564f5f4b6a9199e591f7c", + "sha256:87d092a7c2a44e5f7414ab02fb4145723ebba411425e1a99773531dd4c0e9b8d", + "sha256:8c56ef989342e42b9fcaba7c74b446f0cc9bed546dd00034fa7ad66fc00307ef", + "sha256:9449f5d4d7c516a6118fa9210c4a00f34384cb1d2028672100ee0c6cce49d7f6", + "sha256:bc2301170986ad82d9349a91eb8884e0e191209c45f5541b16aa7c0cfb135978", + "sha256:c132bab45d4bd0fff1d3fe294d92b0a6eb8404e93337b3127bdec9f21de117e6", + "sha256:c3d945b7b577f07a477700f618f46cbc287af3a9222cd73035c6ef527ef2c363", + "sha256:cee18beb4c807b5c0b178f4fa2fae03cef9d51821a358c6890f8b23465b7e5d2", + "sha256:d01dfc5c2b3495184f683574e03c70022674ca9a7be88589c5aba130d835ea90" + ], + "version": "==2.3" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "version": "==0.14" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "ipaddress": { + "hashes": [ + "sha256:64b28eec5e78e7510698f6d4da08800a5c575caa4a286c93d651c5d3ff7b6794", + "sha256:b146c751ea45cad6188dd6cf2d9b757f6f4f8d6ffb96a023e6f2e26eea02a72c" + ], + "version": "==1.0.22" + }, + "jmespath": { + "hashes": [ + "sha256:6a81d4c9aa62caf061cb517b4d9ad1dd300374cd4706997aff9cd6aedd61fc64", + "sha256:f11b4461f425740a1d908e9a3f7365c3d2e569f6ca68a2ff8bc5bcd9676edd63" + ], + "version": "==0.9.3" + }, + "kmsauth": { + "hashes": [ + "sha256:2428e9df216c29271c0b054b925b574212294e200248bf2a0636a4eac036abcf" + ], + "version": "==0.3.0" + }, + "marshmallow": { + "hashes": [ + "sha256:171f409d48b44786b7df2793cbd7f1a9062f0fe2c14d547da536b5010f671ade", + "sha256:c231784b5a5d2b26e50c90f3038004a3552ec27658cde6e0a5a7279d0c5a8e26" + ], + "version": "==2.15.3" + }, + "pycparser": { + "hashes": [ + "sha256:99a8ca03e29851d96616ad0404b4aad7d9ee16f25c9f9708a11faf2810f7b226" + ], + "version": "==2.18" + }, + "python-dateutil": { + "hashes": [ + "sha256:1adb80e7a782c12e52ef9a8182bebeb73f1d7e24e374397af06fb4956c8dc5c0", + "sha256:e27001de32f627c22380a688bcc43ce83504a7bc5da472209b4c70f02829f0b8" + ], + "markers": "python_version >= '2.7'", + "version": "==2.7.3" + }, + "s3transfer": { + "hashes": [ + "sha256:90dc18e028989c609146e241ea153250be451e05ecc0c2832565231dacdf59c1", + "sha256:c7a9ec356982d5e9ab2d4b46391a7d6a950e2b04c472419f5fdec70cc0ada72f" + ], + "version": "==0.1.13" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + } + }, + "develop": { + "atomicwrites": { + "hashes": [ + "sha256:240831ea22da9ab882b551b31d4225591e5e447a68c5e188db5b89ca1d487585", + "sha256:a24da68318b08ac9c9c45029f4a10371ab5b20e4226738e150e6e7c571630ae6" + ], + "version": "==1.1.5" + }, + "attrs": { + "hashes": [ + "sha256:4b90b09eeeb9b88c35bc642cbac057e45a5fd85367b985bd2809c62b7b939265", + "sha256:e0d0eb91441a3b53dab4d9b743eafc1ac44476296a2053b6ca3af0b139faf87b" + ], + "version": "==18.1.0" + }, + "certifi": { + "hashes": [ + "sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7", + "sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0" + ], + "version": "==2018.4.16" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "coverage": { + "hashes": [ + "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", + "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", + "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", + "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", + "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", + "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", + "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", + "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", + "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", + "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", + "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", + "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", + "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", + "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", + "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", + "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", + "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", + "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", + "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", + "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", + "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", + "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", + "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", + "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", + "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", + "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", + "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", + "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", + "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", + "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", + "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80" + ], + "index": "pypi", + "version": "==4.5.1" + }, + "coveralls": { + "hashes": [ + "sha256:32569a43c9dbc13fa8199247580a4ab182ef439f51f65bb7f8316d377a1340e8", + "sha256:664794748d2e5673e347ec476159a9d87f43e0d2d44950e98ed0e27b98da8346" + ], + "index": "pypi", + "version": "==1.3.0" + }, + "docopt": { + "hashes": [ + "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491" + ], + "version": "==0.6.2" + }, + "flake8": { + "hashes": [ + "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0", + "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37" + ], + "index": "pypi", + "version": "==3.5.0" + }, + "idna": { + "hashes": [ + "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", + "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + ], + "version": "==2.7" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "more-itertools": { + "hashes": [ + "sha256:c187a73da93e7a8acc0001572aebc7e3c69daf7bf6881a2cea10650bd4420092", + "sha256:c476b5d3a34e12d40130bc2f935028b5f636df8f372dc2c1c01dc19681b2039e", + "sha256:fcbfeaea0be121980e15bc97b3817b5202ca73d0eae185b4550cbfce2a3ebb3d" + ], + "version": "==4.3.0" + }, + "pluggy": { + "hashes": [ + "sha256:6e3836e39f4d36ae72840833db137f7b7d35105079aee6ec4a62d9f80d594dd1", + "sha256:95eb8364a4708392bae89035f45341871286a333f749c3141c20573d2b3876e1" + ], + "markers": "python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*'", + "version": "==0.7.1" + }, + "py": { + "hashes": [ + "sha256:3fd59af7435864e1a243790d322d763925431213b6b8529c6ca71081ace3bbf7", + "sha256:e31fb2767eb657cbde86c454f02e99cb846d3cd9d61b318525140214fdc0e98e" + ], + "markers": "python_version != '3.3.*' and python_version != '3.2.*' and python_version >= '2.7' and python_version != '3.1.*' and python_version != '3.0.*'", + "version": "==1.5.4" + }, + "pycodestyle": { + "hashes": [ + "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766", + "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9" + ], + "version": "==2.3.1" + }, + "pyflakes": { + "hashes": [ + "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f", + "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805" + ], + "version": "==1.6.0" + }, + "pytest": { + "hashes": [ + "sha256:8214ab8446104a1d0c17fbd218ec6aac743236c6ffbe23abc038e40213c60b88", + "sha256:e2b2c6e1560b8f9dc8dd600b0923183fbd68ba3d9bdecde04467be6dd296a384" + ], + "index": "pypi", + "version": "==3.7.0" + }, + "pytest-mock": { + "hashes": [ + "sha256:53801e621223d34724926a5c98bd90e8e417ce35264365d39d6c896388dcc928", + "sha256:d89a8209d722b8307b5e351496830d5cc5e192336003a485443ae9adeb7dd4c0" + ], + "index": "pypi", + "version": "==1.10.0" + }, + "requests": { + "hashes": [ + "sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1", + "sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a" + ], + "version": "==2.19.1" + }, + "six": { + "hashes": [ + "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", + "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + ], + "version": "==1.11.0" + }, + "urllib3": { + "hashes": [ + "sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf", + "sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5" + ], + "markers": "python_version != '3.0.*' and python_version != '3.1.*' and python_version != '3.3.*' and python_version >= '2.6' and python_version != '3.2.*' and python_version < '4'", + "version": "==1.23" + } + } +} diff --git a/README.md b/README.md index 82c18fd..d0e31f6 100644 --- a/README.md +++ b/README.md @@ -31,21 +31,17 @@ Cd to the bless repo: $ cd bless -Create a virtualenv if you haven't already: +Install package and test dependencies with pipenv: - $ python3.6 -m venv venv + $ make develop Activate the venv: - $ source venv/bin/activate - -Install package and test dependencies: - - (venv) $ make develop + $ pipenv shell Run the tests: - (venv) $ make test + $ make test ## Deployment diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index e553eb2..0000000 --- a/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ --e . -asn1crypto==0.24.0 -boto3==1.7.61 -botocore==1.10.61 -cffi==1.11.5 -cryptography==2.3 -docutils==0.14 -idna==2.7 -ipaddress==1.0.22 -jmespath==0.9.3 -kmsauth==0.3.0 -marshmallow==2.15.3 -pycparser==2.18 -python-dateutil==2.7.3 -s3transfer==0.1.13 -six==1.11.0 diff --git a/setup.py b/setup.py index f63b48c..6575d55 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ about = {} with open(os.path.join(ROOT, "bless", "__about__.py")) as f: - exec (f.read(), about) + exec(f.read(), about) setup( name=about["__title__"], @@ -23,14 +23,5 @@ 'ipaddress', 'marshmallow', 'kmsauth' - ], - extras_require={ - 'tests': [ - 'coverage', - 'flake8', - 'pyflakes', - 'pytest', - 'pytest-mock' - ] - } + ] ) From f82e2a95d4c3734221eeeca45359ac80cbc5292b Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Tue, 31 Jul 2018 16:12:41 -0700 Subject: [PATCH 54/68] Bumping to Release v.0.3.0 Features include: Python 3.6 Lambda support Caching of the KMS decrypted CA Private Key Password. Compressed CA Private Key support, allowing RSA 4096 keys to be set in the Lambda Environment. Issue certificates for ED25519 public keys (RSA CA). New option to validate the remote username against the IAM groups of the calling user. Updated dependencies. --- bless/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bless/__about__.py b/bless/__about__.py index 98bf681..e8805d6 100644 --- a/bless/__about__.py +++ b/bless/__about__.py @@ -9,7 +9,7 @@ "sign SSH public keys.") __uri__ = "https://github.com/Netflix/bless" -__version__ = "0.2.0" +__version__ = "0.3.0" __author__ = "The BLESS developers" __email__ = "security@netflix.com" From 242a586129c1bfb11f46ac1e83610281c2b662be Mon Sep 17 00:00:00 2001 From: Gonzalo Peci Date: Fri, 3 Aug 2018 18:09:13 +0200 Subject: [PATCH 55/68] Add host cert issue hanlder --- bless/aws_lambda/bless_lambda.py | 184 ++++++++++++++---- bless/request/bless_request.py | 35 +++- tests/aws_lambda/test_bless_lambda_host.py | 56 ++++++ ...ss_lambda.py => test_bless_lambda_user.py} | 83 ++++---- ..._request.py => test_bless_request_user.py} | 10 +- 5 files changed, 286 insertions(+), 82 deletions(-) create mode 100644 tests/aws_lambda/test_bless_lambda_host.py rename tests/aws_lambda/{test_bless_lambda.py => test_bless_lambda_user.py} (88%) rename tests/request/{test_bless_request.py => test_bless_request_user.py} (95%) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 51a603a..b08f649 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -9,6 +9,7 @@ import boto3 import os import time +from datetime import timedelta from bless.cache.bless_lambda_cache import BlessLambdaCache from bless.config.bless_config import BLESS_OPTIONS_SECTION, \ @@ -28,7 +29,7 @@ REMOTE_USERNAMES_VALIDATION_OPTION, \ IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION, \ REMOTE_USERNAMES_BLACKLIST_OPTION -from bless.request.bless_request import BlessSchema +from bless.request.bless_request import BlessHostSchema, BlessUserSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType @@ -39,7 +40,17 @@ 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): + """ + Wrapper around lambda_handler_user for backwards compatibility + """ + return lambda_handler_user(*args, **kwargs) + + +def lambda_handler_user( + event, context=None, ca_private_key_password=None, + entropy_check=True, + config_file=None): """ This is the function that will be called when the lambda function starts. :param event: Dictionary of the json request. @@ -52,15 +63,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, entropy_ch :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. """ - # 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 + bless_cache = setup_lambda_cache(ca_private_key_password, config_file) # AWS Region determines configs related to KMS region = bless_cache.region @@ -68,25 +71,17 @@ def lambda_handler(event, context=None, ca_private_key_password=None, entropy_ch # 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) + logger = set_logger(config) certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION, - CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) + 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) + CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) ca_private_key = config.getprivatekey() certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION) # Process cert request - schema = BlessSchema(strict=True) + schema = BlessUserSchema(strict=True) schema.context[USERNAME_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION) schema.context[REMOTE_USERNAMES_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, REMOTE_USERNAMES_VALIDATION_OPTION) @@ -112,20 +107,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, entropy_ch # 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) + check_entropy(config, logger) # cert values determined only by lambda and its configs current_time = int(time.time()) @@ -227,6 +209,86 @@ def lambda_handler(event, context=None, ca_private_key_password=None, entropy_ch return success_response(cert) +def lambda_handler_host( + event, context=None, ca_private_key_password=None, + entropy_check=True, + config_file=None): + """ + This is the function that will be called when the lambda function starts. + :param event: Dictionary of the json request. + :param context: AWS LambdaContext Object + http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + :param ca_private_key_password: For local testing, if the password is provided, skip the KMS + decrypt. + :param entropy_check: For local testing, if set to false, it will skip checking entropy and + won't try to fetch additional random from KMS. + :param config_file: The config file to load the SSH CA private key from, and additional settings. + :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file. + """ + bless_cache = setup_lambda_cache(ca_private_key_password, config_file) + + # Load the deployment config values + config = bless_cache.config + + logger = set_logger(config) + + ca_private_key = config.getprivatekey() + + # Process cert request + schema = BlessHostSchema(strict=True) + + try: + request = schema.load(event).data + except ValidationError as e: + return error_response('InputValidationError', str(e)) + + # todo: kmsauth of hostnames? Other server to hostnames validation? + logger.info('Bless lambda invoked by [public_key: {}]'.format(request.public_key_to_sign)) + + # Make sure we have the ca private key password + if bless_cache.ca_private_key_password is None: + return error_response('ClientError', bless_cache.ca_private_key_password_error) + else: + ca_private_key_password = bless_cache.ca_private_key_password + + # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired + if entropy_check: + check_entropy(config, logger) + + # cert values determined only by lambda and its configs + current_time = int(time.time()) + # todo: config server cert validity range + valid_before = current_time + int(timedelta(days=365).total_seconds()) # Host certificate is valid for at least 1 year + valid_after = current_time + + # Build the cert + ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) + cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.HOST, + request.public_key_to_sign) + + cert_builder.set_valid_before(valid_before) + cert_builder.set_valid_after(valid_after) + + # cert_builder is needed to obtain the SSH public key's fingerprint + key_id = 'request[{}] ssh_key[{}] ca[{}] valid_to[{}]'.format( + context.aws_request_id, cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before)) + ) + + for hostname in request.hostnames.split(','): + cert_builder.add_valid_principal(hostname) + + cert_builder.set_key_id(key_id) + cert = cert_builder.get_cert_file() + + logger.info( + 'Issued a server cert to hostnames[{}] with key_id[{}] and ' + 'valid_from[{}])'.format( + request.hostnames, key_id, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) + return success_response(cert) + + def success_response(cert): return { 'certificate': cert @@ -238,3 +300,51 @@ def error_response(error_type, error_message): '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.path.dirname(__file__), 'bless_deploy.cfg')) + bless_cache = global_bless_cache + else: + bless_cache = global_bless_cache + return bless_cache diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index e70951f..389dbbe 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -92,7 +92,7 @@ def validate_ssh_public_key(public_key): raise ValidationError('Invalid SSH Public Key.') -class BlessSchema(Schema): +class BlessUserSchema(Schema): bastion_ips = fields.Str(validate=validate_ips, required=True) bastion_user = fields.Str(required=True) bastion_user_ip = fields.Str(validate=validate_ips, required=True) @@ -109,7 +109,7 @@ def check_unknown_fields(self, data, original_data): @post_load def make_bless_request(self, data): - return BlessRequest(**data) + return BlessUserRequest(**data) @validates('bastion_user') def validate_bastion_user(self, user): @@ -133,7 +133,7 @@ def validate_remote_usernames(self, remote_usernames): validate_user(remote_username, username_validation, username_blacklist) -class BlessRequest: +class BlessUserRequest: def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_key_to_sign, remote_usernames, kmsauth_token=None): """ @@ -159,3 +159,32 @@ def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_k def __eq__(self, other): return self.__dict__ == other.__dict__ + + +class BlessHostSchema(Schema): + hostnames = fields.Str(required=True) + public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True) + + @validates_schema(pass_original=True) + def check_unknown_fields(self, data, original_data): + unknown = set(original_data) - set(self.fields) + if unknown: + raise ValidationError('Unknown field', unknown) + + @post_load + def make_bless_request(self, data): + return BlessHostRequest(**data) + + +class BlessHostRequest: + def __init__(self, hostnames, public_key_to_sign): + """ + A BlessRequest must have the following key value pairs to be valid. + :param hostnames: The hostnames to make valid for this host certificate. + :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.hostnames = hostnames + self.public_key_to_sign = public_key_to_sign + + def __eq__(self, other): + return self.__dict__ == other.__dict__ diff --git a/tests/aws_lambda/test_bless_lambda_host.py b/tests/aws_lambda/test_bless_lambda_host.py new file mode 100644 index 0000000..f25d1c0 --- /dev/null +++ b/tests/aws_lambda/test_bless_lambda_host.py @@ -0,0 +1,56 @@ +import os + +import pytest + +from bless.aws_lambda.bless_lambda import lambda_handler_host +from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ + EXAMPLE_ED25519_PUBLIC_KEY, EXAMPLE_ECDSA_PUBLIC_KEY + + +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", +} + +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": "wrongfieldname", +} + +os.environ['AWS_REGION'] = 'us-west-2' + + +def test_basic_local_request(): + output = lambda_handler_host(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')) + print(output) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_basic_local_request_with_multiple_hosts(): + output = lambda_handler_host(VALID_TEST_REQUEST_MULTIPLE_HOSTS, 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')) + print(output) + assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + + +def test_invalid_request(): + output = lambda_handler_host(INVALID_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['errorType'] == 'InputValidationError' diff --git a/tests/aws_lambda/test_bless_lambda.py b/tests/aws_lambda/test_bless_lambda_user.py similarity index 88% rename from tests/aws_lambda/test_bless_lambda.py rename to tests/aws_lambda/test_bless_lambda_user.py index c9ad012..563b909 100644 --- a/tests/aws_lambda/test_bless_lambda.py +++ b/tests/aws_lambda/test_bless_lambda_user.py @@ -3,7 +3,7 @@ import pytest -from bless.aws_lambda.bless_lambda import lambda_handler +from bless.aws_lambda.bless_lambda import lambda_handler, lambda_handler_user from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ EXAMPLE_ED25519_PUBLIC_KEY, EXAMPLE_ECDSA_PUBLIC_KEY @@ -162,7 +162,7 @@ class Context(object): } -def test_basic_local_request(): +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, @@ -170,8 +170,16 @@ def test_basic_local_request(): 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, + 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_ed2551(): - output = lambda_handler(VALID_TEST_REQUEST_ED2551, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_ED2551, 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')) @@ -179,7 +187,7 @@ def test_basic_local_request_ed2551(): def test_basic_local_unused_kmsauth_request(): - output = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_KMSAUTH, 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')) @@ -187,7 +195,7 @@ def test_basic_local_unused_kmsauth_request(): def test_basic_local_missing_kmsauth_request(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, + output = lambda_handler_user(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__), @@ -206,7 +214,7 @@ def test_basic_local_username_validation_disabled(monkeypatch): for k, v in extra_environment_variables.items(): monkeypatch.setenv(k, v) - output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_DISABLED, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_DISABLED, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), '')) @@ -224,7 +232,7 @@ def test_basic_local_username_validation_email_remote_usernames_useradd(monkeypa for k, v in extra_environment_variables.items(): monkeypatch.setenv(k, v) - output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), '')) @@ -243,7 +251,7 @@ def test_basic_ca_private_key_file_bz2(monkeypatch): for k, v in extra_environment_variables.items(): monkeypatch.setenv(k, v) - output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), '')) @@ -262,7 +270,7 @@ def test_basic_ca_private_key_env_bz2(monkeypatch): for k, v in extra_environment_variables.items(): monkeypatch.setenv(k, v) - output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), '')) @@ -281,12 +289,13 @@ def test_basic_ca_private_key_file_zlib(monkeypatch): for k, v in extra_environment_variables.items(): monkeypatch.setenv(k, v) - output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), '')) assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') + def test_basic_ca_private_key_env_zlib(monkeypatch): extra_environment_variables = { 'bless_ca_default_password': '', @@ -299,7 +308,7 @@ def test_basic_ca_private_key_env_zlib(monkeypatch): for k, v in extra_environment_variables.items(): monkeypatch.setenv(k, v) - output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), '')) @@ -318,7 +327,7 @@ def test_basic_ca_private_key_file_none_compression(monkeypatch): for k, v in extra_environment_variables.items(): monkeypatch.setenv(k, v) - output = lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), '')) @@ -338,10 +347,10 @@ def test_invalid_uncompressed_with_zlib(monkeypatch): monkeypatch.setenv(k, v) with pytest.raises(zlib.error): - lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, - ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, - entropy_check=False, - config_file=os.path.join(os.path.dirname(__file__), '')) + lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), '')) def test_invalid_uncompressed_with_bz2(monkeypatch): @@ -357,14 +366,14 @@ def test_invalid_uncompressed_with_bz2(monkeypatch): monkeypatch.setenv(k, v) with pytest.raises(OSError): - lambda_handler(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, + lambda_handler_user(VALID_TEST_REQUEST_USERNAME_VALIDATION_EMAIL_REMOTE_USERNAMES_USERADD, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), '')) def test_invalid_username_request(): - output = lambda_handler(INVALID_TEST_REQUEST_USERNAME_INVALID, context=Context, + output = lambda_handler_user(INVALID_TEST_REQUEST_USERNAME_INVALID, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), @@ -373,7 +382,7 @@ def test_invalid_username_request(): def test_invalid_kmsauth_request(): - output = lambda_handler(VALID_TEST_REQUEST_KMSAUTH, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_KMSAUTH, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), @@ -382,7 +391,7 @@ def test_invalid_kmsauth_request(): def test_invalid_request(): - output = lambda_handler(INVALID_TEST_REQUEST, context=Context, + output = lambda_handler_user(INVALID_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')) @@ -391,7 +400,7 @@ def test_invalid_request(): def test_local_request_key_not_found(): with pytest.raises(IOError): - lambda_handler(VALID_TEST_REQUEST, context=Context, + lambda_handler_user(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-broken.cfg')) @@ -399,14 +408,14 @@ def test_local_request_key_not_found(): def test_local_request_config_not_found(): with pytest.raises(ValueError): - lambda_handler(VALID_TEST_REQUEST, context=Context, + lambda_handler_user(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__), 'none')) def test_local_request_invalid_pub_key(): - output = lambda_handler(INVALID_TEST_REQUEST_KEY_TYPE, context=Context, + output = lambda_handler_user(INVALID_TEST_REQUEST_KEY_TYPE, 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')) @@ -414,7 +423,7 @@ def test_local_request_invalid_pub_key(): def test_local_request_extra_field(): - output = lambda_handler(INVALID_TEST_REQUEST_EXTRA_FIELD, context=Context, + output = lambda_handler_user(INVALID_TEST_REQUEST_EXTRA_FIELD, 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')) @@ -422,7 +431,7 @@ def test_local_request_extra_field(): def test_local_request_missing_field(): - output = lambda_handler(INVALID_TEST_REQUEST_MISSING_FIELD, context=Context, + output = lambda_handler_user(INVALID_TEST_REQUEST_MISSING_FIELD, 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')) @@ -430,7 +439,7 @@ def test_local_request_missing_field(): def test_local_request_with_test_user(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, + output = lambda_handler_user(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-with-test-user.cfg')) @@ -438,7 +447,7 @@ def test_local_request_with_test_user(): def test_local_request_with_custom_certificate_extensions(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, + output = lambda_handler_user(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__), @@ -447,7 +456,7 @@ def test_local_request_with_custom_certificate_extensions(): def test_local_request_with_empty_certificate_extensions(): - output = lambda_handler(VALID_TEST_REQUEST, context=Context, + output = lambda_handler_user(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__), @@ -456,7 +465,7 @@ def test_local_request_with_empty_certificate_extensions(): def test_local_request_with_multiple_principals(): - output = lambda_handler(VALID_TEST_REQUEST_MULTIPLE_PRINCIPALS, context=Context, + output = lambda_handler_user(VALID_TEST_REQUEST_MULTIPLE_PRINCIPALS, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), @@ -465,7 +474,7 @@ def test_local_request_with_multiple_principals(): def test_invalid_request_with_multiple_principals(): - output = lambda_handler(INVALID_TEST_REQUEST_MULTIPLE_PRINCIPALS, context=Context, + output = lambda_handler_user(INVALID_TEST_REQUEST_MULTIPLE_PRINCIPALS, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), @@ -478,7 +487,7 @@ def test_invalid_request_with_mismatched_bastion_and_remote(): Test default kmsauth behavior, that a bastion_user and remote_usernames must match :return: ''' - output = lambda_handler(INVALID_TEST_KMSAUTH_REQUEST_USERNAME_DOESNT_MATCH_REMOTE, context=Context, + output = lambda_handler_user(INVALID_TEST_KMSAUTH_REQUEST_USERNAME_DOESNT_MATCH_REMOTE, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), @@ -487,7 +496,7 @@ def test_invalid_request_with_mismatched_bastion_and_remote(): def test_invalid_request_with_unallowed_remote(): - output = lambda_handler(INVALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, + output = lambda_handler_user(INVALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), @@ -497,7 +506,7 @@ def test_invalid_request_with_unallowed_remote(): def test_valid_request_with_allowed_remote(mocker): mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") - output = lambda_handler(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, + output = lambda_handler_user(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), @@ -508,10 +517,10 @@ def test_valid_request_with_allowed_remote(mocker): def test_valid_request_with_allowed_remote_and_allowed_iam_group(mocker): mocker.patch("kmsauth.KMSTokenValidator.decrypt_token") clientmock = mocker.MagicMock() - clientmock.list_groups_for_user.return_value = {"Groups":[{"GroupName":"ssh-alloweduser"}]} + clientmock.list_groups_for_user.return_value = {"Groups": [{"GroupName": "ssh-alloweduser"}]} botomock = mocker.patch('boto3.client') botomock.return_value = clientmock - output = lambda_handler(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, + output = lambda_handler_user(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), @@ -525,7 +534,7 @@ def test_invalid_request_with_allowed_remote_and_not_allowed_iam_group(mocker): clientmock.list_groups_for_user.return_value = {"Groups": [{"GroupName": "ssh-notalloweduser"}]} botomock = mocker.patch('boto3.client') botomock.return_value = clientmock - output = lambda_handler(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, + output = lambda_handler_user(VALID_TEST_KMSAUTH_REQUEST_DIFFERENT_REMOTE_USER, context=Context, ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, entropy_check=False, config_file=os.path.join(os.path.dirname(__file__), @@ -541,7 +550,7 @@ def test_basic_local_request_blacklisted(monkeypatch): for k, v in extra_environment_variables.items(): monkeypatch.setenv(k, v) - output = lambda_handler(INVALID_TEST_REQUEST_BLACKLISTED_REMOTE_USERNAME, context=Context, + output = lambda_handler_user(INVALID_TEST_REQUEST_BLACKLISTED_REMOTE_USERNAME, 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')) diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request_user.py similarity index 95% rename from tests/request/test_bless_request.py rename to tests/request/test_bless_request_user.py index 197945d..d99cc8b 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request_user.py @@ -1,7 +1,7 @@ import pytest from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION, \ REMOTE_USERNAMES_BLACKLIST_OPTION -from bless.request.bless_request import validate_ips, validate_user, USERNAME_VALIDATION_OPTIONS, BlessSchema +from bless.request.bless_request import validate_ips, validate_user, USERNAME_VALIDATION_OPTIONS, BlessUserSchema from marshmallow import ValidationError @@ -141,9 +141,9 @@ def test_validate_user_disabled(test_input): 'uservalid2') ]) def test_validate_multiple_principals(test_input): - BlessSchema().validate_remote_usernames(test_input) + BlessUserSchema().validate_remote_usernames(test_input) - schema = BlessSchema() + schema = BlessUserSchema() schema.context[USERNAME_VALIDATION_OPTION] = USERNAME_VALIDATION_OPTIONS.principal.name schema.context[REMOTE_USERNAMES_VALIDATION_OPTION] = USERNAME_VALIDATION_OPTIONS.principal.name schema.context[REMOTE_USERNAMES_BLACKLIST_OPTION] = 'balrog' @@ -160,13 +160,13 @@ def test_validate_multiple_principals(test_input): ]) def test_invalid_multiple_principals(test_input): with pytest.raises(ValidationError) as e: - BlessSchema().validate_remote_usernames(test_input) + BlessUserSchema().validate_remote_usernames(test_input) assert str(e.value) == 'Principal contains invalid characters.' def test_invalid_user_with_default_context_of_useradd(): with pytest.raises(ValidationError) as e: - BlessSchema().validate_bastion_user('user#invalid') + BlessUserSchema().validate_bastion_user('user#invalid') assert str(e.value) == 'Username contains invalid characters.' From ed85a7f2d3e463747a62885a215fc99e7378081c Mon Sep 17 00:00:00 2001 From: Gonzalo Peci Date: Sat, 4 Aug 2018 12:37:51 +0200 Subject: [PATCH 56/68] Add validations for hostnames and tests --- bless/aws_lambda/bless_lambda.py | 17 +++++--- bless/config/bless_config.py | 12 +++++- bless/config/bless_deploy_example.cfg | 5 +++ bless/request/bless_request.py | 26 +++++++++++- tests/aws_lambda/test_bless_lambda_host.py | 4 +- tests/config/full-with-cert.cfg | 13 ++++++ tests/config/full-zlib.cfg | 3 ++ tests/config/full.cfg | 3 ++ tests/config/test_bless_config.py | 49 ++++++++++++++++++---- tests/request/test_bless_request_host.py | 49 ++++++++++++++++++++++ 10 files changed, 163 insertions(+), 18 deletions(-) create mode 100644 tests/config/full-with-cert.cfg create mode 100644 tests/request/test_bless_request_host.py diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index b08f649..e257fdb 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -9,7 +9,6 @@ import boto3 import os import time -from datetime import timedelta from bless.cache.bless_lambda_cache import BlessLambdaCache from bless.config.bless_config import BLESS_OPTIONS_SECTION, \ @@ -28,7 +27,10 @@ CERTIFICATE_EXTENSIONS_OPTION, \ REMOTE_USERNAMES_VALIDATION_OPTION, \ IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION, \ - REMOTE_USERNAMES_BLACKLIST_OPTION + REMOTE_USERNAMES_BLACKLIST_OPTION, \ + HOSTNAME_VALIDATION_OPTION, \ + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION from bless.request.bless_request import BlessHostSchema, BlessUserSchema from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \ get_ssh_certificate_authority @@ -232,10 +234,16 @@ def lambda_handler_host( logger = set_logger(config) + certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION, + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) + certificate_validity_after_seconds = config.getint(BLESS_OPTIONS_SECTION, + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) + ca_private_key = config.getprivatekey() # Process cert request schema = BlessHostSchema(strict=True) + schema.context[HOSTNAME_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, HOSTNAME_VALIDATION_OPTION) try: request = schema.load(event).data @@ -257,9 +265,8 @@ def lambda_handler_host( # cert values determined only by lambda and its configs current_time = int(time.time()) - # todo: config server cert validity range - valid_before = current_time + int(timedelta(days=365).total_seconds()) # Host certificate is valid for at least 1 year - valid_after = current_time + valid_before = current_time + certificate_validity_after_seconds + valid_after = current_time - certificate_validity_before_seconds # Build the cert ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index a839776..03c6755 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -14,6 +14,10 @@ CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'certificate_validity_before_seconds' CERTIFICATE_VALIDITY_AFTER_SEC_OPTION = 'certificate_validity_after_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 +SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION = 'server_certificate_validity_after_seconds' +SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_DEFAULT = 31536000 ENTROPY_MINIMUM_BITS_OPTION = 'entropy_minimum_bits' ENTROPY_MINIMUM_BITS_DEFAULT = 2048 @@ -35,6 +39,9 @@ 'permit-pty,' \ 'permit-user-rc' +HOSTNAME_VALIDATION_OPTION = 'hostname_validation' +HOSTNAME_VALIDATION_DEFAULT = 'url' + BLESS_CA_SECTION = 'Bless CA' CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file' CA_PRIVATE_KEY_OPTION = 'ca_private_key' @@ -101,7 +108,10 @@ def __init__(self, aws_region, config_file): VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION: VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_DEFAULT, IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION: IAM_GROUP_NAME_VALIDATION_FORMAT_DEFAULT, REMOTE_USERNAMES_BLACKLIST_OPTION: REMOTE_USERNAMES_BLACKLIST_DEFAULT, - CA_PRIVATE_KEY_COMPRESSION_OPTION: CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT + CA_PRIVATE_KEY_COMPRESSION_OPTION: CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT, + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION: SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_DEFAULT, + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION: SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_DEFAULT, + HOSTNAME_VALIDATION_OPTION: HOSTNAME_VALIDATION_DEFAULT } configparser.RawConfigParser.__init__(self, defaults=defaults) self.read(config_file) diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index 4efdc0b..95cd8c0 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -18,6 +18,11 @@ logging_level = INFO # remote_usernames_validation = principal # Configure a regex of blacklisted remote_usernames that will be rejected for any value of remote_usernames_validation. # remote_usernames_blacklist = root|admin.* +# Number of seconds +/- the issued time for the server certificates to be valid +# server_certificate_validity_before_seconds = 120 +# server_certificate_validity_after_seconds = 31536000 +# Configure how server certificate hostnames are validated +# hostname_validation = url # These values are all required to be modified for deployment [Bless CA] diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index 389dbbe..827c2c8 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -9,11 +9,11 @@ from enum import Enum from marshmallow import Schema, fields, post_load, ValidationError, validates_schema from marshmallow import validates -from marshmallow.validate import Email +from marshmallow.validate import Email, URL from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION, \ USERNAME_VALIDATION_DEFAULT, REMOTE_USERNAMES_VALIDATION_DEFAULT, REMOTE_USERNAMES_BLACKLIST_OPTION, \ - REMOTE_USERNAMES_BLACKLIST_DEFAULT + REMOTE_USERNAMES_BLACKLIST_DEFAULT, HOSTNAME_VALIDATION_OPTION, HOSTNAME_VALIDATION_DEFAULT # man 8 useradd USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') @@ -39,6 +39,11 @@ 'principal ' # SSH Certificate Principal. See 'man 5 sshd_config'. 'disabled') # no additional validation of the string. +HOSTNAME_VALIDATION_OPTIONS = Enum('HostNameValidationOptions', + 'url ' # Valid url format + 'disabled' # no validation +) + def validate_ips(ips): try: @@ -92,6 +97,14 @@ def validate_ssh_public_key(public_key): raise ValidationError('Invalid SSH Public Key.') +def validate_hostname(hostname, hostname_validation): + if hostname_validation == HOSTNAME_VALIDATION_OPTIONS.disabled: + return + else: + validator = URL(require_tld=False, schemes='ssh', error='Invalid hostname "{input}".') + validator('ssh://{}'.format(hostname)) + + class BlessUserSchema(Schema): bastion_ips = fields.Str(validate=validate_ips, required=True) bastion_user = fields.Str(required=True) @@ -175,6 +188,15 @@ def check_unknown_fields(self, data, original_data): def make_bless_request(self, data): return BlessHostRequest(**data) + @validates('hostnames') + def validate_hostnames(self, hostnames): + if HOSTNAME_VALIDATION_OPTION in self.context: + hostname_validation = HOSTNAME_VALIDATION_OPTIONS[self.context[HOSTNAME_VALIDATION_OPTION]] + else: + hostname_validation = HOSTNAME_VALIDATION_OPTIONS[HOSTNAME_VALIDATION_DEFAULT] + for hostname in hostnames.split(','): + validate_hostname(hostname, hostname_validation) + class BlessHostRequest: def __init__(self, hostnames, public_key_to_sign): diff --git a/tests/aws_lambda/test_bless_lambda_host.py b/tests/aws_lambda/test_bless_lambda_host.py index f25d1c0..dcca76e 100644 --- a/tests/aws_lambda/test_bless_lambda_host.py +++ b/tests/aws_lambda/test_bless_lambda_host.py @@ -19,12 +19,12 @@ class Context(object): VALID_TEST_REQUEST_MULTIPLE_HOSTS = { "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, - "hostnames": "thisthat.com, thatthis.com", + "hostnames": "thisthat.com,thatthis.com", } INVALID_TEST_REQUEST = { "public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY, - "hostname": "wrongfieldname", + "hostname": "thisthat.com", # Wrong key name } os.environ['AWS_REGION'] = 'us-west-2' diff --git a/tests/config/full-with-cert.cfg b/tests/config/full-with-cert.cfg new file mode 100644 index 0000000..bc19017 --- /dev/null +++ b/tests/config/full-with-cert.cfg @@ -0,0 +1,13 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_after_seconds = 1 +certificate_validity_before_seconds = 1 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +logging_level = DEBUG + +[Bless CA] +us-east-1_password = +us-west-2_password = +default_password = +ca_private_key_file = diff --git a/tests/config/full-zlib.cfg b/tests/config/full-zlib.cfg index 68c96cc..e41e088 100644 --- a/tests/config/full-zlib.cfg +++ b/tests/config/full-zlib.cfg @@ -4,8 +4,11 @@ certificate_validity_after_seconds = 1 certificate_validity_before_seconds = 1 entropy_minimum_bits = 2 random_seed_bytes = 3 +server_certificate_validity_before_seconds = 4 +server_certificate_validity_after_seconds = 5 logging_level = DEBUG username_validation = debian +hostname_validation = disabled [Bless CA] us-east-1_password = diff --git a/tests/config/full.cfg b/tests/config/full.cfg index b3d9f7f..1ad8734 100644 --- a/tests/config/full.cfg +++ b/tests/config/full.cfg @@ -4,8 +4,11 @@ certificate_validity_after_seconds = 1 certificate_validity_before_seconds = 1 entropy_minimum_bits = 2 random_seed_bytes = 3 +server_certificate_validity_before_seconds = 4 +server_certificate_validity_after_seconds = 5 logging_level = DEBUG username_validation = debian +hostname_validation = disabled [Bless CA] us-east-1_password = diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index 9f31620..9f49cf4 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -25,7 +25,13 @@ USERNAME_VALIDATION_DEFAULT, \ REMOTE_USERNAMES_VALIDATION_OPTION, \ CA_PRIVATE_KEY_COMPRESSION_OPTION, \ - CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT + CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT, \ + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_DEFAULT, \ + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \ + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_DEFAULT, \ + HOSTNAME_VALIDATION_OPTION, \ + HOSTNAME_VALIDATION_DEFAULT def test_empty_config(): @@ -43,6 +49,7 @@ def test_config_no_password(): config_file=os.path.join(os.path.dirname(__file__), 'full-with-default.cfg')) assert '' == config.getpassword() + def test_wrong_compression_env_key(monkeypatch): extra_environment_variables = { 'bless_ca_default_password': '', @@ -61,6 +68,7 @@ def test_wrong_compression_env_key(monkeypatch): assert "Compression lzh is not supported." == str(e.value) + def test_none_compression_env_key(monkeypatch): extra_environment_variables = { 'bless_ca_default_password': '', @@ -76,6 +84,7 @@ def test_none_compression_env_key(monkeypatch): assert b'' == config.getprivatekey() + def test_zlib_positive_compression(monkeypatch): extra_environment_variables = { 'bless_ca_default_password': '', @@ -91,6 +100,7 @@ def test_zlib_positive_compression(monkeypatch): assert b'' == config.getprivatekey() + def test_zlib_compression_env_with_uncompressed_key(monkeypatch): extra_environment_variables = { 'bless_ca_default_password': '', @@ -107,10 +117,14 @@ def test_zlib_compression_env_with_uncompressed_key(monkeypatch): with pytest.raises(zlib.error) as e: config.getprivatekey() + 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', @@ -136,11 +150,14 @@ def test_config_environment_override(monkeypatch): 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 '' == config.getpassword() @@ -156,43 +173,59 @@ def test_config_environment_override(monkeypatch): @pytest.mark.parametrize( - "config,region,expected_cert_valid,expected_entropy_min,expected_rand_seed,expected_log_level," - "expected_password,expected_username_validation,expected_key_compression", [ + "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_key_compression", + [ ((os.path.join(os.path.dirname(__file__), 'minimal.cfg')), 'us-west-2', - CERTIFICATE_VALIDITY_SEC_DEFAULT, ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, + CERTIFICATE_VALIDITY_SEC_DEFAULT, + ENTROPY_MINIMUM_BITS_DEFAULT, RANDOM_SEED_BYTES_DEFAULT, + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_DEFAULT, + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_DEFAULT, LOGGING_LEVEL_DEFAULT, '', USERNAME_VALIDATION_DEFAULT, + HOSTNAME_VALIDATION_DEFAULT, CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT ), ((os.path.join(os.path.dirname(__file__), 'full-zlib.cfg')), 'us-west-2', - 1, 2, 3, 'DEBUG', + 1, 2, 3, 4, 5, 'DEBUG', '', 'debian', + 'disabled', 'zlib' ), ((os.path.join(os.path.dirname(__file__), 'full.cfg')), 'us-east-1', - 1, 2, 3, 'DEBUG', + 1, 2, 3, 4, 5, 'DEBUG', '', 'debian', + 'disabled', 'zlib' ) ]) 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): + expected_host_cert_before_valid, expected_host_cert_after_valid, + expected_log_level, expected_password, expected_username_validation, + expected_hostname_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_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_key_compression == config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_COMPRESSION_OPTION) diff --git a/tests/request/test_bless_request_host.py b/tests/request/test_bless_request_host.py new file mode 100644 index 0000000..617ec02 --- /dev/null +++ b/tests/request/test_bless_request_host.py @@ -0,0 +1,49 @@ +import pytest +from bless.request.bless_request import validate_hostname, HOSTNAME_VALIDATION_OPTIONS, BlessHostSchema +from marshmallow import ValidationError + + +@pytest.mark.parametrize("test_input", [ + 'thisthat', + 'this.that', +]) +def test_validate_hostnames(test_input): + validate_hostname(test_input, HOSTNAME_VALIDATION_OPTIONS.url) + + +@pytest.mark.parametrize("test_input", [ + 'this..that', + ['thisthat'], + 'this!that.com' +]) +def test_invalid_hostnames(test_input): + with pytest.raises(ValidationError) as e: + validate_hostname(test_input, HOSTNAME_VALIDATION_OPTIONS.url) + assert str(e.value) == 'Invalid hostname "ssh://{}".'.format(test_input) + + +@pytest.mark.parametrize("test_input", [ + 'this..that', + ['thisthat'], + 'this!that.com', + 'this,that' +]) +def test_invalid_hostnames_with_disabled(test_input): + validate_hostname(test_input, HOSTNAME_VALIDATION_OPTIONS.disabled) + + +@pytest.mark.parametrize("test_input", [ + 'thisthat,this.that', + 'this.that,thishostname' +]) +def test_valid_multiple_hostnames(test_input): + BlessHostSchema().validate_hostnames(test_input) + + +@pytest.mark.parametrize("test_input", [ + 'thisthat, this.that', +]) +def test_invalid_multiple_hostnames(test_input): + with pytest.raises(ValidationError) as e: + BlessHostSchema().validate_hostnames(test_input) + assert str(e.value) == 'Invalid hostname "ssh:// this.that".' From 910f8f9fb6bc4751f8d069c77e1962f73ea827ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Paulo=20K=C3=B6ch?= Date: Tue, 23 Apr 2019 17:27:15 +0100 Subject: [PATCH 57/68] Add link to Amazon Linux repository I had to go and discover the right link. I'd like to save that trouble for other readers. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82c18fd..e1134b6 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ all of the dependencies. AWS Lambda only supports Python 2.7 and BLESS depends [Cryptography](https://cryptography.io/en/latest/), which must be compiled. You will need to compile and include your dependencies before you can publish a working AWS Lambda. -You can use a docker container running amazon linux: +You can use a docker container running [Amazon Linux](https://hub.docker.com/_/amazonlinux): - Execute ```make lambda-deps``` and this will run a container and save all the dependencies in ./aws_lambda_libs Alternatively you can: From f04f83ada8251f5e7beefe78ff4db76f86888b34 Mon Sep 17 00:00:00 2001 From: Alessandro Siragusa Date: Wed, 31 Oct 2018 15:52:36 +0100 Subject: [PATCH 58/68] Remove the -it flag from lambda-deps docker build The flag is not needed and breaks scripts if the input device does not have a TTY --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 3b365d0..9fc379b 100644 --- a/Makefile +++ b/Makefile @@ -48,6 +48,6 @@ compile: lambda-deps: @echo "--> Compiling lambda dependencies" - docker run --rm -it -v ${CURDIR}:/src -w /src amazonlinux:1 make compile + docker run --rm -v ${CURDIR}:/src -w /src amazonlinux:1 make compile .PHONY: develop dev-docs clean test lint coverage publish From a7b454a528aebc0c5c88fc412c647a81920d3dd8 Mon Sep 17 00:00:00 2001 From: Paolo de Dios Date: Thu, 14 Feb 2019 14:19:29 -0800 Subject: [PATCH 59/68] Fix boolean value check on KMSAUTH_SECTION options --- bless/aws_lambda/bless_lambda.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 51a603a..562d853 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -142,7 +142,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, entropy_ch bypass_time_validity_check = False # Authenticate the user with KMS, if key is setup - if config.get(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION): + 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) @@ -154,7 +154,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None, entropy_ch 'unallowed remote_usernames [{}]'.format(request.remote_usernames)) # Check if the user is in the required IAM groups - if config.get(KMSAUTH_SECTION, VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION): + 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) From 5d92a03d39ab08a71657bdf35dd55d4c1d3a828f Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Mon, 20 May 2019 13:49:56 -0700 Subject: [PATCH 60/68] Updating code and dependencies to run as a Python 3.7 lambda with the latest Amazon Linux. --- .travis.yml | 2 +- Makefile | 10 +++----- README.md | 28 +++------------------ bless/config/bless_config.py | 2 +- bless/request/bless_request.py | 4 +-- bless/ssh/public_keys/ed25519_public_key.py | 6 ++--- lambda_compile.sh | 8 ++++++ requirements.txt | 22 ++++++++-------- tests/config/test_bless_config.py | 2 +- tests/request/test_bless_request.py | 6 ++--- tests/ssh/test_ssh_certificate_rsa.py | 2 +- 11 files changed, 37 insertions(+), 55 deletions(-) create mode 100755 lambda_compile.sh diff --git a/.travis.yml b/.travis.yml index 6b8f3ce..5c79b4a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ addons: matrix: include: - - python: "3.6" + - python: "3.7" install: - pip install coveralls diff --git a/Makefile b/Makefile index 9fc379b..bd9ed9e 100644 --- a/Makefile +++ b/Makefile @@ -5,6 +5,7 @@ test: lint develop: @echo "--> Installing dependencies" + pip install --upgrade pip setuptools pip install -r requirements.txt pip install "file://`pwd`#egg=bless[tests]" @echo "" @@ -39,15 +40,10 @@ publish: cd ./publish/bless_lambda && zip -FSr ../bless_lambda.zip . compile: - yum install -y gcc libffi-devel openssl-devel python36 python36-virtualenv - virtualenv-3.6 /tmp/venv - /tmp/venv/bin/pip install --upgrade pip setuptools - /tmp/venv/bin/pip install -e . - cp -r /tmp/venv/lib/python3.6/site-packages/. ./aws_lambda_libs - cp -r /tmp/venv/lib64/python3.6/site-packages/. ./aws_lambda_libs + ./lambda_compile.sh lambda-deps: @echo "--> Compiling lambda dependencies" - docker run --rm -v ${CURDIR}:/src -w /src amazonlinux:1 make compile + docker run --rm -v ${CURDIR}:/src -w /src amazonlinux:2 ./lambda_compile.sh .PHONY: develop dev-docs clean test lint coverage publish diff --git a/README.md b/README.md index e1134b6..4285e91 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Cd to the bless repo: Create a virtualenv if you haven't already: - $ python3.6 -m venv venv + $ python3.7 -m venv venv Activate the venv: @@ -55,34 +55,12 @@ Makefile includes a publish target to package up everything into a deploy-able . the expected locations. ### Compiling BLESS Lambda Dependencies -AWS Lambda has some limitations, and to deploy code as a Lambda Function, you need to package up -all of the dependencies. AWS Lambda only supports Python 2.7 and BLESS depends on -[Cryptography](https://cryptography.io/en/latest/), which must be compiled. You will need to +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. -You can use a docker container running [Amazon Linux](https://hub.docker.com/_/amazonlinux): +BLESS uses a docker container running [Amazon Linux 2](https://hub.docker.com/_/amazonlinux) to package everything up: - Execute ```make lambda-deps``` and this will run a container and save all the dependencies in ./aws_lambda_libs -Alternatively you can: -- Deploy an [Amazon Linux AMI](http://docs.aws.amazon.com/lambda/latest/dg/current-supported-versions.html) -- SSH onto that instance -- Copy BLESS' `setup.py` to the instance -- Copy BLESS' `bless/__about__.py` to the instance at `bless/__about__.py` -- Install BLESS' dependencies: -``` -$ sudo yum install gcc libffi-devel openssl-devel -$ virtualenv venv -$ source venv/bin/activate -(venv) $ pip install --upgrade pip setuptools -(venv) $ pip install -e . -``` -- From that instance, copy off the contents of: -``` -$ cp -r venv/lib/python2.7/site-packages/. aws_lambda_libs -$ cp -r venv/lib64/python2.7/site-packages/. aws_lambda_libs -``` -- put those files in: ./aws_lambda_libs/ - ### Protecting the CA Private Key - Generate a password protected RSA Private Key: ``` diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index a839776..492bace 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -185,7 +185,7 @@ def get(self, section, option, **kwargs): @staticmethod def _environment_key(section, option): - return (re.sub('\W+', '_', section) + '_' + re.sub('\W+', '_', option)).lower() + return (re.sub(r'\W+', '_', section) + '_' + re.sub(r'\W+', '_', option)).lower() @staticmethod def _decompress(data, algorithm): diff --git a/bless/request/bless_request.py b/bless/request/bless_request.py index e70951f..a8cc04b 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request.py @@ -16,7 +16,7 @@ REMOTE_USERNAMES_BLACKLIST_DEFAULT # man 8 useradd -USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z') +USERNAME_PATTERN = re.compile(r'[a-z_][a-z0-9_-]*[$]?\Z') # debian # On Debian, the only constraints are that usernames must neither start @@ -24,7 +24,7 @@ # (':'), a comma (','), or a whitespace (space: ' ', end of line: '\n', # tabulation: '\t', etc.). Note that using a slash ('/') may break the # default algorithm for the definition of the user's home directory. -USERNAME_PATTERN_DEBIAN = re.compile('\A[^-+~][^:,\s]*\Z') +USERNAME_PATTERN_DEBIAN = re.compile(r'\A[^-+~][^:,\s]*\Z') # It appears that most printable ascii is valid, excluding whitespace, #, and commas. # There doesn't seem to be any practical size limits of an SSH Certificate Principal (> 4096B allowed). diff --git a/bless/ssh/public_keys/ed25519_public_key.py b/bless/ssh/public_keys/ed25519_public_key.py index 7d054c6..5199e14 100644 --- a/bless/ssh/public_keys/ed25519_public_key.py +++ b/bless/ssh/public_keys/ed25519_public_key.py @@ -7,7 +7,7 @@ import hashlib from bless.ssh.public_keys.ssh_public_key import SSHPublicKey, SSHPublicKeyType -from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.serialization import ssh class ED25519PublicKey(SSHPublicKey): @@ -46,7 +46,7 @@ def __init__(self, ssh_public_key): except TypeError: raise ValueError('Key is not in the proper format.') - inner_key_type, rest = serialization._ssh_read_next_string(decoded_data) + inner_key_type, rest = ssh._ssh_read_next_string(decoded_data) if inner_key_type != key_type.encode("utf-8"): raise ValueError( @@ -54,7 +54,7 @@ def __init__(self, ssh_public_key): ) # ed25519 public key is a single string https://tools.ietf.org/html/rfc8032#section-5.1.5 - self.a, rest = serialization._ssh_read_next_string(rest) + self.a, rest = ssh._ssh_read_next_string(rest) key_bytes = base64.b64decode(split_ssh_public_key[1]) fingerprint = hashlib.md5(key_bytes).hexdigest() diff --git a/lambda_compile.sh b/lambda_compile.sh new file mode 100755 index 0000000..dd35d83 --- /dev/null +++ b/lambda_compile.sh @@ -0,0 +1,8 @@ +#!/bin/sh + +yum install -y python37 +python3.7 -m venv /tmp/venv +/tmp/venv/bin/pip install --upgrade pip setuptools +/tmp/venv/bin/pip install -e . +cp -r /tmp/venv/lib/python3.7/site-packages/. ./aws_lambda_libs +cp -r /tmp/venv/lib64/python3.7/site-packages/. ./aws_lambda_libs diff --git a/requirements.txt b/requirements.txt index e553eb2..6ff53c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,16 +1,16 @@ -e . asn1crypto==0.24.0 -boto3==1.7.61 -botocore==1.10.61 -cffi==1.11.5 -cryptography==2.3 +boto3==1.9.151 +botocore==1.12.151 +cffi==1.12.3 +cryptography==2.6.1 docutils==0.14 -idna==2.7 ipaddress==1.0.22 -jmespath==0.9.3 +jmespath==0.9.4 kmsauth==0.3.0 -marshmallow==2.15.3 -pycparser==2.18 -python-dateutil==2.7.3 -s3transfer==0.1.13 -six==1.11.0 +marshmallow==2.19.2 +pycparser==2.19 +python-dateutil==2.8.0 +s3transfer==0.2.0 +six==1.12.0 +urllib3==1.24.3 diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index 9f31620..2cad686 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -95,7 +95,7 @@ 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''), + 'bless_ca_ca_private_key': str(base64.b64encode(b''), encoding='ascii'), } for k, v in extra_environment_variables.items(): diff --git a/tests/request/test_bless_request.py b/tests/request/test_bless_request.py index 197945d..6fda58b 100644 --- a/tests/request/test_bless_request.py +++ b/tests/request/test_bless_request.py @@ -100,7 +100,7 @@ def test_validate_user_debian(test_input): @pytest.mark.parametrize("test_input", [ ('uservalid'), ('a32characterusernameyoumustok$'), - ('!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~') + ('!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~') ]) def test_validate_user_principal(test_input): validate_user(test_input, USERNAME_VALIDATION_OPTIONS.principal) @@ -128,7 +128,7 @@ def test_invalid_user_email(test_input): @pytest.mark.parametrize("test_input", [ ('a33characterusernameyoumustbenuts'), ('~:, \n\t@'), - ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~,'), + ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~,'), ]) def test_validate_user_disabled(test_input): validate_user(test_input, USERNAME_VALIDATION_OPTIONS.disabled) @@ -137,7 +137,7 @@ def test_validate_user_disabled(test_input): @pytest.mark.parametrize("test_input", [ ('uservalid'), ('uservalid,uservalid2'), - ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~,' + ('uservalid,!"$%&\'()*+-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~,' 'uservalid2') ]) def test_validate_multiple_principals(test_input): diff --git a/tests/ssh/test_ssh_certificate_rsa.py b/tests/ssh/test_ssh_certificate_rsa.py index 41bcced..ee7033a 100644 --- a/tests/ssh/test_ssh_certificate_rsa.py +++ b/tests/ssh/test_ssh_certificate_rsa.py @@ -1,7 +1,7 @@ import base64 import pytest -from cryptography.hazmat.primitives.serialization import _ssh_read_next_string +from cryptography.hazmat.primitives.serialization.ssh import _ssh_read_next_string from bless.ssh.certificate_authorities.rsa_certificate_authority import RSACertificateAuthority from bless.ssh.certificates.rsa_certificate_builder import RSACertificateBuilder From 7ca78b48d8441f8528adb07305981b54b7ff61bf Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Mon, 20 May 2019 14:14:48 -0700 Subject: [PATCH 61/68] Resolving https://github.com/Netflix/bless/pull/80 . --- README.md | 4 +++- bless_client/bless_client.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4285e91..90ba672 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Run the tests: To deploy an AWS Lambda Function, you need to provide a .zip with the code and all dependencies. The .zip must contain your lambda code and configurations at the top level of the .zip. The BLESS Makefile includes a publish target to package up everything into a deploy-able .zip if they are in -the expected locations. +the expected locations. You will need to setup your own Python 3.7 lambda to deploy the .zip to. ### Compiling BLESS Lambda Dependencies To deploy code as a Lambda Function, you need to package up all of the dependencies. You will need to @@ -140,6 +140,8 @@ random from kms (kms:GenerateRandom) and permissions for logging to CloudWatch L ## Using BLESS After you have [deployed BLESS](#deployment) you can run the sample [BLESS Client](bless_client/bless_client.py) from a system with access to the required [AWS Credentials](http://boto3.readthedocs.io/en/latest/guide/configuration.html). +This client is really just a proof of concept to validate that you have a functional lambda being called with valid +IAM credentials. (venv) $ ./bless_client.py region lambda_function_name bastion_user bastion_user_ip remote_usernames bastion_source_ip bastion_command diff --git a/bless_client/bless_client.py b/bless_client/bless_client.py index bb967d2..b96f56e 100755 --- a/bless_client/bless_client.py +++ b/bless_client/bless_client.py @@ -32,16 +32,16 @@ obtained by appending -cert.pub to identity filenames" e.g. the . """ import json +import os import stat import sys import boto3 -import os def main(argv): if len(argv) < 9 or len(argv) > 10: - print ( + print( 'Usage: bless_client.py region lambda_function_name bastion_user bastion_user_ip ' 'remote_usernames bastion_ips bastion_command ' ' [kmsauth token]') @@ -71,7 +71,7 @@ def main(argv): print('{}\n'.format(response['ResponseMetadata'])) if response['StatusCode'] != 200: - print ('Error creating cert.') + print('Error creating cert.') return -1 payload = json.loads(response['Payload'].read()) From cad1dbfd75104eafd9577322af8d0754fad2de56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dar=C3=ADo=20Here=C3=B1=C3=BA?= Date: Fri, 2 Nov 2018 21:51:33 -0300 Subject: [PATCH 62/68] Typo on #133 * Plus minor formatting proposals --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 90ba672..b4d2989 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,7 @@ included documentation. - Manage your bless_deploy.cfg files outside of this repo. - Provide your desired ./lambda_configs/bless_deploy.cfg prior to Publishing a new Lambda .zip - The required [Bless CA] option values must be set for your environment. -- Every option can be changed in the environment. The environment variable name is contructed +- Every option can be changed in the environment. The environment variable name is constructed as section_name_option_name (all lowercase, spaces replaced with underscores). ### Publish Lambda .zip @@ -152,11 +152,11 @@ You can inspect the contents of a certificate with ssh-keygen directly: $ ssh-keygen -L -f your-cert.pub ## Enabling BLESS Certificates On Servers -Add the following line to /etc/ssh/sshd_config: +Add the following line to `/etc/ssh/sshd_config`: TrustedUserCAKeys /etc/ssh/cas.pub -Add a new file, owned by and only writable by root, at /etc/ssh/cas.pub with the contents: +Add a new file, owned by and only writable by root, at `/etc/ssh/cas.pub` with the contents: ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… #id_rsa.pub of an SSH CA ssh-rsa AAAAB3NzaC1yc2EAAAADAQ… #id_rsa.pub of an offline SSH CA From 68a45d11ad1870de4163757cb25766b7f20d2c94 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Mon, 20 May 2019 15:25:26 -0700 Subject: [PATCH 63/68] Removing the Travis sudo tag. https://github.com/Netflix/bless/issues/87 --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5c79b4a..3464219 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,3 @@ -sudo: false - language: python addons: From 9a310caa7655e17479b38e72caa80f146cf24eb9 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Mon, 20 May 2019 16:29:20 -0700 Subject: [PATCH 64/68] Additional fixes after https://github.com/Netflix/bless/pull/85 . Travis is building again. --- bless/config/bless_config.py | 4 ++-- tests/config/full-with-kmsauth.cfg | 22 ++++++++++++++++++++++ tests/config/test_bless_config.py | 12 +++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 tests/config/full-with-kmsauth.cfg diff --git a/bless/config/bless_config.py b/bless/config/bless_config.py index 492bace..3b87e05 100644 --- a/bless/config/bless_config.py +++ b/bless/config/bless_config.py @@ -45,7 +45,7 @@ KMSAUTH_SECTION = 'KMS Auth' KMSAUTH_USEKMSAUTH_OPTION = 'use_kmsauth' -KMSAUTH_USEKMSAUTH_DEFAULT = False +KMSAUTH_USEKMSAUTH_DEFAULT = "False" KMSAUTH_KEY_ID_OPTION = 'kmsauth_key_id' KMSAUTH_KEY_ID_DEFAULT = '' @@ -63,7 +63,7 @@ REMOTE_USERNAMES_VALIDATION_DEFAULT = 'principal' VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION = 'kmsauth_validate_remote_usernames_against_iam_groups' -VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_DEFAULT = False +VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_DEFAULT = "False" IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION = 'kmsauth_iam_group_name_format' IAM_GROUP_NAME_VALIDATION_FORMAT_DEFAULT = 'ssh-{}' diff --git a/tests/config/full-with-kmsauth.cfg b/tests/config/full-with-kmsauth.cfg new file mode 100644 index 0000000..cc4a832 --- /dev/null +++ b/tests/config/full-with-kmsauth.cfg @@ -0,0 +1,22 @@ +[Bless Options] +# The default values are sane, these are not. +certificate_validity_after_seconds = 1 +certificate_validity_before_seconds = 1 +entropy_minimum_bits = 2 +random_seed_bytes = 3 +logging_level = DEBUG +username_validation = debian + +[Bless CA] +us-east-1_password = +us-west-2_password = +ca_private_key_file = +ca_private_key_compression = zlib + +[KMS Auth] +use_kmsauth = True +kmsauth_key_id = alias/authnz-iad, alias/authnz-sfo +kmsauth_serviceid = kmsauth-prod +kmsauth_remote_usernames_allowed = ubuntu,alloweduser +kmsauth_validate_remote_usernames_against_iam_groups = False +kmsauth_iam_group_name_format = ssh-{} \ No newline at end of file diff --git a/tests/config/test_bless_config.py b/tests/config/test_bless_config.py index 2cad686..fee9289 100644 --- a/tests/config/test_bless_config.py +++ b/tests/config/test_bless_config.py @@ -25,7 +25,7 @@ USERNAME_VALIDATION_DEFAULT, \ REMOTE_USERNAMES_VALIDATION_OPTION, \ CA_PRIVATE_KEY_COMPRESSION_OPTION, \ - CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT + CA_PRIVATE_KEY_COMPRESSION_OPTION_DEFAULT, VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION def test_empty_config(): @@ -196,3 +196,13 @@ def test_configs(config, region, expected_cert_valid, expected_entropy_min, expe USERNAME_VALIDATION_OPTION) assert expected_key_compression == config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_COMPRESSION_OPTION) + +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 From 36fc01b8931976d640c50ec3d6cdc694376abf74 Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Mon, 20 May 2019 16:56:12 -0700 Subject: [PATCH 65/68] Updating readme to indicate that only PEM private keys are supported. https://github.com/Netflix/bless/issues/86 --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b4d2989..bbcd6af 100644 --- a/README.md +++ b/README.md @@ -62,10 +62,11 @@ BLESS uses a docker container running [Amazon Linux 2](https://hub.docker.com/_/ - Execute ```make lambda-deps``` and this will run a container and save all the dependencies in ./aws_lambda_libs ### Protecting the CA Private Key -- Generate a password protected RSA Private Key: +- Generate a password protected RSA Private Key in the PEM format: ``` -$ ssh-keygen -t rsa -b 4096 -f bless-ca- -C "SSH CA Key" +$ ssh-keygen -t rsa -b 4096 -m PEM -f bless-ca- -C "SSH CA Key" ``` +- **Note:** OpenSSH Private Key format is not supported. - Use KMS to encrypt your password. You will need a KMS key per region, and you will need to encrypt your password for each region. You can use the AWS Console to paste in a simple lambda function like this: From 3d8b0c946f4fb48b5777d285593667565ac5d95f Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Tue, 21 May 2019 14:57:49 -0700 Subject: [PATCH 66/68] Refactored https://github.com/Netflix/bless/pull/79 and split out user and host lambda handlers and request schemas. 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. --- Makefile | 2 +- README.md | 8 + bless/aws_lambda/bless_lambda.py | 347 +----------------- bless/aws_lambda/bless_lambda_common.py | 75 ++++ bless/aws_lambda/bless_lambda_host.py | 102 +++++ bless/aws_lambda/bless_lambda_user.py | 193 ++++++++++ bless/cache/bless_lambda_cache.py | 5 + bless/config/bless_deploy_example.cfg | 2 +- bless/request/bless_request_common.py | 18 + bless/request/bless_request_host.py | 62 ++++ ...bless_request.py => bless_request_user.py} | 77 +--- tests/aws_lambda/test_bless_lambda_host.py | 25 +- tests/aws_lambda/test_bless_lambda_user.py | 3 +- tests/request/test_bless_request_host.py | 2 +- tests/request/test_bless_request_user.py | 2 +- 15 files changed, 489 insertions(+), 434 deletions(-) create mode 100644 bless/aws_lambda/bless_lambda_common.py create mode 100644 bless/aws_lambda/bless_lambda_host.py create mode 100644 bless/aws_lambda/bless_lambda_user.py create mode 100644 bless/request/bless_request_common.py create mode 100644 bless/request/bless_request_host.py rename bless/request/{bless_request.py => bless_request_user.py} (71%) diff --git a/Makefile b/Makefile index bd9ed9e..a50356d 100644 --- a/Makefile +++ b/Makefile @@ -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 . diff --git a/README.md b/README.md index bbcd6af..875c698 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/bless/aws_lambda/bless_lambda.py b/bless/aws_lambda/bless_lambda.py index 7ac6519..f04e894 100644 --- a/bless/aws_lambda/bless_lambda.py +++ b/bless/aws_lambda/bless_lambda.py @@ -3,43 +3,7 @@ :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ - -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, \ - HOSTNAME_VALIDATION_OPTION, \ - SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ - SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION -from bless.request.bless_request import BlessHostSchema, BlessUserSchema -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 +from bless.aws_lambda.bless_lambda_user import lambda_handler_user def lambda_handler(*args, **kwargs): @@ -47,312 +11,3 @@ def lambda_handler(*args, **kwargs): Wrapper around lambda_handler_user for backwards compatibility """ return lambda_handler_user(*args, **kwargs) - - -def lambda_handler_user( - event, context=None, ca_private_key_password=None, - entropy_check=True, - config_file=None): - """ - This is the function that will be called when the lambda function starts. - :param event: Dictionary of the json request. - :param context: AWS LambdaContext Object - http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html - :param ca_private_key_password: For local testing, if the password is provided, skip the KMS - decrypt. - :param entropy_check: For local testing, if set to false, it will skip checking entropy and - won't try to fetch additional random from KMS. - :param config_file: The config file to load the SSH CA private key from, and additional settings. - :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file. - """ - bless_cache = setup_lambda_cache(ca_private_key_password, config_file) - - # AWS Region determines configs related to KMS - region = bless_cache.region - - # Load the deployment config values - config = bless_cache.config - - logger = set_logger(config) - - certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION, - CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) - certificate_validity_after_seconds = config.getint(BLESS_OPTIONS_SECTION, - CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) - ca_private_key = config.getprivatekey() - certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION) - - # Process cert request - schema = BlessUserSchema(strict=True) - schema.context[USERNAME_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION) - schema.context[REMOTE_USERNAMES_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, - REMOTE_USERNAMES_VALIDATION_OPTION) - schema.context[REMOTE_USERNAMES_BLACKLIST_OPTION] = config.get(BLESS_OPTIONS_SECTION, - REMOTE_USERNAMES_BLACKLIST_OPTION) - - try: - request = schema.load(event).data - except ValidationError as e: - return error_response('InputValidationError', str(e)) - - logger.info('Bless lambda invoked by [user: {0}, bastion_ips:{1}, public_key: {2}, kmsauth_token:{3}]'.format( - request.bastion_user, - request.bastion_user_ip, - request.public_key_to_sign, - request.kmsauth_token)) - - # Make sure we have the ca private key password - if bless_cache.ca_private_key_password is None: - return error_response('ClientError', bless_cache.ca_private_key_password_error) - else: - ca_private_key_password = bless_cache.ca_private_key_password - - # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired - if entropy_check: - check_entropy(config, logger) - - # cert values determined only by lambda and its configs - current_time = int(time.time()) - test_user = config.get(BLESS_OPTIONS_SECTION, TEST_USER_OPTION) - if test_user and (request.bastion_user == test_user or request.remote_usernames == test_user): - # This is a test call, the lambda will issue an invalid - # certificate where valid_before < valid_after - valid_before = current_time - valid_after = current_time + 1 - bypass_time_validity_check = True - else: - valid_before = current_time + certificate_validity_after_seconds - valid_after = current_time - certificate_validity_before_seconds - bypass_time_validity_check = False - - # Authenticate the user with KMS, if key is setup - if config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION): - if request.kmsauth_token: - # Allow bless to sign the cert for a different remote user than the name of the user who signed it - allowed_remotes = config.get(KMSAUTH_SECTION, KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION) - if allowed_remotes: - allowed_users = allowed_remotes.split(',') - requested_remotes = request.remote_usernames.split(',') - if allowed_users != ['*'] and not all([u in allowed_users for u in requested_remotes]): - return error_response('KMSAuthValidationError', - 'unallowed remote_usernames [{}]'.format(request.remote_usernames)) - - # Check if the user is in the required IAM groups - if config.getboolean(KMSAUTH_SECTION, VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION): - iam = boto3.client('iam') - user_groups = iam.list_groups_for_user(UserName=request.bastion_user) - - group_name_template = config.get(KMSAUTH_SECTION, IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION) - for requested_remote in requested_remotes: - required_group_name = group_name_template.format(requested_remote) - - user_is_in_group = any( - group - for group in user_groups['Groups'] - if group['GroupName'] == required_group_name - ) - - if not user_is_in_group: - return error_response('KMSAuthValidationError', - 'user {} is not in the {} iam group'.format(request.bastion_user, - required_group_name)) - - elif request.remote_usernames != request.bastion_user: - return error_response('KMSAuthValidationError', - 'remote_usernames must be the same as bastion_user') - try: - validator = KMSTokenValidator( - None, - config.getkmsauthkeyids(), - config.get(KMSAUTH_SECTION, KMSAUTH_SERVICE_ID_OPTION), - region - ) - # decrypt_token will raise a TokenValidationError if token doesn't match - validator.decrypt_token( - "2/user/{}".format(request.bastion_user), - request.kmsauth_token - ) - except TokenValidationError as e: - return error_response('KMSAuthValidationError', str(e)) - else: - return error_response('InputValidationError', 'Invalid request, missing kmsauth token') - - # Build the cert - ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) - cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.USER, - request.public_key_to_sign) - for username in request.remote_usernames.split(','): - cert_builder.add_valid_principal(username) - - cert_builder.set_valid_before(valid_before) - cert_builder.set_valid_after(valid_after) - - if certificate_extensions: - for e in certificate_extensions.split(','): - if e: - cert_builder.add_extension(e) - else: - cert_builder.clear_extensions() - - # cert_builder is needed to obtain the SSH public key's fingerprint - key_id = 'request[{}] for[{}] from[{}] command[{}] ssh_key[{}] ca[{}] valid_to[{}]'.format( - context.aws_request_id, request.bastion_user, request.bastion_user_ip, request.command, - cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn, - time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before))) - cert_builder.set_critical_option_source_addresses(request.bastion_ips) - cert_builder.set_key_id(key_id) - cert = cert_builder.get_cert_file(bypass_time_validity_check) - - logger.info( - 'Issued a cert to bastion_ips[{}] for remote_usernames[{}] with key_id[{}] and ' - 'valid_from[{}])'.format( - request.bastion_ips, request.remote_usernames, key_id, - time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) - return success_response(cert) - - -def lambda_handler_host( - event, context=None, ca_private_key_password=None, - entropy_check=True, - config_file=None): - """ - This is the function that will be called when the lambda function starts. - :param event: Dictionary of the json request. - :param context: AWS LambdaContext Object - http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html - :param ca_private_key_password: For local testing, if the password is provided, skip the KMS - decrypt. - :param entropy_check: For local testing, if set to false, it will skip checking entropy and - won't try to fetch additional random from KMS. - :param config_file: The config file to load the SSH CA private key from, and additional settings. - :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file. - """ - bless_cache = setup_lambda_cache(ca_private_key_password, config_file) - - # Load the deployment config values - config = bless_cache.config - - logger = set_logger(config) - - certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION, - SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) - certificate_validity_after_seconds = config.getint(BLESS_OPTIONS_SECTION, - SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) - - ca_private_key = config.getprivatekey() - - # Process cert request - schema = BlessHostSchema(strict=True) - schema.context[HOSTNAME_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, HOSTNAME_VALIDATION_OPTION) - - try: - request = schema.load(event).data - except ValidationError as e: - return error_response('InputValidationError', str(e)) - - # todo: You'll want to bring your own hostnames validation. - logger.info('Bless lambda invoked by [public_key: {}] for hostnames[{}]'.format(request.public_key_to_sign, - request.hostnames)) - - # Make sure we have the ca private key password - if bless_cache.ca_private_key_password is None: - return error_response('ClientError', bless_cache.ca_private_key_password_error) - else: - ca_private_key_password = bless_cache.ca_private_key_password - - # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired - if entropy_check: - check_entropy(config, logger) - - # cert values determined only by lambda and its configs - current_time = int(time.time()) - valid_before = current_time + certificate_validity_after_seconds - valid_after = current_time - certificate_validity_before_seconds - - # Build the cert - ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) - cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.HOST, - request.public_key_to_sign) - - for hostname in request.hostnames.split(','): - cert_builder.add_valid_principal(hostname) - - cert_builder.set_valid_before(valid_before) - cert_builder.set_valid_after(valid_after) - - # cert_builder is needed to obtain the SSH public key's fingerprint - key_id = 'request[{}] ssh_key[{}] ca[{}] valid_to[{}]'.format( - context.aws_request_id, cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn, - time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before)) - ) - - cert_builder.set_key_id(key_id) - cert = cert_builder.get_cert_file() - - logger.info( - 'Issued a server cert to hostnames[{}] with key_id[{}] and ' - 'valid_from[{}])'.format( - request.hostnames, key_id, - time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) - return success_response(cert) - - -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.path.dirname(__file__), 'bless_deploy.cfg')) - bless_cache = global_bless_cache - else: - bless_cache = global_bless_cache - return bless_cache diff --git a/bless/aws_lambda/bless_lambda_common.py b/bless/aws_lambda/bless_lambda_common.py new file mode 100644 index 0000000..41ccbf0 --- /dev/null +++ b/bless/aws_lambda/bless_lambda_common.py @@ -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 diff --git a/bless/aws_lambda/bless_lambda_host.py b/bless/aws_lambda/bless_lambda_host.py new file mode 100644 index 0000000..91ff1ab --- /dev/null +++ b/bless/aws_lambda/bless_lambda_host.py @@ -0,0 +1,102 @@ +""" +.. module: bless.aws_lambda.bless_lambda_host + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import time + +from bless.aws_lambda.bless_lambda_common import success_response, error_response, set_logger, check_entropy, \ + setup_lambda_cache +from bless.config.bless_config import BLESS_OPTIONS_SECTION, SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, HOSTNAME_VALIDATION_OPTION +from bless.request.bless_request_host import 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 +from marshmallow import ValidationError + + +def lambda_handler_host( + event, context=None, ca_private_key_password=None, + entropy_check=True, + config_file=None): + """ + This is the function that will be called when the lambda function starts. + :param event: Dictionary of the json request. + :param context: AWS LambdaContext Object + http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + :param ca_private_key_password: For local testing, if the password is provided, skip the KMS + decrypt. + :param entropy_check: For local testing, if set to false, it will skip checking entropy and + won't try to fetch additional random from KMS. + :param config_file: The config file to load the SSH CA private key from, and additional settings. + :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file. + """ + bless_cache = setup_lambda_cache(ca_private_key_password, config_file) + + # Load the deployment config values + config = bless_cache.config + + logger = set_logger(config) + + certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION, + SERVER_CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) + certificate_validity_after_seconds = config.getint(BLESS_OPTIONS_SECTION, + SERVER_CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) + + ca_private_key = config.getprivatekey() + + # Process cert request + schema = BlessHostSchema(strict=True) + schema.context[HOSTNAME_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, HOSTNAME_VALIDATION_OPTION) + + try: + request = schema.load(event).data + except ValidationError as e: + return error_response('InputValidationError', str(e)) + + # todo: You'll want to bring your own hostnames validation. + logger.info('Bless lambda invoked by [public_key: {}] for hostnames[{}]'.format(request.public_key_to_sign, + request.hostnames)) + + # Make sure we have the ca private key password + if bless_cache.ca_private_key_password is None: + return error_response('ClientError', bless_cache.ca_private_key_password_error) + else: + ca_private_key_password = bless_cache.ca_private_key_password + + # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired + if entropy_check: + check_entropy(config, logger) + + # cert values determined only by lambda and its configs + current_time = int(time.time()) + valid_before = current_time + certificate_validity_after_seconds + valid_after = current_time - certificate_validity_before_seconds + + # Build the cert + ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) + cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.HOST, + request.public_key_to_sign) + + for hostname in request.hostnames.split(','): + cert_builder.add_valid_principal(hostname) + + cert_builder.set_valid_before(valid_before) + cert_builder.set_valid_after(valid_after) + + # cert_builder is needed to obtain the SSH public key's fingerprint + key_id = 'request[{}] ssh_key[{}] ca[{}] valid_to[{}]'.format( + context.aws_request_id, cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before)) + ) + + cert_builder.set_key_id(key_id) + cert = cert_builder.get_cert_file() + + logger.info( + 'Issued a server cert to hostnames[{}] with key_id[{}] and ' + 'valid_from[{}])'.format( + request.hostnames, key_id, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) + return success_response(cert) diff --git a/bless/aws_lambda/bless_lambda_user.py b/bless/aws_lambda/bless_lambda_user.py new file mode 100644 index 0000000..166bec9 --- /dev/null +++ b/bless/aws_lambda/bless_lambda_user.py @@ -0,0 +1,193 @@ +""" +.. module: bless.aws_lambda.bless_lambda_user + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +import time + +import boto3 +from bless.aws_lambda.bless_lambda_common import success_response, error_response, set_logger, check_entropy, \ + setup_lambda_cache +from bless.config.bless_config import BLESS_OPTIONS_SECTION, \ + CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, \ + CERTIFICATE_VALIDITY_AFTER_SEC_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_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 +from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder +from kmsauth import KMSTokenValidator, TokenValidationError +from marshmallow.exceptions import ValidationError + + +def lambda_handler_user( + event, context=None, ca_private_key_password=None, + entropy_check=True, + config_file=None): + """ + This is the function that will be called when the lambda function starts. + :param event: Dictionary of the json request. + :param context: AWS LambdaContext Object + http://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html + :param ca_private_key_password: For local testing, if the password is provided, skip the KMS + decrypt. + :param entropy_check: For local testing, if set to false, it will skip checking entropy and + won't try to fetch additional random from KMS. + :param config_file: The config file to load the SSH CA private key from, and additional settings. + :return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file. + """ + bless_cache = setup_lambda_cache(ca_private_key_password, config_file) + + # AWS Region determines configs related to KMS + region = bless_cache.region + + # Load the deployment config values + config = bless_cache.config + + logger = set_logger(config) + + certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION, + CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION) + certificate_validity_after_seconds = config.getint(BLESS_OPTIONS_SECTION, + CERTIFICATE_VALIDITY_AFTER_SEC_OPTION) + ca_private_key = config.getprivatekey() + certificate_extensions = config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_EXTENSIONS_OPTION) + + # Process cert request + schema = BlessUserSchema(strict=True) + schema.context[USERNAME_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, USERNAME_VALIDATION_OPTION) + schema.context[REMOTE_USERNAMES_VALIDATION_OPTION] = config.get(BLESS_OPTIONS_SECTION, + REMOTE_USERNAMES_VALIDATION_OPTION) + schema.context[REMOTE_USERNAMES_BLACKLIST_OPTION] = config.get(BLESS_OPTIONS_SECTION, + REMOTE_USERNAMES_BLACKLIST_OPTION) + + try: + request = schema.load(event).data + except ValidationError as e: + return error_response('InputValidationError', str(e)) + + logger.info('Bless lambda invoked by [user: {0}, bastion_ips:{1}, public_key: {2}, kmsauth_token:{3}]'.format( + request.bastion_user, + request.bastion_user_ip, + request.public_key_to_sign, + request.kmsauth_token)) + + # Make sure we have the ca private key password + if bless_cache.ca_private_key_password is None: + return error_response('ClientError', bless_cache.ca_private_key_password_error) + else: + ca_private_key_password = bless_cache.ca_private_key_password + + # if running as a Lambda, we can check the entropy pool and seed it with KMS if desired + if entropy_check: + check_entropy(config, logger) + + # cert values determined only by lambda and its configs + current_time = int(time.time()) + test_user = config.get(BLESS_OPTIONS_SECTION, TEST_USER_OPTION) + if test_user and (request.bastion_user == test_user or request.remote_usernames == test_user): + # This is a test call, the lambda will issue an invalid + # certificate where valid_before < valid_after + valid_before = current_time + valid_after = current_time + 1 + bypass_time_validity_check = True + else: + valid_before = current_time + certificate_validity_after_seconds + valid_after = current_time - certificate_validity_before_seconds + bypass_time_validity_check = False + + # Authenticate the user with KMS, if key is setup + if config.getboolean(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION): + if request.kmsauth_token: + # Allow bless to sign the cert for a different remote user than the name of the user who signed it + allowed_remotes = config.get(KMSAUTH_SECTION, KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION) + if allowed_remotes: + allowed_users = allowed_remotes.split(',') + requested_remotes = request.remote_usernames.split(',') + if allowed_users != ['*'] and not all([u in allowed_users for u in requested_remotes]): + return error_response('KMSAuthValidationError', + 'unallowed remote_usernames [{}]'.format(request.remote_usernames)) + + # Check if the user is in the required IAM groups + if config.getboolean(KMSAUTH_SECTION, VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION): + iam = boto3.client('iam') + user_groups = iam.list_groups_for_user(UserName=request.bastion_user) + + group_name_template = config.get(KMSAUTH_SECTION, IAM_GROUP_NAME_VALIDATION_FORMAT_OPTION) + for requested_remote in requested_remotes: + required_group_name = group_name_template.format(requested_remote) + + user_is_in_group = any( + group + for group in user_groups['Groups'] + if group['GroupName'] == required_group_name + ) + + if not user_is_in_group: + return error_response('KMSAuthValidationError', + 'user {} is not in the {} iam group'.format(request.bastion_user, + required_group_name)) + + elif request.remote_usernames != request.bastion_user: + return error_response('KMSAuthValidationError', + 'remote_usernames must be the same as bastion_user') + try: + validator = KMSTokenValidator( + None, + config.getkmsauthkeyids(), + config.get(KMSAUTH_SECTION, KMSAUTH_SERVICE_ID_OPTION), + region + ) + # decrypt_token will raise a TokenValidationError if token doesn't match + validator.decrypt_token( + "2/user/{}".format(request.bastion_user), + request.kmsauth_token + ) + except TokenValidationError as e: + return error_response('KMSAuthValidationError', str(e)) + else: + return error_response('InputValidationError', 'Invalid request, missing kmsauth token') + + # Build the cert + ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password) + cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.USER, + request.public_key_to_sign) + for username in request.remote_usernames.split(','): + cert_builder.add_valid_principal(username) + + cert_builder.set_valid_before(valid_before) + cert_builder.set_valid_after(valid_after) + + if certificate_extensions: + for e in certificate_extensions.split(','): + if e: + cert_builder.add_extension(e) + else: + cert_builder.clear_extensions() + + # cert_builder is needed to obtain the SSH public key's fingerprint + key_id = 'request[{}] for[{}] from[{}] command[{}] ssh_key[{}] ca[{}] valid_to[{}]'.format( + context.aws_request_id, request.bastion_user, request.bastion_user_ip, request.command, + cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before))) + cert_builder.set_critical_option_source_addresses(request.bastion_ips) + cert_builder.set_key_id(key_id) + cert = cert_builder.get_cert_file(bypass_time_validity_check) + + logger.info( + 'Issued a cert to bastion_ips[{}] for remote_usernames[{}] with key_id[{}] and ' + 'valid_from[{}])'.format( + request.bastion_ips, request.remote_usernames, key_id, + time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after)))) + return success_response(cert) diff --git a/bless/cache/bless_lambda_cache.py b/bless/cache/bless_lambda_cache.py index 2082a38..afc4f1c 100644 --- a/bless/cache/bless_lambda_cache.py +++ b/bless/cache/bless_lambda_cache.py @@ -1,3 +1,8 @@ +""" +.. module: bless.cache.bless_lambda_cache + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" import base64 import os diff --git a/bless/config/bless_deploy_example.cfg b/bless/config/bless_deploy_example.cfg index 95cd8c0..0bc94ef 100644 --- a/bless/config/bless_deploy_example.cfg +++ b/bless/config/bless_deploy_example.cfg @@ -11,7 +11,7 @@ random_seed_bytes = 256 logging_level = INFO # Comma separated list of the SSH Certificate extensions to include. Not specifying this uses the ssh-keygen defaults: # certificate_extensions = permit-X11-forwarding,permit-agent-forwarding,permit-port-forwarding,permit-pty,permit-user-rc -# Username validation options are described in bless_request.py:USERNAME_VALIDATION_OPTIONS +# Username validation options are described in bless_request_user.py:USERNAME_VALIDATION_OPTIONS # Configure how bastion_user names are validated. # username_validation = useradd # Configure how remote_usernames names are validated. diff --git a/bless/request/bless_request_common.py b/bless/request/bless_request_common.py new file mode 100644 index 0000000..97f9451 --- /dev/null +++ b/bless/request/bless_request_common.py @@ -0,0 +1,18 @@ +""" +.. module: bless.request.bless_request_common + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from marshmallow import ValidationError + +VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" +VALID_SSH_ED25519_PUBLIC_KEY_HEADER = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5" + + +def validate_ssh_public_key(public_key): + if public_key.startswith(VALID_SSH_RSA_PUBLIC_KEY_HEADER) or public_key.startswith( + VALID_SSH_ED25519_PUBLIC_KEY_HEADER): + pass + # todo other key types + else: + raise ValidationError('Invalid SSH Public Key.') diff --git a/bless/request/bless_request_host.py b/bless/request/bless_request_host.py new file mode 100644 index 0000000..364316b --- /dev/null +++ b/bless/request/bless_request_host.py @@ -0,0 +1,62 @@ +""" +.. module: bless.request.bless_request_host + :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +""" +from enum import Enum + +from bless.config.bless_config import HOSTNAME_VALIDATION_OPTION, HOSTNAME_VALIDATION_DEFAULT +from bless.request.bless_request_common import validate_ssh_public_key +from marshmallow import Schema, fields, validates_schema, ValidationError, post_load, validates +from marshmallow.validate import URL + +HOSTNAME_VALIDATION_OPTIONS = Enum('HostNameValidationOptions', + 'url ' # Valid url format + 'disabled' # no validation + ) + + +def validate_hostname(hostname, hostname_validation): + if hostname_validation == HOSTNAME_VALIDATION_OPTIONS.disabled: + return + else: + validator = URL(require_tld=False, schemes='ssh', error='Invalid hostname "{input}".') + validator('ssh://{}'.format(hostname)) + + +class BlessHostSchema(Schema): + hostnames = fields.Str(required=True) + public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True) + + @validates_schema(pass_original=True) + def check_unknown_fields(self, data, original_data): + unknown = set(original_data) - set(self.fields) + if unknown: + raise ValidationError('Unknown field', unknown) + + @post_load + def make_bless_request(self, data): + return BlessHostRequest(**data) + + @validates('hostnames') + def validate_hostnames(self, hostnames): + if HOSTNAME_VALIDATION_OPTION in self.context: + hostname_validation = HOSTNAME_VALIDATION_OPTIONS[self.context[HOSTNAME_VALIDATION_OPTION]] + else: + hostname_validation = HOSTNAME_VALIDATION_OPTIONS[HOSTNAME_VALIDATION_DEFAULT] + for hostname in hostnames.split(','): + validate_hostname(hostname, hostname_validation) + + +class BlessHostRequest: + def __init__(self, hostnames, public_key_to_sign): + """ + A BlessRequest must have the following key value pairs to be valid. + :param hostnames: Comma-separated list of hostnames (s) to include in this host certificate. + :param public_key_to_sign: The id_XXX.pub that will be used in the SSH request. This is enforced in the issued certificate. + """ + self.hostnames = hostnames + self.public_key_to_sign = public_key_to_sign + + def __eq__(self, other): + return self.__dict__ == other.__dict__ diff --git a/bless/request/bless_request.py b/bless/request/bless_request_user.py similarity index 71% rename from bless/request/bless_request.py rename to bless/request/bless_request_user.py index ceeb66f..1e31d80 100644 --- a/bless/request/bless_request.py +++ b/bless/request/bless_request_user.py @@ -1,19 +1,19 @@ """ -.. module: bless.request.bless_request +.. module: bless.request.bless_request_user :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ import re - -import ipaddress from enum import Enum -from marshmallow import Schema, fields, post_load, ValidationError, validates_schema -from marshmallow import validates -from marshmallow.validate import Email, URL +import ipaddress from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION, \ USERNAME_VALIDATION_DEFAULT, REMOTE_USERNAMES_VALIDATION_DEFAULT, REMOTE_USERNAMES_BLACKLIST_OPTION, \ - REMOTE_USERNAMES_BLACKLIST_DEFAULT, HOSTNAME_VALIDATION_OPTION, HOSTNAME_VALIDATION_DEFAULT + REMOTE_USERNAMES_BLACKLIST_DEFAULT +from bless.request.bless_request_common import validate_ssh_public_key +from marshmallow import Schema, fields, post_load, ValidationError, validates_schema +from marshmallow import validates +from marshmallow.validate import Email # man 8 useradd USERNAME_PATTERN = re.compile(r'[a-z_][a-z0-9_-]*[$]?\Z') @@ -29,8 +29,6 @@ # It appears that most printable ascii is valid, excluding whitespace, #, and commas. # There doesn't seem to be any practical size limits of an SSH Certificate Principal (> 4096B allowed). PRINCIPAL_PATTERN = re.compile(r'[\d\w!"$%&\'()*+\-./:;<=>?@\[\\\]\^`{|}~]+\Z') -VALID_SSH_RSA_PUBLIC_KEY_HEADER = "ssh-rsa AAAAB3NzaC1yc2" -VALID_SSH_ED25519_PUBLIC_KEY_HEADER = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5" USERNAME_VALIDATION_OPTIONS = Enum('UserNameValidationOptions', 'useradd ' # Allowable usernames per 'man 8 useradd' @@ -39,11 +37,6 @@ 'principal ' # SSH Certificate Principal. See 'man 5 sshd_config'. 'disabled') # no additional validation of the string. -HOSTNAME_VALIDATION_OPTIONS = Enum('HostNameValidationOptions', - 'url ' # Valid url format - 'disabled' # no validation -) - def validate_ips(ips): try: @@ -89,22 +82,6 @@ def _validate_principal(principal): raise ValidationError('Principal contains invalid characters.') -def validate_ssh_public_key(public_key): - if public_key.startswith(VALID_SSH_RSA_PUBLIC_KEY_HEADER) or public_key.startswith(VALID_SSH_ED25519_PUBLIC_KEY_HEADER): - pass - # todo other key types - else: - raise ValidationError('Invalid SSH Public Key.') - - -def validate_hostname(hostname, hostname_validation): - if hostname_validation == HOSTNAME_VALIDATION_OPTIONS.disabled: - return - else: - validator = URL(require_tld=False, schemes='ssh', error='Invalid hostname "{input}".') - validator('ssh://{}'.format(hostname)) - - class BlessUserSchema(Schema): bastion_ips = fields.Str(validate=validate_ips, required=True) bastion_user = fields.Str(required=True) @@ -156,7 +133,7 @@ def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_k :param bastion_user: The user on the bastion, who is initiating the SSH request. :param bastion_user_ip: The IP of the user accessing the bastion. :param command: Text information about the SSH request of the user. - :param public_key_to_sign: The id_rsa.pub that will be used in the SSH request. This is + :param public_key_to_sign: The id_XXX.pub that will be used in the SSH request. This is enforced in the issued certificate. :param remote_usernames: Comma-separated list of username(s) or authorized principals on the remote server that will be used in the SSH request. This is enforced in the issued certificate. @@ -172,41 +149,3 @@ def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_k def __eq__(self, other): return self.__dict__ == other.__dict__ - - -class BlessHostSchema(Schema): - hostnames = fields.Str(required=True) - public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True) - - @validates_schema(pass_original=True) - def check_unknown_fields(self, data, original_data): - unknown = set(original_data) - set(self.fields) - if unknown: - raise ValidationError('Unknown field', unknown) - - @post_load - def make_bless_request(self, data): - return BlessHostRequest(**data) - - @validates('hostnames') - def validate_hostnames(self, hostnames): - if HOSTNAME_VALIDATION_OPTION in self.context: - hostname_validation = HOSTNAME_VALIDATION_OPTIONS[self.context[HOSTNAME_VALIDATION_OPTION]] - else: - hostname_validation = HOSTNAME_VALIDATION_OPTIONS[HOSTNAME_VALIDATION_DEFAULT] - for hostname in hostnames.split(','): - validate_hostname(hostname, hostname_validation) - - -class BlessHostRequest: - def __init__(self, hostnames, public_key_to_sign): - """ - A BlessRequest must have the following key value pairs to be valid. - :param hostnames: The hostnames to make valid for this host certificate. - :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.hostnames = hostnames - self.public_key_to_sign = public_key_to_sign - - def __eq__(self, other): - return self.__dict__ == other.__dict__ diff --git a/tests/aws_lambda/test_bless_lambda_host.py b/tests/aws_lambda/test_bless_lambda_host.py index dcca76e..9797f17 100644 --- a/tests/aws_lambda/test_bless_lambda_host.py +++ b/tests/aws_lambda/test_bless_lambda_host.py @@ -1,10 +1,7 @@ import os -import pytest - -from bless.aws_lambda.bless_lambda import lambda_handler_host -from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ - EXAMPLE_ED25519_PUBLIC_KEY, EXAMPLE_ECDSA_PUBLIC_KEY +from bless.aws_lambda.bless_lambda_host import lambda_handler_host +from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD class Context(object): @@ -32,25 +29,25 @@ class Context(object): def test_basic_local_request(): output = lambda_handler_host(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')) + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) print(output) assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') def test_basic_local_request_with_multiple_hosts(): output = lambda_handler_host(VALID_TEST_REQUEST_MULTIPLE_HOSTS, 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')) + ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD, + entropy_check=False, + config_file=os.path.join(os.path.dirname(__file__), 'bless-test.cfg')) print(output) assert output['certificate'].startswith('ssh-rsa-cert-v01@openssh.com ') def test_invalid_request(): output = lambda_handler_host(INVALID_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')) + 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['errorType'] == 'InputValidationError' diff --git a/tests/aws_lambda/test_bless_lambda_user.py b/tests/aws_lambda/test_bless_lambda_user.py index 563b909..40dce14 100644 --- a/tests/aws_lambda/test_bless_lambda_user.py +++ b/tests/aws_lambda/test_bless_lambda_user.py @@ -3,7 +3,8 @@ import pytest -from bless.aws_lambda.bless_lambda import lambda_handler, lambda_handler_user +from bless.aws_lambda.bless_lambda_user import lambda_handler_user +from bless.aws_lambda.bless_lambda import lambda_handler from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD, \ EXAMPLE_ED25519_PUBLIC_KEY, EXAMPLE_ECDSA_PUBLIC_KEY diff --git a/tests/request/test_bless_request_host.py b/tests/request/test_bless_request_host.py index f6acbd9..2846f61 100644 --- a/tests/request/test_bless_request_host.py +++ b/tests/request/test_bless_request_host.py @@ -1,5 +1,5 @@ import pytest -from bless.request.bless_request import validate_hostname, HOSTNAME_VALIDATION_OPTIONS, BlessHostSchema +from bless.request.bless_request_host import HOSTNAME_VALIDATION_OPTIONS, BlessHostSchema, validate_hostname from marshmallow import ValidationError diff --git a/tests/request/test_bless_request_user.py b/tests/request/test_bless_request_user.py index b9cd0d1..511b35b 100644 --- a/tests/request/test_bless_request_user.py +++ b/tests/request/test_bless_request_user.py @@ -1,7 +1,7 @@ import pytest from bless.config.bless_config import USERNAME_VALIDATION_OPTION, REMOTE_USERNAMES_VALIDATION_OPTION, \ REMOTE_USERNAMES_BLACKLIST_OPTION -from bless.request.bless_request import validate_ips, validate_user, USERNAME_VALIDATION_OPTIONS, BlessUserSchema +from bless.request.bless_request_user import validate_ips, validate_user, USERNAME_VALIDATION_OPTIONS, BlessUserSchema from marshmallow import ValidationError From 03666f87e04b819b068662c1a867f4e2deb6ca0e Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 22 May 2019 11:04:15 -0700 Subject: [PATCH 67/68] Adding a sample client that can validte the BLESS host cert lambda. --- bless/request/bless_request_host.py | 2 +- bless_client/bless_client_host.py | 80 +++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 bless_client/bless_client_host.py diff --git a/bless/request/bless_request_host.py b/bless/request/bless_request_host.py index 364316b..5729334 100644 --- a/bless/request/bless_request_host.py +++ b/bless/request/bless_request_host.py @@ -52,7 +52,7 @@ class BlessHostRequest: def __init__(self, hostnames, public_key_to_sign): """ A BlessRequest must have the following key value pairs to be valid. - :param hostnames: Comma-separated list of hostnames (s) to include in this host certificate. + :param hostnames: Comma-separated list of hostname(s) to include in this host certificate. :param public_key_to_sign: The id_XXX.pub that will be used in the SSH request. This is enforced in the issued certificate. """ self.hostnames = hostnames diff --git a/bless_client/bless_client_host.py b/bless_client/bless_client_host.py new file mode 100644 index 0000000..94654a0 --- /dev/null +++ b/bless_client/bless_client_host.py @@ -0,0 +1,80 @@ +#!/usr/bin/env python + +"""bless_client +A sample client to invoke the BLESS Host SSH Cert Lambda function and save the signed SSH Certificate. + +Usage: + bless_client_host.py region lambda_function_name hostnames + + region: AWS region where your lambda is deployed. + + lambda_function_name: The AWS Lambda function's alias or ARN to invoke. + + hostnames: Comma-separated list of hostname(s) to include in this host certificate. + + id_rsa.pub to sign: The id_rsa.pub that will be used in the SSH request. This is + enforced in the issued certificate. + + output id_rsa-cert.pub: The file where the certificate should be saved. Per man SSH(1): + "ssh will also try to load certificate information from the filename + obtained by appending -cert.pub to identity filenames" e.g. the . +""" +import json +import os +import stat +import sys + +import boto3 + + +def main(argv): + if len(argv) != 5: + print( + 'Usage: bless_client_host.py region lambda_function_name hostnames ' + '') + print(len(argv)) + return -1 + + region, lambda_function_name, hostnames, public_key_filename, certificate_filename = argv + + with open(public_key_filename, 'r') as f: + public_key = f.read().strip() + + payload = {'hostnames': hostnames, 'public_key_to_sign': public_key} + + payload_json = json.dumps(payload) + + print('Executing:') + print('payload_json is: \'{}\''.format(payload_json)) + lambda_client = boto3.client('lambda', region_name=region) + response = lambda_client.invoke(FunctionName=lambda_function_name, + InvocationType='RequestResponse', LogType='None', + Payload=payload_json) + print('{}\n'.format(response['ResponseMetadata'])) + + if response['StatusCode'] != 200: + print('Error creating cert.') + return -1 + + payload = json.loads(response['Payload'].read()) + + if 'certificate' not in payload: + print(payload) + return -1 + + cert = payload['certificate'] + + with os.fdopen(os.open(certificate_filename, os.O_WRONLY | os.O_CREAT, 0o600), + 'w') as cert_file: + cert_file.write(cert) + + # If cert_file already existed with the incorrect permissions, fix them. + file_status = os.stat(certificate_filename) + if 0o600 != (file_status.st_mode & 0o777): + os.chmod(certificate_filename, stat.S_IRUSR | stat.S_IWUSR) + + print('Wrote Certificate to: ' + certificate_filename) + + +if __name__ == '__main__': + main(sys.argv[1:]) From a207d1b51a2c4053f5d38daa5aabbd5c36ea762d Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 22 May 2019 11:04:55 -0700 Subject: [PATCH 68/68] Bumping to Release v.0.4.0 Features include: New support for a Host SSH Certificate Lambda. Please consider how you will control who can obtain host certs for which hostnames before using. Updated publishing code to build with the latest Amazon Linux 2. Validated for Python 3.7 Lambda runtime. Updated dependencies. Various typo fixes. --- bless/__about__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bless/__about__.py b/bless/__about__.py index e8805d6..6d53e01 100644 --- a/bless/__about__.py +++ b/bless/__about__.py @@ -9,7 +9,7 @@ "sign SSH public keys.") __uri__ = "https://github.com/Netflix/bless" -__version__ = "0.3.0" +__version__ = "0.4.0" __author__ = "The BLESS developers" __email__ = "security@netflix.com"