From 778094a49e13d85a3a51d886f2506ae172d34b7b Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 8 Dec 2017 10:29:51 -0800 Subject: [PATCH 01/22] Function better in python 3 (#25) * Function better in python 3 * Produce a useful error message for `--gui` and no tkinter --- blessclient/awsmfautils.py | 2 +- blessclient/bless_aws.py | 5 +-- blessclient/bless_cache.py | 1 + blessclient/bless_config.py | 13 ++++---- blessclient/bless_lambda.py | 3 +- blessclient/client.py | 38 ++++++++++++++-------- blessclient/lambda_invocation_exception.py | 1 - blessclient/tokengui.py | 1 + blessclient/user_ip.py | 3 +- setup.cfg | 2 +- setup.py | 7 ++-- tests/blessclient/bless_config_test.py | 8 +++-- 12 files changed, 49 insertions(+), 35 deletions(-) diff --git a/blessclient/awsmfautils.py b/blessclient/awsmfautils.py index 448f869..a1c772e 100644 --- a/blessclient/awsmfautils.py +++ b/blessclient/awsmfautils.py @@ -1,7 +1,7 @@ # Utility functions for working with AWS # # - +from __future__ import absolute_import import os diff --git a/blessclient/bless_aws.py b/blessclient/bless_aws.py index 73e0228..8197270 100644 --- a/blessclient/bless_aws.py +++ b/blessclient/bless_aws.py @@ -1,8 +1,9 @@ +from __future__ import absolute_import import boto3 import logging -from itertools import count, ifilter +from itertools import count from botocore.exceptions import DataNotFoundError -from lambda_invocation_exception import LambdaInvocationException +from .lambda_invocation_exception import LambdaInvocationException from random import randint from time import sleep diff --git a/blessclient/bless_cache.py b/blessclient/bless_cache.py index 3877aaf..33a37c2 100644 --- a/blessclient/bless_cache.py +++ b/blessclient/bless_cache.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import json import logging import os diff --git a/blessclient/bless_config.py b/blessclient/bless_config.py index 8c97885..82de46e 100644 --- a/blessclient/bless_config.py +++ b/blessclient/bless_config.py @@ -1,4 +1,5 @@ -import ConfigParser +from __future__ import absolute_import +from six.moves.configparser import SafeConfigParser class BlessConfig(object): @@ -24,8 +25,8 @@ def _get_region_kms_config(self, region, config): } def parse_config_file(self, config_file): - config = ConfigParser.SafeConfigParser(self.DEFAULT_CONFIG) - loaded = config.readfp(config_file) + config = SafeConfigParser(self.DEFAULT_CONFIG) + config.readfp(config_file) blessconfig = { 'CLIENT_CONFIG': { @@ -34,7 +35,7 @@ def parse_config_file(self, config_file): 'cache_file': config.get('CLIENT', 'cache_file'), 'mfa_cache_dir': config.get('CLIENT', 'mfa_cache_dir'), 'mfa_cache_file': config.get('CLIENT', 'mfa_cache_file'), - 'ip_urls': map(str.strip, config.get('CLIENT', 'ip_urls').split(",")), + 'ip_urls': [s.strip() for s in config.get('CLIENT', 'ip_urls').split(",")], 'update_script': config.get('CLIENT', 'update_script'), 'user_session_length': int(config.get('CLIENT', 'user_session_length')), 'usebless_role_session_length': int(config.get('CLIENT', 'usebless_role_session_length')), @@ -59,7 +60,7 @@ def parse_config_file(self, config_file): } regions = config.get('MAIN', 'region_aliases').split(",") - regions = map(str.strip, regions) + regions = [region.strip() for region in regions] for region in regions: region = region.upper() kms_region_key = 'KMSAUTH_CONFIG_{}'.format(region) @@ -80,7 +81,7 @@ def get_config(self): return self.blessconfig def get_region_alias_from_aws_region(self, aws_region): - for alias, region in self.blessconfig['REGION_ALIAS'].iteritems(): + for alias, region in self.blessconfig['REGION_ALIAS'].items(): if region == aws_region: return alias raise ValueError('Unexpected region: {}'.format(aws_region)) diff --git a/blessclient/bless_lambda.py b/blessclient/bless_lambda.py index b4d456a..5be3492 100644 --- a/blessclient/bless_lambda.py +++ b/blessclient/bless_lambda.py @@ -1,6 +1,7 @@ +from __future__ import absolute_import import boto3 import json -from lambda_invocation_exception import LambdaInvocationException +from .lambda_invocation_exception import LambdaInvocationException from botocore.client import Config from botocore.vendored.requests.exceptions import (ReadTimeout, ConnectTimeout, diff --git a/blessclient/client.py b/blessclient/client.py index cbb0d35..df1bd84 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -1,5 +1,5 @@ #!/usr/local/bin/python - +from __future__ import absolute_import import boto3 from botocore.exceptions import (ClientError, ParamValidationError, @@ -17,19 +17,24 @@ import copy import subprocess import json -from random import randint -import awsmfautils -import tokengui -from bless_aws import BlessAWS -from bless_cache import BlessCache -from user_ip import UserIP -from bless_lambda import BlessLambda -from bless_config import BlessConfig -from lambda_invocation_exception import LambdaInvocationException +import six + +from . import awsmfautils +from .bless_aws import BlessAWS +from .bless_cache import BlessCache +from .user_ip import UserIP +from .bless_lambda import BlessLambda +from .bless_config import BlessConfig +from .lambda_invocation_exception import LambdaInvocationException import logging +try: + from . import tokengui +except ImportError: + tokengui = None + DATETIME_STRING_FORMAT = '%Y%m%dT%H%M%SZ' @@ -87,7 +92,7 @@ def get_regions(region, bless_config): List of regions """ regions = [] - aws_regions = bless_config.get('REGION_ALIAS').values() + aws_regions = tuple(bless_config.get('REGION_ALIAS').values()) try: ndx = aws_regions.index(region) except ValueError: @@ -185,7 +190,7 @@ def get_idfile_from_cmdline(cmdline, default): def get_mfa_token_cli(): sys.stderr.write('Enter your AWS MFA code: ') - mfa_pin = raw_input() + mfa_pin = six.moves.input() return mfa_pin @@ -202,10 +207,15 @@ def get_mfa_token_gui(message): def get_mfa_token(showgui, message): mfa_token = None - if showgui: + if not showgui: + mfa_token = get_mfa_token_cli() + elif tokengui: mfa_token = get_mfa_token_gui(message) else: - mfa_token = get_mfa_token_cli() + raise RuntimeError( + '--gui requested but no tkinter support ' + '(often the `python-tk` package).' + ) return mfa_token diff --git a/blessclient/lambda_invocation_exception.py b/blessclient/lambda_invocation_exception.py index 29fb42b..066d730 100644 --- a/blessclient/lambda_invocation_exception.py +++ b/blessclient/lambda_invocation_exception.py @@ -1,3 +1,2 @@ - class LambdaInvocationException(Exception): pass diff --git a/blessclient/tokengui.py b/blessclient/tokengui.py index 7579180..99f97b9 100644 --- a/blessclient/tokengui.py +++ b/blessclient/tokengui.py @@ -1,3 +1,4 @@ +from __future__ import absolute_import import platform import os from Tkinter import Tk, Label, Entry, Button, ACTIVE, W, mainloop diff --git a/blessclient/user_ip.py b/blessclient/user_ip.py index d10a042..714c1c4 100644 --- a/blessclient/user_ip.py +++ b/blessclient/user_ip.py @@ -1,8 +1,9 @@ +from __future__ import absolute_import import contextlib import logging import string import time -from urllib2 import urlopen +from six.moves.urllib_request import urlopen VALID_IP_CHARACTERS = string.hexdigits + '.:' diff --git a/setup.cfg b/setup.cfg index a41634c..2d75bb0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,4 +20,4 @@ format=pylint max-complexity = 23 max-line-length = 126 exclude = .git,__pycache__,venv,tests/ -ignore = E402, E124, F401, F811, F841, W503 +ignore = E402, E124, W503 diff --git a/setup.py b/setup.py index 687e3b5..16d6402 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,3 @@ -""" - -""" - from setuptools import setup, find_packages setup( @@ -11,7 +7,8 @@ install_requires=[ 'boto3>=1.4.0,<2.0.0', 'psutil>=4.3', - 'kmsauth>=0.1.8' + 'kmsauth>=0.1.8', + 'six', ], author="Chris Steipp", author_email="csteipp@lyft.com", diff --git a/tests/blessclient/bless_config_test.py b/tests/blessclient/bless_config_test.py index d8363e6..e0c8c0f 100644 --- a/tests/blessclient/bless_config_test.py +++ b/tests/blessclient/bless_config_test.py @@ -1,4 +1,6 @@ -import StringIO +from __future__ import unicode_literals + +from io import StringIO import pytest from blessclient.bless_config import BlessConfig @@ -41,7 +43,7 @@ @pytest.fixture def bless_config_test(): - configIO = StringIO.StringIO(TEST_CONFIG) + configIO = StringIO(TEST_CONFIG) config = BlessConfig() config.set_config(config.parse_config_file(configIO)) return config @@ -74,7 +76,7 @@ def test_get_config(): def test_load_config(): config = BlessConfig() - configIO = StringIO.StringIO(TEST_CONFIG) + configIO = StringIO(TEST_CONFIG) conf = config.parse_config_file(configIO) assert conf == { 'KMSAUTH_CONFIG_SFO': { From e0484346050cda522833f7e9b8e6d3a99b6b99c0 Mon Sep 17 00:00:00 2001 From: Michael Sawyer Date: Fri, 1 Dec 2017 19:10:06 -0800 Subject: [PATCH 02/22] Adds support for HashiCorp Vault as CA backend. --- blessclient.cfg.sample | 20 ++- blessclient/bless_config.py | 7 + blessclient/client.py | 201 ++++++++++++++++++++++++- blessclient/vault_ca.py | 22 +++ setup.py | 1 + tests/blessclient/bless_config_test.py | 14 ++ tests/blessclient/client_test.py | 98 ++++++++++++ tests/blessclient/vault_ca_test.py | 57 +++++++ 8 files changed, 416 insertions(+), 4 deletions(-) create mode 100644 blessclient/vault_ca.py create mode 100644 tests/blessclient/vault_ca_test.py diff --git a/blessclient.cfg.sample b/blessclient.cfg.sample index abdb673..2746ddd 100644 --- a/blessclient.cfg.sample +++ b/blessclient.cfg.sample @@ -18,9 +18,13 @@ kms_service_name: bless-production # the internal IP of each should be listed here. bastion_ips: 10.100.1.230,192.168.200.0/24 -# remote_user: The remote username to authorize for SSH within the certificate. +# remote_user: The remote username to authorize for SSH within the certificate. # Defaults to the AWS user requesting the certificate +# ca_backend: This is the Certificate Authority (CA) backend that will be used in order to +# provide signed SSH certificates to the user. Can either be 'hashicorp-vault' or 'bless'. +ca_backend: bless + [CLIENT] # domain_regex: A (python) regex that is tested by the blessclient to determine if we need # to run bless and get a certificate, or if we can skip it. This prevents blessclient from @@ -102,3 +106,17 @@ kmsauthkey: 12345678-abab-cdcd-efef-123456789011 [REGION_EAST] awsregion: us-east-1 kmsauthkey: 22345678-abab-cdcd-efef-123456789012 + +[VAULT] +# vault_addr: Same as environment variable $VAULT_ADDR when using the CLI +vault_addr: https://vault.example.com:1234 + +# auth_mount: Specify the mount point for the desired authentication backend. +# Tested using Okta, but should work for others requiring only username/password. +auth_mount: okta + +# ssh_backend_mount: SSH Key signing backend mount point to use in HashiCorp Vault +ssh_backend_mount: ssh-client-signer + +# ssh_backend_role: SSH Key signing role to use with the above specified mount point. +ssh_backend_role: bless \ No newline at end of file diff --git a/blessclient/bless_config.py b/blessclient/bless_config.py index 82de46e..1234804 100644 --- a/blessclient/bless_config.py +++ b/blessclient/bless_config.py @@ -41,6 +41,7 @@ def parse_config_file(self, config_file): 'usebless_role_session_length': int(config.get('CLIENT', 'usebless_role_session_length')), }, 'BLESS_CONFIG': { + 'ca_backend': config.get('MAIN', 'ca_backend'), 'userrole': config.get('LAMBDA', 'user_role'), 'accountid': config.get('LAMBDA', 'account_id'), 'functionname': config.get('LAMBDA', 'functionname'), @@ -56,6 +57,12 @@ def parse_config_file(self, config_file): 'bastion_ips': config.get('MAIN', 'bastion_ips'), 'remote_user': config.get('MAIN', 'remote_user') }, + 'VAULT_CONFIG': { + 'vault_addr': config.get('VAULT', 'vault_addr'), + 'auth_mount': config.get('VAULT', 'auth_mount'), + 'ssh_backend_mount': config.get('VAULT', 'ssh_backend_mount'), + 'ssh_backend_role': config.get('VAULT', 'ssh_backend_role'), + }, 'REGION_ALIAS': {} } diff --git a/blessclient/client.py b/blessclient/client.py index df1bd84..dbbde03 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -17,6 +17,10 @@ import copy import subprocess import json +import hvac +import getpass + +from random import randint import six @@ -26,6 +30,7 @@ from .user_ip import UserIP from .bless_lambda import BlessLambda from .bless_config import BlessConfig +from .vault_ca import VaultCA from .lambda_invocation_exception import LambdaInvocationException import logging @@ -429,6 +434,188 @@ def update_config_from_env(bless_config): bless_config.set_lambda_config('ipcachelifetime', lifetime) +def get_linux_username(username): + """ + Returns a linux safe username. + :param username: Name of the user (could include @domain.com) + :return: Username string that complies with IEEE Std 1003.1-2001 + """ + match = re.search('[.a-zA-Z]+', username) + return match.group(0) + + +def get_cached_auth_token(bless_cache): + """ + Returns cached Vault auth token if available, otherwise None + :param bless_cache: Bless cache object + :return: Vault auth token or None if no valid token cached + """ + vault_creds = bless_cache.get('vault_creds') + if vault_creds is None or vault_creds['expiration'] is None: + return None + else: + expiration = vault_creds['expiration'] + if datetime.datetime.utcnow() < datetime.datetime.strptime(expiration, '%Y%m%dT%H%M%SZ'): + logging.debug( + 'Using cached vault token, good until {}'.format(expiration)) + return vault_creds['token'] + else: + return None + + +def get_credentials(): + print "Enter Vault username:" + username = raw_input() + password = getpass.getpass(prompt="Password (will be hidden):") + return username, password + + +def auth_okta(client, auth_mount, bless_cache): + """ + Authenticates a user in HashiCorp Vault using Okta + :param bless_cache: Bless Cache to cache auth token + :param auth_mount: Authentication mount point on Vault + :param client: HashiCorp Vault client + :return: Updated HashiCorp Vault client, and linux username + """ + + vault_auth_token = get_cached_auth_token(bless_cache) + if vault_auth_token is not None: + client.token = vault_auth_token + username = get_linux_username(bless_cache.get('vault_creds')['username']) + return client, get_linux_username(username) + else: + username, password = get_credentials() + auth_params = { + 'password': password + } + current_time = datetime.datetime.utcnow() + auth_url = '/v1/auth/{0}/login/{1}'.format(auth_mount, username) + response = client.auth(auth_url, json=auth_params) + + token = response['auth']['client_token'] + expiration = current_time + datetime.timedelta(seconds=response['auth']['lease_duration']) + username = get_linux_username(response['auth']['metadata']['username']) + vault_credentials_cache = { + "token": token, + "expiration": expiration.strftime('%Y%m%dT%H%M%SZ'), + "username": username + } + bless_cache.set('vault_creds', vault_credentials_cache) + bless_cache.save() + return client, get_linux_username(username) + + +def vault_bless(nocache, bless_config): + + vault_addr = bless_config.get('VAULT_CONFIG')['vault_addr'] + auth_mount = bless_config.get('VAULT_CONFIG')['auth_mount'] + bless_cache = get_bless_cache(nocache, bless_config) + bless_lambda_config = bless_config.get_lambda_config() + + user_ip = UserIP( + bless_cache=bless_cache, + maxcachetime=bless_lambda_config['ipcachelifetime'], + ip_urls=bless_config.get_client_config()['ip_urls'], + fixed_ip=os.getenv('BLESSFIXEDIP', False)) + + # Print feedback? + show_feedback = get_stderr_feedback() + + # Create client to connect to HashiCorp Vault + client = hvac.Client(url=vault_addr) + + # Identify the SSH key to be used + clistring = psutil.Process(os.getppid()).cmdline() + identity_file = get_idfile_from_cmdline( + clistring, + os.getenv('HOME', os.getcwd()) + '/.ssh/blessid' + ) + # Define the certificate to be created + cert_file = identity_file + '-cert.pub' + + logging.debug("Using identity file: {}".format(identity_file)) + + # Check if we can skip asking for MFA code + if nocache is not True: + if check_fresh_cert(cert_file, bless_lambda_config, bless_cache, user_ip): + logging.debug("Already have fresh cert") + sys.exit(0) + + # Print feedback information + if show_feedback: + sys.stderr.write( + "Requesting certificate for your public key" + + " (set BLESSQUIET=1 to suppress these messages)\n" + ) + + # Identify and load the public key to be signed + public_key_file = identity_file + '.pub' + with open(public_key_file, 'r') as f: + public_key = f.read() + + # Only sign public keys in correct format. + if public_key[:8] != 'ssh-rsa ': + raise Exception( + 'Refusing to bless {}. Probably not an identity file.'.format(identity_file)) + + # Authenticate user with HashiCorp Vault + client, linux_username = auth_okta(client, auth_mount, bless_cache) + + payload = { + 'valid_principals': linux_username, + 'public_key': public_key, + 'ttl': bless_config.get('BLESS_CONFIG')['certlifetime'], + 'ssh_backend_mount': bless_config.get('VAULT_CONFIG')['ssh_backend_mount'], + 'ssh_backend_role': bless_config.get('VAULT_CONFIG')['ssh_backend_role'] + } + + vault_ca = VaultCA(client) + cert = vault_ca.getCert(payload) + + logging.debug("Got back cert: {}".format(cert)) + + # Error handling + if cert[:29] != 'ssh-rsa-cert-v01@openssh.com ': + error_msg = json.loads(cert) + if ('errorType' in error_msg + and error_msg['errorType'] == 'KMSAuthValidationError' + and nocache is False + ): + logging.debug("KMSAuth error with cached token, purging cache.") + # clear_kmsauth_token_cache(kmsauth_config, bless_cache) + raise LambdaInvocationException('KMSAuth validation error') + + if ('errorType' in error_msg and + error_msg['errorType'] == 'ClientError'): + raise LambdaInvocationException( + 'The BLESS lambda experienced a client error. Consider trying in a different region.' + ) + + if ('errorType' in error_msg and + error_msg['errorType'] == 'InputValidationError'): + raise Exception( + 'The input to the BLESS lambda is invalid. ' + 'Please update your blessclient by running `make update` ' + 'in the bless folder.') + + raise LambdaInvocationException( + 'BLESS client did not recieve a valid cert. Instead got: {}'.format(cert)) + + # Remove old certificate, replacing with new certificate + ssh_agent_remove_bless(identity_file) + with open(cert_file, 'w') as cert_file: + cert_file.write(cert) + ssh_agent_add_bless(identity_file) + + # bless_cache.set('certip', my_ip) + # bless_cache.save() + + logging.debug("Successfully issued cert!") + if show_feedback: + sys.stderr.write("Finished getting certificate.\n") + + def bless(region, nocache, showgui, hostname, bless_config): # Setup loggging setup_logging() @@ -639,13 +826,21 @@ def main(): config_filename = args.config if args.config else get_default_config_filename() with open(config_filename, 'r') as f: bless_config.set_config(bless_config.parse_config_file(f)) + ca_backend = bless_config.get('BLESS_CONFIG')['ca_backend'] if re.match(bless_config.get_client_config()['domain_regex'], args.host) or args.host == 'BLESS': start_region = get_region_from_code(args.region, bless_config) success = False for region in get_regions(start_region, bless_config): try: - bless(region, args.nocache, args.gui, args.host, bless_config) - success = True + if ca_backend.lower() == 'hashicorp-vault': + vault_bless(args.nocache, bless_config) + success = True + elif ca_backend.lower() == 'bless': + bless(region, args.nocache, args.gui, args.host, bless_config) + success = True + else: + sys.stderr.write('{0} is an invalid CA backend'.format(ca_backend)) + sys.exit(1) break except (ClientError, LambdaInvocationException, ConnectionError, EndpointConnectionError) as e: @@ -654,7 +849,7 @@ def main(): if success: sys.exit(0) else: - sys.stderr.write('Could not connect to BLESS in any configured region.\n') + sys.stderr.write('Could not sign SSH public key.\n') sys.exit(1) else: sys.exit(1) diff --git a/blessclient/vault_ca.py b/blessclient/vault_ca.py new file mode 100644 index 0000000..3f8aeaa --- /dev/null +++ b/blessclient/vault_ca.py @@ -0,0 +1,22 @@ +class VaultCA(object): + + def __init__(self, client): + self.client = client + + def getCert(self, payload): + client = self.client + ssh_url = '{0}/sign/{1}'.format(payload['ssh_backend_mount'], payload['ssh_backend_role']) + response = client.write( + ssh_url, + valid_principals=payload['valid_principals'], + public_key=payload['public_key'], + ttl=payload['ttl'] + ) + + # EXTRACT PAYLOAD FROM RESPONSE + payload = response['data'] + if 'signed_key' not in payload: + raise Exception('No certificate in response.') + + # RETURN CERTIFICATE IF ALL GOES WELL + return payload['signed_key'] diff --git a/setup.py b/setup.py index 16d6402..5ee3964 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,7 @@ 'psutil>=4.3', 'kmsauth>=0.1.8', 'six', + 'hvac' ], author="Chris Steipp", author_email="csteipp@lyft.com", diff --git a/tests/blessclient/bless_config_test.py b/tests/blessclient/bless_config_test.py index e0c8c0f..b64255c 100644 --- a/tests/blessclient/bless_config_test.py +++ b/tests/blessclient/bless_config_test.py @@ -7,6 +7,7 @@ TEST_CONFIG = """ [MAIN] +ca_backend: bless region_aliases: iad, SFO kms_service_name: bless-production bastion_ips: 10.0.0.0/8,192.168.192.1 @@ -39,6 +40,12 @@ [REGION_IAD] awsregion: us-east-1 kmsauthkey: zxywvuts-0123-4567-8910-abcdefghijkl + +[VAULT] +vault_addr: https://vault.example.com:1234 +auth_mount: okta +ssh_backend_mount: ssh-client-signer +ssh_backend_role: bless """ @pytest.fixture @@ -91,6 +98,7 @@ def test_load_config(): }, 'REGION_ALIAS': {'IAD': 'us-east-1', 'SFO': 'us-west-2'}, 'BLESS_CONFIG': { + 'ca_backend': 'bless', 'ipcachelifetime': 60, 'functionname': 'lyft_bless', 'functionversion': 'PROD-1-2', @@ -113,6 +121,12 @@ def test_load_config(): 'update_script': 'autoupdate.sh', 'user_session_length': 3600, 'usebless_role_session_length': 3600, # comes from BlessConfig.DEFAULT_CONFIG + }, + 'VAULT_CONFIG': { + 'vault_addr': 'https://vault.example.com:1234', + 'auth_mount': 'okta', + 'ssh_backend_mount': 'ssh-client-signer', + 'ssh_backend_role': 'bless' } } diff --git a/tests/blessclient/client_test.py b/tests/blessclient/client_test.py index 8412060..d1d89ac 100644 --- a/tests/blessclient/client_test.py +++ b/tests/blessclient/client_test.py @@ -23,6 +23,7 @@ def bless_config(): 'accountid': '111111111111' }, 'CLIENT_CONFIG': { + 'ip_urls': 'http://api.ipify.org, http://canihazip.com', 'domain_regex': '(i-.*|.*\\.example\\.com|\\A10\\.0(?:\\.[0-9]{1,3}){2}\\Z)$', 'cache_dir': '.aws-mfa/session', 'cache_file': 'bless_cache.json', @@ -39,6 +40,12 @@ def bless_config(): 'kmskey': 'zxywvuts-0123-4567-8910-abcdefghijkl', 'awsregion': 'us-east-1', 'context': {'to': 'bless-production', 'user_type': 'user'} + }, + 'VAULT_CONFIG': { + 'vault_addr': 'https://vault.example.com:1234', + 'auth_mount': 'okta', + 'ssh_backend_mount': 'ssh-client-signer', + 'ssh_backend_role': 'bless' } }) return bc @@ -254,3 +261,94 @@ def null_bless_cache(): bless_cache = BlessCache(None, None, BlessCache.CACHEMODE_DISABLED) bless_cache.cache = {} return bless_cache + + +def test_get_linux_username_Email(): + username = client.get_linux_username("john.doe@example.com") + assert username == "john.doe" + + +def test_get_linux_username_EmailWithSpecialChars(): + username = client.get_linux_username("john.doe+test!#$%&'*+-/=?^_`{|}~abc@example.com") + assert username == "john.doe" + + +def test_get_cached_auth_token_isEmpty(null_bless_cache): + cache = null_bless_cache + returned = client.get_cached_auth_token(cache) + assert returned == None + + +def test_get_cached_auth_token_isValid(mocker): + cachemock = mocker.MagicMock() + cachemock.get.return_value = { + "token": "test-token", + "expiration": ( + datetime.datetime.utcnow() + + datetime.timedelta(hours=1) + ).strftime('%Y%m%dT%H%M%SZ'), + "username": "john.doe" + } + returned = client.get_cached_auth_token(cachemock) + assert returned == "test-token" + + +def test_get_cached_auth_token_isExpired(mocker): + cachemock = mocker.MagicMock() + cachemock.get.return_value = { + "token": "test-token", + "expiration": ( + datetime.datetime.utcnow() - + datetime.timedelta(hours=1) + ).strftime('%Y%m%dT%H%M%SZ'), + "username": "john.doe" + } + returned = client.get_cached_auth_token(cachemock) + assert returned == None + + +@pytest.fixture(scope='module') +def mock_get_credentials(): + username = "john.doe" + password = "password" + + def mockreturn(): + return username, password + + return mockreturn + + +def test_auth_okta_noCache(mocker, monkeypatch, null_bless_cache, mock_get_credentials): + + clientmock = mocker.MagicMock() + clientmock.auth.return_value = { + "auth": { + "client_token": "test-token", + "lease_duration": 500, + "metadata": { + "username": "john.doe" + } + } + } + + monkeypatch.setattr(client, 'get_credentials', mock_get_credentials) + new_client, new_username = client.auth_okta(clientmock, "test_mount", null_bless_cache) + assert new_username == "john.doe" + + +def test_auth_okta_Cache(mocker): + class MockClient(object): + def __init__(self): + self.token = "test-token" + + cachemock = mocker.MagicMock() + cachemock.get.return_value = { + "token": "test-token", + "expiration": ( + datetime.datetime.utcnow() + + datetime.timedelta(hours=1) + ).strftime('%Y%m%dT%H%M%SZ'), + "username": "john.doe" + } + new_client, new_username = client.auth_okta(MockClient(), "test", cachemock) + assert new_username == "john.doe" diff --git a/tests/blessclient/vault_ca_test.py b/tests/blessclient/vault_ca_test.py new file mode 100644 index 0000000..1c0f6c3 --- /dev/null +++ b/tests/blessclient/vault_ca_test.py @@ -0,0 +1,57 @@ +import pytest +from blessclient.vault_ca import VaultCA +import hvac + + +TESTVAULTCONFIG = { + 'vault_addr': 'https://vault.example.com:1234' +} + + +def test_getCert(mocker): + clientmock = mocker.MagicMock() + clientmock.write.return_value = { + 'StatusCode': 200, + 'data': { + 'signed_key': "The Cert" + } + } + hvacmock = mocker.patch('hvac.Client') + hvacmock.return_value = clientmock + client = hvac.Client(TESTVAULTCONFIG['vault_addr']) + vault_ca = VaultCA(client) + returned = vault_ca.getCert( + { + 'ssh_backend_mount': 'foo', + 'ssh_backend_role': 'bar', + 'valid_principals': 'test', + 'public_key': 'ssh-rsa stuff', + 'ttl': '500' + } + ) + assert returned == 'The Cert' + + +def test_getCert_NoCert(mocker): + clientmock = mocker.MagicMock() + clientmock.write.return_value = { + 'StatusCode': 403, + 'data': { + 'error': "Forbidden" + } + } + hvacmock = mocker.patch('hvac.Client') + hvacmock.return_value = clientmock + client = hvac.Client(TESTVAULTCONFIG['vault_addr']) + vault_ca = VaultCA(client) + with pytest.raises(Exception) as excinfo: + vault_ca.getCert( + { + 'ssh_backend_mount': 'foo', + 'ssh_backend_role': 'bar', + 'valid_principals': 'test', + 'public_key': 'ssh-rsa stuff', + 'ttl': '500' + } + ) + assert 'No certificate in response.' in str(excinfo.value) \ No newline at end of file From 66ddc5b81cc6ffa441b890890784e48f77e3a274 Mon Sep 17 00:00:00 2001 From: Michael Sawyer Date: Mon, 4 Dec 2017 12:42:19 -0800 Subject: [PATCH 03/22] Authenticate with HashiCorp Vault if cached token has been invalidated. --- blessclient/client.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/blessclient/client.py b/blessclient/client.py index dbbde03..ff4b254 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -571,7 +571,22 @@ def vault_bless(nocache, bless_config): } vault_ca = VaultCA(client) - cert = vault_ca.getCert(payload) + try: + cert = vault_ca.getCert(payload) + except hvac.exceptions.Forbidden: + bless_cache = get_bless_cache(True, bless_config) + client, linux_username = auth_okta(client, auth_mount, bless_cache) + + payload = { + 'valid_principals': linux_username, + 'public_key': public_key, + 'ttl': bless_config.get('BLESS_CONFIG')['certlifetime'], + 'ssh_backend_mount': bless_config.get('VAULT_CONFIG')['ssh_backend_mount'], + 'ssh_backend_role': bless_config.get('VAULT_CONFIG')['ssh_backend_role'] + } + + vault_ca = VaultCA(client) + cert = vault_ca.getCert(payload) logging.debug("Got back cert: {}".format(cert)) From 814fbc5c590ab3bfdabe7270e05f6cda326a47ff Mon Sep 17 00:00:00 2001 From: Brady Law Date: Tue, 23 Jan 2018 10:22:51 -0800 Subject: [PATCH 04/22] Fix blessclient focus using Applescript command --- blessclient/tokengui.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/blessclient/tokengui.py b/blessclient/tokengui.py index 99f97b9..01c3620 100644 --- a/blessclient/tokengui.py +++ b/blessclient/tokengui.py @@ -32,19 +32,10 @@ def doGUI(self, hostname=None): self.master.attributes('-topmost', True) self.master.focus_force() self.e1.focus_set() + if platform.system() == 'Darwin': - try: - from Cocoa import ( - NSRunningApplication, - NSApplicationActivateIgnoringOtherApps - ) - - app = NSRunningApplication.runningApplicationWithProcessIdentifier_( - os.getpid() - ) - app.activateWithOptions_(NSApplicationActivateIgnoringOtherApps) - except ImportError: - pass + # Hack to get the GUI dialog focused in OSX + os.system('/usr/bin/osascript -e \'tell app "Finder" to set frontmost of process "python" to true\'') mainloop() From c5fb55efe9583fdead3acdce6aaef9f217765ef6 Mon Sep 17 00:00:00 2001 From: Chris Steipp Date: Tue, 23 Jan 2018 17:18:01 -0800 Subject: [PATCH 05/22] Update version to 0.3.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 16d6402..1547975 100644 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ setup( name="blessclient", - version="0.2.0", + version="0.3.0", packages=find_packages(exclude=["test*"]), install_requires=[ 'boto3>=1.4.0,<2.0.0', From 71d323bd3e2a58936e939e7d5bda79450613f8c1 Mon Sep 17 00:00:00 2001 From: Michael Sawyer Date: Fri, 2 Mar 2018 09:32:44 -0800 Subject: [PATCH 06/22] Default to BLESS backend. --- blessclient/client.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/blessclient/client.py b/blessclient/client.py index e3023d4..0c109ec 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -858,12 +858,9 @@ def main(): if ca_backend.lower() == 'hashicorp-vault': vault_bless(args.nocache, bless_config) success = True - elif ca_backend.lower() == 'bless': + else: bless(region, args.nocache, args.gui, args.host, bless_config) success = True - else: - sys.stderr.write('{0} is an invalid CA backend'.format(ca_backend)) - sys.exit(1) break except (ClientError, LambdaInvocationException, ConnectionError, EndpointConnectionError) as e: From 16ed885b7104e85448409e088362e2e6a03102b5 Mon Sep 17 00:00:00 2001 From: Michael Sawyer Date: Tue, 6 Mar 2018 09:41:39 -0800 Subject: [PATCH 07/22] Revert "Default to BLESS backend." This reverts commit 71d323b --- blessclient/client.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/blessclient/client.py b/blessclient/client.py index 0c109ec..e3023d4 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -858,9 +858,12 @@ def main(): if ca_backend.lower() == 'hashicorp-vault': vault_bless(args.nocache, bless_config) success = True - else: + elif ca_backend.lower() == 'bless': bless(region, args.nocache, args.gui, args.host, bless_config) success = True + else: + sys.stderr.write('{0} is an invalid CA backend'.format(ca_backend)) + sys.exit(1) break except (ClientError, LambdaInvocationException, ConnectionError, EndpointConnectionError) as e: From 4b8ff74062acec45246e245da640af69e667f460 Mon Sep 17 00:00:00 2001 From: Michael Sawyer Date: Tue, 6 Mar 2018 09:42:58 -0800 Subject: [PATCH 08/22] Defaults ca_backend to bless. Updates tests. --- blessclient/bless_config.py | 11 ++- tests/blessclient/bless_config_test.py | 114 ++++++++++++++----------- 2 files changed, 73 insertions(+), 52 deletions(-) diff --git a/blessclient/bless_config.py b/blessclient/bless_config.py index 4c95448..a156df8 100644 --- a/blessclient/bless_config.py +++ b/blessclient/bless_config.py @@ -9,6 +9,7 @@ class BlessConfig(object): 'usebless_role_session_length': '3600', 'update_sshagent': 'true', 'remote_user': None, + 'ca_backend': 'bless', } def __init__(self): @@ -59,14 +60,16 @@ def parse_config_file(self, config_file): 'bastion_ips': config.get('MAIN', 'bastion_ips'), 'remote_user': config.get('MAIN', 'remote_user') }, - 'VAULT_CONFIG': { + 'REGION_ALIAS': {} + } + + if blessconfig['BLESS_CONFIG']['ca_backend'].lower() == 'hashicorp-vault': + blessconfig['VAULT_CONFIG'] = { 'vault_addr': config.get('VAULT', 'vault_addr'), 'auth_mount': config.get('VAULT', 'auth_mount'), 'ssh_backend_mount': config.get('VAULT', 'ssh_backend_mount'), 'ssh_backend_role': config.get('VAULT', 'ssh_backend_role'), - }, - 'REGION_ALIAS': {} - } + } regions = config.get('MAIN', 'region_aliases').split(",") regions = [region.strip() for region in regions] diff --git a/tests/blessclient/bless_config_test.py b/tests/blessclient/bless_config_test.py index cd56b96..1cf77c7 100644 --- a/tests/blessclient/bless_config_test.py +++ b/tests/blessclient/bless_config_test.py @@ -5,9 +5,17 @@ from blessclient.bless_config import BlessConfig -TEST_CONFIG = """ +BLESS_CONFIG = """ [MAIN] ca_backend: bless +""" + +VAULT_CONFIG = """ +[MAIN] +ca_backend: hashicorp-vault +""" + +TEST_CONFIG = """ region_aliases: iad, SFO kms_service_name: bless-production bastion_ips: 10.0.0.0/8,192.168.192.1 @@ -49,9 +57,49 @@ ssh_backend_role: bless """ +BASE_EXPECTED_CONF = { + 'KMSAUTH_CONFIG_SFO': { + 'kmskey': 'abcdefgh-0123-4567-8910-abcdefghijkl', + 'awsregion': 'us-west-2', + 'context': {'to': 'bless-production', 'user_type': 'user'} + }, + 'KMSAUTH_CONFIG_IAD': { + 'kmskey': 'zxywvuts-0123-4567-8910-abcdefghijkl', + 'awsregion': 'us-east-1', + 'context': {'to': 'bless-production', 'user_type': 'user'} + }, + 'REGION_ALIAS': {'IAD': 'us-east-1', 'SFO': 'us-west-2'}, + 'BLESS_CONFIG': { + 'ca_backend': 'bless', + 'ipcachelifetime': 60, + 'functionname': 'lyft_bless', + 'functionversion': 'PROD-1-2', + 'userrole': 'use-bless', + 'timeoutconfig': {'read': 10, 'connect': 5}, + 'certlifetime': 120, + 'accountid': '111111111111' + }, + 'AWS_CONFIG': { + 'bastion_ips': '10.0.0.0/8,192.168.192.1', + 'remote_user': 'foo' + }, + 'CLIENT_CONFIG': { + 'domain_regex': '(i-.*|.*\\.example\\.com|\\A10\\.0(?:\\.[0-9]{1,3}){2}\\Z)$', + 'cache_file': 'bless_cache.json', + 'mfa_cache_dir': '.aws-mfa/session', + 'cache_dir': '.aws-mfa/session', + 'mfa_cache_file': 'token_cache.json', + 'ip_urls': ['http://checkip.amazonaws.com', 'http://api.ipify.org'], + 'update_script': 'autoupdate.sh', + 'user_session_length': 3600, + 'usebless_role_session_length': 3600, # comes from BlessConfig.DEFAULT_CONFIG + 'update_sshagent': False + } +} + @pytest.fixture def bless_config_test(): - configIO = StringIO(TEST_CONFIG) + configIO = StringIO(BLESS_CONFIG + TEST_CONFIG) config = BlessConfig() config.set_config(config.parse_config_file(configIO)) return config @@ -82,55 +130,25 @@ def test_get_config(): assert config.get_config() == {'foo': 'bar'} -def test_load_config(): +def test_load_bless_config(): + config = BlessConfig() + configIO = StringIO(BLESS_CONFIG + TEST_CONFIG) + conf = config.parse_config_file(configIO) + assert conf == BASE_EXPECTED_CONF + +def test_load_vault_config(): config = BlessConfig() - configIO = StringIO(TEST_CONFIG) + configIO = StringIO(VAULT_CONFIG + TEST_CONFIG) conf = config.parse_config_file(configIO) - assert conf == { - 'KMSAUTH_CONFIG_SFO': { - 'kmskey': 'abcdefgh-0123-4567-8910-abcdefghijkl', - 'awsregion': 'us-west-2', - 'context': {'to': 'bless-production', 'user_type': 'user'} - }, - 'KMSAUTH_CONFIG_IAD': { - 'kmskey': 'zxywvuts-0123-4567-8910-abcdefghijkl', - 'awsregion': 'us-east-1', - 'context': {'to': 'bless-production', 'user_type': 'user'} - }, - 'REGION_ALIAS': {'IAD': 'us-east-1', 'SFO': 'us-west-2'}, - 'BLESS_CONFIG': { - 'ca_backend': 'bless', - 'ipcachelifetime': 60, - 'functionname': 'lyft_bless', - 'functionversion': 'PROD-1-2', - 'userrole': 'use-bless', - 'timeoutconfig': {'read': 10, 'connect': 5}, - 'certlifetime': 120, - 'accountid': '111111111111' - }, - 'AWS_CONFIG': { - 'bastion_ips': '10.0.0.0/8,192.168.192.1', - 'remote_user': 'foo' - }, - 'CLIENT_CONFIG': { - 'domain_regex': '(i-.*|.*\\.example\\.com|\\A10\\.0(?:\\.[0-9]{1,3}){2}\\Z)$', - 'cache_file': 'bless_cache.json', - 'mfa_cache_dir': '.aws-mfa/session', - 'cache_dir': '.aws-mfa/session', - 'mfa_cache_file': 'token_cache.json', - 'ip_urls': ['http://checkip.amazonaws.com', 'http://api.ipify.org'], - 'update_script': 'autoupdate.sh', - 'user_session_length': 3600, - 'usebless_role_session_length': 3600, # comes from BlessConfig.DEFAULT_CONFIG - 'update_sshagent': False - }, - 'VAULT_CONFIG': { - 'vault_addr': 'https://vault.example.com:1234', - 'auth_mount': 'okta', - 'ssh_backend_mount': 'ssh-client-signer', - 'ssh_backend_role': 'bless' - } + expectedConf = BASE_EXPECTED_CONF + expectedConf['VAULT_CONFIG'] = { + 'vault_addr': 'https://vault.example.com:1234', + 'auth_mount': 'okta', + 'ssh_backend_mount': 'ssh-client-signer', + 'ssh_backend_role': 'bless' } + expectedConf['BLESS_CONFIG']['ca_backend'] = "hashicorp-vault" + assert conf == expectedConf def test_get_region_alias_from_aws_region(bless_config_test): From 7bbcabd27ac889dca056ef2a6d6475299d12fa14 Mon Sep 17 00:00:00 2001 From: James Addison <406005+jayaddison@users.noreply.github.com> Date: Mon, 16 Apr 2018 12:06:46 -0700 Subject: [PATCH 09/22] Check client error type during KMS token retrieval --- blessclient/client.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/blessclient/client.py b/blessclient/client.py index e3023d4..7e9e25e 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -865,8 +865,14 @@ def main(): sys.stderr.write('{0} is an invalid CA backend'.format(ca_backend)) sys.exit(1) break - except (ClientError, LambdaInvocationException, ConnectionError, - EndpointConnectionError) as e: + except ClientError as e: + if e.response.get('Error', {}).get('Code') == 'InvalidSignatureException': + sys.stderr.write( + 'Your authentication signature was rejected by AWS; try checking your system ' + + 'date & timezone settings are correct') + logging.info( + 'Lambda execution error: {}. Trying again in the alternate region.'.format(str(e))) + except (LambdaInvocationException, ConnectionError, EndpointConnectionError) as e: logging.info( 'Lambda execution error: {}. Trying again in the alternate region.'.format(str(e))) if success: From b79e84fbebf80492287a899ff83e3956252380a4 Mon Sep 17 00:00:00 2001 From: James Addison <406005+jayaddison@users.noreply.github.com> Date: Mon, 16 Apr 2018 12:22:06 -0700 Subject: [PATCH 10/22] Check client error type during user identity retrieval --- blessclient/client.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/blessclient/client.py b/blessclient/client.py index 7e9e25e..c4b13b6 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -391,6 +391,12 @@ def get_username(aws, bless_cache): awsmfautils.unset_token() user = aws.iam_client().get_user()['User'] except ClientError as e: + if e.response.get('Error', {}).get('Code') == 'SignatureDoesNotMatch': + sys.stderr.write( + "Your authentication signature was rejected by AWS; try checking your system " + + "date & timezone settings are correct") + raise + sys.stderr.write( "Can't get your user information from AWS! Either you don't have your user" " aws credentials set as [default] in ~/.aws/credentials, or you have another" @@ -869,7 +875,7 @@ def main(): if e.response.get('Error', {}).get('Code') == 'InvalidSignatureException': sys.stderr.write( 'Your authentication signature was rejected by AWS; try checking your system ' + - 'date & timezone settings are correct') + 'date & timezone settings are correct\n') logging.info( 'Lambda execution error: {}. Trying again in the alternate region.'.format(str(e))) except (LambdaInvocationException, ConnectionError, EndpointConnectionError) as e: From ed134fe784ebaa46649c52686bd55f186f15a73f Mon Sep 17 00:00:00 2001 From: James Addison <406005+jayaddison@users.noreply.github.com> Date: Mon, 16 Apr 2018 12:26:52 -0700 Subject: [PATCH 11/22] Add newlines --- blessclient/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessclient/client.py b/blessclient/client.py index c4b13b6..38c8b59 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -394,13 +394,13 @@ def get_username(aws, bless_cache): if e.response.get('Error', {}).get('Code') == 'SignatureDoesNotMatch': sys.stderr.write( "Your authentication signature was rejected by AWS; try checking your system " + - "date & timezone settings are correct") + "date & timezone settings are correct\n") raise sys.stderr.write( "Can't get your user information from AWS! Either you don't have your user" " aws credentials set as [default] in ~/.aws/credentials, or you have another" - " process setting AWS credentials for a service account in your environment.") + " process setting AWS credentials for a service account in your environment.\n") raise username = user['UserName'] bless_cache.set('username', username) From a83b54ffe357ba6d66dfa9fba74688a4d10a93d5 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 18 Apr 2018 10:22:33 -0700 Subject: [PATCH 12/22] Restore python3 support in blessclient (#33) * Fix flake8 lint errors * Restore python3 support in blessclient * Set up test harness --- .travis.yml | 20 +++++++++++--------- blessclient/bless_aws.py | 2 +- blessclient/bless_cache.py | 2 +- blessclient/client.py | 20 +++++++++----------- blessclient/user_ip.py | 4 ++-- setup.cfg | 2 +- tests/blessclient/client_test.py | 8 ++++---- tox.ini | 8 ++++++++ 8 files changed, 37 insertions(+), 29 deletions(-) create mode 100644 tox.ini diff --git a/.travis.yml b/.travis.yml index c693a5d..8bb7d9a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,14 @@ sudo: false language: python - matrix: - include: - - python: "2.7" - -install: - - make develop - -script: - - make test + include: + - env: TOXENV=py36 + python: 3.6 + include: + - env: TOXENV=py27 + python: 2.7 +install: pip install tox +script: tox +cache: + directories: + - $HOME/.cache/pip diff --git a/blessclient/bless_aws.py b/blessclient/bless_aws.py index 8197270..0ce5b72 100644 --- a/blessclient/bless_aws.py +++ b/blessclient/bless_aws.py @@ -40,7 +40,7 @@ def iam_client(self): try: self.iam = boto3.client('iam') break - except DataNotFoundError as e: + except DataNotFoundError: logging.exception('DataNotFoundError when trying to get the iam client.') t = self.retry_policy(attempt) if t is None: diff --git a/blessclient/bless_cache.py b/blessclient/bless_cache.py index 33a37c2..04a7876 100644 --- a/blessclient/bless_cache.py +++ b/blessclient/bless_cache.py @@ -44,7 +44,7 @@ def loadCache(self): with open(cache_file_path, 'r') as cache: try: self.cache = json.load(cache) - except: + except Exception: logging.error("Corrupted cache, using empty cache") logging.debug("Cache loaded: {}".format(self.cache)) diff --git a/blessclient/client.py b/blessclient/client.py index 38c8b59..b436fdf 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -20,8 +20,6 @@ import hvac import getpass -from random import randint - import six from . import awsmfautils @@ -97,7 +95,7 @@ def get_regions(region, bless_config): List of regions """ regions = [] - aws_regions = tuple(bless_config.get('REGION_ALIAS').values()) + aws_regions = tuple(sorted(bless_config.get('REGION_ALIAS').values())) try: ndx = aws_regions.index(region) except ValueError: @@ -253,7 +251,7 @@ def get_kmsauth_token(creds, config, username, cache): config['awsregion'], aws_creds=creds, token_lifetime=60 - ).get_token() + ).get_token().decode('US-ASCII') except kmsauth.ServiceConnectionError: logging.debug("Network failure for kmsauth") raise LambdaInvocationException('Connection error getting kmsauth token.') @@ -354,7 +352,7 @@ def save_cached_creds(token_data, bless_config): def ssh_agent_remove_bless(identity_file): DEVNULL = open(os.devnull, 'w') try: - current = subprocess.check_output(['ssh-add', '-l']) + current = subprocess.check_output(['ssh-add', '-l']).decode('UTF-8') match = re.search(re.escape(identity_file), current) if match: subprocess.check_call( @@ -367,7 +365,7 @@ def ssh_agent_remove_bless(identity_file): def ssh_agent_add_bless(identity_file): DEVNULL = open(os.devnull, 'w') subprocess.check_call(['ssh-add', identity_file], stderr=DEVNULL) - current = subprocess.check_output(['ssh-add', '-l']) + current = subprocess.check_output(['ssh-add', '-l']).decode('UTF-8') if not re.search(re.escape(identity_file), current): logging.debug("Could not add '{}' to ssh-agent".format(identity_file)) sys.stderr.write( @@ -470,8 +468,8 @@ def get_cached_auth_token(bless_cache): def get_credentials(): - print "Enter Vault username:" - username = raw_input() + print("Enter Vault username:") + username = six.moves.input() password = getpass.getpass(prompt="Password (will be hidden):") return username, password @@ -690,7 +688,7 @@ def bless(region, nocache, showgui, hostname, bless_config): role_creds = get_blessrole_credentials( aws.iam_client(), None, bless_config, bless_cache) logging.debug("Default creds used to assume role use-bless") - except: + except Exception: pass # TODO if role_creds is None: @@ -709,7 +707,7 @@ def bless(region, nocache, showgui, hostname, bless_config): role_creds = get_blessrole_credentials( aws.iam_client(), creds, bless_config, bless_cache) logging.debug("Assumed role use-bless using cached creds") - except: + except Exception: pass if role_creds is None: @@ -810,7 +808,7 @@ def bless(region, nocache, showgui, hostname, bless_config): else: logging.info( "Skipping loading identity into the running ssh-agent " - 'because this was disabled in the blessclient config.' ) + 'because this was disabled in the blessclient config.') bless_cache.set('certip', my_ip) bless_cache.save() diff --git a/blessclient/user_ip.py b/blessclient/user_ip.py index 714c1c4..fd72037 100644 --- a/blessclient/user_ip.py +++ b/blessclient/user_ip.py @@ -53,12 +53,12 @@ def _fetchIP(self, url): try: with contextlib.closing(urlopen(url, timeout=2)) as f: if f.getcode() == 200: - content = f.read().strip()[:40] + content = f.read().decode().strip()[:40] for c in content: if c not in VALID_IP_CHARACTERS: raise ValueError("Public IP response included invalid character '{}'.".format(c)) return content - except: + except Exception: logging.debug('Could not refresh public IP from {}'.format(url), exc_info=True) return None diff --git a/setup.cfg b/setup.cfg index 2d75bb0..e299fa9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -17,7 +17,7 @@ format=pylint # The current value is set so that the build doesn't fail. At least we won't # make the software more complex. # We should target 10. Likewise, 90 for line length. -max-complexity = 23 +max-complexity = 25 max-line-length = 126 exclude = .git,__pycache__,venv,tests/ ignore = E402, E124, W503 diff --git a/tests/blessclient/client_test.py b/tests/blessclient/client_test.py index d1d89ac..47d6f9b 100644 --- a/tests/blessclient/client_test.py +++ b/tests/blessclient/client_test.py @@ -92,7 +92,7 @@ def test_clear_kmsauth_token_cache(null_bless_cache): def test_get_kmsauth_token(mocker, null_bless_cache): tokenmock = mocker.MagicMock() - tokenmock.get_token.return_value = 'KMSTOKEN' + tokenmock.get_token.return_value = b'KMSTOKEN' genermock = mocker.patch('kmsauth.KMSTokenGenerator') genermock.return_value = tokenmock kmsconfig = {'awsregion': 'us-east-1', 'context': {}, 'kmskey': None} @@ -150,7 +150,7 @@ def test_uncache_creds(): def test_ssh_agent_remove_bless(mocker): outputmock = mocker.patch('subprocess.check_output') - outputmock.return_value = '4096 SHA256:hwnh3ccCcxVUo6T6htWvHdkCx/UsNklwy2uQuiBaTLQ /Users/foobar/.ssh/blessid (RSA-CERT)' + outputmock.return_value = b'4096 SHA256:hwnh3ccCcxVUo6T6htWvHdkCx/UsNklwy2uQuiBaTLQ /Users/foobar/.ssh/blessid (RSA-CERT)' callmock = mocker.patch('subprocess.check_call') client.ssh_agent_remove_bless('blessid') outputmock.assert_called_once() @@ -159,7 +159,7 @@ def test_ssh_agent_remove_bless(mocker): def test_ssh_agent_add_bless(mocker): outputmock = mocker.patch('subprocess.check_output') - outputmock.return_value = '4096 SHA256:hwnh3ccCcxVUo6T6htWvHdkCx/UsNklwy2uQuiBaTLQ /Users/foobar/.ssh/blessid (RSA-CERT)' + outputmock.return_value = b'4096 SHA256:hwnh3ccCcxVUo6T6htWvHdkCx/UsNklwy2uQuiBaTLQ /Users/foobar/.ssh/blessid (RSA-CERT)' callmock = mocker.patch('subprocess.check_call') client.ssh_agent_add_bless('.ssh/blessid') outputmock.assert_called_once() @@ -168,7 +168,7 @@ def test_ssh_agent_add_bless(mocker): def test_ssh_agent_add_bless_failed(mocker): outputmock = mocker.patch('subprocess.check_output') - outputmock.return_value = '' + outputmock.return_value = b'' callmock = mocker.patch('subprocess.check_call') logmock = mocker.patch('logging.debug') writemock = mocker.patch('sys.stderr.write') diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..6e0cb2b --- /dev/null +++ b/tox.ini @@ -0,0 +1,8 @@ +[tox] +envlist = py27,py36 + +[testenv] +deps = -rrequirements-dev.txt +commands = + pytest {posargs:tests} + flake8 blessclient tests setup.py From ad176fb3da1a98fa75082a97f5ffb462280d71b4 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 18 Apr 2018 10:30:41 -0700 Subject: [PATCH 13/22] Fix yaml oops in .travis.yml --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8bb7d9a..f418591 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,6 @@ matrix: include: - env: TOXENV=py36 python: 3.6 - include: - env: TOXENV=py27 python: 2.7 install: pip install tox From 00a7df6febb76d631e9db6c87170d927e520d5cf Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 20 Apr 2018 14:57:45 -0700 Subject: [PATCH 14/22] Fix tkinter import for python 3 --- blessclient/tokengui.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessclient/tokengui.py b/blessclient/tokengui.py index 01c3620..caaa83f 100644 --- a/blessclient/tokengui.py +++ b/blessclient/tokengui.py @@ -1,7 +1,7 @@ from __future__ import absolute_import import platform import os -from Tkinter import Tk, Label, Entry, Button, ACTIVE, W, mainloop +from six.moves.tkinter import Tk, Label, Entry, Button, ACTIVE, W, mainloop class TokenInputGUI(object): From 86b37bb803f9ff66563162dd92ec7f0b4dc00448 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 2 Jul 2018 09:25:37 -0700 Subject: [PATCH 15/22] Fix blessclient for python3.7 --- .gitignore | 13 +++++++------ .travis.yml | 10 ++++++++++ blessclient/bless_config.py | 2 +- blessclient/client.py | 4 +--- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index 919c708..0f384be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ *.pyc -.coverage -.cache .DS_Store -coverage.xml -venv/ -blessclient.run +/.coverage +/.pytest_cache +/.tox +blessclient.cfg blessclient.egg-info/ +blessclient.run build/ -blessclient.cfg +coverage.xml +venv/ diff --git a/.travis.yml b/.travis.yml index f418591..fbceabb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,16 @@ matrix: python: 3.6 - env: TOXENV=py27 python: 2.7 + # https://github.com/deadsnakes/travis-ci-python3.7-example + - env: TOXENV=py37 + sudo: required + dist: xenial + addons: + apt: + sources: + - sourceline: 'deb http://ppa.launchpad.net/deadsnakes/ppa/ubuntu xenial main' + packages: + - python3.7-dev install: pip install tox script: tox cache: diff --git a/blessclient/bless_config.py b/blessclient/bless_config.py index a156df8..05ea153 100644 --- a/blessclient/bless_config.py +++ b/blessclient/bless_config.py @@ -8,7 +8,7 @@ class BlessConfig(object): 'user_session_length': '64800', 'usebless_role_session_length': '3600', 'update_sshagent': 'true', - 'remote_user': None, + 'remote_user': '', 'ca_backend': 'bless', } diff --git a/blessclient/client.py b/blessclient/client.py index b436fdf..8713c3c 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -756,9 +756,7 @@ def bless(region, nocache, showgui, hostname, bless_config): my_ip = userIP.getIP() ip_list = "{},{}".format(my_ip, bless_config.get_aws_config()['bastion_ips']) - remote_user = bless_config.get_aws_config()['remote_user'] - if remote_user is None: - remote_user = username + remote_user = bless_config.get_aws_config()['remote_user'] or username payload = { 'bastion_user': username, 'bastion_user_ip': my_ip, From e336c0712872cadcb4f53c2da03f4f89cee89527 Mon Sep 17 00:00:00 2001 From: Michael Peterson Date: Fri, 17 Aug 2018 13:10:40 -0400 Subject: [PATCH 16/22] Fix the osacript to work with versioned names of python (e.g. 'python3.7') --- blessclient/tokengui.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/blessclient/tokengui.py b/blessclient/tokengui.py index caaa83f..5728986 100644 --- a/blessclient/tokengui.py +++ b/blessclient/tokengui.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import platform import os +import subprocess from six.moves.tkinter import Tk, Label, Entry, Button, ACTIVE, W, mainloop @@ -35,7 +36,10 @@ def doGUI(self, hostname=None): if platform.system() == 'Darwin': # Hack to get the GUI dialog focused in OSX - os.system('/usr/bin/osascript -e \'tell app "Finder" to set frontmost of process "python" to true\'') + # https://stackoverflow.com/questions/1892339/how-to-make-a-tkinter-window-jump-to-the-front/37235492#37235492 + tmpl = 'tell application "System Events" to set frontmost of every process whose unix id is {} to true' + script = tmpl.format(os.getpid()) + subprocess.check_call(['/usr/bin/osascript', '-e', script]) mainloop() From 54312ad6ed1aa363fe5f0fd80bdedd3b9c207830 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Wed, 22 Aug 2018 12:31:54 -0700 Subject: [PATCH 17/22] Use expanduser to more reliably get homedir When the current working directory was missing this was crashing with: ```python Traceback (most recent call last): File "/home/asottile/workspace/blessclient/bless", line 11, in load_entry_point('blessclient', 'console_scripts', 'blessclient')() File "/home/asottile/workspace/blessclient/python-blessclient/blessclient/client.py", line 864, in main bless(region, args.nocache, args.gui, args.host, bless_config) File "/home/asottile/workspace/blessclient/python-blessclient/blessclient/client.py", line 648, in bless bless_cache = get_bless_cache(nocache, bless_config) File "/home/asottile/workspace/blessclient/python-blessclient/blessclient/client.py", line 284, in get_bless_cache os.getenv('HOME', os.getcwd()), FileNotFoundError: [Errno 2] No such file or directory ``` --- blessclient/client.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/blessclient/client.py b/blessclient/client.py index 8713c3c..27b826f 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -281,7 +281,7 @@ def setup_logging(): def get_bless_cache(nocache, bless_config): client_config = bless_config.get_client_config() cachedir = os.path.join( - os.getenv('HOME', os.getcwd()), + os.path.expanduser('~'), client_config['cache_dir']) cachemode = BlessCache.CACHEMODE_RECACHE if nocache else BlessCache.CACHEMODE_ENABLED return BlessCache(cachedir, client_config['cache_file'], cachemode) @@ -653,7 +653,7 @@ def bless(region, nocache, showgui, hostname, bless_config): clistring = psutil.Process(os.getppid()).cmdline() identity_file = get_idfile_from_cmdline( clistring, - os.getenv('HOME', os.getcwd()) + '/.ssh/blessid' + os.path.expanduser('~/.ssh/blessid'), ) cert_file = identity_file + '-cert.pub' From 6442227fd3a04d2f996e78e3ac135ac9c3c429d4 Mon Sep 17 00:00:00 2001 From: "Ole Mathias Aa. Heggem" Date: Wed, 3 Oct 2018 23:24:39 +0200 Subject: [PATCH 18/22] Fix comment in sample config (#44) --- blessclient.cfg.sample | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/blessclient.cfg.sample b/blessclient.cfg.sample index 682acc2..43669ad 100644 --- a/blessclient.cfg.sample +++ b/blessclient.cfg.sample @@ -63,9 +63,9 @@ update_script: update_blessclient.sh # is 3600 seconds (1 hour). The value must be in the range 900-3600. # update_sshagent: Specifies whether the identity key should be automatically added to the -running ssh-agent. If this option is set to 'true', the key and the ssh certificate retrieved -from lambda are added to the agent. If this option is set to 'false', the key is not added -to the agent. The default is 'true'. +# running ssh-agent. If this option is set to 'true', the key and the ssh certificate retrieved +# from lambda are added to the agent. If this option is set to 'false', the key is not added +# to the agent. The default is 'true'. [LAMBDA] # user_role: IAM Role that the user will assume, in order to run the BLESS Lambda. This From 2c35382e6bbee541ce1292661e10f575a3d42e62 Mon Sep 17 00:00:00 2001 From: James Addison <406005+jayaddison@users.noreply.github.com> Date: Wed, 12 Dec 2018 10:14:11 -0800 Subject: [PATCH 19/22] Remove stale documentation suggestion --- blessclient/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blessclient/client.py b/blessclient/client.py index 27b826f..e8b2439 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -199,7 +199,7 @@ def get_mfa_token_cli(): def get_mfa_token_gui(message): sys.stderr.write( - "Enter your AWS MFA token in the gui dialog. Alternatively, run mfa.sh first.\n") + "Enter your AWS MFA token in the gui dialog.\n") tig = tokengui.TokenInputGUI() if message == 'BLESS': message = None From 6df84b4089d7f74ddf5e0f660dc37b6c408a4137 Mon Sep 17 00:00:00 2001 From: James Addison Date: Wed, 12 Dec 2018 10:29:00 -0800 Subject: [PATCH 20/22] Ignore W504 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index e299fa9..31f59ba 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,4 +20,4 @@ format=pylint max-complexity = 25 max-line-length = 126 exclude = .git,__pycache__,venv,tests/ -ignore = E402, E124, W503 +ignore = E402, E124, W503, W504 From 5ebc322da1c70d53c73bddd1beca46687beaae0a Mon Sep 17 00:00:00 2001 From: James Addison Date: Wed, 12 Dec 2018 10:35:30 -0800 Subject: [PATCH 21/22] Use PPA sourceline --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index fbceabb..59faea6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ matrix: addons: apt: sources: - - sourceline: 'deb http://ppa.launchpad.net/deadsnakes/ppa/ubuntu xenial main' + - sourceline: 'ppa:deadsnakes/ppa' packages: - python3.7-dev install: pip install tox From 437ae7ccee77d3612a25d9a34ae62ae6c7d0b79e Mon Sep 17 00:00:00 2001 From: Vivian <2908189+vivianho@users.noreply.github.com> Date: Thu, 6 Jun 2019 13:36:12 -0700 Subject: [PATCH 22/22] Update readme to deprecate blessclient (#53) --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a0bd2d4..ad209c4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ -# Blessclient +# Blessclient -- DEPRECATED + +**NOTE**: We have deprecated python-blessclient and it is no longer actively maintained. A recommended alternative is [blessclient in Go](https://github.com/chanzuckerberg/blessclient). + A client for interacting with [BLESS](https://github.com/lyft/bless) services from users' laptops. Blessclient optimizes to ensure that users can always use ssh as they normally would with a fixed key, with minimal delay. [Netflix's BLESS](https://github.com/netflix/bless) was designed to issue short-lived certificates to users after they logged into a bastion service, that would be used to authenticate the user to other hosts within the cluster. Lyft wanted to use ephemeral ssh certificates for our users too, but wanted to issue these certificates directly to users' laptops, instead of on the bastion. We were able to accomplish this by making a few modifications to Netflix's BLESS and deploying this project, blessclient, to our users' laptops. Doing this allowed Lyft to improve security by extending the existing multi-factor authentication (MFA) setup that we had with AWS to SSH, as well as simplifying our provisioning and deprovisioning process.