Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
c3982e7
Adding compiled libs and make_ca.sh
Aug 1, 2016
d5e4b14
Authenticate username before issuing cert
Jun 23, 2016
0b51f41
Update bless_client.py to be a little more flexible
acmcelwee Jun 13, 2016
c39306d
update kmsauth to use bless servicename
Jul 26, 2016
5d5178a
Add configs to repo
Aug 24, 2016
a0463c8
Adding compiled libs and make_ca.sh
Aug 1, 2016
8aea272
Authenticate username before issuing cert
Jun 23, 2016
21f3c6b
Update bless_client.py to be a little more flexible
acmcelwee Jun 13, 2016
591fdf7
update kmsauth to use bless servicename
Jul 26, 2016
898b720
Add configs to repo
Aug 24, 2016
15c0f49
Merge Netflix master changes
Aug 24, 2016
8674996
Update make_ca.sh for multiregion
Aug 25, 2016
32e27fd
Handle mutliregional kmsauth
Aug 25, 2016
61d48bc
Allow cidr blocks in ip validation
Aug 25, 2016
42f88e4
Fix cross-region kmsauth key
Aug 25, 2016
cbdd04e
Use a single name for kmsauth service 'to' context
vivianho Aug 29, 2016
eb8e3ee
syntax error
vivianho Aug 29, 2016
a9e2224
fixed a test
vivianho Aug 29, 2016
aaeaa31
Merge pull request #2 from lyft/SEC-619-use-single-name-for-kmsauth-s…
vivianho Aug 29, 2016
9963289
Bump kmsauth version to 0.1.7
Aug 30, 2016
782b0a2
Update all aws_lambda_libs
Aug 30, 2016
3c304e2
Re-add with all the crypto pieces
Aug 31, 2016
dc2b475
Merge pull request #3 from lyft/lyft_base-bump-kmsauth
Stype Aug 31, 2016
9006fe1
added a doc for deploying lambda
vivianho Aug 31, 2016
f5367b0
Refactor kmsauth integration
Aug 31, 2016
f8eafe7
Add us-west-2 kmsauth key to kmsauth_key_id list
Aug 31, 2016
842f348
Merge pull request #4 from lyft/SEC-634-lyft-docs
Stype Aug 31, 2016
8c2f461
Merge pull request #5 from lyft/lyft_base_kmsauth-updates
Stype Aug 31, 2016
cf607ed
fixed typos in lyft readme
vivianho Sep 8, 2016
153cba7
Merge pull request #7 from lyft/fixed-typos-in-lyft-readme
vivianho Sep 9, 2016
5949cbd
added bless runbook
vivianho Sep 13, 2016
2e0500e
more runbook
vivianho Sep 13, 2016
eb9ef52
merged with master
vivianho Sep 14, 2016
2d87d45
Merge pull request #9 from lyft/bless-runbook
vivianho Sep 14, 2016
ee49507
moved runbook
vivianho Sep 20, 2016
7b450ce
Merge pull request #12 from lyft/moved-runbook-to-blessclient-repo
vivianho Sep 22, 2016
b7de752
Make validity before/after independant
Sep 28, 2016
4382a53
Update lyft config to make certificates valid for 30 minutes
Sep 28, 2016
1f38d6c
Fix config name for after
Sep 29, 2016
6b3597d
Merge pull request #13 from lyft/SEC-765-increase-cert-expiration
Stype Sep 29, 2016
4c87f26
updated kmsauth key
vivianho Sep 30, 2016
e12b118
Merge pull request #14 from lyft/bump-kmsauth-key-id
vivianho Oct 3, 2016
52d4690
added test option to lambda request
vivianho Oct 3, 2016
8f1a561
instead of a test flag, look for a test user
vivianho Oct 5, 2016
30964c6
made or more explicit
vivianho Oct 5, 2016
d6cadd7
Merge pull request #15 from lyft/SEC-616-lambda-test-mode
vivianho Oct 5, 2016
8f0fb34
added lambda info line to print request immediately after lambda is i…
vivianho Oct 7, 2016
e5ff609
Merge pull request #16 from lyft/SEC-868-add-info-request-line
vivianho Oct 7, 2016
d3be4b2
updated setup.py
vivianho Oct 10, 2016
4e22acf
Merge pull request #17 from lyft/hotfix-fix-setup-py
vivianho Oct 10, 2016
6043ef6
removed aws lambda libs and configs from public repo
vivianho Oct 10, 2016
8026587
Merge pull request #18 from lyft/SEC-870-remove-libs-and-config
vivianho Oct 10, 2016
cc2afb6
SEC-869 Gracefully handle anticipated kmsauth errors
Oct 11, 2016
48d856d
import exception
Oct 11, 2016
0c7de34
Merge pull request #20 from lyft/sec-869
Stype Oct 11, 2016
1d0ea00
updated log line
vivianho Oct 11, 2016
9ca1ad0
Merge pull request #22 from lyft/updated-log-line
vivianho Oct 11, 2016
39595ba
removed make_ca script from lyft_base
vivianho Oct 12, 2016
ae53a9b
catch clienterror from boto
vivianho Nov 8, 2016
e003b24
fixed failing test
vivianho Nov 8, 2016
9570e17
Merge pull request #23 from lyft/fix-test
vivianho Nov 8, 2016
86ec3bf
merged with master
vivianho Nov 8, 2016
ea940f8
Merge pull request #24 from lyft/catch-service-unavailable-exception
vivianho Nov 8, 2016
e2fe0f4
catch validation error and exit gracefully
vivianho Nov 29, 2016
65ce8d5
rename validationerror -> inputvalidationerror
vivianho Nov 29, 2016
7e9a991
Merge pull request #26 from lyft/catch-validation-error
vivianho Nov 29, 2016
0c657a0
fixes 'ValueError: too many values to unpack' for args
Jan 9, 2017
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
2 changes: 0 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,3 @@ htmlcov/
libs/
publish/
venv/
aws_lambda_libs/
lambda_configs/
89 changes: 73 additions & 16 deletions bless/aws_lambda/bless_lambda.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,15 @@

import boto3
import os
from kmsauth import KMSTokenValidator, TokenValidationError
from botocore.exceptions import ClientError
from marshmallow.exceptions import ValidationError
from bless.config.bless_config import BlessConfig, BLESS_OPTIONS_SECTION, \
CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION, ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \
BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION
CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION, CERTIFICATE_VALIDITY_AFTER_SEC_OPTION, \
ENTROPY_MINIMUM_BITS_OPTION, RANDOM_SEED_BYTES_OPTION, \
BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION, LOGGING_LEVEL_OPTION, KMSAUTH_SECTION, \
KMSAUTH_USEKMSAUTH_OPTION, KMSAUTH_SERVICE_ID_OPTION, TEST_USER_OPTION

from bless.request.bless_request import BlessSchema
from bless.ssh.certificate_authorities.ssh_certificate_authority_factory import \
get_ssh_certificate_authority
Expand Down Expand Up @@ -49,23 +55,47 @@ def lambda_handler(event, context=None, ca_private_key_password=None,
logger = logging.getLogger()
logger.setLevel(numeric_level)

certificate_validity_window_seconds = config.getint(BLESS_OPTIONS_SECTION,
CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION)
certificate_validity_before_seconds = config.getint(BLESS_OPTIONS_SECTION,
CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION)
certificate_validity_after_seconds = config.getint(BLESS_OPTIONS_SECTION,
CERTIFICATE_VALIDITY_AFTER_SEC_OPTION)
entropy_minimum_bits = config.getint(BLESS_OPTIONS_SECTION, ENTROPY_MINIMUM_BITS_OPTION)
random_seed_bytes = config.getint(BLESS_OPTIONS_SECTION, RANDOM_SEED_BYTES_OPTION)
ca_private_key_file = config.get(BLESS_CA_SECTION, CA_PRIVATE_KEY_FILE_OPTION)
password_ciphertext_b64 = config.getpassword()

# Process cert request
schema = BlessSchema(strict=True)
try:
request = schema.load(event).data
except ValidationError as e:
return {
'errorType': 'InputValidationError',
'errorMessage': str(e)
}

logger.info('Bless lambda invoked by [user: {0}, bastion_ips:{1}, public_key: {2}, kmsauth_token:{3}]'.format(
request.bastion_user,
request.bastion_user_ip,
request.public_key_to_sign,
request.kmsauth_token))

# read the private key .pem
with open(os.path.join(os.path.dirname(__file__), ca_private_key_file), 'r') as f:
ca_private_key = f.read()

# decrypt ca private key password
if ca_private_key_password is None:
kms_client = boto3.client('kms', region_name=region)
ca_password = kms_client.decrypt(
CiphertextBlob=base64.b64decode(password_ciphertext_b64))
ca_private_key_password = ca_password['Plaintext']
try:
ca_password = kms_client.decrypt(
CiphertextBlob=base64.b64decode(password_ciphertext_b64))
ca_private_key_password = ca_password['Plaintext']
except ClientError as e:
return {
'errorType': 'ClientError',
'errorMessage': str(e)
}

# if running as a Lambda, we can check the entropy pool and seed it with KMS if desired
if entropy_check:
Expand All @@ -83,14 +113,41 @@ def lambda_handler(event, context=None, ca_private_key_password=None,
with open('/dev/urandom', 'w') as urandom:
urandom.write(random_seed)

# Process cert request
schema = BlessSchema(strict=True)
request = schema.load(event).data

# cert values determined only by lambda and its configs
current_time = int(time.time())
valid_before = current_time + certificate_validity_window_seconds
valid_after = current_time - certificate_validity_window_seconds
test_user = config.get(BLESS_OPTIONS_SECTION, TEST_USER_OPTION)
if (test_user and (request.bastion_user == test_user or
request.remote_username == test_user)):
# This is a test call, the lambda will issue an invalid
# certificate where valid_before < valid_after
valid_before = current_time
valid_after = current_time + 1
else:
valid_before = current_time + certificate_validity_after_seconds
valid_after = current_time - certificate_validity_before_seconds

# Authenticate the user with KMS, if key is setup
if config.get(KMSAUTH_SECTION, KMSAUTH_USEKMSAUTH_OPTION):
if request.kmsauth_token:
try:
validator = KMSTokenValidator(
None,
config.getkmsauthkeyids(),
config.get(KMSAUTH_SECTION, KMSAUTH_SERVICE_ID_OPTION),
region
)
# decrypt_token will raise a TokenValidationError if token doesn't match
validator.decrypt_token(
"2/user/{}".format(request.remote_username),
request.kmsauth_token
)
except TokenValidationError as e:
return {
'errorType': 'KMSAuthValidationError',
'errorMessage': str(e)
}
else:
raise ValueError('Invalid request, missing kmsauth token')

# Build the cert
ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password)
Expand All @@ -105,13 +162,13 @@ def lambda_handler(event, context=None, ca_private_key_password=None,
context.aws_request_id, request.bastion_user, request.bastion_user_ip, request.command,
cert_builder.ssh_public_key.fingerprint, context.invoked_function_arn,
time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_before)))
cert_builder.set_critical_option_source_address(request.bastion_ip)
cert_builder.set_critical_option_source_address('{},{}'.format(request.bastion_user_ip, request.bastion_ips))
cert_builder.set_key_id(key_id)
cert = cert_builder.get_cert_file()

logger.info(
'Issued a cert to bastion_ip[{}] for the remote_username of [{}] with the key_id[{}] and '
'Issued a cert to bastion_ips[{}] for the remote_username of [{}] with the key_id[{}] and '
'valid_from[{}])'.format(
request.bastion_ip, request.remote_username, key_id,
request.bastion_ips, request.remote_username, key_id,
time.strftime("%Y/%m/%d %H:%M:%S", time.gmtime(valid_after))))
return cert
36 changes: 33 additions & 3 deletions bless/config/bless_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
import ConfigParser

BLESS_OPTIONS_SECTION = 'Bless Options'
CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION = 'certificate_validity_seconds'
CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION = 'certificate_validity_before_seconds'
CERTIFICATE_VALIDITY_AFTER_SEC_OPTION = 'certificate_validity_after_seconds'
CERTIFICATE_VALIDITY_SEC_DEFAULT = 60 * 2

ENTROPY_MINIMUM_BITS_OPTION = 'entropy_minimum_bits'
Expand All @@ -18,12 +19,25 @@
LOGGING_LEVEL_OPTION = 'logging_level'
LOGGING_LEVEL_DEFAULT = 'INFO'

TEST_USER_OPTION = 'test_user'
TEST_USER_DEFAULT = None

BLESS_CA_SECTION = 'Bless CA'
CA_PRIVATE_KEY_FILE_OPTION = 'ca_private_key_file'
KMS_KEY_ID_OPTION = 'kms_key_id'

REGION_PASSWORD_OPTION_SUFFIX = '_password'

KMSAUTH_SECTION = 'KMS Auth'
KMSAUTH_USEKMSAUTH_OPTION = 'use_kmsauth'
KMSAUTH_USEKMSAUTH_DEFAULT = False

KMSAUTH_KEY_ID_OPTION = 'kmsauth_key_id'
KMSAUTH_KEY_ID_DEFAULT = ''

KMSAUTH_SERVICE_ID_OPTION = 'kmsauth_serviceid'
KMSAUTH_SERVICE_ID_DEFAULT = None


class BlessConfig(ConfigParser.RawConfigParser):
def __init__(self, aws_region, config_file):
Expand All @@ -38,16 +52,24 @@ def __init__(self, aws_region, config_file):
:param config_file: Path to the connfig file.
"""
self.aws_region = aws_region
defaults = {CERTIFICATE_VALIDITY_WINDOW_SEC_OPTION: CERTIFICATE_VALIDITY_SEC_DEFAULT,
defaults = {CERTIFICATE_VALIDITY_BEFORE_SEC_OPTION: CERTIFICATE_VALIDITY_SEC_DEFAULT,
CERTIFICATE_VALIDITY_AFTER_SEC_OPTION: CERTIFICATE_VALIDITY_SEC_DEFAULT,
ENTROPY_MINIMUM_BITS_OPTION: ENTROPY_MINIMUM_BITS_DEFAULT,
RANDOM_SEED_BYTES_OPTION: RANDOM_SEED_BYTES_DEFAULT,
LOGGING_LEVEL_OPTION: LOGGING_LEVEL_DEFAULT}
LOGGING_LEVEL_OPTION: LOGGING_LEVEL_DEFAULT,
TEST_USER_OPTION: TEST_USER_DEFAULT,
KMSAUTH_SERVICE_ID_OPTION: KMSAUTH_SERVICE_ID_DEFAULT,
KMSAUTH_KEY_ID_OPTION: KMSAUTH_KEY_ID_DEFAULT,
KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_DEFAULT}
ConfigParser.RawConfigParser.__init__(self, defaults=defaults)
self.read(config_file)

if not self.has_section(BLESS_OPTIONS_SECTION):
self.add_section(BLESS_OPTIONS_SECTION)

if not self.has_section(KMSAUTH_SECTION):
self.add_section(KMSAUTH_SECTION)

if not self.has_option(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX):
raise ValueError("No Region Specific Password Provided.")

Expand All @@ -57,3 +79,11 @@ def getpassword(self):
:return: A Base64 encoded KMS CiphertextBlob.
"""
return self.get(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX)

def getkmsauthkeyids(self):
"""
Returns a list of kmsauth keys used for validation (so a key generated
in one region can validate in another).
:return: A list of kmsauth key id's
"""
return map(str.strip, self.get(KMSAUTH_SECTION, KMSAUTH_KEY_ID_OPTION).split(','))
17 changes: 15 additions & 2 deletions bless/config/bless_deploy_example.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# This section and its options are optional
[Bless Options]
# Number of seconds +/- the issued time for the certificate to be valid
certificate_validity_window_seconds = 120
certificate_validity_after_seconds = 120
certificate_validity_before_seconds = 120
# Minimum number of bits in the system entropy pool before requiring an additional seeding step
entropy_minimum_bits = 2048
# Number of bytes of random to fetch from KMS to seed /dev/urandom
Expand All @@ -18,4 +19,16 @@ 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>

# This section is optional
[KMS Auth]
# Enable kmsauth, to ensure the certificate's username matches the AWS user
# use_kmsauth = True

# One or multiple KMS keys, setup for kmsauth (see github.com/lyft/python-kmsauth)
# kmsauth_key_id = arn:aws:kms:us-east-1:000000012345:key/eeff5544-6677-8899-9988-aaaabbbbcccc

# If using kmsauth, you need to set the kmsauth service name. Users need to set the 'to'
# context to this same service name when they create a kmsauth token.
# kmsauth_serviceid = bless-production
20 changes: 12 additions & 8 deletions bless/request/bless_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@
USERNAME_PATTERN = re.compile('[a-z_][a-z0-9_-]*[$]?\Z')


def validate_ip(ip):
def validate_ips(ips):
try:
ipaddress.ip_address(ip)
for ip in ips.split(','):
ipaddress.ip_network(ip, strict=True)
except ValueError:
raise ValidationError('Invalid IP address.')

Expand All @@ -26,24 +27,25 @@ def validate_user(user):


class BlessSchema(Schema):
bastion_ip = fields.Str(validate=validate_ip)
bastion_ips = fields.Str(validate=validate_ips)
bastion_user = fields.Str(validate=validate_user)
bastion_user_ip = fields.Str(validate=validate_ip)
bastion_user_ip = fields.Str(validate=validate_ips)
command = fields.Str()
public_key_to_sign = fields.Str()
remote_username = fields.Str(validate=validate_user)
kmsauth_token = fields.Str()

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


class BlessRequest:
def __init__(self, bastion_ip, bastion_user, bastion_user_ip, command, public_key_to_sign,
remote_username):
def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_key_to_sign,
remote_username, kmsauth_token=None):
"""
A BlessRequest must have the following key value pairs to be valid.
:param bastion_ip: The source IP where the SSH connection will be initiated from. This is
:param bastion_ips: The source IPs where the SSH connection will be initiated from. This is
enforced in the issued certificate.
:param bastion_user: The user on the bastion, who is initiating the SSH request.
:param bastion_user_ip: The IP of the user accessing the bastion.
Expand All @@ -52,13 +54,15 @@ def __init__(self, bastion_ip, bastion_user, bastion_user_ip, command, public_ke
enforced in the issued certificate.
:param remote_username: The username on the remote server that will be used in the SSH
request. This is enforced in the issued certificate.
:param kmsauth_token: An optional kms auth token to authenticate the user
"""
self.bastion_ip = bastion_ip
self.bastion_ips = bastion_ips
self.bastion_user = bastion_user
self.bastion_user_ip = bastion_user_ip
self.command = command
self.public_key_to_sign = public_key_to_sign
self.remote_username = remote_username
self.kmsauth_token = kmsauth_token

def __eq__(self, other):
return self.__dict__ == other.__dict__
14 changes: 9 additions & 5 deletions bless_client/bless_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
"ssh will also try to load certificate information from the filename
obtained by appending -cert.pub to identity filenames" e.g. the <id_rsa.pub to sign>.
"""
import base64
import json
import stat
import sys
Expand All @@ -40,25 +39,30 @@


def main(argv):
if len(argv) != 9:
if len(argv) < 9 or len(argv) > 10:
print (
'Usage: bless_client.py region lambda_function_name bastion_user bastion_user_ip '
'remote_username bastion_ip bastion_command <id_rsa.pub to sign> '
'<output id_rsa-cert.pub>')
'<output id_rsa-cert.pub> [kmsauth token]')
return -1

region, lambda_function_name, bastion_user, bastion_user_ip, remote_username, bastion_ip, \
bastion_command, public_key_filename, certificate_filename = argv
bastion_command, public_key_filename, certificate_filename = argv[0:9]

with open(public_key_filename, 'r') as f:
public_key = f.read()

payload = {'bastion_user': bastion_user, 'bastion_user_ip': bastion_user_ip,
'remote_username': remote_username, 'bastion_ip': bastion_ip,
'remote_username': remote_username, 'bastion_ips': bastion_ip,
'command': bastion_command, 'public_key_to_sign': public_key}

if len(argv) == 10:
payload['kmsauth_token'] = argv[9]

payload_json = json.dumps(payload)

print('Executing:')
print('payload_json is: \'{}\''.format(payload_json))
lambda_client = boto3.client('lambda', region_name=region)
response = lambda_client.invoke(FunctionName=lambda_function_name,
InvocationType='RequestResponse', LogType='None',
Expand Down
7 changes: 4 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os

from setuptools import setup
from setuptools import setup, find_packages

ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__)))

Expand All @@ -16,7 +16,7 @@
url=about["__uri__"],
description=about["__summary__"],
license=about["__license__"],
packages=[],
packages=find_packages(exclude=["test*"]),
install_requires=[
'boto3==1.3.1',
'botocore==1.4.37',
Expand All @@ -32,7 +32,8 @@
'pyasn1==0.1.9',
'pycparser==2.14',
'python-dateutil==2.5.3',
'six==1.10.0'
'six==1.10.0',
'kmsauth==0.1.7'
],
extras_require={
'tests': [
Expand Down
Loading