AWS Cross-Account CLI/API Authentication with MFA - Part 3
Or How I Lost My Hair So You Don't Have To - Finding A Better Way™
Part 3 - Finding A Better Way™
New Story:
We’d like to login with our MFA token exactly once per work day, no more.
We’d like to use that single MFA session for cross-account access to a variety of accounts, without being additionally prompted for MFA.
We’d like this all to work with SDKs like boto3 just as transparently as it does with the CLI.
In short, we’d like our CLI experience to Not Suck
Where we’re going we don’t need roads!
The sad truth is that the current state of the CLI tools and SDKs simply
don’t afford such a user-friendly experience out of the box. So as much as
we tried to avoid it, we’ll need to build a flux capacitor a helper
script if we want to CLI at 88 miles per hour without going mad.
The Jump Profile
Our login profile doesn’t use MFA. Our cheddar/brie/havarti profiles can’t work without MFA. The SDKs won’t use or save cached credentials to disk. What we need is a bridge between login and the cheeses. What we need is a Jump Profile™.
Taking a hint from that painful AWS documentation page,
we’re going to use the GetSessionToken API to generate our MFA-flagged
temporary credentials. And we’re going to save them to our
~/.aws/credentials
file. Just like that sad video said to do in the first
place. Dammit, I hate when they’re right. :(
BUT! We are NOT going to use that profile directly, haha take that AWS documentation video guy! And we sure as heck aren’t going to manually copy/paste all that line noise back and forth every time. We are software professionals after all, not barbarians.
Instead we are going to use this temporary profile, what I’ll call a Jump
Profile, as a bridge between our non-MFA login and our MFA-only
account cheese profiles. Our ~/.aws/config
for this will look like:
[profile login]
region = us-east-1
output = json
[profile login-mfa]
region = us-east-1
output = json
[profile cheddar]
source_profile = login-mfa
role_arn = arn:aws:iam::111111111111:role/TestRole
role_session_name = test-user
region = us-east-1
output = json
[profile brie]
source_profile = login-mfa
role_arn = arn:aws:iam::222222222222:role/TestRole
role_session_name = test-user
region = us-east-1
output = json
[profile havarti]
source_profile = login-mfa
role_arn = arn:aws:iam::333333333333:role/TestRole
role_session_name = test-user
region = us-east-1
output = json
Note how the source_profile
values of our account cheese profiles now
reference the login-mfa profile instead of the login profle. We
have also removed all the mfa_serial
options entirely because now we’ll be
handling MFA now outside of the config files.
How it all works now is this:
- The user runs a helper script that uses her login profile
credentials, plus a supplied MFA token, to generate MFA-flagged session
credentials and save them to the login-mfa profile in
~/.aws/credentials
- The real profiles (cheddar, etc) all use
role_arn
combined with the credentials of the login-mfa profile to AssumeRole for cross-account access. This works because the login-mfa credentials are flagged as MFA = true. - The user doesn’t need to worry about the login-mfa credentials expiring for 12 hours (by default and optionally upto 36 hours).
- The various AssumeRole credentials will still expire after 1 hour, but since both the CLI and SDKs are happy to refresh them automatically and transparently, our user doesn’t even notice.
The Helper Script
To glue all this together we need a modest helper script to bridge the gap. Any SDK supported language would do, but I’ve written this one up in Python. I’ve chosen Python here primarily because since the CLI is built on Python and boto3 I can be assured this will be supported wherever it’s useful. This avoids needing to distribute language runtimes, SDKs, etc. The script is as brief and portable as I could reasonably make it.
As always, this code is provided strictly asis. Use at your own risk, no warranty is neither expressed or implied, get your pets spayed or neutered, and don’t run with scissors.
Copy/paste it from below or download it from GitHub.
Lets take this puppy out for a walk!
With the helper script firmly in hand and saved to aws_login.py
, our
~/.aws/config
setup as above, and our AccessKey/Secret saved against our
login profile from Part 1 of this journey, we try a few commands out:
$ ./aws_login.py
Using your 'login' profile and MFA to login to AWS
Enter MFA code for arn:aws:iam::111111111111:mfa/test-user:
MFA login succeeded, saving your credentials to your 'login-mfa' profile.
Your AssumeRole profiles should use the directive 'source_profile = login-mfa' to leverage this identity
Credentials saved. Your temporary identity is:
{ u'Account': '111111111111',
u'Arn': 'arn:aws:iam::111111111111:user/test-user',
u'UserId': 'AIDBI2UVWQDJ4265ZUQKK'}
$ aws --profile cheddar iam list-users
{
"Users": [
{
"UserName": "test-user",
"PasswordLastUsed": "2018-02-13T01:12:29Z",
"CreateDate": "2018-01-29T23:58:26Z",
"UserId": "AIDBI2UVWQDJ4265ZUQKK",
"Path": "/",
"Arn": "arn:aws:iam::111111111111:user/test-user"
}
]
}
$ aws --profile brie iam list-users
{
"Users": []
}
$ aws --profile havarti iam list-users
{
"Users": []
}
$ for profile in cheddar brie havarti; do
> for region in $(aws --profile $profile ec2 describe-regions --query "Regions[][RegionName]" --output text); do
> export AWS_PROFILE="$profile"
> export AWS_DEFAULT_REGION="$region"
> ./some_python_boto3_script.py
> done
>done
Cheers from profile cheddar, region ap-south-1
Cheers from profile cheddar, region eu-west-3
Cheers from profile cheddar, region eu-west-2
Cheers from profile cheddar, region eu-west-1
Cheers from profile cheddar, region ap-northeast-2
Cheers from profile cheddar, region ap-northeast-1
...
Cheers from profile brie, region ap-south-1
Cheers from profile brie, region eu-west-3
Cheers from profile brie, region eu-west-2
Cheers from profile brie, region eu-west-1
Cheers from profile brie, region ap-northeast-2
Cheers from profile brie, region ap-northeast-1
...
Cheers from profile havarti, region ap-south-1
Cheers from profile havarti, region eu-west-3
Cheers from profile havarti, region eu-west-2
Cheers from profile havarti, region eu-west-1
Cheers from profile havarti, region ap-northeast-2
Cheers from profile havarti, region ap-northeast-1
...
And that’s it. We’re done. A one time ask for our MFA token and we get to securely access all our accounts in any region using either the CLI or API. We won’t be bothered again for hours.
Happy users, happy admins, happy security folks. Whoohoo!
Cons:
This approach isn’t perfect, there are a few significant shortcomings.
-
We can’t avoid a helper script and all the issues that creates as we described at the end of part 1. Although we did minimize those issues by writing it in Python.
-
This solution is complex and advanced. It requires advanced understanding of AWS IAM, complex and non-obvious config file setups for the users, training on the custom tools, and significant end user support. There are a lot of ways this can fail.
-
While we are no longer constantly prompted for MFA (which is awesome!), alas this method will never re-prompt for MFA (which is not awesome). Once the credentials expire we just get errors back, causing our commands/scripts to fail, etc. That’s fine if you’re doing one line commands at the CLI (just call
aws_login.py
again), but less fine if you’re executing a long running script and it dies 3/4ths of the way through.
Hopefully in the future AWS will see fit to borrow the “device” access model that Azure uses for their CLI where you authorize your device much like HBOGo authorizes your Roku. But until then this works quite well once it’s setup.