This article is co-authored with a generative AI. Facts have been cross-checked against official documentation where possible, but errors may remain. Please verify against primary sources before making any important decisions.

This article is a personal record of work in the author's own environment. It does not recommend any particular tool or procedure, and it does not guarantee the accuracy or safety of its contents. Apply the commands and settings here at your own responsibility, after verifying them thoroughly in a test environment or similar. The author accepts no liability for any damage arising from the use of this article.

Background

When using the AWS CLI / SDK, it has long been common to write IAM (Identity and Access Management) user access keys into ~/.aws/credentials in plaintext.

[myprofile]
aws_access_key_id = AKIA...
aws_secret_access_key = ...

This file format is the AWS standard and still works functionally, but because it is plaintext, it leaves room for exposure through paths like the following:

  • Ending up in backups (Time Machine, iCloud Drive, copies to external disks)
  • Editor / IDE workspace indexes and caches
  • cat ~/.aws/credentials left in shell history (~/.zsh_history)
  • Accidentally committed to git
  • Read by other processes on the same machine (misconfigured permissions)

AWS itself now recommends IAM Identity Center (formerly AWS Single Sign-On) and aws sso login. Running with short-lived session tokens (ASIA...) so that long-lived keys (AKIA...) are not kept locally is becoming the standard.

That said, there are cases where Identity Center is not adopted, or cannot be.

When IAM Identity Center is hard to choose

From what I have looked into, in situations like the following the cost of introducing Identity Center may not pay off, or it may not be selectable for structural reasons:

  • Using just a single account as an individual. The operational overhead of enabling Identity Center and maintaining permission sets may not be judged worth the benefit
  • A shared IAM user used by several people (each member does not have an individual Identity Center user)
  • A standalone account not under AWS Organizations
  • Legacy integrations or external services that assume IAM user keys

This article covers, for such environments, a setup that does not keep ~/.aws/credentials in plaintext but instead stores the key material in the macOS Keychain and retrieves it as a temporary session only when needed.

Overview of aws-vault

aws-vault stores IAM user keys in an OS keystore (the macOS Keychain, Windows Credential Manager, Linux Secret Service, and so on) and hands a temporary session token to the AWS CLI / SDK via credential_process.

It was originally published as 99designs/aws-vault. The final release of that repository is v7.2.0 (March 2023), and in June 2025 the README announced "This project has been abandoned and it's not receiving any more updates." The community active fork ByteNess/aws-vault is now updated continuously, and the Homebrew formula (brew install aws-vault) switched to it as of June 2025 as well. The command examples in this article assume the ByteNess fork (v7.10.7 at the time of writing).

On macOS you can install it via Homebrew:

brew install aws-vault         # Homebrew formula (ByteNess fork, the currently active version)
# brew install --cask aws-vault-binary  # 99designs' final binary (v7.2.0, no longer maintained)

The two CLIs are effectively compatible; the ByteNess fork is newer and continues to be updated. Regarding code signing of the distributed binaries: 99designs' final release (v7.2.0) ships a binary inside a DMG with an Apple Developer ID signature and hardened runtime, whereas the ByteNess releases are raw binaries with only an ad-hoc signature. However, the Homebrew formula (brew install aws-vault) builds from source, so this signing difference is effectively irrelevant. Continuing to use the old 99designs binary means you no longer receive security patches or dependency updates, so for a new setup the formula route is the realistic choice.

The mechanism looks like this:

[AWS CLI / SDK]
   ↓ calls credential_process
[aws-vault export --duration=12h myprofile]
   ↓ retrieves the long-lived key (AKIA...) from the Keychain
   ↓ calls STS GetSessionToken (or AssumeRole)
[returns a temporary session token (ASIA...)]
   ↓
[AWS CLI / SDK calls the AWS API]

The long-lived key itself is not written out as a file; it stays inside the Keychain, and what is handed to the application is a temporary session issued by STS (Security Token Service). It expires after the period set with --duration (12 hours in this article's examples; for GetSessionToken, an IAM user can specify between 15 minutes and 36 hours, and the default when unspecified is 12 hours).

Configuring ~/.aws/config

Once you switch to aws-vault, ~/.aws/credentials can be operated as an empty file. The settings for each profile are written in ~/.aws/config as a credential_process.

Without MFA:

[profile myprofile]
region = ap-northeast-1
credential_process = sh -c "AWS_VAULT_KEYCHAIN_NAME=login AWS_VAULT_PROMPT=osascript aws-vault export --format=json --duration=12h myprofile"

There are two points to note.

AWS_VAULT_KEYCHAIN_NAME=login tells aws-vault to use the standard macOS login keychain. aws-vault's default behavior is to create a dedicated keychain (aws-vault.keychain), but aligning with the login keychain makes it easier to integrate Touch ID and password management with other apps.

AWS_VAULT_PROMPT=osascript makes the MFA (multi-factor authentication) code input dialog a native macOS dialog. The input prompt then appears even when aws-vault is called from an external process such as an IDE or a CI wrapper, rather than a terminal.

With MFA:

[profile myprofile]
region = ap-northeast-1
mfa_serial = arn:aws:iam::123456789012:mfa/myuser-totp
credential_process = sh -c "AWS_VAULT_KEYCHAIN_NAME=login AWS_VAULT_PROMPT=osascript aws-vault export --format=json --duration=12h myprofile"

When mfa_serial is set, aws-vault requests a 6-digit TOTP (Time-based One-Time Password) code when issuing a session. Once entered, a session valid for the specified period (12 hours in the example above) is cached, so you do not have to enter it frequently.

Note that aws-vault does not appear to support U2F (Universal 2nd Factor; FIDO-compliant security keys). To make use of MFA, you need to register a TOTP device (Google Authenticator, 1Password's TOTP feature, and so on) on the IAM user.

Migrating existing plaintext profiles

To register an existing key into the Keychain, passing the key values via environment variables is the simplest:

AWS_VAULT_KEYCHAIN_NAME=login \
AWS_ACCESS_KEY_ID=AKIA... \
AWS_SECRET_ACCESS_KEY=... \
  aws-vault add --backend=keychain --env --no-add-config myprofile
  • --env reads the key values from environment variables rather than standard input
  • --no-add-config suppresses the automatic append to ~/.aws/config (because we want to manage the config ourselves)

On success it prints Added credentials to profile "myprofile" in vault. After that, remove the corresponding profile section from ~/.aws/credentials.

Verification:

aws --profile myprofile sts get-caller-identity

If mfa_serial is set, a macOS dialog appears first. Entering the code returns a result like this:

{
    "UserId": "AID...",
    "Account": "123456789012",
    "Arn": "arn:aws:iam::123456789012:user/myuser"
}

You can check the session cache state with aws-vault list:

Profile         Credentials     Sessions
=======         ===========     ========
myprofile       myprofile       sts.GetSessionToken:11h59m51s

Key rotation

aws-vault has a rotate subcommand that performs "issue a new key → update the Keychain → delete the old key" in a single command.

rotate deletes the old access key on the AWS side. Before running it, confirm that no CI, other machine, deployment script, Terraform, and so on references the same IAM user's access key. For a shared IAM user in particular, you also need to confirm that no other user is using the same key. Checking the behavior in a test environment beforehand is safer.

AWS_VAULT_KEYCHAIN_NAME=login AWS_VAULT_PROMPT=osascript \
  aws-vault rotate myprofile

Example output (key IDs masked):

Rotating credentials stored for profile 'myprofile' using a session from profile 'myprofile' (takes 10-20 seconds)
Creating a new access key
Created new access key ****************BCIU
Deleted 1 sessions for myprofile
Deleting old access key ****************QYXF
Deleted old access key ****************QYXF
Finished rotating access key

Even for a shared account where iam:CreateAccessKey has an MFA-required condition (aws:MultiFactorAuthPresent), depending on how the policy condition is designed, calling it through aws-vault's MFA session may pass.

Pitfalls encountered in practice

Here are a few small pitfalls I hit while carrying out the migration.

The MFA code is only accepted as half-width digits

The osascript dialog goes through the macOS IME, so depending on the input mode, full-width digits can be entered. The AWS API accepts only half-width digits, so it is rejected like this:

ValidationError: Value '831405' at 'tokenCode' failed to satisfy
  constraint: Member must satisfy regular expression pattern: [\d]*

Getting into the habit of switching to alphanumeric mode before entering helps.

A TOTP code cannot be used twice in a row

From what I experienced, sending the same TOTP code generated in one 30-second window twice in a row appears to be rejected. I could not find an explicit statement in the AWS API reference, but in a case where I failed the first time (e.g. a typo) and immediately retried, the same code did not go through, and I had to wait until the app showed the next code.

AccessDenied: MultiFactorAuthentication failed with invalid MFA one time pass code.

U2F security keys cannot be used from aws-vault

Even if you have registered a U2F device (a Yubikey or other FIDO device) on the IAM side, aws-vault does not support U2F, so it cannot be set as mfa_serial. Add a TOTP device separately. For example, in the console you choose "Authenticator app", read the QR code with a TOTP app (1Password, Google Authenticator, and so on), and enter two consecutive codes.

When CreateAccessKey is denied

On a shared account where IAM operations have an explicit deny, calling aws iam create-access-key directly with the plaintext key is rejected:

AccessDenied: User ... is not authorized to perform: iam:CreateAccessKey
  ... with an explicit deny in an identity-based policy

In that case, first register the existing key into aws-vault, issue an MFA session, and then try aws-vault rotate again; it may pass by satisfying the aws:MultiFactorAuthPresent condition.

Cleaning up backup files and CSVs

If you make a backup like ~/.aws/credentials.bak.YYYYMMDD-HHMMSS during the migration, plaintext keys remain there. The same applies to the accessKeys.csv downloaded when you create a key in the AWS console — the secret access key is shown on screen the moment you open it. After importing into aws-vault, delete these promptly.

Whether to keep the default profile

The [default] section is used implicitly when --profile is omitted, so in an operation that moves between multiple accounts, it is an easy place to accidentally operate the wrong account. One approach is to abolish [default] and replace it with a name that reflects its purpose (for example s3-admin or a <account-name>-<role> naming scheme). Making it a habit to always specify --profile makes the intent of a command easier to read.

How much to trust aws-vault itself

"Is it fine to entrust secret information like an IAM user key to a third-party OSS (open source software) tool?" is a natural question, so let's organize it.

  • The maintainer has changed: As mentioned above, the 99designs version is announced as abandoned, and the current active fork is ByteNess. The Homebrew formula tracks ByteNess, so if you install via brew install aws-vault you automatically get the maintained version. Continuing to use an old binary downloaded directly means you do not receive responses to dependency vulnerabilities and the like, so an operation that pulls in the latest via the formula or a source build is preferable.
  • Trust of the acquisition path: The Homebrew core formula runs SHA256 checksum verification, giving it resistance against mirror tampering. When downloading a binary directly from GitHub Releases, verify the signature and hash yourself.
  • Known vulnerabilities: In the past, a DNS Rebinding vulnerability was reported in the 99designs version and fixed in v5.4.4 / v4.7.2. This was an issue around the EC2 metadata server started by aws-vault exec --server; a credential_process-based usage like this article does not use --server, so it is a setup less likely to be affected. Even so, rather than concluding "it does not apply to my usage", it is safer to maintain an operation that updates regularly, including dependencies.
  • Relation to local compromise: The aws-vault process runs with macOS user privileges, and macOS access controls apply when it accesses the login keychain. After you choose "Always Allow", the key can be retrieved without re-authentication, so if the aws-vault binary itself or the shell that calls it is compromised, there remains a risk that keys are extracted from the Keychain. This is not specific to aws-vault; it can be organized as a constraint common to local file management and OS keychain management (countermeasures at this layer — EDR (Endpoint Detection and Response), FileVault, vetting of developer tools — are a separate domain).

In short, it can be positioned as something that lowers risk in stages compared to plaintext files, but is not an absolute defense.

Assessing practicality

Here are my impressions from operating it across several profiles.

Benefits:

  • ~/.aws/credentials can be made an empty file, removing the local-file-based exposure paths
  • The 12-hour temporary session cache keeps MFA input frequency within an acceptable range
  • aws-vault rotate lowers the bar for rotation operations
  • The existing AWS CLI / SDK works transparently via credential_process, so no changes to application code are needed

Trade-offs:

  • A dependency on the macOS Keychain. It assumes the login keychain is in an unlocked state
  • For profiles without MFA, while the exposure paths can be reduced, once a key does leak, what an attacker can do is no different from the plaintext era (blocking the path and the size of the post-leak privileges are separate matters)
  • The setup is macOS-specific, so porting to Windows / Linux, or having an entire team adopt the same configuration, requires separate design

A separate layer from malware countermeasures

The concern "if the machine itself is infected with malware, won't keys be pulled from the Keychain?" is, rather than something specific to aws-vault, in the domain of OS-level security in general (EDR, FileVault, firmware updates, vetting of developer tools, and so on). Avoiding plaintext with aws-vault is primarily aimed at blocking "accidental exposure from local files" (backups, editor caches, exfiltration via scp, mixing into git, and so on); it helps to keep in mind that it is not in itself a defense against a sophisticated attacker.

In an environment that does not adopt IAM Identity Center, combining aws-vault, the Keychain, TOTP MFA, and periodic rotation can lower risk in stages compared to plaintext file operation. If you have the scale and operational structure to migrate to Identity Center, that is the main path; otherwise, this feels like a practical option that holds up in real use.

Things that are effective to use alongside it:

  • Inspect past commits for leaks with a secret scanner such as gitleaks
  • Consolidate TOTP into a password manager such as 1Password to centralize MFA management
  • Run aws-vault rotate periodically to avoid a state where long-lived keys grow "stale"

References