From 21a417bacb2a50fe7da2c8520f8a9489e3ffe9b8 Mon Sep 17 00:00:00 2001 From: Jorge Dias Date: Tue, 14 Mar 2017 22:07:25 +0100 Subject: [PATCH 01/46] 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 02/46] 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 03/46] 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 04/46] 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 05/46] 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 06/46] 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 07/46] 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 08/46] 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 09/46] 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 d8f6d1eb7b1282f70e51c7c8edd8a753402b398d Mon Sep 17 00:00:00 2001 From: Russell Lewis Date: Wed, 5 Apr 2017 16:04:59 -0700 Subject: [PATCH 10/46] 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 11/46] 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 12/46] 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 13/46] 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 14/46] 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 15/46] 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 16/46] 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 17/46] 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 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 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 18/46] 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 19/46] 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 20/46] 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 21/46] 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 22/46] 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 23/46] 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 24/46] =?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 25/46] 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 AAAAHHNzaC1yc2EtY2VydC12MDFAb3BlbnNzaC5jb20AAAAgTT+1G4NGB597AOV52iJ+8G4RV8Nqoertt1uc+gFgprsAAAADAQABAAACAQDTuckaZVjP4tP+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/MmPLhXwAAAAAAAAAAAAAAAQAAAAl1c2VyQHRlc3QAAAAMAAAACHVzZXJuYW1lAAAAAFb8Ck4AAAAAVvwMpgAAAAAAAACCAAAAFXBlcm1pdC1YMTEtZm9yd2FyZGluZwAAAAAAAAAXcGVybWl0LWFnZW50LWZvcndhcmRpbmcAAAAAAAAAFnBlcm1pdC1wb3J0LWZvcndhcmRpbmcAAAAAAAAACnBlcm1pdC1wdHkAAAAAAAAADnBlcm1pdC11c2VyLXJjAAAAAAAAAAAAAAIXAAAAB3NzaC1yc2EAAAADAQABAAACAQDiNCZ8XiUnbVF7R7u8dzFVS3QzdohQq/AfMQkhrHZiflXSNrARdMXO5efx2VcgE4AgU7fXU+3husxQp0ITyVFAfroVLvRHisLtz6sPZlp018v7Gv0fvsrMBY1NUsscLPogW4hvSBtQSKl2Pnjrl2n/vJBBYvO+241ODa5dFPyi9qqQicIRYd77uVwUNuoof03GVUWj8jkwVajg9DDXlV7NCZFXP4HeJix1oBb3CK0zZpisEdBzUgz6WGy8aJftw232mPQhP7FHrSwOjsTuBBd/D0Cj2PlHdv8sqXOLgvxk9VXxHOAC2VQmWAvArrd4WG+Xn98XWm+1uIpFyMlNr7T0f59Sbv+Z11RV9WNBebtEoWZM/EiO/wSxdouEiDImYTU4KlY/Jq9i6cEhwdOJuvHfRrD5w9ZkEMZXH8e6W3zgI15p01d5kJp+E51tSsSh9J+zSRNY70ct9Z9wq7N2jCenLquvoMWyuHIP1+PENej56bmMzok7Q+YRLruES+HIPOjIuew5nITcG9QrL8RSa7niQeuSeHnyA06prqaQ0FY5GTsx9lEc0cxhNms3XED4HFeWkEImboqnmecRvaBMAf9pDVbt4bDOPboCfTgRh7ZQ6HpguGJEJCXtHoxSVXPLijsxpl34xnFyottQKuYxrxLA0W9w3LixHHjn10aVP+fTqQAAAg8AAAAHc3NoLXJzYQAAAgDEuvFBz2iuvJ4ojytisq+t5fgjsKsbfsg6K9djie4FUfvoVbVxU2zRDRBIAmFkjq8+jxSUFz5jA6FZf8AZHY/kbuMhZmXD/QfXDuMbO3ufRdvH+VRn53Rbdu8vOaLAOcDDEl4dritLG0ZNdglOEGiOXfnfs3LgkEErdxq6dK5Tq1eYHBxtchCPnqPHfb9b51gfkI5FUnZJJiSzDhQJWkv/cV4QNJ+otfLrdTNBRCuHyuhnR9JPJYY/fTkEAp/M8xRQZfAMGIO1Gu2rp7vRDjlQO8JHAZ+ckX2f1geF9/YG2tIjOfbKq6DSwC/sh4h4ggd6lL7TfXsFQV3gtjziAbJLgYxA+yuekudIQlXChVszp81f7bV5cMHB4nfb5FnyJU6ld2UiuZps3aBz5fxnt0LDyRLnmOv0JPNcBOc1Xzqw0ZVeCPYAmDueKMJU4jpkfttndAr6rQapc70Xyo8y2G/IOqwEoAg1XolStyRDdRKESK1b5BkTyGUcMejiJ3sRzws6yQPyQLonthnonU/sNbSku5HgIe7yk88hDpbR5M+s0a8IXKermAOZ59vK5CU1SXW0PFqJOtCP6qNeQzn8iwqrealCc7FSOzk5OhS20cnju+GuZci6GLH3Mt95bM9eCyx3nue1oIQJV+UbYf6Y1WLKgLpJ7l6A4bDqx+DJuTlDoA== 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 26/46] 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 27/46] 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 28/46] 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 29/46] 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 30/46] 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 31/46] 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 32/46] 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 33/46] 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 34/46] 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 35/46] 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 36/46] 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 37/46] 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 38/46] 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 39/46] 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 40/46] 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 41/46] 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 42/46] 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 43/46] 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 44/46] 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 45/46] 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 46/46] 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"