While the AWS console gives you a nice point and click interface, and really helps you explore the vast service catalog of AWS, the use of the CLI should not be neglected.

Some of the advantages of the CLI:

  • Reusable, can the same command multiple times, perhaps with slight modification for quickly creating multiple instances of similar resources.
  • Reproducible, can run the same command, to reproduce exactly the same kind of resource as has been created before.
  • Programmable, can create scripts that gather info from configuration files and then run commands to combine the other two advantages.

I do not list ‘Convenience’ here, as that is much more of a taste and preference.

Why MFA?

Enabling MFA increases the security of your AWS account by quite a lot. Without it, anyone who can get control of your email account can take over your account.

For more information on why and how to enable MFA, please look at the AWS documentation

MFA using the Console

When you log into the console with a browser, you are faced with a dialog asking you for ‘Account ID or alias’, ‘IAM user name’ and password.

Authentication dialog
Authentication Dialog in AWS Console

After that you will get an ‘Multi-factor Authentication’ dialog

MFA dialog
MFA Dialog in AWS Console

After you have entered valid MFA code, you get access to the Console and can use it as usual.

But enough introduction.

MFA using the CLI

This is the part that I wanted to write about, using multiple accounts with even more roles. For this example we will imagine a person, Bob Johnson, who has an IAM account in 2 accounts, and can assume 3 roles in the third account.

This gets hairy quickly, but let us not skimp on the details. So to realize the imagination, first let us define user accounts. The following credentials are meant to belong to the same physical user.

Account ACME

  • IAM user: bob
  • AWS AccountID: 12345NAN1234
  • Access key: NOTETHISISNOTREALKEY
  • Access secret: ANDTHISISNOTEITHERavalidKEY+Butpleasetry
  • IAM Roles: AcmeAdminRole

Account FooBar

  • IAM user: bjohnson
  • AWS AccountID: 78902NAN7890
  • Access key: THISISNOTAREALKEYYES
  • Access secret: SPOILERALERTThisKeyISNOTVALIDJustExample

Account BasZoo

  • AWS AccountID: 34322NAN4567
  • IAM Roles allowed to be assumed from account 78902NAN7890
    • DeveloperRole
    • ArchitectRole
    • AdminRole

This was the scenario, now let us look at how to implement this.

Implementation

First we need to look at how to configure the CLI.

If you have a working CLI configuration and want to try this, just copy your working folder away to a safe place and start with a clean configuration.

Basic user

We begin here. We define the 2 accounts in ~/.aws/config

[AcmeAccount]
mfa_serial = arn:aws:iam::12345NAN1234:mfa/bob
output = json

[FoobarAccount]
mfa_serial = arn:aws:iam::78902NAN7890:mfa/bjohnson
output = json

And corresponding credentials in ~/.aws/credentials

[AcmeAccount]
aws_access_key_id = NOTETHISISNOTREALKEY
aws_secret_access_key = ANDTHISISNOTEITHERavalidKEY+Butpleasetry

[FoobarAccount]
aws_access_key_id = THISISNOTAREALKEYYES
aws_secret_access_key = SPOILERALERTThisKeyISNOTVALIDJustExample

The IAM identity

To map the IAM identity to the accounts, we add the following to ~/.aws/config

[profile acme]
user_arn = arn:aws:iam::12345NAN1234:user/bob
source_profile = AcmeAccount

[profile foobar]
user_arn = arn:aws:iam::78902NAN7890:user/bjohnson
source_profile = FoobarAccount

This gives us profiles that are connected to the account defined before. And this is the only place that we want to refer to AcmeAccount and FoobarAccount.

Then we can use the aws sts get-session-token command to get temporary credentials with the profile. I wrote a simple python wrapper around that command. I call the script refresh_mfa.py Check the end for the script.

We want to match 1 to 1 on identities and accounts in this part.

Temporary credentials

To get new temporary credentials, we use the script with the IAM profile name as an argument.

$ refresh_mfa.py acme
Creating acme credentials profile
OTP from device: 324235
$ refresh_mfa.py foobar
Creating foobar credentials profile
OTP from device: 424242

This will create an entry in ~/.aws/credentials that will look something like this:

[acme]
aws_access_key_id = ASITISTHISISNOTVALID
aws_secret_access_key = nosuprisehereeitherisit?Ihopenot+Random!
aws_session_token = Yeah+ThisIs+some+serious+lenghty/string+so/I+cannotBebothered+mTFcLwZSciG+Uv35QUwBH6lGCMxAD1bES2OaC+Lt1ARrm4Xk0JVI76QKFbl9Ww1s+i0gU+orRnQ78JP6rVZvDPlBFLm8DveWOaJV+SKwAYN65xdjSo5yA+IUl2iAwcbaQImLJLCPxxOWULfLg25HSWUT7MpjLX7Q4yGcYhpnivX8hMoSGGIRu6MnJS/irpWOGbyo+hLm8DveWOaJV+SKwA52dn

[foobar]
aws_access_key_id = ASIBASIFASITROLOLOLO
aws_secret_access_key = yetanotheraccesskey+3eFvZbrBAYmF2NqGfuYb
aws_session_token = SVGD76eBPRU32Zp/mTFcLwZSciG+Uv35QUwBH6lGCMxAD1bES2OaC+Lt1ARrm4Xk0JVI76QKFbl9QoGZXIvYXdzEDkaDBPaqBhgOdfHf2Vv6M0AlPp4/LprUNJABJxhWWOPe8aUy5ZKtTMbxR+6O6lmQn8lD7+6hAhKP178JasTJipBN1hTKCBgC+HEkWBKthk8vroZx7pCt+cGLoeU2KCFGxtyw5Cp/IghAe9uV4MUa7kfNew4hc+D+8yiQis42g2go/R72LQT2eWbd/o56A8JE

These entries are temporary, all 3 values will be changed when you run refresh_mfa.py next time.

And now we can use the profile acme to run aws commands

$ aws --profile acme --region eu-west-3 cloudformation list-stacks
{
    "StackSummaries": []
}

Roles and profiles

This rather cumbersome setup becomes really useful when we add different profiles and roles to the mix. To complete the setup we imagined earlier in the document, we can now add the following to the ~/.aws/config file

[profile baszooDev]
region = eu-central-1
role_arn = arn:aws:iam::34322NAN4567:role/DeveloperRole
source_profile = foobar

[profile baszooDevUs]
region = us-east-1
role_arn = arn:aws:iam::34322NAN4567:role/DeveloperRole
source_profile = foobar

[profile baszooArch]
region = eu-central-1
role_arn = arn:aws:iam::34322NAN4567:role/ArchitectRole
source_profile = foobar

[profile baszooAdmin]
region = eu-west-1
role_arn = arn:aws:iam::34322NAN4567:role/AdminRole
source_profile = foobar

[profile acmeAdmin]
region = us-west-1
role_arn = arn:aws:iam::12345NAN1234:role/AcmeAdminRole
source_profile = acme

With this in place we can now run aws commands with different roles, just by varying the named profile.

$ aws --profile baszooDev ec2 describe-instances --query Reservations[].Instances[].InstanceId
[
    "i-024253342be542beb",
    "i-0b42ec24299542858",
    "i-0242cac4271042d9c",
    "i-084273d422b04233b",
    "i-0142bb34263142bb1",
    "i-014249342a9e428b0",
    "i-0942ee24201742d30",
    "i-0d42b824211b42716"
]

The only difference between the baszooDev and the baszooDevUs profile is the region. So if Bob uses the baszooDevUs profile, he asks for instances in us-east-1

$ aws --profile baszooDevUs ec2 describe-instances --query Reservations[].Instances[].InstanceId
[]

And you only need the MFA authentication for the foobar or acme profile. And while the temporary credentials are valid for e.g. foobar, one can use a profile that sources foobar like baszooArch and baszooDev.

Complete configuration files and script

Here are the complete configuration files for the accounts, as described previously

~/.aws/config

[AcmeAccount]
mfa_serial = arn:aws:iam::12345NAN1234:mfa/bob
output = json

[FoobarAccount]
mfa_serial = arn:aws:iam::78902NAN7890:mfa/bjohnson
output = json

[profile acme]
user_arn = arn:aws:iam::12345NAN1234:user/bob
source_profile = AcmeAccount

[profile foobar]
user_arn = arn:aws:iam::78902NAN7890:user/bjohnson
source_profile = FoobarAccount

[profile baszooDev]
region = eu-central-1
role_arn = arn:aws:iam::34322NAN4567:role/DeveloperRole
source_profile = foobar

[profile baszooArch]
region = eu-central-1
role_arn = arn:aws:iam::34322NAN4567:role/ArchitectRole
source_profile = foobar

[profile baszooAdmin]
region = eu-west-1
role_arn = arn:aws:iam::34322NAN4567:role/AdminRole
source_profile = foobar

[profile acmeAdmin]
region = us-west-1
role_arn = arn:aws:iam::12345NAN1234:role/AcmeAdminRole
source_profile = acme

~/.aws/credentials

[AcmeAccount]
aws_access_key_id = NOTETHISISNOTREALKEY
aws_secret_access_key = ANDTHISISNOTEITHERavalidKEY+Butpleasetry

[FoobarAccount]
aws_access_key_id = THISISNOTAREALKEYYES
aws_secret_access_key = SPOILERALERTThisKeyISNOTVALIDJustExample

refresh_mfa.py

#!/usr/bin/python3
import sys
import configparser
import json
import os
from os.path import expanduser

if(len(sys.argv) <= 1 ):
    exit("Need named profile")


home = expanduser("~")
requestedProfile = sys.argv[1]
awsConfig = configparser.ConfigParser()
awsCred   = configparser.ConfigParser()

awsConfig.read("%s/.aws/config" % home)
awsCred.read('%s/.aws/credentials' % home)

try:
    mfaARN = awsConfig[awsConfig["profile " + requestedProfile]['source_profile']]['mfa_serial']
except KeyError:
    try:
        mfaARN = awsConfig['default']['mfa_serial']
    except KeyError:
        exit("Need MFA serial in config file")

profiles = set( awsCred.sections())
configprofiles = set( awsConfig.sections())

if( requestedProfile in profiles and "profile " + requestedProfile in configprofiles):
    print("Updating %s profile" % requestedProfile)
else:
    if( "profile " + requestedProfile in configprofiles):
        print("Creating %s credentials profile" % requestedProfile)
        awsCred.add_section(requestedProfile)
    else:
        exit("No such profile \"%s\" in config" % requestedProfile )

try:
    OneTimeNumber = int(input("OTP from device: "))
except ValueError:
    exit("OTP must be a number")


response = os.popen("aws --profile %s sts get-session-token --serial-number  %s --token-code %s" % ( awsConfig["profile " + requestedProfile]['source_profile'],
                                                                                                 mfaARN,
                                                                                                 str(OneTimeNumber).zfill(6))).read()

try:
    myjson = json.loads(response)
except json.decoder.JSONDecodeError:
    exit("AWS was not happy with that one")

awsCred[requestedProfile]['aws_access_key_id']     = myjson['Credentials']['AccessKeyId']
awsCred[requestedProfile]['aws_secret_access_key'] = myjson['Credentials']['SecretAccessKey']
awsCred[requestedProfile]['aws_session_token']     = myjson['Credentials']['SessionToken']

with open('%s/.aws/credentials' % home, 'w') as awsCredfile:
    awsCred.write(awsCredfile)

Jónas Helgi Pálsson

Senior Systems Consultant at Redpill Linpro

Jónas joined Redpill Linpro over a decade ago and has in that period worked as both a consultant and a system administrator. Main focus currently for Jónas is AWS and infrastructure on that platform. Previously been working with KVM and OpenStack, dabbles with programming and has a soft spot for openSUSE.

Just-Make-toolbox

make is a utility for automating builds. You specify the source and the build file and make will determine which file(s) have to be re-built. Using this functionality in make as an all-round tool for command running as well, is considered common practice. Yes, you could write Shell scripts for this instead and they would be probably equally good. But using make has its own charm (and gets you karma points).

Even this ... [continue reading]

Containerized Development Environment

Published on February 28, 2024

Ansible-runner

Published on February 27, 2024