Verified20 commandsAI-assisted

Configuration & IAM

.md

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

Diagram

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 myprofile

aws 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).

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 = json

Listing 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 to

get-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 3600

This 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 3600

enable-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.json

Attaching 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 v1

A 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 anything

Tip

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 specifically

The 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 AKIAEXAMPLE

Warning

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/ --delete

The 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_PROFILE outliving the terminal session it was meant for — a forgotten env var silently overrides --profile flags in scripts run later from the same shell. aws configure list catches this instantly; a bare echo $AWS_PROFILE does not (it won't show a config-file default).
  • Confusing a role's trust policy with its permissions policycreate-role --assume-role-policy-document only 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 --query reduces 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-seconds deliberately on assume-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.