From b04b27b99e415277b7cd8cc252c52ef54a4f63f7 Mon Sep 17 00:00:00 2001 From: jaymed Date: Fri, 6 Oct 2017 16:13:39 -0700 Subject: [PATCH 01/26] Allow user to disable automatic loading of identity into running ssh-agent --- blessclient.cfg.sample | 5 +++++ blessclient/bless_config.py | 9 +++++++++ blessclient/client.py | 14 ++++++++++---- tests/blessclient/bless_config_test.py | 4 ++++ 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/blessclient.cfg.sample b/blessclient.cfg.sample index e18f06f..29d3116 100644 --- a/blessclient.cfg.sample +++ b/blessclient.cfg.sample @@ -55,6 +55,11 @@ update_script: update_blessclient.sh # tokens for when the user assumes the role necessary to call the BLESS Lambda. The default # is 3600 seconds (1 hour). The value must be in the range 900-3600. +# update_sshagent: Specifies whether the identify 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. + [LAMBDA] # user_role: IAM Role that the user will assume, in order to run the BLESS Lambda. This # role should be in the same AWS account as your Lambda. diff --git a/blessclient/bless_config.py b/blessclient/bless_config.py index e7096f9..7b757bd 100644 --- a/blessclient/bless_config.py +++ b/blessclient/bless_config.py @@ -6,6 +6,7 @@ class BlessConfig(object): DEFAULT_CONFIG = { 'user_session_length': '64800', 'usebless_role_session_length': '3600', + 'update_sshagent': 'true', } def __init__(self): @@ -22,6 +23,13 @@ def _get_region_kms_config(self, region, config): 'awsregion': config.get(section, 'awsregion') } + def _strtobool(self, config): + if str(config).lower() in ("y", "yes", "true", "on", "1"): + return True + if str(config).lower() in ("n", "no", "false", "off", "0"): + return False + raise ValueError('Unexpected value: {}'.format(config)) + def parse_config_file(self, config_file): config = ConfigParser.SafeConfigParser(self.DEFAULT_CONFIG) loaded = config.readfp(config_file) @@ -37,6 +45,7 @@ def parse_config_file(self, config_file): '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')), + 'update_sshagent': bool(self._strtobool(config.get('CLIENT', 'update_sshagent'))), }, 'BLESS_CONFIG': { 'userrole': config.get('LAMBDA', 'user_role'), diff --git a/blessclient/client.py b/blessclient/client.py index 7b5d129..44e91b6 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -578,10 +578,16 @@ def bless(region, nocache, showgui, hostname, bless_config): raise LambdaInvocationException( 'BLESS client did not recieve a valid cert. Instead got: {}'.format(cert)) - ssh_agent_remove_bless(identity_file) - with open(cert_file, 'w') as cert_file: - cert_file.write(cert) - ssh_agent_add_bless(identity_file) + # Check if we can skip adding identity into the running ssh-agent + if bless_config.get_client_config()['update_sshagent'] is True: + ssh_agent_remove_bless(identity_file) + with open(cert_file, 'w') as cert_file: + cert_file.write(cert) + ssh_agent_add_bless(identity_file) + else: + logging.info( + "Skipping loading identity into the running ssh-agent " + 'because this was disabled in the blessclient config.' ) bless_cache.set('certip', my_ip) bless_cache.save() diff --git a/tests/blessclient/bless_config_test.py b/tests/blessclient/bless_config_test.py index cc5ce5b..0c32617 100644 --- a/tests/blessclient/bless_config_test.py +++ b/tests/blessclient/bless_config_test.py @@ -18,6 +18,7 @@ ip_urls: http://checkip.amazonaws.com, http://api.ipify.org update_script: autoupdate.sh user_session_length: 3600 +update_sshagent: false [LAMBDA] user_role: use-bless @@ -109,6 +110,7 @@ def test_load_config(): 'update_script': 'autoupdate.sh', 'user_session_length': 3600, 'usebless_role_session_length': 3600, # comes from BlessConfig.DEFAULT_CONFIG + 'update_sshagent': False } } @@ -122,6 +124,8 @@ def test_get_region_alias_from_aws_region(bless_config_test): def test_get_configs(bless_config_test): client_config = bless_config_test.get_client_config() assert 'domain_regex' in client_config + assert bool(client_config['update_sshagent']) is False + assert type(client_config['update_sshagent']).__name__ == 'bool' lambda_config = bless_config_test.get_lambda_config() assert 'functionname' in lambda_config aws_config = bless_config_test.get_aws_config() From 0f9c365533e2c95df773ab79fe0bc2018705e553 Mon Sep 17 00:00:00 2001 From: jaymed Date: Fri, 6 Oct 2017 16:29:29 -0700 Subject: [PATCH 02/26] Small typo fix in update_sshagent config description and using config.getboolean function which already does a good job of converting strings to boolean values. --- blessclient.cfg.sample | 4 ++-- blessclient/bless_config.py | 9 +-------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/blessclient.cfg.sample b/blessclient.cfg.sample index 29d3116..91ceaab 100644 --- a/blessclient.cfg.sample +++ b/blessclient.cfg.sample @@ -55,10 +55,10 @@ update_script: update_blessclient.sh # tokens for when the user assumes the role necessary to call the BLESS Lambda. The default # is 3600 seconds (1 hour). The value must be in the range 900-3600. -# update_sshagent: Specifies whether the identify key should be automatically added to the +# 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. +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 diff --git a/blessclient/bless_config.py b/blessclient/bless_config.py index 7b757bd..a21ed45 100644 --- a/blessclient/bless_config.py +++ b/blessclient/bless_config.py @@ -23,13 +23,6 @@ def _get_region_kms_config(self, region, config): 'awsregion': config.get(section, 'awsregion') } - def _strtobool(self, config): - if str(config).lower() in ("y", "yes", "true", "on", "1"): - return True - if str(config).lower() in ("n", "no", "false", "off", "0"): - return False - raise ValueError('Unexpected value: {}'.format(config)) - def parse_config_file(self, config_file): config = ConfigParser.SafeConfigParser(self.DEFAULT_CONFIG) loaded = config.readfp(config_file) @@ -45,7 +38,7 @@ def parse_config_file(self, config_file): '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')), - 'update_sshagent': bool(self._strtobool(config.get('CLIENT', 'update_sshagent'))), + 'update_sshagent': config.getboolean('CLIENT', 'update_sshagent'), }, 'BLESS_CONFIG': { 'userrole': config.get('LAMBDA', 'user_role'), From 45e1d5e4ff2e606fab03fd11f63e21651de95ead Mon Sep 17 00:00:00 2001 From: jaymed Date: Mon, 9 Oct 2017 13:19:09 -0700 Subject: [PATCH 03/26] fixing issue with cert file not being written when update_sshagent is set to false. it should always be written. --- blessclient/client.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blessclient/client.py b/blessclient/client.py index 44e91b6..ad5869e 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -578,11 +578,12 @@ def bless(region, nocache, showgui, hostname, bless_config): raise LambdaInvocationException( 'BLESS client did not recieve a valid cert. Instead got: {}'.format(cert)) + with open(cert_file, 'w') as cert_file: + cert_file.write(cert) + # Check if we can skip adding identity into the running ssh-agent if bless_config.get_client_config()['update_sshagent'] is True: ssh_agent_remove_bless(identity_file) - with open(cert_file, 'w') as cert_file: - cert_file.write(cert) ssh_agent_add_bless(identity_file) else: logging.info( From edf1412403bfafb5aa8401479d4175b54701106a Mon Sep 17 00:00:00 2001 From: jaymed Date: Thu, 9 Nov 2017 08:14:16 -0800 Subject: [PATCH 04/26] Based on feedback from user stype, fixing bug where new rsa cert is added to agent each time it runs. Need to delete the cert from the agent before writing out the new file. --- blessclient/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blessclient/client.py b/blessclient/client.py index ad5869e..03dda88 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -578,12 +578,13 @@ def bless(region, nocache, showgui, hostname, bless_config): raise LambdaInvocationException( 'BLESS client did not recieve a valid cert. Instead got: {}'.format(cert)) + # Remove RSA identity from ssh-agent (if it exists) + ssh_agent_remove_bless(identity_file) with open(cert_file, 'w') as cert_file: cert_file.write(cert) # Check if we can skip adding identity into the running ssh-agent if bless_config.get_client_config()['update_sshagent'] is True: - ssh_agent_remove_bless(identity_file) ssh_agent_add_bless(identity_file) else: logging.info( From 82c1b22b79834caaa4f5d05f222950ad7d73466e Mon Sep 17 00:00:00 2001 From: jaymed Date: Thu, 9 Nov 2017 08:14:16 -0800 Subject: [PATCH 05/26] Based on feedback from user stype, fixing bug where new rsa cert is added to agent each time it runs. Need to delete the cert from the agent before writing out the new file. --- blessclient/client.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/blessclient/client.py b/blessclient/client.py index ad5869e..03dda88 100755 --- a/blessclient/client.py +++ b/blessclient/client.py @@ -578,12 +578,13 @@ def bless(region, nocache, showgui, hostname, bless_config): raise LambdaInvocationException( 'BLESS client did not recieve a valid cert. Instead got: {}'.format(cert)) + # Remove RSA identity from ssh-agent (if it exists) + ssh_agent_remove_bless(identity_file) with open(cert_file, 'w') as cert_file: cert_file.write(cert) # Check if we can skip adding identity into the running ssh-agent if bless_config.get_client_config()['update_sshagent'] is True: - ssh_agent_remove_bless(identity_file) ssh_agent_add_bless(identity_file) else: logging.info( From e0484346050cda522833f7e9b8e6d3a99b6b99c0 Mon Sep 17 00:00:00 2001 From: Michael Sawyer Date: Fri, 1 Dec 2017 19:10:06 -0800 Subject: [PATCH 06/26] 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 07/26] 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 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] 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 20/26] 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 21/26] 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 22/26] 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 23/26] 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 24/26] 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 25/26] 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 26/26] 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.