Configuration & IAM
.mdVerified against aws-cli/2.33.6, flags verified via `aws <cmd> help` run locally, plus current · official docs
What it is and where it fits#
The AWS CLI is the single command-line surface over every AWS service's API — the same underlying
botocore client that powers the Python SDK (boto3), so anything you can script with the CLI you
can also do in application code, and the credential/config resolution described below is shared by
both. It sits one layer below Terraform/CloudFormation/CDK: those tools describe desired state and
diff against it, while the CLI issues one imperative API call at a time — which is exactly why it's
the tool of choice for debugging a running system, one-off operational tasks, and CI/CD glue scripts,
rather than for provisioning infrastructure that should stay declarative and reviewable. This page
covers the part every other AWS CLI page in this series assumes you already have working: profiles,
authentication (including the account-wide move away from static access keys), and the IAM commands
for managing who can do what. The companion pages cover compute, storage, networking, and
observability once you're actually authenticated against an account.
Installation#
# Official installer (Linux x86_64) — the recommended method, not pip/apt
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip && sudo ./aws/install
# macOS
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /
aws --version # confirm the install: aws-cli/2.x.x Python/... Note
AWS CLI v1 (pip install awscli) is still technically maintained but v2 has been the recommended
version for years — it bundles its own Python runtime (no separate interpreter dependency), adds
SSO support natively, and is what every command on this page assumes. If aws --version reports a
1.x line, upgrade before following anything here.
How the CLI resolves credentials and configuration#
Before touching any specific command, understand the one mechanism that explains nearly every "why is this using the wrong account" confusion: the CLI checks a fixed, ordered list of sources for both credentials and settings (region, output format, ...), and the first source that has a value for a given setting wins — later sources are never consulted for that setting, even if they also define it.
The chain runs highest-precedence-first: an explicit --profile/--region flag beats an environment
variable, which beats a config file value, which beats an EC2 instance role. This is precisely why
export AWS_PROFILE=staging can silently make every command in a shell target the wrong account until
you close that terminal — the env var outranks whatever aws configure list last showed you.
💡 The fastest way out of "which credentials is this actually using" is not re-reading your shell
history — it's aws configure list, which prints not just the resolved value for each setting but
where it came from (env, config-file, sso, iam-role, ...). Reach for it first, every time.
Setting up a profile — static access keys (legacy/service-account use)#
aws configure --profile myprofile # interactive: access key, secret, region, output format
aws configure set region us-east-1 --profile myprofile
aws configure set output json --profile myprofileaws configure only ever writes to and reads from the config/credentials files — it deliberately
ignores environment variables and instance roles, so re-running it won't "fix" a session that's
actually being overridden by an env var. Static long-lived access key pairs are the credential type
this creates; keep reading for why IAM Identity Center is now the recommended path for anything a
human types into a terminal.
Important
Static IAM user access keys are the credential type AWS itself now actively recommends against for human access. They don't expire on their own, they're the single most common thing that ends up leaked in a public git repo, and every key you create is another secret someone has to rotate and revoke. Reserve them for the narrow case of a workload or third-party tool that genuinely can't use a role (most CI systems, EC2, ECS, and Lambda all have better options — see the CI/CD recipe and the pitfalls section below).
Setting up IAM Identity Center (SSO) — the recommended path for humans#
aws configure sso-session # define a reusable SSO session (start URL, region, scopes)
aws configure sso --profile myprofile # interactively bind a profile to an account + permission set
aws sso login --profile myprofile # opens a browser, exchanges the login for a cached SSO token
aws sso login --profile myprofile --no-browser # print a URL/code instead of auto-opening a browser (headless boxes)configure sso-session sets up the shared piece (your organization's Identity Center start URL and
region) once; configure sso then creates or updates individual profiles against it, each bound to a
specific account and permission set. sso login is the everyday command — it retrieves and caches a
temporary access token, and the CLI transparently exchanges it for short-lived AWS credentials
(commonly 8-12 hours, configured by your Identity Center administrator) on every subsequent call using
that profile. There's no long-lived secret sitting on disk to leak, and access simply stops working
once the session expires — no key to remember to revoke.
# ~/.aws/config
[sso-session my-org]
sso_start_url = https://my-org.awsapps.com/start
sso_region = us-east-1
sso_registration_scopes = sso:account:access
[profile staging]
sso_session = my-org
sso_account_id = 111122223333
sso_role_name = DeveloperAccess
region = us-east-1
output = jsonListing and switching profiles#
aws configure list-profiles # every profile defined in ~/.aws/config and ~/.aws/credentials
aws configure list --profile myprofile # resolved config for one profile + where each value came from
export AWS_PROFILE=myprofile # switch the active profile for the current shell
aws sts get-caller-identity # confirm which identity/account the active profile resolves toget-caller-identity is the single cheapest, safest call to run before anything destructive — it
costs nothing, needs no permissions beyond being authenticated at all, and immediately answers "am I
actually pointed at the account I think I am."
Assuming a role — cross-account access#
aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/DeployRole \
--role-session-name my-session \
--duration-seconds 3600This prints temporary AccessKeyId/SecretAccessKey/SessionToken credentials to stdout — export
them as env vars, or (the far more common pattern) configure a profile block with role_arn +
source_profile in ~/.aws/config so the CLI assumes the role automatically on every call using that
profile, no manual export step required:
[profile deploy]
role_arn = arn:aws:iam::111122223333:role/DeployRole
source_profile = my-base-profile
role_session_name = deploy-session--role-session-name isn't cosmetic — it lands in CloudTrail logs for every subsequent API call made
with those temporary credentials, and some IAM policies key access decisions off it via the
sts:RoleSessionName condition. Use a value that identifies who or what actually assumed the role
(a username, a CI job ID), not a generic placeholder.
MFA and temporary session tokens#
aws iam enable-mfa-device --user-name jane \
--serial-number arn:aws:iam::111122223333:mfa/jane \
--authentication-code1 123456 --authentication-code2 789012
aws iam list-mfa-devices --user-name jane
aws sts get-session-token \
--serial-number arn:aws:iam::111122223333:mfa/jane \
--token-code 123456 --duration-seconds 3600enable-mfa-device needs two consecutive codes from the device (--authentication-code1/2) to
prove it's correctly synced, not one. get-session-token is what actually enforces MFA for subsequent
calls — it returns temporary credentials carrying an MFA-authenticated flag, which some IAM policies
require via an aws:MultiFactorAuthPresent condition (a common pattern for gating destructive actions
like iam:DeleteRole behind "must have entered an MFA code in the last hour").
Managing IAM users, groups, and roles#
aws iam list-users
aws iam create-user --user-name jane
aws iam create-access-key --user-name jane # generates a new access key pair for that user
aws iam list-access-keys --user-name jane
aws iam create-group --group-name Developers
aws iam add-user-to-group --group-name Developers --user-name jane
aws iam attach-group-policy --group-name Developers --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess
aws iam create-role \
--role-name MyServiceRole \
--assume-role-policy-document file://trust-policy.jsonAttaching a policy to a group grants it to every current and future member — cheaper to maintain at
scale than attaching the same policy to each user individually, and it's the standard way to keep
permissions consistent as a team grows. --assume-role-policy-document is the role's trust policy —
who is allowed to assume it — not the permissions the role grants; permissions come from a separate
policy attached with attach-role-policy, a distinction that trips up nearly everyone the first time
they hand-author a role from scratch.
// trust-policy.json — allows EC2 instances to assume this role
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}Attaching and inspecting policies#
aws iam attach-role-policy --role-name MyServiceRole --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
aws iam list-attached-role-policies --role-name MyServiceRole
aws iam detach-role-policy --role-name MyServiceRole --policy-arn arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess
aws iam list-policies --scope Local # only customer-managed policies, not the ~1500 AWS-managed ones--scope Local is worth remembering the first time list-policies dumps every AWS-managed policy in
existence at you — Local restricts the listing to policies your account actually authored.
Managing customer-managed policy documents and versions#
aws iam create-policy --policy-name MyAppPolicy --policy-document file://policy.json
aws iam list-policy-versions --policy-arn arn:aws:iam::111122223333:policy/MyAppPolicy
aws iam create-policy-version \
--policy-arn arn:aws:iam::111122223333:policy/MyAppPolicy \
--policy-document file://policy-v2.json --set-as-default
aws iam delete-policy-version --policy-arn arn:aws:iam::111122223333:policy/MyAppPolicy --version-id v1A managed policy keeps up to 5 versions. create-policy-version doesn't overwrite — it adds a new
version and, only with --set-as-default, makes it the one actually in effect. Once you hit 5
versions, create-policy-version fails outright until you delete-policy-version an old one; scripts
that iterate on a policy in a loop need to account for this.
Testing permissions before granting them#
aws iam simulate-principal-policy \
--policy-source-arn arn:aws:iam::111122223333:role/MyServiceRole \
--action-names s3:GetObject s3:PutObject \
--resource-arns arn:aws:s3:::my-bucket/*simulate-principal-policy evaluates what a real user/role/group would be allowed to do against a
given action and resource, combining every policy attached to that principal (identity-based, group,
and — if you pass --resource-policy — a resource-based policy too) without making the actual API
call. This is the tool for answering "will this role actually be able to do X" before shipping a
change, rather than finding out from a production AccessDenied error.
Filtering output with --query and --output#
aws iam list-users --query 'Users[].UserName' --output text
aws iam list-roles --query "Roles[?contains(RoleName, 'Deploy')].RoleName" --output table--query uses JMESPath against the JSON response — it runs client-side after the API call, so it
doesn't reduce API load or pagination, only the output you see. --output controls the rendering
format: json (default), text, table, or yaml. Combine --output text with --query for values
you want to pipe straight into another command (xargs, a shell loop) without parsing JSON.
Auditing IAM at the account level#
aws iam generate-credential-report # kick off async report generation
aws iam get-credential-report --output text --query 'Content' | base64 -d > report.csv
aws iam get-account-authorization-details # full snapshot: users, groups, roles, policies, and links
aws iam list-access-keys --user-name jane --query 'AccessKeyMetadata[].[AccessKeyId,CreateDate]'generate-credential-report is async by design — the account-wide CSV (last password use, MFA
status, access key age, last rotation) can take a few seconds to build, so get-credential-report
should be retried (or called after a short pause) rather than assumed ready immediately after
generation. This is the single fastest way to find IAM users with access keys that haven't rotated in
months, a very common finding in a first security review of an older account.
Rotating access keys#
aws iam create-access-key --user-name jane # create a second, parallel key
aws iam update-access-key --user-name jane --access-key-id AKIA... --status Inactive
aws iam delete-access-key --user-name jane --access-key-id AKIA...Standard rotation is create-new → update apps to use it → deactivate the old one (not delete) → confirm
nothing broke → delete. update-access-key --status Inactive is reversible; delete-access-key is
not.
Caution
A user can hold at most 2 access keys at once. If a service is already using both slots and needs
rotation, you must deactivate (not delete) one before create-access-key will succeed — trying to
create a third fails outright, which is a common surprise mid-incident when someone's rushing to
rotate a leaked key under time pressure.
Real-world scenario: onboarding a new engineer without issuing static keys#
A platform team standardizing on IAM Identity Center for a 40-person engineering org:
# One-time, per engineer, done by the engineer themselves — no secrets change hands
aws configure sso-session --profile-name company-sso
aws sso login --profile staging
aws sso login --profile prod
aws sts get-caller-identity --profile staging # confirm the right account/role before doing anythingTip
Give every engineer one profile per account/role combination, named for what it is, not who they
are (staging, prod-readonly, prod-deploy) — a profile name tied to a person breaks the moment
that person changes teams, while a role-shaped name stays correct as people rotate through it.
Real-world scenario: cross-account deploy role chaining#
A CI pipeline running in a shared "tools" account needs to deploy into three separate application
accounts (dev, staging, prod), each with its own DeployRole:
# ~/.aws/config in the CI runner
[profile tools-base]
# base identity: an IAM role attached to the CI runner itself (e.g. an EC2/ECS task role)
[profile deploy-dev]
role_arn = arn:aws:iam::111111111111:role/DeployRole
source_profile = tools-base
[profile deploy-prod]
role_arn = arn:aws:iam::333333333333:role/DeployRole
source_profile = tools-base
mfa_serial = arn:aws:iam::999999999999:mfa/ci-break-glass # require MFA even for automation, for prod specificallyThe pipeline never holds a static credential for any application account — it starts from one base
identity in the tools account and chains AssumeRole calls outward, each scoped narrowly by that
account's own DeployRole trust policy (which should itself restrict sts:ExternalId or the calling
principal, not just the account).
Real-world scenario: emergency response to a leaked access key#
# 1. Immediately deactivate — reversible, buys time without breaking a legitimate in-flight process
aws iam update-access-key --user-name jane --access-key-id AKIAEXAMPLE --status Inactive
# 2. Confirm what that key could actually reach, for the incident writeup
aws iam list-attached-user-policies --user-name jane
aws iam list-groups-for-user --user-name jane
# 3. Check CloudTrail for any calls made with it before deactivation
aws cloudtrail lookup-events --lookup-attributes AttributeKey=AccessKeyId,AttributeValue=AKIAEXAMPLE
# 4. Once confirmed safe, delete permanently and issue a replacement via a non-static path if possible
aws iam delete-access-key --user-name jane --access-key-id AKIAEXAMPLEWarning
Deactivate before you investigate, not after. The instinct to "figure out what happened first" on
a leaked key costs exactly the minutes an attacker needs — update-access-key --status Inactive
takes effect immediately and is trivially reversible if it turns out to be a false alarm, so there is
no real cost to acting first and asking questions second.
CI/CD recipe: GitHub Actions with OIDC — no static keys at all#
# .github/workflows/deploy.yml
name: Deploy
on: [push]
permissions:
id-token: write # required for GitHub's OIDC token
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::111122223333:role/GitHubActionsDeployRole
aws-region: us-east-1
- run: aws s3 sync ./dist s3://my-bucket/ --deleteThe IAM role's trust policy restricts which GitHub repo/branch can assume it via a token.actions. githubusercontent.com OIDC provider condition — no AWS_ACCESS_KEY_ID secret lives in the repo at
all, and a compromised GitHub Actions secret has nothing AWS-shaped to steal in the first place.
Common pitfalls#
export AWS_PROFILEoutliving the terminal session it was meant for — a forgotten env var silently overrides--profileflags in scripts run later from the same shell.aws configure listcatches this instantly; a bareecho $AWS_PROFILEdoes not (it won't show a config-file default).- Confusing a role's trust policy with its permissions policy —
create-role --assume-role-policy-documentonly controls who can assume the role, not what it can do once assumed. A role with an open trust policy and no attached permissions policy can be assumed by anyone but do nothing. - Assuming
--queryreduces API cost or pagination — it's a client-side JMESPath filter applied after the full response arrives; it doesn't change what the API itself returns or how many pages a paginated call fetches. - Treating access key rotation as delete-then-create — this creates a window with zero working credentials for anything still using the old key. Always create-new → cut over → deactivate → delete.
- Not setting
--duration-secondsdeliberately onassume-role— the default (1 hour) may be too short for a long-running job or too long for a sensitive one; both directions are worth a conscious choice rather than the default.
Exit codes / when to reach for something else#
The CLI exits 0 on success and 1... on most failures, but the reason always needs the actual error
text — aws doesn't distinguish "access denied" from "resource not found" from "throttled" by exit
code alone; always inspect stderr (or --output json and the response body) rather than branching on
exit status in a script. For managing IAM resources declaratively — roles, policies, trust
relationships that should be reviewable and version-controlled — prefer Terraform or CloudFormation
over hand-run aws iam commands; reach for the CLI here for one-off investigation, emergency response,
and bootstrapping the very first identity a Terraform pipeline needs to run at all.