Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 43 additions & 12 deletions bless/aws_lambda/bless_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,23 @@
import os
from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION, \
CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \
BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION
from bless.request.bless_request import BlessSchema
BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, CERTIFICATE_TYPE_OPTION
from bless.request.bless_request import BlessUserSchema, BlessHostSchema
from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \
get_ssh_certificate_authority
from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType
from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder


def get_certificate_type(certificate_type_option):
if certificate_type_option == 'user':
return 1
elif certificate_type_option == 'host':
return 2
else:
raise ValueError('Invalid certificate type option: {}'.format(certificate_type_option))


def lambda_handler(event, context=None, ca_private_key_password=None,
entropy_check=True,
config_file=os.path.join(os.path.dirname(__file__), 'bless_deploy.cfg')):
Expand All @@ -31,6 +40,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None,
decrypt.
:param entropy_check: For local testing, if set to false, it will skip checking entropy and
won't try to fetch additional random from KMS
:param certificate_type: Type of certificate to be generated
:param config_file: The config file to load the SSH CA private key from, and additional settings
:return: the SSH Certificate that can be written to id_rsa-cert.pub or similar file.
"""
Expand All @@ -41,6 +51,7 @@ def lambda_handler(event, context=None, ca_private_key_password=None,
config = BlessConfig(region,
config_file=config_file)

certificate_type = get_certificate_type(config.get(BLESS_OPTIONS_SECTION, CERTIFICATE_TYPE_OPTION))
logging_level = config.get(BLESS_OPTIONS_SECTION, LOGGING_LEVEL_OPTION)
numeric_level = getattr(logging, logging_level.upper(), None)
if not isinstance(numeric_level, int):
Expand Down Expand Up @@ -84,7 +95,10 @@ def lambda_handler(event, context=None, ca_private_key_password=None,
urandom.write(random_seed)

# Process cert request
schema = BlessSchema(strict=True)
if certificate_type == SSHCertificateType.HOST:
schema = BlessHostSchema(strict=True)
else:
schema = BlessUserSchema(strict=True)
request = schema.load(event).data

# cert values determined only by lambda and its configs
Expand All @@ -94,24 +108,41 @@ def lambda_handler(event, context=None, ca_private_key_password=None,

# Build the cert
ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password)
cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.USER,
cert_builder = get_ssh_certificate_builder(ca, certificate_type,
request.public_key_to_sign)
cert_builder.add_valid_principal(request.remote_username)
if certificate_type == SSHCertificateType.USER:
cert_builder.add_valid_principal(request.remote_username)
# cert_builder is needed to obtain the SSH public key's fingerprint
key_id = 'request[{}] for[{}] from[{}] command[{}] ssh_key:[{}] ca:[{}] valid_to[{}]'.format(
context.aws_request_id, request.bastion_user, request.bastion_user_ip, request.command,
cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn,
time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before)))
cert_builder.set_critical_option_source_address(request.bastion_ip)
elif certificate_type == SSHCertificateType.HOST:
for remote_hostname in request.remote_hostnames:
cert_builder.add_valid_principal(remote_hostname)
key_id = 'request[{}] ssh_key:[{}] ca:[{}] valid_to[{}]'.format(
context.aws_request_id, cert_builder.ssh_public_key.fingerprint,
context.invoked_function_arn, time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before)))
else:
raise ValueError("Unknown certificate type")

cert_builder.set_valid_before(valid_before)
cert_builder.set_valid_after(valid_after)

# cert_builder is needed to obtain the SSH public key's fingerprint
key_id = 'request[{}] for[{}] from[{}] command[{}] ssh_key:[{}] ca:[{}] valid_to[{}]'.format(
context.aws_request_id, request.bastion_user, request.bastion_user_ip, request.command,
cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn,
time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before)))
cert_builder.set_critical_option_source_address(request.bastion_ip)
cert_builder.set_key_id(key_id)
cert = cert_builder.get_cert_file()

if certificate_type == SSHCertificateType.HOST:
remote_name = ', '.join(request.remote_hostnames)
bastion_ip = None
else:
remote_name = request.remote_username
bastion_ip = request.bastion_ip

logger.info(
'Issued a cert to bastion_ip[{}] for the remote_username of [{}] with the key_id[{}] and '
'valid_from[{}])'.format(
request.bastion_ip, request.remote_username, key_id,
bastion_ip, remote_name, key_id,
time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after))))
return cert
6 changes: 5 additions & 1 deletion bless/config/bless_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
LOGGING_LEVEL_OPTION = 'logging_level'
LOGGING_LEVEL_DEFAULT = 'INFO'

CERTIFICATE_TYPE_OPTION = 'certificate_type'
CERTIFICATE_TYPE_DEFAULT = 'user'

BLESS_CA_SECTION = 'Bless CA'
CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file'
KMS_KEY_ID_OPTION = 'kms_key_id'
Expand All @@ -41,7 +44,8 @@ def __init__(self, aws_region, config_file):
defaults = {CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION: CERTIFICATE_VALIDITY_SEC_DEFAULT,
ENTROPY_MINIMUM_BITS_OPTION: ENTROPY_MINIMUM_BITS_DEFAULT,
RANDOM_SEED_BYTES_OPTION: RANDOM_SEED_BYTES_DEFAULT,
LOGGING_LEVEL_OPTION: LOGGING_LEVEL_DEFAULT}
LOGGING_LEVEL_OPTION: LOGGING_LEVEL_DEFAULT,
CERTIFICATE_TYPE_OPTION: CERTIFICATE_TYPE_DEFAULT}
ConfigParser.RawConfigParser.__init__(self, defaults=defaults)
self.read(config_file)

Expand Down
6 changes: 4 additions & 2 deletions bless/config/bless_deploy_example.cfg
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
# This section and its options are optional
[Bless Options]
# Number of seconds +/- the issued time for the certificate to be valid
certificate_validity_window_seconds = 120
certificate_validity_seconds = 120
# Minimum number of bits in the system entropy pool before requiring an additional seeding step
entropy_minimum_bits = 2048
# Number of bytes of random to fetch from KMS to seed /dev/urandom
random_seed_bytes = 256
# Set the logging level
logging_level = INFO
# Type of certificate (user or host)
certificate_type = 'user'

# These values are all required to be modified for deployment
[Bless CA]
Expand All @@ -18,4 +20,4 @@ kms_key_id = <alias/key_name>
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>
# Specify the file name of your SSH CA's Private Key in PEM format.
ca_private_key_file = <INSERT_YOUR_ENCRYPTED_PEM_FILE_NAME>
ca_private_key_file = <INSERT_YOUR_ENCRYPTED_PEM_FILE_NAME>
60 changes: 55 additions & 5 deletions bless/request/bless_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

# man 8 useradd
USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z')
HOSTNAME_PATTERN = re.compile('[a-z0-9_.-]+')


def validate_ip(ip):
Expand All @@ -25,20 +26,56 @@ def validate_user(user):
raise ValidationError('Username contains invalid characters')


def validate_host(hostname):
if len(hostname) > 64:
raise ValidationError('Hostname is too long')
if HOSTNAME_PATTERN.match(hostname) is None:
raise ValidationError('Hostname contains invalid characters')


class BlessSchema(Schema):
public_key_to_sign = fields.Str()

@post_load
def make_bless_request(self, data):
return BlessRequest(**data)


class BlessUserSchema(BlessSchema):
bastion_ip = fields.Str(validate=validate_ip)
bastion_user = fields.Str(validate=validate_user)
bastion_user_ip = fields.Str(validate=validate_ip)
command = fields.Str()
public_key_to_sign = fields.Str()
remote_username = fields.Str(validate=validate_user)

@post_load
def make_bless_request(self, data):
return BlessRequest(**data)
return BlessUserRequest(**data)


class BlessHostSchema(BlessSchema):
remote_hostnames = fields.List(fields.Str())

@post_load
def make_bless_request(self, data):
return BlessHostRequest(**data)


class BlessRequest:
def __init__(self, public_key_to_sign):
"""
A BlessRequest must have the following key value pairs to be valid.
:param public_key_to_sign: The id_rsa.pub that will be used in the SSH request. This is
enforced in the issued certificate.
"""

self.public_key_to_sign = public_key_to_sign

def __eq__(self, other):
return self.__dict__ == other.__dict__


class BlessUserRequest(BlessRequest):
def __init__(self, bastion_ip, bastion_user, bastion_user_ip, command, public_key_to_sign,
remote_username):
"""
Expand All @@ -51,14 +88,27 @@ def __init__(self, bastion_ip, bastion_user, bastion_user_ip, command, public_ke
:param public_key_to_sign: The id_rsa.pub that will be used in the SSH request. This is
enforced in the issued certificate.
:param remote_username: The username on the remote server that will be used in the SSH
request. This is enforced in the issued certificate.
"""

self.bastion_ip = bastion_ip
self.bastion_user = bastion_user
self.bastion_user_ip = bastion_user_ip
self.command = command
self.public_key_to_sign = public_key_to_sign
self.remote_username = remote_username

def __eq__(self, other):
return self.__dict__ == other.__dict__

class BlessHostRequest(BlessRequest):
def __init__(self, public_key_to_sign, remote_hostnames):
"""
A BlessRequest must have the following key value pairs to be valid.
:param bastion_ip: The source IP where the SSH connection will be initiated from. This is
enforced in the issued 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.
:param remote_hostnames: A list of hostnames on the server for which the certificate is valid
request. This is enforced in the issued certificate.
"""

self.public_key_to_sign = public_key_to_sign
self.remote_hostnames = remote_hostnames
8 changes: 8 additions & 0 deletions tests/aws_lambda/bless-test-host.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[Bless Options]
certificate_type = host

[Bless CA]
ca_private_key_file = ../../tests/aws_lambda/only-use-for-unit-tests.pem
kms_key_id = alias/foo
us-east-1_password = bogus-password-for-unit-test
us-west-2_password = bogus-password-for-unit-test
27 changes: 27 additions & 0 deletions tests/aws_lambda/test_bless_lambda_host.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import os
import pytest

from bless.aws_lambda.bless_lambda import lambda_handler
from bless.ssh.certificates.ssh_certificate_builder import SSHCertificateType
from tests.ssh.vectors import EXAMPLE_RSA_PUBLIC_KEY, RSA_CA_PRIVATE_KEY_PASSWORD


class Context(object):
aws_request_id = 'bogus aws_request_id'
invoked_function_arn = 'bogus invoked_function_arn'


VALID_TEST_REQUEST = {
"remote_hostnames": ["example.com", "example1.com"],
"public_key_to_sign": EXAMPLE_RSA_PUBLIC_KEY,
}

os.environ['AWS_REGION'] = 'us-west-2'


def test_basic_local_request():
cert = lambda_handler(VALID_TEST_REQUEST, context=Context,
ca_private_key_password=RSA_CA_PRIVATE_KEY_PASSWORD,
entropy_check=False,
config_file=os.path.join(os.path.dirname(__file__), 'bless-test-host.cfg'))
assert cert.startswith('ssh-rsa-cert-v01@openssh.com ')