├── .gitignore ├── README.md ├── aws-sts-console-url.py └── vault-aws-creds.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | .idea/ 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vault-aws-creds 2 | 3 | [![Project Status: Unsupported – The project has reached a stable, usable state but the author(s) have ceased all work on it. A new maintainer may be desired.](https://www.repostatus.org/badges/2.1.0/unsupported.svg)](https://www.repostatus.org/#unsupported) 4 | 5 | Python helper to export Vault-provided temporary AWS credentials into the environment. 6 | Also includes a helper script to generate a Console login URL from STS temporary credentials (from Vault). 7 | 8 | ## Requirements 9 | 10 | Python 2.7+ or Python 3. No external dependencies. 11 | 12 | ## Installation 13 | 14 | 1. Place (or symlink) ``vault-aws-creds.py`` somewhere on your system and make it executable. 15 | 2. ``export VAULT_ADDR=
``; it's recommended to 16 | put that in your ``~/.bashrc`` as well. 17 | 3. Add ``eval $(vault-aws-creds.py -w)`` to your shell initialization file (i.e. ``~/.bashrc``). 18 | If vault-aws-creds.py is not on your PATH, specify the absolute path to it in the 19 | above snippet. This will setup a function that allows vault-aws-creds.py to export environment 20 | variables back into your _existing_ shell process. 21 | 4. *(optional)* If you wish to use the Console login URL generator, place 22 | (or symlink) ``aws-sts-console-url.py`` somewhere on your system and make it 23 | executable. 24 | 25 | ## Usage 26 | 27 | ### List available accounts 28 | 29 | ```bash 30 | $ vault-aws-creds 31 | Available Accounts: 32 | "aws_dev" a.k.a. "dev" 33 | "aws_prod" a.k.a. "prod" 34 | "aws_uat" a.k.a. "uat" 35 | ``` 36 | 37 | __Note:__ This requires that your token have "read" access to ``sys/mounts``. 38 | 39 | ### List available roles for account "dev" 40 | 41 | ```bash 42 | $ vault-aws-creds --roles dev 43 | Available Vault Roles for Account 'aws_dev/': 44 | administrator 45 | dba 46 | deploy 47 | developer 48 | readonly 49 | ``` 50 | 51 | __Note:__ This requires that your token have "list" access to ``roles`` under the specified mountpoint (i.e. ``aws_dev/roles`` in the above example). 52 | 53 | ### Get STS credentials for the "foo" role in the "dev" account 54 | 55 | ```bash 56 | $ vault-aws-creds dev foo 57 | Got credentials for account 'aws_dev/' role 'foo' 58 | Request ID (for troubleshooting): c0e952d4-61ea-72e8-7b56-2df50538eacf 59 | Lease (credentials) will expire in: 59m 59s 60 | Outputting the following for shell evaluation: 61 | export AWS_REGION='us-east-1' 62 | export AWS_DEFAULT_REGION='us-east-1' 63 | export AWS_ACCESS_KEY_ID='ASIAxxxxxxxxxxxxxxxx' 64 | export AWS_SECRET_ACCESS_KEY='8xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxE' 65 | export AWS_SESSION_TOKEN='F...F' 66 | ``` 67 | 68 | "foo" will now be stored in ~/.vault-aws-creds.conf as the default role for the 69 | "dev" ("aws_dev/") account. To get new creds for the same role, you can omit 70 | the role name: 71 | 72 | ```bash 73 | $ vault-aws-creds dev 74 | Got credentials for account 'aws_dev/' role 'foo' 75 | Request ID (for troubleshooting): b02d0346-cce2-911f-d853-17cf8aa591a2 76 | Lease (credentials) will expire in: 59m 59s 77 | Outputting the following for shell evaluation: 78 | export AWS_REGION='us-east-1' 79 | export AWS_DEFAULT_REGION='us-east-1' 80 | export AWS_ACCESS_KEY_ID='ASIAzzzzzzzzzzzzzzzz' 81 | export AWS_SECRET_ACCESS_KEY='8zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzE' 82 | export AWS_SESSION_TOKEN='F...F' 83 | ``` 84 | 85 | ### Get 4-hour-lifetime STS credentials for the "bar" role in the "prod" account 86 | 87 | (Note: this requires that your user in Vault have "update" capabilities for the sts path. Users of older Vault installations may only have "read".) 88 | 89 | ```bash 90 | $ vault-aws-creds --ttl=4h prod bar 91 | Got credentials for account 'aws_dev/' role 'foo' 92 | Request ID (for troubleshooting): b02d0346-cce2-911f-d853-17cf8aa591a2 93 | Lease (credentials) will expire in: 3h 59m 59s 94 | Outputting the following for shell evaluation: 95 | export AWS_REGION='us-east-1' 96 | export AWS_DEFAULT_REGION='us-east-1' 97 | export AWS_ACCESS_KEY_ID='ASIAzzzzzzzzzzzzzzzz' 98 | export AWS_SECRET_ACCESS_KEY='8zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzE' 99 | export AWS_SESSION_TOKEN='F...F' 100 | ``` 101 | 102 | ### Get IAM User credentials for the "foo" role in the "dev" account 103 | 104 | ```bash 105 | $ vault-aws-creds --iam dev foo 106 | Got credentials for account 'aws_dev/' role 'foo' 107 | Request ID (for troubleshooting): e123a94c-4819-f75d-22b1-d754ec92f589 108 | Lease (credentials) will expire in: 1h 109 | To renew, run: vault renew aws_dev/creds/foo/54078039-7b6c-be74-5fde-0adb3b209317 110 | Outputting the following for shell evaluation: 111 | export AWS_REGION='us-east-1' 112 | export AWS_DEFAULT_REGION='us-east-1' 113 | export AWS_ACCESS_KEY_ID='AKIAxxxxxxxxxxxxxxxx' 114 | export AWS_SECRET_ACCESS_KEY='AzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzB' 115 | unset AWS_SESSION_TOKEN 116 | ``` 117 | 118 | ## aws-sts-console-url.py Usage 119 | 120 | ``aws-sts-console-url.py`` is a script that uses STS temporary credentials 121 | from Vault to generate a pre-signed AWS Console login URL, allowing Console 122 | access with temporary credentials from Vault. **This can only be used with STS 123 | temporary credentials, i.e. not ``--iam`` credentials from ``vault-aws-creds``.** 124 | 125 | To use, first obtain STS temporary credentials with ``vault-aws-creds`` as shown 126 | above. Then, run ``aws-sts-console-url.py``; a Console login URL will be displayed 127 | to STDOUT. Alternatively, you can pass in the `-b` or `--browser` flag which 128 | will open the console automatically in your default browser 129 | `aws-sts-console-url.py --browser`. 130 | 131 | ## Suggested Vault Policies 132 | 133 | In addition to the required policies to retrieve the credentials you need, 134 | listing available accounts and roles requires the following policy on your token: 135 | 136 | ``` 137 | # allows user to list mounts, to find all AWS secret backends 138 | path "sys/mounts" { 139 | capabilities = ["read"] 140 | } 141 | 142 | # allows user to list available roles for AWS secret backends 143 | # this assumes that all AWS backend mountpoints begin with "aws_" 144 | path "aws_*/roles" { 145 | capabilities = ["list"] 146 | } 147 | ``` 148 | -------------------------------------------------------------------------------- /aws-sts-console-url.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Script to use STS Temporary Credentials from environment variables to generate 4 | an AWS Console login URL. 5 | 6 | Canonical source of latest version: 7 | 8 | 9 | For further information, see also: 10 | https://docs.aws.amazon.com/IAM/latest/UserGuide/ 11 | id_roles_providers_enable-console-custom-url.html 12 | 13 | Installation 14 | ------------ 15 | 16 | Make this script executable and copy or symlink it somewhere on your PATH. 17 | 18 | Usage 19 | ----- 20 | 21 | ``aws-sts-console-url.py`` 22 | 23 | License 24 | ------- 25 | 26 | Free for any use provided that changes and improvements are sent back to me. 27 | 28 | Changelog 29 | --------- 30 | 31 | (be sure to increment __version__ with Changelog additions!!) 32 | 33 | 0.2.8 2018-09-20 Chris Bartlett : 34 | - Add ``-b`` / ``--browser`` option to aws-sts-console-url.py to automatically 35 | open the console URL in your default browser. 36 | 37 | 0.2.7 2018-02-25 Jason Antman : 38 | - change to vault-aws-creds.py 39 | 40 | 0.2.6 2018-02-25 Jason Antman : 41 | - change to vault-aws-creds.py 42 | 43 | 0.2.5 2018-02-20 Jason Antman : 44 | - change to vault-aws-creds.py 45 | 46 | 0.2.4 2018-01-29 Jason Antman : 47 | - change to vault-aws-creds.py 48 | 49 | 0.2.3 2018-01-03 Jason Antman : 50 | - Initial version (versioned in sync with vault-aws-creds.py) 51 | """ 52 | 53 | import sys 54 | import os 55 | import argparse 56 | import logging 57 | import json 58 | import webbrowser 59 | 60 | if sys.version_info[0] == 2: 61 | from httplib import HTTPSConnection, HTTPConnection 62 | import ConfigParser 63 | from urlparse import urlparse 64 | from urllib import quote_plus 65 | else: 66 | from http.client import HTTPSConnection, HTTPConnection 67 | import configparser as ConfigParser 68 | from urllib.parse import urlparse, quote_plus 69 | 70 | __version__ = '0.2.8' # increment version in other scripts in sync with this 71 | __author__ = 'jason@jasonantman.com' 72 | _SRC_URL = 'https://github.com/jantman/vault-aws-creds/blob/master/' \ 73 | 'aws-sts-console-url.py' 74 | 75 | DEFAULT_REGION = 'us-east-1' 76 | 77 | FORMAT = "[%(asctime)s %(levelname)s] %(message)s" 78 | logging.basicConfig(level=logging.WARNING, format=FORMAT) 79 | logger = logging.getLogger() 80 | 81 | 82 | class StsUrlGenerator(object): 83 | 84 | def __init__(self): 85 | self.creds = self._get_creds_from_env() 86 | 87 | def _get_creds_from_env(self): 88 | """ 89 | Get AWS credentials from environment variables. 90 | 91 | :return: dict of AWS credentials, suitable for passing to the 92 | https://signin.aws.amazon.com/federation API. 93 | :rtype: dict 94 | """ 95 | res = {} 96 | logger.debug('Getting AWS credentials from environment') 97 | for varname, key in { 98 | 'AWS_ACCESS_KEY_ID': 'sessionId', 99 | 'AWS_SECRET_ACCESS_KEY': 'sessionKey', 100 | 'AWS_SESSION_TOKEN': 'sessionToken' 101 | }.items(): 102 | if varname not in os.environ: 103 | raise RuntimeError( 104 | 'ERROR: %s environment variable must be set to use this ' 105 | 'script.' % varname 106 | ) 107 | res[key] = os.environ[varname] 108 | logger.info('Got AWS credentials from environment variables') 109 | return res 110 | 111 | def generate(self, browser=False): 112 | """ 113 | Generate an STS login URL, and print it to STDOUT. 114 | """ 115 | signin_token = self._get_signin_token(self.creds) 116 | logger.debug('Ok, got valid signin token.') 117 | url = 'https://signin.aws.amazon.com/federation' \ 118 | '?Action=login' \ 119 | '&Issuer=%s' \ 120 | '&Destination=%s' \ 121 | '&SigninToken=%s' % ( 122 | quote_plus(_SRC_URL), 123 | quote_plus('https://console.aws.amazon.com/'), 124 | signin_token 125 | ) 126 | sys.stderr.write( 127 | 'The following sign-in URL must be used within 15 minutes:\n' 128 | ) 129 | print(url) 130 | if browser: 131 | webbrowser.open_new_tab(url) 132 | 133 | 134 | def _get_signin_token(self, creds): 135 | """ 136 | GET the generated Signin Token from the federation endpoint 137 | 138 | :param creds: credentials to pass to the federation endpoint 139 | :type creds: dict 140 | :return: signin token returned by the federation endpoint 141 | :rtype: str 142 | """ 143 | host = 'signin.aws.amazon.com' 144 | req_path = 'https://signin.aws.amazon.com/federation' \ 145 | '?Action=getSigninToken' \ 146 | '&Session=%s' % quote_plus(json.dumps(creds)) 147 | logger.debug('HTTPS GET request to %s: %s', host, req_path) 148 | conn = HTTPSConnection(host, 443) 149 | conn.request('GET', req_path) 150 | resp = conn.getresponse() 151 | logger.debug('Response: HTTP %s %s', resp.status, resp.reason) 152 | logger.debug('Headers: %s', resp.getheaders()) 153 | body = resp.read() 154 | logger.debug('Body: %s', body.strip()) 155 | if resp.status != 200: 156 | logger.critical('AWS Federation endpoint responded HTTP %s %s: %s', 157 | resp.status, resp.reason, body) 158 | raise RuntimeError('Error obtaining console signin credentials.') 159 | try: 160 | b = json.loads(body)['SigninToken'] 161 | except Exception: 162 | logger.critical( 163 | 'AWS Federation endpoint returned an invalid response: %s', 164 | body 165 | ) 166 | raise RuntimeError('Invalid response from AWS Federation endpoint.') 167 | return b 168 | 169 | 170 | def set_log_info(): 171 | """set logger level to INFO""" 172 | set_log_level_format(logging.INFO, 173 | '%(asctime)s %(levelname)s:%(name)s:%(message)s') 174 | 175 | 176 | def set_log_debug(): 177 | """set logger level to DEBUG, and debug-level output format""" 178 | set_log_level_format( 179 | logging.DEBUG, 180 | "%(asctime)s [%(levelname)s %(filename)s:%(lineno)s - " 181 | "%(name)s.%(funcName)s() ] %(message)s" 182 | ) 183 | 184 | 185 | def set_log_level_format(level, format): 186 | """ 187 | Set logger level and format. 188 | 189 | :param level: logging level; see the :py:mod:`logging` constants. 190 | :type level: int 191 | :param format: logging formatter format string 192 | :type format: str 193 | """ 194 | formatter = logging.Formatter(fmt=format) 195 | logger.handlers[0].setFormatter(formatter) 196 | logger.setLevel(level) 197 | 198 | 199 | def parse_args(argv): 200 | """ 201 | Parse command line arguments 202 | :param argv: command line arguments, not including script name 203 | (i.e. ``sys.argv[1:]``) 204 | :type argv: list 205 | :return: parsed command line arguments 206 | :rtype: argparse.Namespace 207 | """ 208 | p = argparse.ArgumentParser( 209 | description='Generate and print to STDOUT an AWS Console login URL, ' 210 | 'based on STS temporary credentials in environment ' 211 | 'variables.' 212 | ) 213 | p.add_argument('-b', '--browser', dest='browser', action='store_true', default=False, 214 | help='open console URL in new tab in default browser') 215 | p.add_argument('-v', '--verbose', dest='verbose', action='count', default=0, 216 | help='verbose output. specify twice for debug-level output.') 217 | p.add_argument('-V', '--version', action='store_true', default=False, 218 | help='Print version number and exit', dest='version') 219 | args = p.parse_args(argv) 220 | if args.version: 221 | sys.stderr.write( 222 | "aws-sts-console-url.py version %s <%s>\n" % ( 223 | __version__, _SRC_URL 224 | ) 225 | ) 226 | raise SystemExit(1) 227 | return args 228 | 229 | if __name__ == "__main__": 230 | args = parse_args(sys.argv[1:]) 231 | 232 | # set logging level 233 | if args.verbose > 1: 234 | set_log_debug() 235 | elif args.verbose == 1: 236 | set_log_info() 237 | 238 | StsUrlGenerator().generate(browser=args.browser) 239 | -------------------------------------------------------------------------------- /vault-aws-creds.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Script to export Vault-derived temporary AWS creds in the environment. 4 | 5 | Canonical source of latest version: 6 | 7 | 8 | Installation 9 | ------------ 10 | 11 | Add a wrapper to your ``~/.bashrc`` to allow this script to set env vars in 12 | the current shell. The proper wrapper function will be output by running 13 | ``vault-aws-creds.py --wrapper-func``. 14 | 15 | Without such a wrapper, you'll need to manually pass this script's output 16 | through your shell's evaluation function, i.e. ``eval vault-aws-creds.py `` 17 | 18 | Usage 19 | ----- 20 | 21 | see `vault-aws-creds.py -h` 22 | 23 | Development 24 | ----------- 25 | 26 | __IMPORTANT__: 27 | 28 | - The only thing that should ever be printed to STDOUT is bash code to be 29 | evaluated by the shell (i.e. environment variable exports). 30 | - All logging, errors, and warnings to the user must go to STDERR. 31 | - Any critical exceptions should raise a VaultException with a helpful message. 32 | 33 | License 34 | ------- 35 | 36 | Free for any use provided that changes and improvements are sent back to me. 37 | 38 | Changelog 39 | --------- 40 | 41 | (be sure to increment __version__ with Changelog additions!!) 42 | 43 | 0.2.9 2020-06-17 Jason Antman : 44 | - Fix bug where the ``-t`` / ``--ttl`` CLI option was ignored, instead using the 45 | saved value from ``~/.vault-aws-creds.conf`` 46 | 47 | 0.2.8 2018-09-20 Chris Bartlett : 48 | - Add ``-b`` / ``--browser`` option to aws-sts-console-url.py to automatically 49 | open the console URL in your default browser. 50 | 51 | 0.2.7 2018-02-25 Jason Antman : 52 | - Fix bug in last release, where if we couldn't connect to Vault, the bashrc 53 | ``eval $(vault-aws-creds.py -w)`` would hang indefinitely. Fixed by changing 54 | ``VaultAwsCredExporter.bash_wrapper`` from a property to a static method, and 55 | calling it (and exiting) before class initialization. 56 | 57 | 0.2.6 2018-02-25 Jason Antman : 58 | - Update README and ``bash_wrapper()`` to use eval for retrieving shell rc 59 | file function, instead of hard-coding. This allows updating the wrapper 60 | function automatically. Thanks to Gerard Hickey 61 | for this PR. 62 | - The ``--region`` / ``-r`` CLI argument was formerly completely ignored. Change 63 | code to use that argument if present, or else ``AWS_REGION`` env var if 64 | present or else ``AWS_DEFAULT_REGION`` if present. Thanks to Gerard Hickey 65 | for this PR. 66 | - Fix #11 - When checking to see if the current token has capabilities to use 67 | a given credential path, consider this true if it has any of ['read', 68 | 'update', 'sudo', 'root']. Previously, we required "read" specifically, which 69 | is not valid in all cases. Thanks to Gerard Hickey 70 | for this PR. 71 | - Fix #10 - Specific, clearer error message if unable to connect to Vault. 72 | 73 | 0.2.5 2018-02-20 Jason Antman : 74 | - store and retrieve TTL value per-mountpoint from config file, like role. 75 | 76 | 0.2.4 2018-01-29 Jason Antman : 77 | - support --ttl option for STS creds. NOTICE: This changes the STS credential 78 | calls from read (GET) to update (POST) in keeping with the current API 79 | documentation. Your Vault policies may need to change for this. 80 | 81 | 0.2.3 2018-01-03 Jason Antman : 82 | - Fix version number in script 83 | 84 | 0.2.2 2017-09-09 Jason Antman : 85 | - Fix #3 - Correct misleading/incorrect log output 86 | 87 | 0.2.1 2017-09-01 Jason Antman : 88 | - Remove often-incorrect warning about STS creds not making IAM calls 89 | 90 | 0.2.0 2017-08-08 Jason Antman : 91 | - Fix numerous Python3 errors 92 | 93 | 0.1.2 2017-08-08 Marcus Smith: 94 | - Fix author-specific shebang line 95 | - Fix usage instructions - VAULT_ADDR must be exported before running -w 96 | 97 | 0.1.1 2017-08-07 Jason Antman : 98 | - Properly handle -h/--help through bash wrapper 99 | 100 | 0.1.0 2017-08-01 Jason Antman : 101 | - Initial version 102 | """ 103 | 104 | import sys 105 | import os 106 | import argparse 107 | import logging 108 | from textwrap import dedent 109 | import json 110 | import re 111 | import socket 112 | 113 | if sys.version_info[0] == 2: 114 | from httplib import HTTPSConnection, HTTPConnection 115 | import ConfigParser 116 | from urlparse import urlparse 117 | else: 118 | from http.client import HTTPSConnection, HTTPConnection 119 | import configparser as ConfigParser 120 | from urllib.parse import urlparse 121 | 122 | if ( 123 | sys.version_info[0] < 3 or 124 | sys.version_info[0] == 3 and sys.version_info[1] < 3 125 | ): 126 | SOCKET_EXC = socket.error 127 | else: 128 | SOCKET_EXC = ConnectionError 129 | 130 | __version__ = '0.2.9' # increment version in other scripts in sync with this 131 | __author__ = 'jason@jasonantman.com' 132 | _SRC_URL = 'https://github.com/jantman/vault-aws-creds/blob/master/' \ 133 | 'vault-aws-creds.py' 134 | 135 | DEFAULT_REGION = 'us-east-1' 136 | 137 | FORMAT = "[%(asctime)s %(levelname)s] %(message)s" 138 | logging.basicConfig(level=logging.WARNING, format=FORMAT) 139 | logger = logging.getLogger() 140 | 141 | 142 | def humantime(int_seconds): 143 | """convert integer seconds to human time""" 144 | s = int_seconds 145 | res = '' 146 | day = 86400 147 | if s >= 86400: 148 | res += '{c}d '.format(c=int(s / day)) 149 | s = s % 86400 150 | if s >= 3600: 151 | res += '{c}h '.format(c=int(s / 3600)) 152 | s = s % 3600 153 | if s >= 60: 154 | res += '{c}m '.format(c=int(s / 60)) 155 | s = s % 60 156 | if s > 0: 157 | res += '{c}s'.format(c=s) 158 | return res 159 | 160 | 161 | def bold(s): 162 | return "\033[1m%s\033[0m" % s 163 | 164 | 165 | def red(s): 166 | return "\033[31m%s\033[0m" % s 167 | 168 | 169 | def green(s): 170 | return "\033[32m%s\033[0m" % s 171 | 172 | 173 | class VaultException(Exception): 174 | 175 | pass 176 | 177 | 178 | class VaultAwsCredExporter(object): 179 | 180 | def __init__(self, config_path, region=None, ttl=None): 181 | self._config_path = os.path.abspath(config_path) 182 | self._ttl = ttl 183 | self._config = self._read_config(self._config_path) 184 | if region is None: 185 | region = self._get_conf('defaults', 'region_name') 186 | if region is None: 187 | region = DEFAULT_REGION 188 | self._region = region 189 | self._set_conf('defaults', 'region_name', region) 190 | self._cli_region = region 191 | self._v_addr, self._v_token = self._set_vault_creds() 192 | self._v_scheme, self._v_host, self._v_port = self._parse_vault_url( 193 | self._v_addr 194 | ) 195 | logger.debug('VAULT_ADDR=%s; scheme=%s host=%s port=%d', 196 | self._v_addr, self._v_scheme, self._v_host, self._v_port) 197 | self._test_vault_creds() 198 | 199 | def _read_config(self, conf_path): 200 | """ 201 | Read in the config file. Return a SafeConfigParser. 202 | 203 | :param conf_path: conf file path 204 | :type conf_path: str 205 | :return: ConfigParser 206 | :rtype: ConfigParser.SafeConfigParser 207 | """ 208 | conf = ConfigParser.SafeConfigParser() 209 | conf.add_section('roles') 210 | logger.debug('Attempting to read config from: %s', conf_path) 211 | conf.read(conf_path) 212 | return conf 213 | 214 | def _set_conf(self, section, option, value): 215 | """ 216 | Set a config value and then write the config file to disk. 217 | 218 | :param section: config file section name 219 | :type section: str 220 | :param option: option name in section 221 | :type option: str 222 | :param value: value to set for the option 223 | :type value: str 224 | """ 225 | if not self._config.has_section(section): 226 | self._config.add_section(section) 227 | logger.debug('_set_conf: section=%s option=%s value=%s', 228 | section, option, value) 229 | self._config.set(section, option, value) 230 | if sys.version_info[0] == 2: 231 | mode = 'wb' 232 | else: 233 | mode = 'w' 234 | with open(self._config_path, mode) as fh: 235 | logger.debug('Writing config to: %s', self._config_path) 236 | self._config.write(fh) 237 | 238 | def _get_conf(self, section, option): 239 | """ 240 | Attempt to read a given value (section and option) from the config 241 | file. If it does not exist, return a default if defined, or otherwise 242 | None. 243 | 244 | :param section: section name in config file 245 | :type section: str 246 | :param option: option name in section 247 | :type option: str 248 | :return: value or None 249 | """ 250 | if not self._config.has_section(section): 251 | logger.debug('Config does not have section: %s', section) 252 | return None 253 | try: 254 | return self._config.get(section, option) 255 | except Exception as ex: 256 | logger.debug('Exception getting config %s/%s: %s', 257 | section, option, ex) 258 | return None 259 | 260 | def _get_conf_bool(self, section, option, default=False): 261 | v = self._get_conf(section, option) 262 | if v is None: 263 | return default 264 | if v == 'true': 265 | return True 266 | return False 267 | 268 | def _set_conf_bool(self, section, option, value): 269 | if value is True: 270 | self._set_conf(section, option, 'true') 271 | return 272 | self._set_conf(section, option, 'false') 273 | 274 | @staticmethod 275 | def bash_wrapper(): 276 | """ 277 | Return the string bash wrapper function to execute this command and 278 | evaluate the STDOUT. 279 | 280 | :return: bash wrapper function for this command 281 | :rtype: str 282 | """ 283 | p = os.path.realpath(__file__) 284 | wrapper = "function vault-aws-creds() {\n" \ 285 | " x=$(%s --called-from-wrapper \"$@\");\n" \ 286 | " [[ \"$?\" == \"0\" ]] && eval \"$x\";\n" \ 287 | "}\n" \ 288 | " # generated by vault-aws-creds.py version %s\n" \ 289 | " # <%s>\n" \ 290 | " # This executes the script with the supplied " \ 291 | "arguments and then\n" \ 292 | " # evaluates the STDOUT. It lets us export env vars in " \ 293 | "an existing session.\n" % (p, __version__, _SRC_URL) 294 | return wrapper 295 | 296 | def _set_vault_creds(self): 297 | """ 298 | Get the Vault address and token. Confirm the token is valid, or raise 299 | an error otherwise. Return the address and token. 300 | 301 | :returns: 2-tuple of (VAULT_ADDR, VAULT_TOKEN) if token is valid 302 | :rtype: tuple 303 | """ 304 | addr = os.environ.get('VAULT_ADDR', None) 305 | tkn = os.environ.get('VAULT_TOKEN', None) 306 | if addr is None: 307 | raise VaultException('Vault address not found; you must export the ' 308 | 'VAULT_ADDR environment variable.') 309 | if tkn is not None: 310 | logger.debug( 311 | 'Using Vault Token from VAULT_TOKEN environment variable' 312 | ) 313 | return addr, tkn 314 | p = os.path.expanduser('~/.vault-token') 315 | if not os.path.exists(p): 316 | raise VaultException( 317 | 'VAULT_TOKEN environment variable is not set and ~/.vault-token' 318 | ' does not exist. Please run "vault auth" to authenticate to ' 319 | 'Vault.' 320 | ) 321 | logger.debug('Reading Vault token from: %s', p) 322 | with open(p, 'r') as fh: 323 | tkn = fh.read().strip() 324 | return addr, tkn 325 | 326 | def _parse_vault_url(self, addr): 327 | """ 328 | Parse the VAULT_ADDR into host and port. Return a 2-tuple of them. 329 | 330 | :param addr: VAULT_ADDR 331 | :type addr: str 332 | :return: 3-tuple (str scheme, str host, int port) 333 | :rtype: tuple 334 | """ 335 | p = urlparse(addr) 336 | nl = p.netloc 337 | if ':' not in nl: 338 | return p.scheme, nl, 8200 # 8200 is default Vault port 339 | host, port = nl.split(':') 340 | return p.scheme, host, int(port) 341 | 342 | def _vault_request(self, method, path, body=None, headers={}, redir=None): 343 | """ 344 | Send an HTTPS request to Vault. Return the response. 345 | 346 | :param method: HTTP method 347 | :type method: str 348 | :param path: Vault API path 349 | :type path: str 350 | :param body: request body 351 | :type body: str 352 | :param headers: additional headers to add to request 353 | :type headers: dict 354 | :param redir: Vault redirect location 355 | :type redir: str 356 | :return: HTTP response 357 | :rtype: str 358 | """ 359 | if redir is None: 360 | scheme = self._v_scheme 361 | host = self._v_host 362 | port = self._v_port 363 | else: 364 | scheme, host, port = self._parse_vault_url(redir) 365 | if 'X-Vault-Token' not in headers: 366 | headers['X-Vault-Token'] = self._v_token 367 | if scheme == 'https': 368 | kls = HTTPSConnection 369 | else: 370 | kls = HTTPConnection 371 | conn = kls(host, port) 372 | logger.debug('%s request to %s:%s - %s %s (body: %s)', 373 | scheme, host, port, method, path, body) 374 | try: 375 | conn.request(method, path, body, headers) 376 | except SOCKET_EXC as exc: 377 | sys.stderr.write( 378 | 'ERROR Connecting to Vault at %s://%s:%s - %s\n' % ( 379 | scheme, host, port, exc 380 | ) 381 | ) 382 | raise SystemExit(1) 383 | resp = conn.getresponse() 384 | logger.debug('Response: HTTP %s %s', resp.status, resp.reason) 385 | logger.debug('Headers: %s', resp.getheaders()) 386 | loc = resp.getheader('location', None) 387 | if loc is not None and 300 <= resp.status < 400: 388 | logger.debug('Vault %s redirect to: %s', resp.status, loc) 389 | return self._vault_request(method, path, body, headers, loc) 390 | resp_body = resp.read() 391 | logger.debug('Body: %s', resp_body.strip()) 392 | # test for auth failure 393 | try: 394 | b = json.loads(resp_body) 395 | except Exception: 396 | b = {} 397 | if 'errors' in b and len(b['errors']) > 0: 398 | if 'permission denied' in b['errors']: 399 | raise VaultException( 400 | 'Vault request got a "permission denied" error; your token' 401 | ' is probably expired. Run "vault auth" to reauthenticate ' 402 | 'to Vault.' 403 | ) 404 | raise VaultException( 405 | "Vault request %s %s resulted in error(s): %s" % ( 406 | method, path, b['errors'] 407 | ) 408 | ) 409 | return resp_body 410 | 411 | def _test_vault_creds(self): 412 | """ 413 | Confirm that the Vault token is valid. Exit otherwise. 414 | """ 415 | raw = self._vault_request('GET', '/v1/auth/token/lookup-self') 416 | res = json.loads(raw) 417 | logger.info('Vault token is authenticated as %s (accessor: %s)', 418 | res['data'].get('display_name', 'unknown'), 419 | res['data'].get('accessor', 'unknown')) 420 | 421 | def _get_aws_mountpoints(self): 422 | """ 423 | Return a dict of account name to mountpoint for all AWS mounts in Vault. 424 | 425 | :return: account name to mountpoint 426 | :rtype: dict 427 | :raises: VaultException 428 | """ 429 | res = json.loads(self._vault_request('GET', '/v1/sys/mounts')) 430 | mpoints = {} 431 | for k, v in res.items(): 432 | if not isinstance(v, type({})): 433 | continue 434 | if v.get('type', 'unknown') != 'aws': 435 | continue 436 | mpoints[k.strip('/')] = k 437 | if k.startswith('aws_'): 438 | mpoints[k.strip('/')[4:]] = k 439 | logger.debug('AWS Mountpoints: %s', mpoints) 440 | return mpoints 441 | 442 | def list_roles(self, mpoint): 443 | """ 444 | List the available Vault roles for the specified account (in Vault). 445 | 446 | :param mpoint: account name to list roles for 447 | :type mpoint: str 448 | :return: roles that the current user has access to 449 | :rtype: list 450 | """ 451 | try: 452 | res = json.loads( 453 | self._vault_request('LIST', '/v1/%sroles' % mpoint) 454 | ) 455 | except VaultException as ex: 456 | if 'permission denied' not in str(ex): 457 | raise 458 | logger.error( 459 | 'ERROR: Your token does not have permission to list available ' 460 | 'roles for the %s mount point; you will have to obtain your ' 461 | 'role name for this account from your Vault administrator.' 462 | '' % mpoint 463 | ) 464 | return [] 465 | roles = [] 466 | required_caps = ['read', 'update', 'sudo', 'root'] 467 | for rname in res['data']['keys']: 468 | path = '%ssts/%s' % (mpoint, rname) 469 | logger.debug('Checking capabilities for: %s', path) 470 | caps = json.loads( 471 | self._vault_request('POST', '/v1/sys/capabilities-self', 472 | body=json.dumps({'path': path})) 473 | ) 474 | logger.debug('Capabilities: %s', caps['capabilities']) 475 | if any(cap in caps['capabilities'] for cap in required_caps): 476 | # if any of required_caps are in caps['capabilities'] ... 477 | roles.append(rname) 478 | logger.debug('Vault Roles for %s mountpoint: %s', mpoint, roles) 479 | return roles 480 | 481 | def get_creds(self, mountpoint, role_name, iam=False, store_role=True, 482 | store_ttl=True): 483 | """ 484 | Given an AWS secret backend mountpoint and a role name, return export 485 | statements to set credentials for that role. 486 | 487 | :param mountpoint: AWS backend mountpoint to read from (account) 488 | :type mountpoint: str 489 | :param role_name: Vault role name to get creds for 490 | :type role_name: str 491 | :param iam: Whether to get actual IAM User creds; if false, STS creds 492 | :type iam: bool 493 | :param store_role: if True, store role selection in config file and 494 | use config file for default role selection 495 | :type store_role: bool 496 | :param store_ttl: if True, store TTL selection in config file and use 497 | config file for default TTL selection 498 | :type store_ttl: bool 499 | :return: string of bash source code to export credentials for the role 500 | :rtype: str 501 | """ 502 | if role_name is None and not store_role: 503 | raise VaultException( 504 | "When running with -R/--no-stored-role, you must specify a " 505 | "Vault role name to get credentials for." 506 | ) 507 | if role_name is None: 508 | role_name = self._get_conf('roles', mountpoint) 509 | iam = self._get_conf_bool('use_iam', mountpoint) 510 | if role_name is None: 511 | # not stored in config 512 | raise VaultException( 513 | "No role name specified on command line, and no default " 514 | "role name for account '%s' is stored in the config file. " 515 | "You must explicitly specify a role name; it will be stored " 516 | "as the default for future invocations." % mountpoint 517 | ) 518 | default_ttl = self._get_conf('ttl', mountpoint) 519 | ttl = None 520 | if self._ttl is not None: 521 | ttl = self._ttl 522 | elif default_ttl is not None: 523 | ttl = default_ttl 524 | body = None 525 | if iam: 526 | path = "/v1/%screds/%s" % (mountpoint, role_name) 527 | logger.info( 528 | 'Getting AWS credentials via path: {0} body: {1}'.format( 529 | path,body 530 | ) 531 | ) 532 | creds = json.loads(self._vault_request('GET', path, body=body)) 533 | else: 534 | path = "/v1/%ssts/%s" % (mountpoint, role_name) 535 | if ttl: 536 | body = json.dumps({'ttl': ttl}) 537 | logger.info( 538 | 'Getting AWS credentials via path: {0} body: {1}'.format( 539 | path, body 540 | ) 541 | ) 542 | creds = json.loads(self._vault_request('POST', path, body=body)) 543 | data = creds.get('data', {}) 544 | if 'lease_id' not in creds or 'access_key' not in data: 545 | raise VaultException( 546 | "Requested credentials from Vault but received an invalid " 547 | "response: %s" % creds 548 | ) 549 | sys.stderr.write("Got credentials for account '%s' role '%s'\n" % ( 550 | mountpoint, role_name 551 | )) 552 | sys.stderr.write( 553 | "Request ID (for troubleshooting): %s\n" % creds['request_id'] 554 | ) 555 | sys.stderr.write( 556 | "Lease (credentials) will expire in: %s\n" % humantime( 557 | creds['lease_duration'] 558 | ) 559 | ) 560 | if creds.get('renewable', False): 561 | sys.stderr.write( 562 | "To renew, run: vault renew %s\n" % creds['lease_id'] 563 | ) 564 | 565 | if self._cli_region: 566 | region = self._cli_region 567 | else: 568 | region = os.environ.get( 569 | 'AWS_REGION', 570 | os.environ.get('AWS_DEFAULT_REGION', None) 571 | ) 572 | if region is None: 573 | region = self._region 574 | exports = [ 575 | "export AWS_REGION='%s'" % region, 576 | "export AWS_DEFAULT_REGION='%s'" % region, 577 | "export AWS_ACCESS_KEY_ID='%s'" % data['access_key'], 578 | "export AWS_SECRET_ACCESS_KEY='%s'" % data['secret_key'] 579 | ] 580 | sess = data.get('security_token', None) 581 | if sess is not None: 582 | exports.append("export AWS_SESSION_TOKEN='%s'" % sess) 583 | else: 584 | exports.append("unset AWS_SESSION_TOKEN") 585 | sys.stderr.write( 586 | "Outputting the following for shell evaluation:" 587 | "\n%s\n" % "\n".join(["\t%s" % x for x in exports]) 588 | ) 589 | if store_role: 590 | self._set_conf('roles', mountpoint, role_name) 591 | self._set_conf_bool('iam', mountpoint, iam) 592 | if store_ttl and ttl is not None: 593 | self._set_conf('ttl', mountpoint, ttl) 594 | return "\n".join(exports) 595 | 596 | def mountpoint_for_account(self, acct_name): 597 | """ 598 | Given an account name input on the command line, return the Vault 599 | mountpoint corresponding to that account name. 600 | 601 | :param acct_name: account name as input on the CLI 602 | :type acct_name: str 603 | :return: Vault mountpoint for that account 604 | :rtype: str 605 | """ 606 | mpoints = {} 607 | try: 608 | mpoints = self._get_aws_mountpoints() 609 | except VaultException as ex: 610 | if 'permission denied' not in str(ex): 611 | raise 612 | if acct_name in mpoints: 613 | logger.info('Account name "%s" mountpoint: %s', 614 | acct_name, mpoints[acct_name]) 615 | return mpoints[acct_name] 616 | logger.warning('Account name "%s" not found in list of %d mountpoints; ' 617 | 'using as an explicit mountpoint for AWS backend.', 618 | acct_name, len(mpoints)) 619 | if not acct_name.endswith('/'): 620 | acct_name += '/' 621 | return acct_name 622 | 623 | 624 | def set_log_info(): 625 | """set logger level to INFO""" 626 | set_log_level_format(logging.INFO, 627 | '%(asctime)s %(levelname)s:%(name)s:%(message)s') 628 | 629 | 630 | def set_log_debug(): 631 | """set logger level to DEBUG, and debug-level output format""" 632 | set_log_level_format( 633 | logging.DEBUG, 634 | "%(asctime)s [%(levelname)s %(filename)s:%(lineno)s - " 635 | "%(name)s.%(funcName)s() ] %(message)s" 636 | ) 637 | 638 | 639 | def set_log_level_format(level, format): 640 | """ 641 | Set logger level and format. 642 | 643 | :param level: logging level; see the :py:mod:`logging` constants. 644 | :type level: int 645 | :param format: logging formatter format string 646 | :type format: str 647 | """ 648 | formatter = logging.Formatter(fmt=format) 649 | logger.handlers[0].setFormatter(formatter) 650 | logger.setLevel(level) 651 | 652 | 653 | def parse_args(argv): 654 | """ 655 | Parse command line arguments 656 | :param argv: command line arguments, not including script name 657 | (i.e. ``sys.argv[1:]``) 658 | :type argv: list 659 | :return: parsed command line arguments 660 | :rtype: argparse.Namespace 661 | """ 662 | epil = dedent(""" 663 | Usage Examples 664 | -------------- 665 | 666 | First, export the address to your Vault instance and authenticate. It's 667 | recommended that you set VAULT_ADDR in your shell profile (~/.bashrc): 668 | export VAULT_ADDR=https://my.vault:8200 669 | vault auth 670 | 671 | Then, add the wrapper function to your ~/.bashrc: 672 | ./vault-aws-creds.py --wrapper-func >> ~/.bashrc 673 | 674 | List available accounts: 675 | vault-aws-creds 676 | 677 | List available roles for account "dev": 678 | vault-aws-creds --roles dev 679 | 680 | Get STS credentials for the "foo" role in the "dev" account: 681 | vault-aws-creds dev foo 682 | 683 | Same as above, but with a 4-hour TTL: 684 | vault-aws-creds --ttl 4h dev foo 685 | 686 | Get IAM User credentials for the "foo" role in the "dev" account: 687 | vault-aws-creds --iam dev foo 688 | 689 | Get STS credentials for your last-used role in the "dev" account: 690 | vault-aws-creds dev 691 | 692 | The latest source can be found at: 693 | <%s> 694 | """ % _SRC_URL) 695 | p = argparse.ArgumentParser( 696 | description='Retrieve temporary AWS credentials from Vault and print ' 697 | 'bash export lines for them. Intended to be run from a ' 698 | 'bash wrapper function.', 699 | epilog=epil, formatter_class=argparse.RawTextHelpFormatter, 700 | add_help=False 701 | ) 702 | p.add_argument('-h', '--help', dest='help', action='store_true', 703 | default=False, help='show this help message and exit') 704 | p.add_argument('-w', '--wrapper-func', dest='show_wrapper', 705 | action='store_true', default=False, help='print bash wrapper' 706 | ' function to STDOUT and exit') 707 | p.add_argument('-v', '--verbose', dest='verbose', action='count', default=0, 708 | help='verbose output. specify twice for debug-level output.') 709 | p.add_argument('-V', '--version', action='store_true', default=False, 710 | help='Print version number and exit', dest='version') 711 | conf = os.path.abspath(os.path.expanduser('~/.vault-aws-creds.conf')) 712 | p.add_argument('-c', '--config-file', dest='config', action='store', 713 | type=str, default=conf, 714 | help='Path to config file (default: %s)' % conf) 715 | p.add_argument('--called-from-wrapper', dest='wrapper_called', 716 | action='store_true', default=False, help='DO NOT USE') 717 | p.add_argument('-r', '--region', dest='region', action='store', type=str, 718 | default=None, 719 | help='AWS_REGION to export if not already set') 720 | p.add_argument('-t', '--ttl', dest='ttl', action='store', type=str, 721 | default=None, 722 | help='TTL to request STS creds with; must be specified in ' 723 | '[0-9]+[hms] format like "30m" or "5h".') 724 | p.add_argument('ACCOUNT', action='store', default=None, nargs='?', 725 | help='AWS account name (Vault AWS backend mount point, or ' 726 | 'mount point after "aws_" prefix); if omitted, all ' 727 | 'accounts you have access to will be listed.') 728 | p.add_argument('--roles', dest='list_roles', action='store_true', 729 | default=False, help='List available roles for account') 730 | p.add_argument('--iam', dest='iam', action='store_true', default=False, 731 | help='If specified, get IAM User credentials. Otherwise, get' 732 | ' STS credentials.') 733 | p.add_argument('-R', '--no-stored-role', dest='store_role', 734 | action='store_false', default=True, 735 | help='Do not store role selection in, or use previous role ' 736 | 'selection from, config file') 737 | p.add_argument('-T', '--no-stored-ttl', dest='store_ttl', 738 | action='store_false', default=True, 739 | help='Do not store TTL selection in, or use previous TTL ' 740 | 'selection from, config file') 741 | p.add_argument('ROLE', action='store', default=None, nargs='?', 742 | help='Vault role name to get creds for in the account; ' 743 | 'if --no-stored-role is not specified, the role name ' 744 | 'for each account will be stored to and retrieved from ' 745 | 'the config file.') 746 | args = p.parse_args(argv) 747 | if args.help: 748 | p.print_help(sys.stderr) 749 | raise SystemExit(1) 750 | if args.version: 751 | sys.stderr.write("vault-aws-creds.py version %s\n" % __version__) 752 | raise SystemExit(1) 753 | if args.ttl is not None: 754 | if args.iam: 755 | raise RuntimeError('Error: --ttl can only be used with STS creds.') 756 | if not re.match(r'^\d+[hms]$', args.ttl): 757 | raise RuntimeError('Error: Invalid TTL format: %s' % args.ttl) 758 | return args 759 | 760 | 761 | if __name__ == "__main__": 762 | args = parse_args(sys.argv[1:]) 763 | 764 | # set logging level 765 | if args.verbose > 1: 766 | set_log_debug() 767 | elif args.verbose == 1: 768 | set_log_info() 769 | 770 | if args.show_wrapper: 771 | print(VaultAwsCredExporter.bash_wrapper()) 772 | raise SystemExit(1) 773 | 774 | try: 775 | exporter = VaultAwsCredExporter(args.config, args.region, args.ttl) 776 | 777 | if not args.wrapper_called: 778 | sys.stderr.write( 779 | bold('vault-aws-creds.py should be called through a bash wrapper ' 780 | 'function; run with "-w" to output the appropriate ' 781 | 'function.') + "\n" 782 | ) 783 | if args.ACCOUNT is None: 784 | mpoints = {} 785 | try: 786 | mpoints = exporter._get_aws_mountpoints() 787 | except VaultException as ex: 788 | if 'permission denied' not in str(ex): 789 | raise 790 | logger.error( 791 | 'ERROR: Your token does not have permission to list Vault ' 792 | 'mount points; you will have to obtain the AWS backend mount ' 793 | 'points for your accounts from your Vault administrator.' 794 | ) 795 | mpoint_listing = {} 796 | for name, mpoint in mpoints.items(): 797 | if mpoint not in mpoint_listing: 798 | mpoint_listing[mpoint] = [] 799 | mpoint_listing[mpoint].append(name) 800 | sys.stderr.write("Available Accounts:\n") 801 | for mpoint in sorted(mpoint_listing.keys()): 802 | sys.stderr.write(' a.k.a. '.join( 803 | ['"%s"' % x for x in mpoint_listing[mpoint]] 804 | ) + "\n") 805 | raise SystemExit(1) 806 | # ok, we have an account name 807 | acct = exporter.mountpoint_for_account(args.ACCOUNT) 808 | logger.info('Using mountpoint: %s', acct) 809 | if args.list_roles: 810 | sys.stderr.write("Available Vault Roles for Account '%s':\n" % acct) 811 | for rname in exporter.list_roles(acct): 812 | sys.stderr.write(rname + "\n") 813 | raise SystemExit(1) 814 | print(exporter.get_creds(acct, args.ROLE, iam=args.iam, 815 | store_role=args.store_role, 816 | store_ttl=args.store_ttl)) 817 | except VaultException as ex: 818 | sys.stderr.write("ERROR: %s\n" % ex) 819 | raise SystemExit(1) 820 | --------------------------------------------------------------------------------