Using MFA/2FA with AWS CLI/CDK and 1Password for MacOS

When you’re using AWS on the CLI or with the CDK, you have to set up your credentials and you commonly have an Access Key and a Secret Key. Those are static and if anyone get a hold of them, they can do whatever they like with your AWS account. I had a co-worker once who had a commit with his AWS keys in it. Not even the latest commit, but one in the history. People have bots that are constantly scanning GitHub for AWS keys. He came in on Monday and got a bill for over $10,000 because someone had used his keys to spin up a bunch of EC2 instances and other resources. That was a fun conversation with AWS support…
It’s easy to make mistakes, we’re all human. So it’s a good idea to enable MFA/2FA on your AWS account. This means that in addition to your Access Key and Secret Key, you also need a time-based one-time password (TOTP) from an authenticator app like 1Password. This adds an extra layer of security to your AWS account, but it can be a huge pain to use the cli or cdk with it enabled because you always have to first get a temporary session token from aws sts
. I’ve worked out a script that makes it easy to let 1Password handle the MFA/2FA and your AWS keys so nothing is statically stored on your machine.
Let’s look at how to set this up.
Prerequisites
- You need to have the AWS CLI installed. You can find instructions here.
- You can also install it with homebrew with
brew install awscli
- You can also install it with homebrew with
- You’ll also need to have MFA enabled on your AWS account. You can find instructions here.
- You need to have 1Password installed and set up. You can find instructions here.
Setting Up 1Password
First, make sure you have 1Password CLI installed. You can find instructions here. Don’t forget to enable Integrate with 1Password CLI
in your 1Password app settings under Developer
.
1
2
3
4
brew install 1password-cli
# sign in once
eval $(op signin)
Add an entry in 1Password for your AWS credentials. Let’s use the same one that you used to add your MFA in the Prerequisites above.
- Add a new text field named
Access Key ID
and put your AWS Access Key ID in it. - Add a new password field named
Secret Access Key
and put your AWS Secret Access Key in it. - Add a new text field named
mfa serial
and put the ARN of your MFA device in it. You can find this in the AWS console under IAM > Users > Security Credentials > Assigned MFA device. Usually it looks something likearn:aws:iam::123456789012:mfa/your-username
.
Now we’re ready to set up our magical script.
The Script (where the magic happens)
You’ll want to create a file named credential_process.sh
in your ~/.aws/
directory.
The file should look like this:
note: if you’re homebrew is set up to install to a different location, make sure to update the PATH variable accordingly.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#!/bin/bash
set -e
op_vault=""
op_item=""
aws_credential_duration=""
export PATH="/opt/homebrew/bin:$PATH"
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--op-vault)
op_vault="$2"; shift 2 ;;
--op-vault=*)
op_vault="${1#*=}"; shift ;;
--op-item)
op_item="$2"; shift 2 ;;
--op-item=*)
op_item="${1#*=}"; shift ;;
--duration|--aws-credential-duration)
aws_credential_duration="$2"; shift 2 ;;
--duration=*|--aws-credential-duration=*)
aws_credential_duration="${1#*=}"; shift ;;
--)
shift; break ;;
*)
echo "Unknown option: $1" >&2
esac
done
if [[ -z "$op_vault" || -z "$op_item" || -z "$aws_credential_duration" ]]; then
echo "Missing required options" >&2
exit 1
fi
if ! [[ "$aws_credential_duration" =~ ^[0-9]+$ ]]; then
echo "Error: --duration must be an integer (seconds)." >&2
exit 1
fi
if [[ "$aws_credential_duration" -gt 3600 ]]; then
echo "Warning: --duration has a max of 3600 seconds, setting to 3600." >&2
fi
}
main() {
local mfa_token
local fields
mfa_token="$(op item get "$op_item" --otp --vault "$op_vault")"
fields="$(op item get "$op_item" --fields "label=Access Key ID,label=Secret Access Key,label=mfa serial" --reveal --vault "$op_vault")"
IFS=',' read -r access_key secret_key mfa_serial <<< "$fields"
AWS_ACCESS_KEY_ID="$access_key" \
AWS_SECRET_ACCESS_KEY="$secret_key" \
aws sts get-session-token \
--serial-number "$mfa_serial" \
--token-code "$mfa_token" \
--duration-seconds "$aws_credential_duration" \
--output json \
--query 'Credentials | {Version: `1`, AccessKeyId: AccessKeyId, SecretAccessKey: SecretAccessKey, SessionToken: SessionToken, Expiration: Expiration}'
}
parse_args "$@"
main
You should be able to copy and paste that as-is. Once you save the file, make it executable with chmod +x ~/.aws/credential_process.sh
.
What it does is ensures the homebrew install directory is on your path, then it uses the 1Password CLI to first get the current MFA token, then it reads those fields we set up for the Access Key, Secret Key, and MFA serial. Finally, it calls aws sts get-session-token
with those values to get a temporary session token that is valid for the duration you specify. It needs to be in a very specific JSON format so AWS can read it, which is what the --query
at the end is for. The only tools being used here are op
and aws sts
, the credentials are never stored on your machine or sent anywhere else.
Setting Up AWS CLI/CDK to Use 1Password
- Navigate to
~/.aws/
andls
to see if you have aconfig
andcredentials
file.- If you have both, then delete the
credentials
file. We won’t be using it.
- If you have both, then delete the
- Open the
config
file in your favorite text editor likenvim config
, if you’re not using nvim or vim, make sure totouch config
to create it if it doesn’t exist. - We’ll want our config to look something like this:
1
2
3
4
[default]
region = us-east-1 # or your preferred region
output = json
credential_process = /Users/<your-username>/.aws/credential_process.sh --op-vault "Your 1Password Vault Name" --op-item "Your 1Password Item Name" --duration 3600
Save your config file and exit your text editor.
note: the maximum duration you can set is 3600 seconds (1 hour). If you set it higher, sts will just set it to 1 hour.
You should now be able to test it with aws s3 ls
and you’ll get prompted to unlock 1Password if it’s not already unlocked and then it will list your S3 buckets (assuming you have the necessary permissions).
You can re-use that same config for other profiles in the config
file as well, just by adding the credential_process
line to each profile section.
Troubleshooting
- If you get an error along the lines of
Credential Process Authentication is not yet supported by the AWS Shell Plugin
, it’s because you’re using the 1Passwordaws
shell plugin which doesn’t supportcredential_process
and won’t work with this setup. You can test it by runningunalias aws
and then tryingaws s3 ls
again. For a more permanent fix, remove the plugin by editing the~/.config/op/plugins.sh
file and removing the line that aliasesaws
.
Happy coding!