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 concretize 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 identitys and accounts in this part.

Temporary credentials

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

bob@pigz:~$ refresh_mfa.py acme
Creating acme credentials profile
OTP from device: 324235
bob@pigz:~$
bob@pigz:~$ refresh_mfa.py foobar
Creating foobar credentials profile
OTP from device: 424242
bob@pigz:~$

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

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

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.

bob@pigz:~$ 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"
]
bob@pigz:~$

The only differance 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

bob@pigz:~$ aws --profile baszooDevUs ec2 describe-instances --query Reservations[].Instances[].InstanceId
[]
bob@pigz:~$

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 configurate 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.

Ansible/AWX network performance investigation

When we introduced the network configuration using Ansible and AWX at a customer, we gradually extended the configuration scope. Over time, more and more configuration got added into the configuration pool and this lead to longer and longer run-times for the playbooks.

While the job-execution got really simple by using AWX instead of the plain CLI method for Ansible, the time to finish drew heavily on that benefit.

A complete job-run over the network infrastructure took at least ... [continue reading]

FOSDEM 2020

Published on March 19, 2020

A look at our new routers

Published on January 17, 2020