├── setup.cfg ├── aws-mfa ├── .travis.yml ├── .gitignore ├── awsmfa ├── util.py ├── config.py └── __init__.py ├── setup.py ├── LICENSE └── README.md /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /aws-mfa: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import awsmfa 4 | 5 | if __name__ == "__main__": 6 | awsmfa.main() 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | install: 6 | - pip install flake8 7 | script: 8 | - flake8 --filename=aws-mfa aws-mfa 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.bak 3 | *.sw[po] 4 | 5 | .Python 6 | env/ 7 | build/ 8 | develop-eggs/ 9 | dist/ 10 | downloads/ 11 | eggs/ 12 | .eggs/ 13 | lib/ 14 | lib64/ 15 | parts/ 16 | sdist/ 17 | var/ 18 | *.egg-info/ 19 | .installed.cfg 20 | *.egg 21 | -------------------------------------------------------------------------------- /awsmfa/util.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def log_error_and_exit(logger, message): 5 | """Log an error message and exit with error""" 6 | logger.error(message) 7 | sys.exit(1) 8 | 9 | 10 | def prompter(): 11 | try: 12 | console_input = raw_input 13 | except NameError: 14 | console_input = input 15 | 16 | return console_input 17 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | from codecs import open 4 | from os import path 5 | 6 | here = path.abspath(path.dirname(__file__)) 7 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 8 | long_description = f.read() 9 | 10 | setup( 11 | name='aws-mfa', 12 | version='0.0.12', 13 | description='Manage AWS MFA Security Credentials', 14 | long_description=long_description, 15 | long_description_content_type='text/markdown', 16 | license='MIT', 17 | author='Brian Nuszkowski', 18 | author_email='brian@bnuz.co', 19 | packages=['awsmfa'], 20 | scripts=['aws-mfa'], 21 | entry_points={ 22 | 'console_scripts': [ 23 | 'aws-mfa=awsmfa:main', 24 | ], 25 | }, 26 | url='https://github.com/broamski/aws-mfa', 27 | install_requires=['boto3'] 28 | ) 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Brian Nuszkowski 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /awsmfa/config.py: -------------------------------------------------------------------------------- 1 | import getpass 2 | 3 | try: 4 | import configparser 5 | from configparser import NoOptionError, NoSectionError 6 | except ImportError: 7 | import ConfigParser as configparser # noqa 8 | from ConfigParser import NoOptionError, NoSectionError # noqa 9 | 10 | from awsmfa.util import log_error_and_exit, prompter 11 | 12 | 13 | def initial_setup(logger, config, config_path): 14 | console_input = prompter() 15 | 16 | profile_name = console_input('Profile name to [%s]: ' % ("default")) 17 | if profile_name is None or profile_name == "": 18 | profile_name = "default" 19 | 20 | profile_name = "{}-long-term".format(profile_name) 21 | aws_access_key_id = getpass.getpass('aws_access_key_id: ') 22 | if aws_access_key_id is None or aws_access_key_id == "": 23 | log_error_and_exit(logger, "You must supply aws_access_key_id") 24 | aws_secret_access_key = getpass.getpass('aws_secret_access_key: ') 25 | if aws_secret_access_key is None or aws_secret_access_key == "": 26 | log_error_and_exit(logger, "You must supply aws_secret_access_key") 27 | 28 | config.add_section(profile_name) 29 | config.set(profile_name, 'aws_access_key_id', aws_access_key_id) 30 | config.set(profile_name, 'aws_secret_access_key', aws_secret_access_key) 31 | with open(config_path, 'w') as configfile: 32 | config.write(configfile) 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | aws-mfa: Easily manage your AWS Security Credentials when using Multi-Factor Authentication (MFA) 2 | ================================================================================================= 3 | 4 | **aws-mfa** makes it easy to manage your AWS SDK Security Credentials when Multi-Factor Authentication (MFA) is enforced on your AWS account. It automates the process of obtaining temporary credentials from the [AWS Security Token Service](http://docs.aws.amazon.com/STS/latest/APIReference/Welcome.html) and updating your [AWS Credentials](https://blogs.aws.amazon.com/security/post/Tx3D6U6WSFGOK2H/A-New-and-Standardized-Way-to-Manage-Credentials-in-the-AWS-SDKs) file (located at `~/.aws/credentials`). Traditional methods of managing MFA-based credentials requires users to write their own bespoke scripts/wrappers to fetch temporary credentials from STS and often times manually update their AWS credentials file. 5 | 6 | The concept behind **aws-mfa** is that there are 2 types of credentials: 7 | 8 | * `long-term` - Your typcial AWS access keys, consisting of an `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` 9 | 10 | * `short-term` - A temporary set of credentials that are generated by AWS STS using your `long-term` credentials in combination with your MFA device serial number (either a hardware device serial number or virtual device ARN) and one time token code. Your short term credentials are the credentials that are actively utilized by the AWS SDK in use. 11 | 12 | 13 | If you haven't yet enabled multi-factor authentication for AWS API access, check out the [AWS article](http://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa_configure-api-require.html) on doing so. 14 | 15 | 16 | Installation: 17 | ------------- 18 | Option 1 19 | ```sh 20 | $ pip install aws-mfa 21 | ``` 22 | 23 | Option 2 24 | ```sh 25 | 1. Clone this repo 26 | 2. $ python setup.py install 27 | ``` 28 | 29 | Credentials File Setup 30 | ---------------------- 31 | 32 | In a typical AWS credentials file (located at `~/.aws/credentials`), credentials are stored in sections, denoted by a pair of brackets: `[]`. The `[default]` section stores your default credentials. You can store multiple sets of credentials using different profile names. If no profile is specified, the `[default]` section is always used. 33 | 34 | By default long term credential sections are identified by the convention `[-long-term]` and short term credentials are identified by the typical convention: `[]`. The following illustrates how you would configure you credentials file using **aws-mfa** with your default credentials: 35 | 36 | ```ini 37 | [default-long-term] 38 | aws_access_key_id = YOUR_LONGTERM_KEY_ID 39 | aws_secret_access_key = YOUR_LONGTERM_ACCESS_KEY 40 | ``` 41 | 42 | After running `aws-mfa`, your credentials file would read: 43 | 44 | ```ini 45 | [default-long-term] 46 | aws_access_key_id = YOUR_LONGTERM_KEY_ID 47 | aws_secret_access_key = YOUR_LONGTERM_ACCESS_KEY 48 | 49 | 50 | [default] 51 | aws_access_key_id = 52 | aws_secret_access_key = 53 | aws_security_token = 54 | ``` 55 | 56 | Similarly, if you utilize a credentials profile named **development**, your credentials file would look like: 57 | 58 | ```ini 59 | [development-long-term] 60 | aws_access_key_id = YOUR_LONGTERM_KEY_ID 61 | aws_secret_access_key = YOUR_LONGTERM_ACCESS_KEY 62 | ``` 63 | 64 | 65 | After running `aws-mfa`, your credentials file would read: 66 | 67 | ```ini 68 | [development-long-term] 69 | aws_access_key_id = YOUR_LONGTERM_KEY_ID 70 | aws_secret_access_key = YOUR_LONGTERM_ACCESS_KEY 71 | 72 | [development] 73 | aws_access_key_id = 74 | aws_secret_access_key = 75 | aws_security_token = 76 | ``` 77 | 78 | The default naming convention for the credential section can be overriden by using the `--long-term-suffix` and 79 | `--short-term-suffix` command line arguments. For example, in a multi account scenario you can have one AWS account 80 | that manages the IAM users for your organization and have other AWS accounts for development, staging and production 81 | environments. 82 | 83 | After running `aws-mfa` once for each environment with a different value for `--short-term-suffix`, your credentials 84 | file would read: 85 | 86 | ```ini 87 | [myorganization-long-term] 88 | aws_access_key_id = YOUR_LONGTERM_KEY_ID 89 | aws_secret_access_key = YOUR_LONGTERM_ACCESS_KEY 90 | 91 | [myorganization-development] 92 | aws_access_key_id = 93 | aws_secret_access_key = 94 | aws_security_token = 95 | 96 | [myorganization-staging] 97 | aws_access_key_id = 98 | aws_secret_access_key = 99 | aws_security_token = 100 | 101 | [myorganization-production] 102 | aws_access_key_id = 103 | aws_secret_access_key = 104 | aws_security_token = 105 | ``` 106 | 107 | This allows you to access multiple environments without the need to run `aws-mfa` each time you want to switch 108 | environments. 109 | 110 | If you don't like the a long term suffix, you can omit it by passing the value `none` for the `--long-term-suffix` 111 | command line argument. After running ``aws-mfa`` once for each environment with a different value for 112 | `--short-term-suffix`, your credentials file would read: 113 | 114 | ```ini 115 | [myorganization] 116 | aws_access_key_id = YOUR_LONGTERM_KEY_ID 117 | aws_secret_access_key = YOUR_LONGTERM_ACCESS_KEY 118 | 119 | [myorganization-development] 120 | aws_access_key_id = 121 | aws_secret_access_key = 122 | aws_security_token = 123 | 124 | [myorganization-staging] 125 | aws_access_key_id = 126 | aws_secret_access_key = 127 | aws_security_token = 128 | 129 | [myorganization-production] 130 | aws_access_key_id = 131 | aws_secret_access_key = 132 | aws_security_token = 133 | ``` 134 | 135 | Usage 136 | ----- 137 | 138 | ``` 139 | --device arn:aws:iam::123456788990:mfa/dudeman 140 | The MFA Device ARN. This value can also be provided 141 | via the environment variable 'MFA_DEVICE' or the 142 | ~/.aws/credentials variable 'aws_mfa_device'. 143 | --duration DURATION The duration, in seconds, that the temporary 144 | credentials should remain valid. Minimum value: 900 145 | (15 minutes). Maximum: 129600 (36 hours). Defaults to 146 | 43200 (12 hours), or 3600 (one hour) when using 147 | '--assume-role'. This value can also be provided via 148 | the environment variable 'MFA_STS_DURATION'. 149 | --profile PROFILE If using profiles, specify the name here. The default 150 | profile name is 'default'. The value can also be 151 | provided via the environment variable 'AWS_PROFILE'. 152 | --long-term-suffix LONG_TERM_SUFFIX 153 | To identify the long term credential section by 154 | [-LONG_TERM_SUFFIX]. Use 'none' to 155 | identify the long term credential section by 156 | []. Omit to identify the long term 157 | credential section by [-long-term]. 158 | --short-term-suffix SHORT_TERM_SUFFIX 159 | To identify the short term credential section by 160 | [-SHORT_TERM_SUFFIX]. Omit or use 'none' 161 | to identify the short term credential section by 162 | []. 163 | --assume-role arn:aws:iam::123456788990:role/RoleName 164 | The ARN of the AWS IAM Role you would like to assume, 165 | if specified. This value can also be provided via the 166 | environment variable 'MFA_ASSUME_ROLE' 167 | --role-session-name ROLE_SESSION_NAME 168 | Friendly session name required when using --assume- 169 | role. By default, this is your local username. 170 | ``` 171 | 172 | **Argument precedence**: Command line arguments take precedence over environment variables. 173 | 174 | Usage Example 175 | ------------- 176 | 177 | Run **aws-mfa** *before* running any of your scripts that use any AWS SDK. 178 | 179 | 180 | Using command line arguments: 181 | 182 | ```sh 183 | $> aws-mfa --duration 1800 --device arn:aws:iam::123456788990:mfa/dudeman 184 | INFO - Using profile: default 185 | INFO - Your credentials have expired, renewing. 186 | Enter AWS MFA code for device [arn:aws:iam::123456788990:mfa/dudeman] (renewing for 1800 seconds):123456 187 | INFO - Success! Your credentials will expire in 1800 seconds at: 2015-12-21 23:07:09+00:00 188 | ``` 189 | 190 | Using environment variables: 191 | 192 | ```sh 193 | export MFA_DEVICE=arn:aws:iam::123456788990:mfa/dudeman 194 | $> aws-mfa --duration 1800 195 | INFO - Using profile: default 196 | INFO - Your credentials have expired, renewing. 197 | Enter AWS MFA code for device [arn:aws:iam::123456788990:mfa/dudeman] (renewing for 1800 seconds):123456 198 | INFO - Success! Your credentials will expire in 1800 seconds at: 2015-12-21 23:07:09+00:00 199 | ``` 200 | 201 | ```sh 202 | export MFA_DEVICE=arn:aws:iam::123456788990:mfa/dudeman 203 | export MFA_STS_DURATION=1800 204 | $> aws-mfa 205 | INFO - Using profile: default 206 | INFO - Your credentials have expired, renewing. 207 | Enter AWS MFA code for device [arn:aws:iam::123456788990:mfa/dudeman] (renewing for 1800 seconds):123456 208 | INFO - Success! Your credentials will expire in 1800 seconds at: 2015-12-21 23:07:09+00:00 209 | ``` 210 | 211 | Output of running **aws-mfa** while credentials are still valid: 212 | 213 | ```sh 214 | $> aws-mfa 215 | INFO - Using profile: default 216 | INFO - Your credentials are still valid for 1541.791134 seconds they will expire at 2015-12-21 23:07:09 217 | ``` 218 | 219 | Using a profile: (profiles allow you to reference different sets of credentials, perhaps for different users or different regions) 220 | 221 | ```sh 222 | $> aws-mfa --duration 1800 --device arn:aws:iam::123456788990:mfa/dudeman --profile development 223 | INFO - Using profile: development 224 | Enter AWS MFA code for device [arn:aws:iam::123456788990:mfa/dudeman] (renewing for 1800 seconds):666666 225 | INFO - Success! Your credentials will expire in 1800 seconds at: 2015-12-21 23:09:04+00:00 226 | ``` 227 | 228 | Using a profile that is set via the environment variable `AWS_PROFILE`: 229 | 230 | ```sh 231 | $> export AWS_PROFILE=development 232 | $> aws-mfa --duration 1800 --device arn:aws:iam::123456788990:mfa/dudeman 233 | INFO - Using profile: development 234 | Enter AWS MFA code for device [arn:aws:iam::123456788990:mfa/dudeman] (renewing for 1800 seconds):666666 235 | INFO - Success! Your credentials will expire in 1800 seconds at: 2015-12-21 23:09:04+00:00 236 | ``` 237 | 238 | Assuming a role: 239 | 240 | ```sh 241 | $> aws-mfa --duration 1800 --device arn:aws:iam::123456788990:mfa/dudeman --assume-role arn:aws:iam::123456788990:role/some-role --role-session-name some-role-session 242 | INFO - Validating credentials for profile: default with assumed role arn:aws:iam::123456788990:role/some-role 243 | INFO - Obtaining credentials for a new role or profile. 244 | Enter AWS MFA code for device [arn:aws:iam::123456788990:mfa/dudeman] (renewing for 1800 seconds):123456 245 | INFO - Success! Your credentials will expire in 1800 seconds at: 2016-10-24 18:58:17+00:00 246 | ``` 247 | 248 | Assuming a role: Assume a role specified in your `long-term` configuration 249 | 250 | ```ini 251 | [default-long-term] 252 | aws_access_key_id = YOUR_LONGTERM_KEY_ID 253 | aws_secret_access_key = YOUR_LONGTERM_ACCESS_KEY 254 | assume_role = arn:aws:iam::123456788990:role/some-role 255 | ``` 256 | 257 | ```sh 258 | $> aws-mfa --duration 1800 --device arn:aws:iam::123456788990:mfa/dudeman --role-session-name some-role-session 259 | ``` 260 | 261 | Assuming a role using a profile: 262 | 263 | ```sh 264 | $> aws-mfa --duration 1800 --device arn:aws:iam::123456788990:mfa/dudeman --profile development --assume-role arn:aws:iam::123456788990:role/some-role --role-session-name some-role-session 265 | INFO - Validating credentials for profile: development with assumed role arn:aws:iam::123456788990:role/some-role 266 | INFO - Obtaining credentials for a new role or profile. 267 | Enter AWS MFA code for device [arn:aws:iam::123456788990:mfa/dudeman] (renewing for 1800 seconds):123456 268 | INFO - Success! Your credentials will expire in 1800 seconds at: 2016-10-24 18:58:17+00:00 269 | ``` 270 | 271 | Assuming a role in multiple accounts and be able to work with both accounts simultaneously (i.e. production an staging): 272 | 273 | ```sh 274 | $> aws-mfa —profile myorganization --assume-role arn:aws:iam::222222222222:role/Administrator --short-term-suffix production --long-term-suffix none --role-session-name production 275 | INFO - Validating credentials for profile: myorganization-production with assumed role arn:aws:iam::222222222222:role/Administrator 276 | INFO - Your credentials have expired, renewing. 277 | Enter AWS MFA code for device [arn:aws:iam::111111111111:mfa/me] (renewing for 3600 seconds):123456 278 | INFO - Success! Your credentials will expire in 3600 seconds at: 2017-07-10 07:16:43+00:00 279 | 280 | $> aws-mfa —profile myorganization --assume-role arn:aws:iam::333333333333:role/Administrator --short-term-suffix staging --long-term-suffix none --role-session-name staging 281 | INFO - Validating credentials for profile: myorganization-staging with assumed role arn:aws:iam::333333333333:role/Administrator 282 | INFO - Your credentials have expired, renewing. 283 | Enter AWS MFA code for device [arn:aws:iam::111111111111:mfa/me] (renewing for 3600 seconds):123456 284 | INFO - Success! Your credentials will expire in 3600 seconds at: 2017-07-10 07:16:44+00:00 285 | 286 | $> aws s3 list-objects —bucket my-production-bucket —profile myorganization-production 287 | 288 | $> aws s3 list-objects —bucket my-staging-bucket —profile myorganization-staging 289 | ``` -------------------------------------------------------------------------------- /awsmfa/__init__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | try: 3 | import configparser 4 | from configparser import NoOptionError, NoSectionError 5 | except ImportError: 6 | import ConfigParser as configparser 7 | from ConfigParser import NoOptionError, NoSectionError 8 | import datetime 9 | import getpass 10 | import logging 11 | import os 12 | import sys 13 | import boto3 14 | 15 | from botocore.exceptions import ClientError, ParamValidationError 16 | from awsmfa.config import initial_setup 17 | from awsmfa.util import log_error_and_exit, prompter 18 | 19 | logger = logging.getLogger('aws-mfa') 20 | 21 | AWS_CREDS_PATH = '%s/.aws/credentials' % (os.path.expanduser('~'),) 22 | 23 | 24 | def main(): 25 | parser = argparse.ArgumentParser() 26 | parser.add_argument('--device', 27 | required=False, 28 | metavar='arn:aws:iam::123456788990:mfa/dudeman', 29 | help="The MFA Device ARN. This value can also be " 30 | "provided via the environment variable 'MFA_DEVICE' or" 31 | " the ~/.aws/credentials variable 'aws_mfa_device'.") 32 | parser.add_argument('--duration', 33 | type=int, 34 | help="The duration, in seconds, that the temporary " 35 | "credentials should remain valid. Minimum value: " 36 | "900 (15 minutes). Maximum: 129600 (36 hours). " 37 | "Defaults to 43200 (12 hours), or 3600 (one " 38 | "hour) when using '--assume-role'. This value " 39 | "can also be provided via the environment " 40 | "variable 'MFA_STS_DURATION'. ") 41 | parser.add_argument('--profile', 42 | help="If using profiles, specify the name here. The " 43 | "default profile name is 'default'. The value can " 44 | "also be provided via the environment variable " 45 | "'AWS_PROFILE'.", 46 | required=False) 47 | parser.add_argument('--long-term-suffix', '--long-suffix', 48 | help="The suffix appended to the profile name to" 49 | "identify the long term credential section", 50 | required=False) 51 | parser.add_argument('--short-term-suffix', '--short-suffix', 52 | help="The suffix appended to the profile name to" 53 | "identify the short term credential section", 54 | required=False) 55 | parser.add_argument('--assume-role', '--assume', 56 | metavar='arn:aws:iam::123456788990:role/RoleName', 57 | help="The ARN of the AWS IAM Role you would like to " 58 | "assume, if specified. This value can also be provided" 59 | " via the environment variable 'MFA_ASSUME_ROLE'", 60 | required=False) 61 | parser.add_argument('--role-session-name', 62 | help="Friendly session name required when using " 63 | "--assume-role", 64 | default=getpass.getuser(), 65 | required=False) 66 | parser.add_argument('--force', 67 | help="Refresh credentials even if currently valid.", 68 | action="store_true", 69 | required=False) 70 | parser.add_argument('--log-level', 71 | help="Set log level", 72 | choices=[ 73 | 'CRITICAL', 'ERROR', 'WARNING', 74 | 'INFO', 'DEBUG', 'NOTSET' 75 | ], 76 | required=False, 77 | default='DEBUG') 78 | parser.add_argument('--setup', 79 | help="Setup a new log term credentials section", 80 | action="store_true", 81 | required=False) 82 | parser.add_argument('--token', '--mfa-token', 83 | type=str, 84 | help="Provide MFA token as an argument", 85 | required=False) 86 | args = parser.parse_args() 87 | 88 | level = getattr(logging, args.log_level) 89 | setup_logger(level) 90 | 91 | if not os.path.isfile(AWS_CREDS_PATH): 92 | console_input = prompter() 93 | create = console_input("Could not locate credentials file at {}, " 94 | "would you like to create one? " 95 | "[y/n]".format(AWS_CREDS_PATH)) 96 | if create.lower() == "y": 97 | with open(AWS_CREDS_PATH, 'a'): 98 | pass 99 | else: 100 | log_error_and_exit(logger, 'Could not locate credentials file at ' 101 | '%s' % (AWS_CREDS_PATH,)) 102 | 103 | config = get_config(AWS_CREDS_PATH) 104 | 105 | if args.setup: 106 | initial_setup(logger, config, AWS_CREDS_PATH) 107 | return 108 | 109 | validate(args, config) 110 | 111 | 112 | def get_config(aws_creds_path): 113 | config = configparser.RawConfigParser() 114 | try: 115 | config.read(aws_creds_path) 116 | except configparser.ParsingError: 117 | e = sys.exc_info()[1] 118 | log_error_and_exit(logger, "There was a problem reading or parsing " 119 | "your credentials file: %s" % (e.args[0],)) 120 | return config 121 | 122 | 123 | def validate(args, config): 124 | if not args.profile: 125 | if os.environ.get('AWS_PROFILE'): 126 | args.profile = os.environ.get('AWS_PROFILE') 127 | else: 128 | args.profile = 'default' 129 | 130 | if not args.long_term_suffix: 131 | long_term_name = '%s-long-term' % (args.profile,) 132 | elif args.long_term_suffix.lower() == 'none': 133 | long_term_name = args.profile 134 | else: 135 | long_term_name = '%s-%s' % (args.profile, args.long_term_suffix) 136 | 137 | if not args.short_term_suffix or args.short_term_suffix.lower() == 'none': 138 | short_term_name = args.profile 139 | else: 140 | short_term_name = '%s-%s' % (args.profile, args.short_term_suffix) 141 | 142 | if long_term_name == short_term_name: 143 | log_error_and_exit(logger, 144 | "The value for '--long-term-suffix' cannot " 145 | "be equal to the value for '--short-term-suffix'") 146 | 147 | if args.assume_role: 148 | role_msg = "with assumed role: %s" % (args.assume_role,) 149 | elif config.has_option(args.profile, 'assumed_role_arn'): 150 | role_msg = "with assumed role: %s" % ( 151 | config.get(args.profile, 'assumed_role_arn')) 152 | else: 153 | role_msg = "" 154 | logger.info('Validating credentials for profile: %s %s' % 155 | (short_term_name, role_msg)) 156 | reup_message = "Obtaining credentials for a new role or profile." 157 | 158 | try: 159 | key_id = config.get(long_term_name, 'aws_access_key_id') 160 | access_key = config.get(long_term_name, 'aws_secret_access_key') 161 | except NoSectionError: 162 | log_error_and_exit(logger, 163 | "Long term credentials session '[%s]' is missing. " 164 | "You must add this section to your credentials file " 165 | "along with your long term 'aws_access_key_id' and " 166 | "'aws_secret_access_key'" % (long_term_name,)) 167 | except NoOptionError as e: 168 | log_error_and_exit(logger, e) 169 | 170 | # get device from param, env var or config 171 | if not args.device: 172 | if os.environ.get('MFA_DEVICE'): 173 | args.device = os.environ.get('MFA_DEVICE') 174 | elif config.has_option(long_term_name, 'aws_mfa_device'): 175 | args.device = config.get(long_term_name, 'aws_mfa_device') 176 | else: 177 | log_error_and_exit(logger, 178 | 'You must provide --device or MFA_DEVICE or set ' 179 | '"aws_mfa_device" in ".aws/credentials"') 180 | 181 | # get assume_role from param or env var 182 | if not args.assume_role: 183 | if os.environ.get('MFA_ASSUME_ROLE'): 184 | args.assume_role = os.environ.get('MFA_ASSUME_ROLE') 185 | elif config.has_option(long_term_name, 'assume_role'): 186 | args.assume_role = config.get(long_term_name, 'assume_role') 187 | 188 | # get duration from param, env var or set default 189 | if not args.duration: 190 | if os.environ.get('MFA_STS_DURATION'): 191 | args.duration = int(os.environ.get('MFA_STS_DURATION')) 192 | else: 193 | args.duration = 3600 if args.assume_role else 43200 194 | 195 | # If this is False, only refresh credentials if expired. Otherwise 196 | # always refresh. 197 | force_refresh = False 198 | 199 | # Validate presence of short-term section 200 | if not config.has_section(short_term_name): 201 | logger.info("Short term credentials section %s is missing, " 202 | "obtaining new credentials." % (short_term_name,)) 203 | if short_term_name == 'default': 204 | try: 205 | config.add_section(short_term_name) 206 | # a hack for creating a section named "default" 207 | except ValueError: 208 | configparser.DEFAULTSECT = short_term_name 209 | config.set(short_term_name, 'CREATE', 'TEST') 210 | config.remove_option(short_term_name, 'CREATE') 211 | else: 212 | config.add_section(short_term_name) 213 | force_refresh = True 214 | # Validate option integrity of short-term section 215 | else: 216 | required_options = ['assumed_role', 217 | 'aws_access_key_id', 'aws_secret_access_key', 218 | 'aws_session_token', 'aws_security_token', 219 | 'expiration'] 220 | try: 221 | short_term = {} 222 | for option in required_options: 223 | short_term[option] = config.get(short_term_name, option) 224 | except NoOptionError: 225 | logger.warn("Your existing credentials are missing or invalid, " 226 | "obtaining new credentials.") 227 | force_refresh = True 228 | 229 | try: 230 | current_role = config.get(short_term_name, 'assumed_role_arn') 231 | except NoOptionError: 232 | current_role = None 233 | 234 | if args.force: 235 | logger.info("Forcing refresh of credentials.") 236 | force_refresh = True 237 | # There are not credentials for an assumed role, 238 | # but the user is trying to assume one 239 | elif current_role is None and args.assume_role: 240 | logger.info(reup_message) 241 | force_refresh = True 242 | # There are current credentials for a role and 243 | # the role arn being provided is the same. 244 | elif (current_role is not None and 245 | args.assume_role and current_role == args.assume_role): 246 | pass 247 | # There are credentials for a current role and the role 248 | # that is attempting to be assumed is different 249 | elif (current_role is not None and 250 | args.assume_role and current_role != args.assume_role): 251 | logger.info(reup_message) 252 | force_refresh = True 253 | # There are credentials for a current role and no role arn is 254 | # being supplied 255 | elif current_role is not None and args.assume_role is None: 256 | logger.info(reup_message) 257 | force_refresh = True 258 | 259 | should_refresh = True 260 | 261 | # Unless we're forcing a refresh, check expiration. 262 | if not force_refresh: 263 | exp = datetime.datetime.strptime( 264 | config.get(short_term_name, 'expiration'), '%Y-%m-%d %H:%M:%S') 265 | diff = exp - datetime.datetime.utcnow() 266 | if diff.total_seconds() <= 0: 267 | logger.info("Your credentials have expired, renewing.") 268 | else: 269 | should_refresh = False 270 | logger.info( 271 | "Your credentials are still valid for %s seconds" 272 | " they will expire at %s" 273 | % (diff.total_seconds(), exp)) 274 | 275 | if should_refresh: 276 | get_credentials(short_term_name, key_id, access_key, args, config) 277 | 278 | 279 | def get_credentials(short_term_name, lt_key_id, lt_access_key, args, config): 280 | if args.token: 281 | logger.debug("Received token as argument") 282 | mfa_token = '%s' % (args.token) 283 | else: 284 | console_input = prompter() 285 | mfa_token = console_input('Enter AWS MFA code for device [%s] ' 286 | '(renewing for %s seconds):' % 287 | (args.device, args.duration)) 288 | 289 | client = boto3.client( 290 | 'sts', 291 | aws_access_key_id=lt_key_id, 292 | aws_secret_access_key=lt_access_key 293 | ) 294 | 295 | if args.assume_role: 296 | 297 | logger.info("Assuming Role - Profile: %s, Role: %s, Duration: %s", 298 | short_term_name, args.assume_role, args.duration) 299 | if args.role_session_name is None: 300 | log_error_and_exit(logger, "You must specify a role session name " 301 | "via --role-session-name") 302 | 303 | try: 304 | response = client.assume_role( 305 | RoleArn=args.assume_role, 306 | RoleSessionName=args.role_session_name, 307 | DurationSeconds=args.duration, 308 | SerialNumber=args.device, 309 | TokenCode=mfa_token 310 | ) 311 | except ClientError as e: 312 | log_error_and_exit(logger, 313 | "An error occured while calling " 314 | "assume role: {}".format(e)) 315 | except ParamValidationError: 316 | log_error_and_exit(logger, "Token must be six digits") 317 | 318 | config.set( 319 | short_term_name, 320 | 'assumed_role', 321 | 'True', 322 | ) 323 | config.set( 324 | short_term_name, 325 | 'assumed_role_arn', 326 | args.assume_role, 327 | ) 328 | else: 329 | logger.info("Fetching Credentials - Profile: %s, Duration: %s", 330 | short_term_name, args.duration) 331 | try: 332 | response = client.get_session_token( 333 | DurationSeconds=args.duration, 334 | SerialNumber=args.device, 335 | TokenCode=mfa_token 336 | ) 337 | except ClientError as e: 338 | log_error_and_exit( 339 | logger, 340 | "An error occured while calling assume role: {}".format(e)) 341 | except ParamValidationError: 342 | log_error_and_exit( 343 | logger, 344 | "Token must be six digits") 345 | 346 | config.set( 347 | short_term_name, 348 | 'assumed_role', 349 | 'False', 350 | ) 351 | config.remove_option(short_term_name, 'assumed_role_arn') 352 | 353 | # aws_session_token and aws_security_token are both added 354 | # to support boto and boto3 355 | options = [ 356 | ('aws_access_key_id', 'AccessKeyId'), 357 | ('aws_secret_access_key', 'SecretAccessKey'), 358 | ('aws_session_token', 'SessionToken'), 359 | ('aws_security_token', 'SessionToken'), 360 | ] 361 | 362 | for option, value in options: 363 | config.set( 364 | short_term_name, 365 | option, 366 | response['Credentials'][value] 367 | ) 368 | # Save expiration individiually, so it can be manipulated 369 | config.set( 370 | short_term_name, 371 | 'expiration', 372 | response['Credentials']['Expiration'].strftime('%Y-%m-%d %H:%M:%S') 373 | ) 374 | with open(AWS_CREDS_PATH, 'w') as configfile: 375 | config.write(configfile) 376 | logger.info( 377 | "Success! Your credentials will expire in %s seconds at: %s" 378 | % (args.duration, response['Credentials']['Expiration'])) 379 | sys.exit(0) 380 | 381 | 382 | def setup_logger(level=logging.DEBUG): 383 | stdout_handler = logging.StreamHandler(stream=sys.stdout) 384 | stdout_handler.setFormatter( 385 | logging.Formatter('%(levelname)s - %(message)s')) 386 | stdout_handler.setLevel(level) 387 | logger.addHandler(stdout_handler) 388 | logger.setLevel(level) 389 | 390 | 391 | if __name__ == "__main__": 392 | main() 393 | --------------------------------------------------------------------------------