The Security Hurdle
Most production AWS accounts enforce MFA (Multi-Factor Authentication) via IAM policy conditions. The policy looks something like:
{
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "true"
}
}
}
This means: even if your IAM user has an Allow policy for S3, the request is denied unless it was authenticated with MFA. Your permanent access keys alone do not satisfy this condition — they prove you know the secret key, but not that you physically hold your MFA device right now.
Running aws s3 ls with permanent keys against such an account returns:
An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied
This is one of the most common "why doesn't my CLI work?" moments for engineers new to a security-hardened account.
The Fix: STS AssumeRole or GetSessionToken
The solution is to exchange your permanent credentials plus a live MFA code for temporary session credentials issued by AWS STS (Security Token Service). These temporary credentials carry the aws:MultiFactorAuthPresent: true flag that satisfies the policy condition.
Step 1: Get your MFA device ARN
aws iam list-mfa-devices --user-name alice
# Note the "SerialNumber" field, e.g.:
# arn:aws:iam::123456789012:mfa/alice
Step 2: Request temporary credentials
aws sts get-session-token \
--serial-number arn:aws:iam::123456789012:mfa/alice \
--token-code 847291 \
--duration-seconds 43200
Output:
{
"Credentials": {
"AccessKeyId": "ASIA...TEMP",
"SecretAccessKey": "temp-secret...",
"SessionToken": "FwoGZXIvYXdz...(long token)...",
"Expiration": "2024-01-15T22:00:00Z"
}
}
Step 3: Export all three values
export AWS_ACCESS_KEY_ID=ASIA...TEMP
export AWS_SECRET_ACCESS_KEY=temp-secret...
export AWS_SESSION_TOKEN=FwoGZXIvYXdz...
Now your CLI calls carry MFA proof and will succeed. The session expires after --duration-seconds (max 129,600 seconds / 36 hours for IAM users).
Automating the MFA Dance
Manually exporting three variables is tedious. A common pattern is a shell function:
# Add to ~/.bashrc or ~/.zshrc
mfa-login() {
local mfa_arn="arn:aws:iam::123456789012:mfa/alice"
local token_code=$1
local creds=$(aws sts get-session-token \
--serial-number "$mfa_arn" \
--token-code "$token_code" \
--output json)
export AWS_ACCESS_KEY_ID=$(echo $creds | jq -r '.Credentials.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(echo $creds | jq -r '.Credentials.SecretAccessKey')
export AWS_SESSION_TOKEN=$(echo $creds | jq -r '.Credentials.SessionToken')
echo "MFA session active until $(echo $creds | jq -r '.Credentials.Expiration')"
}
# Usage:
mfa-login 847291
Key Point: In modern AWS setups, the preferred pattern is not get-session-token but sts assume-role with MFA. You assume a role (e.g., arn:aws:iam::PROD_ACCOUNT:role/DevOpsEngineer) and pass your MFA token as part of the assume-role call. This combines cross-account access and MFA enforcement in a single step — the standard pattern in enterprise multi-account architectures.