Skip to content

Commit

Permalink
Add validations for hostnames and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
pecigonzalo committed Sep 20, 2018
1 parent 242a586 commit ed85a7f
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 18 deletions.
17 changes: 12 additions & 5 deletions bless/aws_lambda/bless_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -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, \
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
12 changes: 11 additions & 1 deletion bless/config/bless_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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'
Expand Down Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions bless/config/bless_deploy_example.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
26 changes: 24 additions & 2 deletions bless/request/bless_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions tests/aws_lambda/test_bless_lambda_host.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
13 changes: 13 additions & 0 deletions tests/config/full-with-cert.cfg
Original file line number Diff line number Diff line change
@@ -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 = <INSERT_US-EAST-1_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>
us-west-2_password = <INSERT_US-WEST-2_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>
default_password = <INSERT_DEFAULT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>
ca_private_key_file = <INSERT_YOUR_ENCRYPTED_PEM_FILE_NAME>
3 changes: 3 additions & 0 deletions tests/config/full-zlib.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <INSERT_US-EAST-1_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>
Expand Down
3 changes: 3 additions & 0 deletions tests/config/full.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <INSERT_US-EAST-1_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>
Expand Down
49 changes: 41 additions & 8 deletions tests/config/test_bless_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -43,6 +49,7 @@ def test_config_no_password():
config_file=os.path.join(os.path.dirname(__file__), 'full-with-default.cfg'))
assert '<INSERT_DEFAULT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>' == config.getpassword()


def test_wrong_compression_env_key(monkeypatch):
extra_environment_variables = {
'bless_ca_default_password': '<INSERT_DEFAULT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>',
Expand All @@ -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': '<INSERT_DEFAULT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>',
Expand All @@ -76,6 +84,7 @@ def test_none_compression_env_key(monkeypatch):

assert b'<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>' == config.getprivatekey()


def test_zlib_positive_compression(monkeypatch):
extra_environment_variables = {
'bless_ca_default_password': '<INSERT_DEFAULT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>',
Expand All @@ -91,6 +100,7 @@ def test_zlib_positive_compression(monkeypatch):

assert b'<INSERT_YOUR_ENCRYPTED_PEM_FILE_CONTENT>' == config.getprivatekey()


def test_zlib_compression_env_with_uncompressed_key(monkeypatch):
extra_environment_variables = {
'bless_ca_default_password': '<INSERT_DEFAULT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>',
Expand All @@ -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',
Expand All @@ -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 '<INSERT_US-EAST-1_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>' == config.getpassword()
Expand All @@ -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,
'<INSERT_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>',
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',
'<INSERT_US-WEST-2_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>',
'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',
'<INSERT_US-EAST-1_KMS_ENCRYPTED_BASE64_ENCODED_PEM_PASSWORD_HERE>',
'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)
49 changes: 49 additions & 0 deletions tests/request/test_bless_request_host.py
Original file line number Diff line number Diff line change
@@ -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".'

0 comments on commit ed85a7f

Please sign in to comment.