Repository: 99designs/aws-vault Branch: master Commit: 74e2f7ac256f Files: 78 Total size: 232.0 KB Directory structure: gitextract_qqh0xl3v/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ └── bug_report.md │ └── workflows/ │ ├── go.yml │ └── stale.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── Makefile ├── README.md ├── USAGE.md ├── bin/ │ └── create-dmg ├── cli/ │ ├── add.go │ ├── add_test.go │ ├── clear.go │ ├── exec.go │ ├── exec_test.go │ ├── export.go │ ├── export_test.go │ ├── global.go │ ├── list.go │ ├── list_test.go │ ├── login.go │ ├── proxy.go │ ├── remove.go │ └── rotate.go ├── contrib/ │ ├── _aws-vault-proxy/ │ │ ├── Dockerfile │ │ ├── docker-compose.yml │ │ ├── go.mod │ │ ├── go.sum │ │ └── main.go │ ├── completions/ │ │ ├── bash/ │ │ │ └── aws-vault.bash │ │ ├── fish/ │ │ │ └── aws-vault.fish │ │ └── zsh/ │ │ └── aws-vault.zsh │ ├── docker/ │ │ └── Dockerfile │ └── scripts/ │ ├── aws-configure-with-env-vars.sh │ ├── aws-iam-create-yubikey-mfa.sh │ └── aws-iam-resync-yubikey-mfa.sh ├── go.mod ├── go.sum ├── iso8601/ │ ├── iso8601.go │ └── iso8601_test.go ├── main.go ├── prompt/ │ ├── kdialog.go │ ├── osascript.go │ ├── prompt.go │ ├── terminal.go │ ├── wincredui_windows.go │ ├── ykman.go │ └── zenity.go ├── server/ │ ├── ec2alias_bsd.go │ ├── ec2alias_linux.go │ ├── ec2alias_windows.go │ ├── ec2proxy.go │ ├── ec2proxy_default.go │ ├── ec2proxy_unix.go │ ├── ec2server.go │ ├── ecsserver.go │ └── httplog.go └── vault/ ├── assumeroleprovider.go ├── assumerolewithwebidentityprovider.go ├── cachedsessionprovider.go ├── config.go ├── config_test.go ├── credentialkeyring.go ├── credentialprocessprovider.go ├── credentialprocessprovider_test.go ├── executeprocess.go ├── federationtokenprovider.go ├── getuser.go ├── keyringprovider.go ├── mfa.go ├── oidctokenkeyring.go ├── sessionkeyring.go ├── sessionkeyring_test.go ├── sessiontokenprovider.go ├── ssorolecredentialsprovider.go ├── stsendpointresolver.go ├── vault.go └── vault_test.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- - [ ] I am using the latest release of AWS Vault - [ ] I have provided my `.aws/config` (redacted if necessary) - [ ] I have provided the debug output using `aws-vault --debug` (redacted if necessary) ================================================ FILE: .github/workflows/go.yml ================================================ name: Continuous Integration on: push: pull_request: branches: - master permissions: contents: read jobs: test: name: test strategy: matrix: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/setup-go@v3 with: go-version: '1.20' - uses: actions/checkout@v3 - name: Run tests run: go test -race ./... lint: permissions: contents: read # for actions/checkout to fetch code pull-requests: read # for golangci/golangci-lint-action to fetch pull requests name: lint strategy: matrix: os: [macos-latest, ubuntu-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/setup-go@v3 with: go-version: '1.20' - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@v3.4.0 with: version: v1.52.0 ================================================ FILE: .github/workflows/stale.yml ================================================ # See https://github.com/actions/stale name: Mark and close stale issues on: schedule: - cron: '15 10 * * *' jobs: stale: runs-on: ubuntu-latest permissions: issues: write steps: - uses: actions/stale@v7 with: days-before-stale: 180 days-before-close: 7 stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs.' exempt-issue-labels: pinned,security,feature ================================================ FILE: .gitignore ================================================ /aws-vault /aws-vault-* /SHA256SUMS ================================================ FILE: .golangci.yml ================================================ linters: enable: - bodyclose - contextcheck - depguard - durationcheck - dupl - errchkjson - errname - exhaustive - exportloopref - gofmt - goimports - makezero - misspell - nakedret - nilerr - nilnil - noctx - prealloc - revive # - rowserrcheck - thelper - tparallel - unconvert - unparam # - wastedassign - whitespace ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 99designs Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ VERSION=$(shell git describe --tags --candidates=1 --dirty) BUILD_FLAGS=-ldflags="-X main.Version=$(VERSION)" -trimpath CERT_ID ?= Developer ID Application: 99designs Inc (NRM9HVJ62Z) SRC=$(shell find . -name '*.go') go.mod INSTALL_DIR ?= ~/bin .PHONY: binaries clean release install ifeq ($(shell uname), Darwin) aws-vault: $(SRC) go build -ldflags="-X main.Version=$(VERSION)" -o $@ . codesign --options runtime --timestamp --sign "$(CERT_ID)" $@ else aws-vault: $(SRC) go build -ldflags="-X main.Version=$(VERSION)" -o $@ . endif install: aws-vault mkdir -p $(INSTALL_DIR) rm -f $(INSTALL_DIR)/aws-vault cp -a ./aws-vault $(INSTALL_DIR)/aws-vault binaries: aws-vault-linux-amd64 aws-vault-linux-arm64 aws-vault-linux-ppc64le aws-vault-linux-arm7 aws-vault-darwin-amd64 aws-vault-darwin-arm64 aws-vault-windows-386.exe aws-vault-windows-arm64.exe aws-vault-freebsd-amd64 dmgs: aws-vault-darwin-amd64.dmg aws-vault-darwin-arm64.dmg clean: rm -f ./aws-vault ./aws-vault-*-* ./SHA256SUMS release: binaries dmgs SHA256SUMS @echo "\nTo create a new release run:\n\n gh release create --title $(VERSION) $(VERSION) \ aws-vault-darwin-amd64.dmg \ aws-vault-darwin-arm64.dmg \ aws-vault-freebsd-amd64 \ aws-vault-linux-amd64 \ aws-vault-linux-arm64 \ aws-vault-linux-arm7 \ aws-vault-linux-ppc64le \ aws-vault-windows-386.exe \ aws-vault-windows-arm64.exe \ SHA256SUMS\n" @echo "\nTo update homebrew-cask run:\n\n brew bump-cask-pr --version $(shell echo $(VERSION) | sed 's/v\(.*\)/\1/') aws-vault\n" aws-vault-darwin-amd64: $(SRC) GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 SDKROOT=$(shell xcrun --sdk macosx --show-sdk-path) go build $(BUILD_FLAGS) -o $@ . aws-vault-darwin-arm64: $(SRC) GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 SDKROOT=$(shell xcrun --sdk macosx --show-sdk-path) go build $(BUILD_FLAGS) -o $@ . aws-vault-freebsd-amd64: $(SRC) GOOS=freebsd GOARCH=amd64 go build $(BUILD_FLAGS) -o $@ . aws-vault-linux-amd64: $(SRC) GOOS=linux GOARCH=amd64 go build $(BUILD_FLAGS) -o $@ . aws-vault-linux-arm64: $(SRC) GOOS=linux GOARCH=arm64 go build $(BUILD_FLAGS) -o $@ . aws-vault-linux-ppc64le: $(SRC) GOOS=linux GOARCH=ppc64le go build $(BUILD_FLAGS) -o $@ . aws-vault-linux-arm7: $(SRC) GOOS=linux GOARCH=arm GOARM=7 go build $(BUILD_FLAGS) -o $@ . aws-vault-windows-386.exe: $(SRC) GOOS=windows GOARCH=386 go build $(BUILD_FLAGS) -o $@ . aws-vault-windows-arm64.exe: $(SRC) GOOS=windows GOARCH=arm64 go build $(BUILD_FLAGS) -o $@ . aws-vault-darwin-amd64.dmg: aws-vault-darwin-amd64 ./bin/create-dmg aws-vault-darwin-amd64 $@ aws-vault-darwin-arm64.dmg: aws-vault-darwin-arm64 ./bin/create-dmg aws-vault-darwin-arm64 $@ SHA256SUMS: binaries dmgs shasum -a 256 \ aws-vault-darwin-amd64.dmg \ aws-vault-darwin-arm64.dmg \ aws-vault-freebsd-amd64 \ aws-vault-linux-amd64 \ aws-vault-linux-arm64 \ aws-vault-linux-arm7 \ aws-vault-linux-ppc64le \ aws-vault-windows-386.exe \ aws-vault-windows-arm64.exe \ > $@ ================================================ FILE: README.md ================================================ # AWS Vault [![Downloads](https://img.shields.io/github/downloads/99designs/aws-vault/total.svg)](https://github.com/99designs/aws-vault/releases) [![Continuous Integration](https://github.com/99designs/aws-vault/workflows/Continuous%20Integration/badge.svg)](https://github.com/99designs/aws-vault/actions) > [!WARNING] > This project has been abandoned and it's not receiving any more updates. If you want to continue to receive updates > or contribute, please feel free to look at the active fork at: https://github.com/ByteNess/aws-vault AWS Vault is a tool to securely store and access AWS credentials in a development environment. AWS Vault stores IAM credentials in your operating system's secure keystore and then generates temporary credentials from those to expose to your shell and applications. It's designed to be complementary to the AWS CLI tools, and is aware of your [profiles and configuration in `~/.aws/config`](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html#cli-config-files). Check out the [announcement blog post](https://99designs.com.au/tech-blog/blog/2015/10/26/aws-vault/) for more details. ## Installing You can install AWS Vault: - by downloading the [latest release](https://github.com/99designs/aws-vault/releases/latest) - on macOS with [Homebrew](https://formulae.brew.sh/formula/aws-vault): `brew install aws-vault` - on macOS with [MacPorts](https://ports.macports.org/port/aws-vault/summary): `port install aws-vault` - on Windows with [Chocolatey](https://chocolatey.org/packages/aws-vault): `choco install aws-vault` - on Windows with [Scoop](https://scoop.sh/): `scoop install aws-vault` - on Linux with [Homebrew on Linux](https://formulae.brew.sh/formula/aws-vault): `brew install aws-vault` - on [Arch Linux](https://www.archlinux.org/packages/community/x86_64/aws-vault/): `pacman -S aws-vault` - on [Gentoo Linux](https://github.com/gentoo/guru/tree/master/app-admin/aws-vault): `emerge --ask app-admin/aws-vault` ([enable Guru first](https://wiki.gentoo.org/wiki/Project:GURU/Information_for_End_Users)) - on [FreeBSD](https://www.freshports.org/security/aws-vault/): `pkg install aws-vault` - on [OpenSUSE](https://software.opensuse.org/package/aws-vault): enable devel:languages:go repo then `zypper install aws-vault` - with [Nix](https://search.nixos.org/packages?show=aws-vault&query=aws-vault): `nix-env -i aws-vault` - with [asdf-vm](https://github.com/karancode/asdf-aws-vault): `asdf plugin-add aws-vault https://github.com/karancode/asdf-aws-vault.git && asdf install aws-vault ` ## Documentation Config, usage, tips and tricks are available in the [USAGE.md](./USAGE.md) file. ## Vaulting Backends The supported vaulting backends are: * [macOS Keychain](https://support.apple.com/en-au/guide/keychain-access/welcome/mac) * [Windows Credential Manager](https://support.microsoft.com/en-au/help/4026814/windows-accessing-credential-manager) * Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5)) * [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5) * [Pass](https://www.passwordstore.org/) * Encrypted file Use the `--backend` flag or `AWS_VAULT_BACKEND` environment variable to specify. ## Quick start ```shell # Store AWS credentials for the "jonsmith" profile $ aws-vault add jonsmith Enter Access Key Id: ABDCDEFDASDASF Enter Secret Key: %%% # Execute a command (using temporary credentials) $ aws-vault exec jonsmith -- aws s3 ls bucket_1 bucket_2 # open a browser window and login to the AWS Console $ aws-vault login jonsmith # List credentials $ aws-vault list Profile Credentials Sessions ======= =========== ======== jonsmith jonsmith - # Start a subshell with temporary credentials $ aws-vault exec jonsmith Starting subshell /bin/zsh, use `exit` to exit the subshell $ aws s3 ls bucket_1 bucket_2 ``` ## How it works `aws-vault` uses Amazon's STS service to generate [temporary credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html) via the `GetSessionToken` or `AssumeRole` API calls. These expire in a short period of time, so the risk of leaking credentials is reduced. AWS Vault then exposes the temporary credentials to the sub-process in one of two ways 1. **Environment variables** are written to the sub-process. Notice in the below example how the AWS credentials get written out ```shell $ aws-vault exec jonsmith -- env | grep AWS AWS_VAULT=jonsmith AWS_DEFAULT_REGION=us-east-1 AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=%%% AWS_SECRET_ACCESS_KEY=%%% AWS_SESSION_TOKEN=%%% AWS_CREDENTIAL_EXPIRATION=2020-04-16T11:16:27Z ``` 2. **Local metadata server** is started. This approach has the advantage that anything that uses Amazon's SDKs will automatically refresh credentials as needed, so session times can be as short as possible. ```shell $ aws-vault exec --server jonsmith -- env | grep AWS AWS_VAULT=jonsmith AWS_DEFAULT_REGION=us-east-1 AWS_REGION=us-east-1 AWS_CONTAINER_CREDENTIALS_FULL_URI=%%% AWS_CONTAINER_AUTHORIZATION_TOKEN=%%% ``` The default is to use environment variables, but you can opt-in to the local instance metadata server with the `--server` flag on the `exec` command. ## Roles and MFA [Best-practice](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#delegate-using-roles) is to [create Roles to delegate permissions](https://docs.aws.amazon.com/cli/latest/userguide/cli-roles.html). For security, you should also require that users provide a one-time key generated from a multi-factor authentication (MFA) device. First you'll need to create the users and roles in IAM, as well as [setup an MFA device](https://docs.aws.amazon.com/IAM/latest/UserGuide/GenerateMFAConfigAccount.html). You can then [set up IAM roles to enforce MFA](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html#cli-configure-role-mfa). Here's an example configuration using roles and MFA: ```ini [default] region = us-east-1 [profile jonsmith] mfa_serial = arn:aws:iam::111111111111:mfa/jonsmith [profile foo-readonly] source_profile = jonsmith role_arn = arn:aws:iam::22222222222:role/ReadOnly [profile foo-admin] source_profile = jonsmith role_arn = arn:aws:iam::22222222222:role/Administrator mfa_serial = arn:aws:iam::111111111111:mfa/jonsmith [profile bar-role1] source_profile = jonsmith role_arn = arn:aws:iam::333333333333:role/Role1 mfa_serial = arn:aws:iam::111111111111:mfa/jonsmith [profile bar-role2] source_profile = bar-role1 role_arn = arn:aws:iam::333333333333:role/Role2 mfa_serial = arn:aws:iam::111111111111:mfa/jonsmith ``` Here's what you can expect from aws-vault | Command | Credentials | Cached | MFA | |------------------------------------------|-----------------------------|---------------|-----| | `aws-vault exec jonsmith --no-session` | Long-term credentials | No | No | | `aws-vault exec jonsmith` | session-token | session-token | Yes | | `aws-vault exec foo-readonly` | role | No | No | | `aws-vault exec foo-admin` | session-token + role | session-token | Yes | | `aws-vault exec foo-admin --duration=2h` | role | role | Yes | | `aws-vault exec bar-role2` | session-token + role + role | session-token | Yes | | `aws-vault exec bar-role2 --no-session` | role + role | role | Yes | ## Development The [macOS release builds](https://github.com/99designs/aws-vault/releases) are code-signed to avoid extra prompts in Keychain. You can verify this with: ```shell $ codesign --verify --verbose $(which aws-vault) ``` If you are developing or compiling the aws-vault binary yourself, you can [generate a self-signed certificate](https://support.apple.com/en-au/guide/keychain-access/kyca8916/mac) by accessing Keychain Access > Certificate Assistant > Create Certificate -> Certificate Type: Code Signing. You can then sign your binary with: ```shell $ go build . $ codesign --sign ./aws-vault ``` ## References and Inspiration * https://github.com/pda/aws-keychain * https://docs.aws.amazon.com/IAM/latest/UserGuide/MFAProtectedAPI.html * https://docs.aws.amazon.com/IAM/latest/UserGuide/IAMBestPractices.html#create-iam-users * https://github.com/makethunder/awsudo * https://github.com/AdRoll/hologram * https://github.com/realestate-com-au/credulous * https://github.com/dump247/aws-mock-metadata * https://boto.readthedocs.org/en/latest/boto_config_tut.html ================================================ FILE: USAGE.md ================================================ # Usage - [Usage](#usage) - [Getting Help](#getting-help) - [Typical use-cases for aws-vault](#typical-use-cases-for-aws-vault) - [Use-case 1: aws-vault is the executor and provides the environment](#use-case-1-aws-vault-is-the-executor-and-provides-the-environment) - [Use-case 2: aws-vault is a "master credentials vault" for AWS SDK](#use-case-2-aws-vault-is-a-master-credentials-vault-for-aws-sdk) - [Use-case 3: aws-vault is a "MFA session cache" for AWS SDK](#use-case-3-aws-vault-is-a-mfa-session-cache-for-aws-sdk) - [Use-case 4: aws-vault caches alternative credential sources](#use-case-4-aws-vault-caches-alternative-credential-sources) - [Config](#config) - [AWS config file](#aws-config-file) - [`include_profile`](#include_profile) - [`session_tags` and `transitive_session_tags`](#session_tags-and-transitive_session_tags) - [`source_identity`](#source_identity) - [`mfa_process`](#mfa_process) - [Environment variables](#environment-variables) - [Backends](#backends) - [Keychain](#keychain) - [Managing credentials](#managing-credentials) - [Using multiple profiles](#using-multiple-profiles) - [Listing profiles and credentials](#listing-profiles-and-credentials) - [Removing credentials](#removing-credentials) - [Rotating credentials](#rotating-credentials) - [Managing Sessions](#managing-sessions) - [Executing a command](#executing-a-command) - [Logging into AWS console](#logging-into-aws-console) - [Removing stored sessions](#removing-stored-sessions) - [Using --no-session](#using---no-session) - [Session duration](#session-duration) - [Using `--server`](#using---server) - [`--ec2-server`](#--ec2-server) - [`--ecs-server`](#--ecs-server) - [Temporary credentials limitations with STS, IAM](#temporary-credentials-limitations-with-sts-iam) - [MFA](#mfa) - [Gotchas with MFA config](#gotchas-with-mfa-config) - [Single Sign On (SSO)](#single-sign-on-sso) - [Assuming roles with web identities](#assuming-roles-with-web-identities) - [Using `credential_process`](#using-credential_process) - [Invoking `aws-vault` via `credential_process`](#invoking-aws-vault-via-credential_process) - [Invoking `credential_process` via `aws-vault`](#invoking-credential_process-via-aws-vault) - [Using a Yubikey](#using-a-yubikey) - [Prerequisites](#prerequisites) - [Setup](#setup) - [Usage](#usage-1) - [Shell completion](#shell-completion) - [Desktop apps](#desktop-apps) - [Docker](#docker) ## Getting Help Context-sensitive help is available for every command in `aws-vault`. ```shell # Show general help about aws-vault $ aws-vault --help # Show longer help about all options in aws-vault $ aws-vault --help-long # Show the most detailed information about the exec command $ aws-vault exec --help ``` ## Typical use-cases for aws-vault There are a few different ways aws-vault can be used ### Use-case 1: aws-vault is the executor and provides the environment Use aws-vault exclusively as a command executor, where aws-vault provides the environment and runs a command. ```ini ; master creds added with 'aws-vault add my_profile_master' [profile my_profile_master] [profile my_profile_role] source_profile=my_profile_master role_arn=xxx ``` ```bash aws-vault exec my_profile_master ./my-command # success, uses sts session generated by aws-vault aws-vault exec my_profile_role ./my-command # success, uses role creds generated by aws-vault AWS_PROFILE=my_profile_master ./my-command # Not expected to be functional AWS_PROFILE=my_profile_role ./my-command # Not expected to be functional ``` In this scenario, the profile name and aws config is used exclusively by aws-vault, which provides the environment for the command to run in. This is a very unix-y and 12-factor approach. It's the original and the primary use-case of aws-vault - it's why `aws-vault exec` exists. ### Use-case 2: aws-vault is a "master credentials vault" for AWS SDK aws-vault can be used in `credential_process` in the AWS config to provide master creds. This is more in-line with the AWS SDK way of approaching the problem via `credential_process` and `AWS_PROFILE` ```ini ; master creds added with 'aws-vault add my_profile_master' [profile my_profile_master] credential_process = aws-vault export --format=json --no-session my_profile_master [profile my_profile_role] source_profile=my_profile_master role_arn=xxx ``` ```bash aws-vault exec my_profile_master ./my-command # success (uses master creds) aws-vault exec my_profile_role ./my-command # success (aws-vault role) AWS_PROFILE=my_profile_master ./my-command # success (uses credential_process to get aws-vault master creds) AWS_PROFILE=my_profile_role ./my-command # success (SDK role) ``` ### Use-case 3: aws-vault is a "MFA session cache" for AWS SDK Very similar to Use-case 2, aws-vault can be used to cache STS MFA credentials between profiles. This means you are not forced to re-authenticate with MFA every time you switch profiles ```ini ; master creds added with 'aws-vault add my_profile_master' [profile my_profile_master] mfa_serial=mmm credential_process = aws-vault export --format=json my_profile_master [profile my_profile_role] source_profile=my_profile_master mfa_serial=mmm role_arn=xxx1 [profile my_profile_role2] source_profile=my_profile_master mfa_serial=mmm role_arn=xxx2 ``` ```bash aws-vault exec my_profile_master ./my-command # success (STS session) aws-vault exec my_profile_role ./my-command # success (role) AWS_PROFILE=my_profile_master ./my-command # success (uses credential_process to get aws-vault session) AWS_PROFILE=my_profile_role ./my-command # success (uses aws-vault session + SDK role) ``` ### Use-case 4: aws-vault caches alternative credential sources aws-vault caches credentials from alternative credential sources like `sso_start_url`, `web_identity_token_process`, `credential_process` ```ini [profile my_profile_using_sso] sso_start_url = https://mycompany.awsapps.com/start [profile my_profile_using_process] credential_process = my-custom-creds-cmd ``` ```bash aws-vault exec my_profile_using_sso ./my-command # success, uses aws-vault caching aws-vault exec my_profile_using_process ./my-command # success, uses aws-vault caching AWS_PROFILE=my_profile_using_sso ./my-command # success, no caching AWS_PROFILE=my_profile_using_process ./my-command # success, no caching ``` ## Config ### AWS config file aws-vault uses your `~/.aws/config` to load AWS config. This should work identically to the config specified by the [aws-cli docs](https://docs.aws.amazon.com/cli/latest/topic/config-vars.html). #### `include_profile` (Note: aws-vault v5 calls this `parent_profile`) AWS Vault also recognises an extra config variable, `include_profile`, which is not recognised by the aws-cli. This variable allows a profile to load configuration horizontally from another profile. This is a flexible mechanism for more complex configurations. For example you can use it in "mixin" style where you import a common fragment. In this example, the `root`, `order-dev` and `order-staging-admin` profiles include the `region`, `mfa_serial` and `source_profile` configuration from `common`. ```ini ; The "common" profile here operates as a "config fragment" rather than a profile [profile common] region=eu-west-1 mfa_serial=arn:aws:iam::123456789:mfa/johnsmith source_profile = root [profile root] include_profile = common [profile order-dev] include_profile = common role_arn=arn:aws:iam::123456789:role/developers [profile order-staging-admin] include_profile = common role_arn=arn:aws:iam::123456789:role/administrators ``` Or you could use it in "parent" style where you conflate the fragment with the profile. In this example the `order-dev` and `order-staging-admin` profiles include the `region`, `mfa_serial` and `source_profile` configuration from `root`, while also using the credentials stored against the `root` profile as the source credentials `source_profile = root` ```ini ; The "root" profile here operates as a profile, a config fragment as well as a source_profile [profile root] region=eu-west-1 mfa_serial=arn:aws:iam::123456789:mfa/johnsmith source_profile = root [profile order-dev] include_profile = root role_arn=arn:aws:iam::123456789:role/developers [profile order-staging-admin] include_profile = root role_arn=arn:aws:iam::123456789:role/administrators ``` #### `session_tags` and `transitive_session_tags` It is possible to set [session tags](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_session-tags.html) when `AssumeRole` is used. Two custom config variables could be defined for that: `session_tags` and `transitive_session_tags`. The former defines a comma separated key=value list of tags and the latter is a comma separated list of tags that should be persisted during role chaining: ```ini [profile root] region=eu-west-1 [profile order-dev] source_profile = root role_arn=arn:aws:iam::123456789:role/developers session_tags = key1=value1,key2=value2,key3=value3 transitive_session_tags = key1,key2 ``` #### `source_identity` It is possible to set [source identity](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_control-access_monitor.html) when `AssumeRole` is used. Custom config variable `source_identity` allows you to set the value. ```ini [profile root] region=eu-west-1 [profile order-dev] source_profile = root role_arn=arn:aws:iam::123456789:role/developers source_identity=your_user_name ``` #### `mfa_process` If you have a method to generate an MFA token, you can use it with `aws-vault` by specifying the `mfa_process` option in a profile of your `~/.aws/config` file. The value of `mfa_process` should be a command that will output the MFA token to stdout. For example, to use `pass` to retrieve an MFA token from a password store entry, you could use the following: ```ini [profile foo] mfa_serial=arn:aws:iam::123456789:mfa/johnsmith mfa_process=pass otp my_aws_mfa ``` Or another example using 1Password ```ini [profile foo] mfa_serial=arn:aws:iam::123456789:mfa/johnsmith mfa_process=op item get my_aws_mfa --otp ``` WARNING: Use of this option runs against security best practices. It is recommended that you use a dedicated MFA device. ### Environment variables To configure the default flag values of `aws-vault` and its subcommands: * `AWS_VAULT_BACKEND`: Secret backend to use (see the flag `--backend`) * `AWS_VAULT_KEYCHAIN_NAME`: Name of macOS keychain to use (see the flag `--keychain`) * `AWS_VAULT_PROMPT`: Prompt driver to use (see the flag `--prompt`) * `AWS_VAULT_PASS_PASSWORD_STORE_DIR`: Pass password store directory (see the flag `--pass-dir`) * `AWS_VAULT_PASS_CMD`: Name of the pass executable (see the flag `--pass-cmd`) * `AWS_VAULT_PASS_PREFIX`: Prefix to prepend to the item path stored in pass (see the flag `--pass-prefix`) * `AWS_VAULT_FILE_DIR`: Directory for the "file" password store (see the flag `--file-dir`) * `AWS_VAULT_FILE_PASSPHRASE`: Password for the "file" password store * `AWS_CONFIG_FILE`: The location of the AWS config file To override the AWS config file (used in the `exec`, `login` and `rotate` subcommands): * `AWS_REGION`: The AWS region * `AWS_DEFAULT_REGION`: The AWS region, applied only if `AWS_REGION` isn't set * `AWS_STS_REGIONAL_ENDPOINTS`: STS endpoint resolution logic, must be "regional" or "legacy" * `AWS_MFA_SERIAL`: The identification number of the MFA device to use * `AWS_ROLE_ARN`: Specifies the ARN of an IAM role in the active profile * `AWS_ROLE_SESSION_NAME`: Specifies the name to attach to the role session in the active profile To override session durations (used in `exec` and `login`): * `AWS_SESSION_TOKEN_TTL`: Expiration time for the `GetSessionToken` credentials. Defaults to 1h * `AWS_CHAINED_SESSION_TOKEN_TTL`: Expiration time for the `GetSessionToken` credentials when chaining profiles. Defaults to 8h * `AWS_ASSUME_ROLE_TTL`: Expiration time for the `AssumeRole` credentials. Defaults to 1h * `AWS_FEDERATION_TOKEN_TTL`: Expiration time for the `GetFederationToken` credentials. Defaults to 1h * `AWS_MIN_TTL`: The minimum expiration time allowed for a credential. Defaults to 5m Note that the session durations above expect a unit after the number (e.g. 12h or 43200s). To override or set session tagging (used in `exec`): * `AWS_SESSION_TAGS`: Comma separated key-value list of tags passed with the `AssumeRole` call, overrides `session_tags` profile config variable * `AWS_TRANSITIVE_TAGS`: Comma separated list of transitive tags passed with the `AssumeRole` call, overrides `transitive_session_tags` profile config variable To override or set the source identity (used in `exec` and `login`): * `AWS_SOURCE_IDENTITY`: Specifies the source identity for assumed role sessions ## Backends You can choose among different pluggable secret storage backends. You can set the backend using the `--backend` flag or the `AWS_VAULT_BACKEND` environment variable. Run `aws-vault --help` to see what your `--backend` flag supports. ### Keychain If you're looking to configure the amount of time between having to enter your Keychain password for each usage of a particular profile, you can do so through Keychain: 1. Open "Keychain Access" 2. Open the aws-vault keychain. If you do not have "aws-vault" in the sidebar of the Keychain app, then you can do "File -> Add Keychain" and select the `aws-vault.keychain-db`. This is typically created in `Users/{USER}/Library/Keychains`. 3. Right click on aws-vault keychain, and select "Change Settings for Keychain 'aws-vault" 4. Update "Lock after X minutes of inactivity" to your desired value. 5. Hit save. ![keychain-image](https://imgur.com/ARkr5Ba.png) ## Managing credentials ### Using multiple profiles In addition to using IAM roles to assume temporary privileges as described in [README.md](./USAGE.md), aws-vault can also be used with multiple profiles directly. This allows you to use multiple separate AWS accounts that have no relation to one another, such as work and home. ```shell # Store AWS credentials for the "home" profile $ aws-vault add home Enter Access Key Id: ABDCDEFDASDASF Enter Secret Key: % # Execute a command using temporary credentials $ aws-vault exec home -- aws s3 ls bucket_1 bucket_2 # store credentials for the "work" profile $ aws-vault add work Enter Access Key Id: ABDCDEFDASDASF Enter Secret Key: % # Execute a command using temporary credentials $ aws-vault exec work -- aws s3 ls another_bucket ``` Here is an example `~/.aws/config` file, to help show the configuration. It defines two AWS accounts: "home" and "work", both of which use MFA. The work account provides two roles, allowing the user to become either profile. ```ini [default] region = us-east-1 [profile home] mfa_serial = arn:aws:iam::111111111111:mfa/home-account [profile work] mfa_serial = arn:aws:iam::111111111111:mfa/work-account role_arn = arn:aws:iam::111111111111:role/ReadOnly [profile work-admin] role_arn = arn:aws:iam::111111111111:role/Administrator source_profile = work ``` ### Listing profiles and credentials You can use the `aws-vault list` command to list out the defined profiles, and any session associated with them. ```shell $ aws-vault list Profile Credentials Sessions ======= =========== ======== home home work work 1525456570 work-read-only work work-admin work ``` ### Removing credentials The `aws-vault remove` command can be used to remove credentials. It works similarly to the `aws-vault add` command. ```shell # Remove AWS credentials for the "work" profile $ aws-vault remove work Delete credentials for profile "work"? (y|N) y Deleted credentials. ``` ### Rotating credentials Regularly rotating your access keys is a critical part of credential management. You can do this with the `aws-vault rotate ` command as often as you like. [Restrictions on IAM access](#temporary-credentials-limitations-with-sts-iam) using `GetSessionToken` means you will need to have [configured MFA](#mfa) or use the `--no-session` flag. The minimal IAM policy required to rotate your own credentials is: ```json { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "iam:CreateAccessKey", "iam:DeleteAccessKey", "iam:GetUser" ], "Resource": [ "arn:aws:iam::*:user/${aws:username}" ] } ] } ``` ## Managing Sessions ### Executing a command Running `aws-vault exec` will run a command with AWS credentials. When using exec, you may find it useful to use the builtin `--` feature in bash, zsh and other POSIX shells. For example ```shell aws-vault exec myprofile -- aws s3 ls ``` Using `--` signifies the end of the `aws-vault` options, and allows the shell autocomplete to kick in and offer autocompletions for the proceeding command. If you use `exec` without specifying a command, AWS Vault will create a new interactive subshell. Note that when creating an interactive subshell, bash, zsh and other POSIX shells will execute the `~/.bashrc` or `~/.zshrc` file. If you have local variables, functions or aliases (for example your `PS1` prompt), ensure that they are defined in the rc file so they get executed when the subshell begins. ### Logging into AWS console You can use the `aws-vault login` command to open a browser window and login to AWS Console for a given account: ```shell $ aws-vault login work ``` If you have credentials already available in your environment, aws-vault will use these credentials to sign you in to the AWS console. ```shell $ export AWS_ACCESS_KEY_ID=%%% $ export AWS_SECRET_ACCESS_KEY=%%% $ export AWS_SESSION_TOKEN=%%% $ aws-vault login ``` ### Removing stored sessions If you want to remove sessions managed by `aws-vault` before they expire, you can do this with `aws-vault clear` command. You can also specify a profile to remove sessions for this profile only. ```shell aws-vault clear [profile] ``` ### Using --no-session AWS Vault will typically create temporary credentials using a combination of `GetSessionToken` and `AssumeRole`, depending on the config. The `GetSessionToken` call is made with MFA if available, and the resulting session is cached in the backend vault and can be used to assume roles from different profiles without further MFA prompts. If you wish to skip the `GetSessionToken` call, you can use the `--no-session` flag. However, consider that if you use `--no-session` with a profile using IAM credentials and NO `role_arn`, then your IAM credentials will be directly exposed to the terminal/application you are running. This is the opposite of what you are normally trying to achieve by using AWS Vault. You can easily witness that by doing ```shell aws-vault exec -- env | grep AWS ``` You'll see an `AWS_ACCESS_KEY_ID` of the form `ASIAxxxxxx` which is a temporary one. Doing ```shell aws-vault exec --no-session -- env | grep AWS ``` You'll see your IAM user `AWS_ACCESS_KEY_ID` of the form `AKIAxxxxx` directly exposed, as well as the corresponding `AWS_SECRET_KEY_ID`. ### Session duration If you try to [assume a role](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html) from a temporary session or another role, AWS considers that as [role chaining](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#iam-term-role-chaining) and limits your ability to assume the target role to 1h. Trying to use a duration longer than 1h may result in an error: ``` aws-vault: error: Failed to get credentials for default: ValidationError: The requested DurationSeconds exceeds the MaxSessionDuration set for this role. status code: 400, request id: aa58fa50-4a5e-11e9-9566-293ea5c350ee ``` For that reason, AWS Vault will not use `GetSessionToken` if `--duration` or the role's `duration_seconds` is longer than 1h. ### Using `--server` There may be scenarios where you'd like to assume a role for a long length of time, or perhaps when using a tool where using temporary sessions on demand is preferable. For example, when using a tool like [Terraform](https://www.terraform.io/), you need to have AWS credentials available to the application for the entire duration of the infrastructure change. AWS Vault can run a background server to imitate the metadata endpoint that you would have on an EC2 or ECS instance. When your application uses the AWS SDK to locate credentials, it will automatically connect to this server that will issue a new set of temporary credentials (using the same profile as the one the server was started with). This server will continue to generate temporary credentials any time the application requests it. #### `--ec2-server` This approach has the major security drawback that while this `aws-vault` server runs, any application wanting to connect to AWS will be able to do so, using the profile the server was started with. Thanks to `aws-vault`, the credentials are not exposed, but the ability to use them to connect to AWS is! To use `--ec2-server`, AWS Vault needs root/administrator privileges in order to bind to the privileged port. AWS Vault runs a minimal proxy as the root user, proxying through to the real aws-vault instance. #### `--ecs-server` The ECS Credential provider binds to a random, ephemeral port and requires an authorization token, which offers the following advantages over the EC2 Metadata provider: 1. Does not require root/administrator privileges 2. Allows multiple providers simultaneously for discrete processes 3. Mitigates the security issues that accompany the EC2 Metadata Service because the address is not well-known and the authorization token is only exposed to the subprocess via environment variables However, this will only work with the AWS SDKs [that support `AWS_CONTAINER_CREDENTIALS_FULL_URI`](https://docs.aws.amazon.com/sdkref/latest/guide/feature-container-credentials.html). The ECS server also responds to requests on `/role-arn/YOUR_ROLE_ARN` with the role credentials, making it usable with `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` when combined with a reverse proxy (see the Docker section below). ### Temporary credentials limitations with STS, IAM When using temporary credentials you are restricted from using some STS and IAM APIs (see [here](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_request.html#stsapi_comparison)). The restriction is enforced with `InvalidClientTokenId` error response. ```shell $ aws-vault exec -- aws iam get-user An error occurred (InvalidClientTokenId) when calling the GetUser operation: The security token included in the request is invalid ``` For restricted IAM operation you can add MFA to the IAM User and update your ~/.aws/config file with [MFA configuration](#mfa). Alternately you may avoid the temporary session entirely by using `--no-session`. ## MFA To enable MFA for a profile, specify the `mfa_serial` in `~/.aws/config`. You can retrieve the MFA's serial (ARN) in the web console, under IAM > Users > `` > Security Configuration. If you have an account with an MFA associated, but you don't provide the ARN, you are unable to call IAM services, even if you have the correct permissions to do so. AWS Vault will attempt to re-use a `GetSessionToken` between profiles that share a common `mfa_serial`. In the following example, aws-vault will cache and re-use sessions between role1 and role2. This means you don't have to continually enter MFA codes if the MFA method is the same. ```ini [profile tom] mfa_serial = arn:aws:iam::111111111111:mfa/tom [profile role1] source_profile = tom role_arn = arn:aws:iam::22222222222:role/role1 mfa_serial = arn:aws:iam::111111111111:mfa/tom [profile role2] source_profile = tom role_arn = arn:aws:iam::33333333333:role/role2 mfa_serial = arn:aws:iam::111111111111:mfa/tom ``` Be sure to specify the `mfa_serial` for the source profile (in the above example `tom`) so that aws-vault can match the common `mfa_serial`. You can also set the `mfa_serial` with the environment variable `AWS_MFA_SERIAL`. ### Gotchas with MFA config aws-vault v4 would inherit the `mfa_serial` from the `source_profile`. While this was intuitive for some, it made certain configurations difficult to express and is different behaviour to the aws-cli. aws-vault v5 corrected this problem. The `mfa_serial` must be specified for _each_ profile, the same way the aws-cli interprets the configuration. If you wish to avoid specifying the `mfa_serial` for each profile, consider using the `mfa_serial` in the `[default]` section, the `AWS_MFA_SERIAL` environment variable, or [`include_profile`](#include_profile). For example: ```ini [profile jon] mfa_serial = arn:aws:iam::111111111111:mfa/jon source_profile=jon [profile role1] role_arn = arn:aws:iam::22222222222:role/role1 include_profile = jon [profile role2] role_arn = arn:aws:iam::33333333333:role/role2 include_profile = jon ``` ## Single Sign On (SSO) _AWS IAM Identity Center provides single sign on, and was previously known as AWS SSO._ If your organization uses [AWS IAM Identity Center](https://aws.amazon.com/iam/identity-center/) for single sign on, AWS Vault provides a method for using the credential information defined by [`aws sso`](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html) from v2 of the AWS CLI. The configuration options are as follows: * `sso_session` Name of the `[sso-session]` section in the same file with the common options, or: * `sso_start_url` The URL that points to the organization's AWS IAM Identity Center user portal. * `sso_region` The AWS Region that contains the AWS IAM Identity Center user portal host. This is separate from, and can be a different region than the default CLI region parameter. * `sso_account_id` The AWS account ID that contains the IAM role that you want to use with this profile. * `sso_role_name` The name of the Identity Center Permission Group that defines the user's permissions when using this profile. Here is an example configuration using AWS IAM Identity Center for single sign on. ```ini [profile Administrator-123456789012] sso_start_url=https://aws-sso-portal.awsapps.com/start sso_region=eu-west-1 sso_account_id=123456789012 sso_role_name=Administrator ``` ## Assuming roles with web identities AWS supports assuming roles using [web identity federation and OpenID Connect](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-role.html#cli-configure-role-oidc), including login using Amazon, Google, Facebook or any other OpenID Connect server. The configuration options are as follows: * `web_identity_token_file` A file that contains an OpenID Connect identity token. The token is loaded and passed as the `WebIdentityToken` argument of the `AssumeRoleWithWebIdentity` operation. * `web_identity_token_process` A command that executes to generate an OpenID Connect identity token. The token written to the command's standard out is passed as the `WebIdentityToken` argument of the `AssumeRoleWithWebIdentity` operation. This is a custom option supported only by `aws-vault`. An example configuration using a static token: ```ini [profile role1] role_arn = arn:aws:iam::22222222222:role/role1 web_identity_token_file = /path/to/token.txt ``` An example using a token generated by an external command: ```ini [profile role2] role_arn = arn:aws:iam::33333333333:role/role2 web_identity_token_process = oidccli raw ``` ## Using `credential_process` The [AWS CLI config](https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes) supports sourcing credentials directly from an external process, using `credential_process`. ### Invoking `aws-vault` via `credential_process` ```ini [profile home] credential_process = aws-vault export --format=json home ``` If `mfa_serial` is set, please define the prompt driver (for example `osascript` for macOS), else the prompt will not show up. ```ini [profile work] mfa_serial = arn:aws:iam::123456789012:mfa/jonsmith credential_process = aws-vault --prompt=osascript export --format=json work ``` Note that `credential_process` is designed for retrieving master credentials, while aws-vault outputs STS credentials by default. If a role is present, the AWS CLI/SDK uses the master credentials from the `credential_process` to generate STS credentials itself. So depending on your use-case, it might make sense for aws-vault to output master credentials by using a profile without a role and the `--no-session` argument. For example: ```ini [profile jon] credential_process = aws-vault export --no-session --format=json jon [profile work] mfa_serial = arn:aws:iam::123456789012:mfa/jonsmith role_arn = arn:aws:iam::33333333333:role/role2 source_profile = jon ``` If you're using `credential_process` in your config to invoke `aws-vault exec` you should not use `aws-vault exec` on the command line to execute commands directly - the AWS SDK executes `aws-vault` for you. ### Invoking `credential_process` via `aws-vault` When executing a profile via `aws-vault exec` that has `credential_process` set, `aws-vault` will execute the specified command to obtain a credential. This will allow `aws-vault` to cache credentials obtained via `credential_process`. ## Using a Yubikey Yubikeys can be used with AWS Vault via Yubikey's OATH-TOTP support. TOTP is necessary because FIDO-U2F is unsupported on the AWS CLI and SDKs; even though it's supported on the AWS Console. ### Prerequisites 1. [A Yubikey that supports OATH-TOTP](https://support.yubico.com/support/solutions/articles/15000006419-using-your-yubikey-with-authenticator-codes) 2. `ykman`, the [YubiKey Manager CLI](https://github.com/Yubico/yubikey-manager) tool. You can verify these prerequisites by running `ykman info` and checking `OATH` is enabled. ### Setup 1. Log into the AWS web console with your IAM user credentials, and navigate to _My Security Credentials_ 2. Under _Multi-factor authentication (MFA)_, click `Manage MFA device` and add a Virtual MFA device 3. Instead of showing the QR code, click on `Show secret key` and copy the key. 4. On a command line, run: ```shell ykman oath accounts add -t arn:aws:iam::${ACCOUNT_ID}:mfa/${MFA_DEVICE_NAME} ``` replacing `${ACCOUNT_ID}` with your AWS account ID and `${MFA_DEVICE_NAME}` with the name you gave to the MFA device. It will prompt you for a base32 text and you can input the key from step 3. Notice the above command uses `-t` which requires you to touch your YubiKey to generate authentication codes. 5. Now you have to enter two consecutive MFA codes into the AWS website to assign your key to your AWS login. Just run `ykman oath accounts code arn:aws:iam::${ACCOUNT_ID}:mfa/${MFA_DEVICE_NAME}` to get an authentication code. The codes are re-generated every 30 seconds, so you have to run this command twice with about 30 seconds in between to get two distinct codes. Enter the two codes in the AWS form and click `Assign MFA`. A script can be found at [contrib/scripts/aws-iam-create-yubikey-mfa.sh](contrib/scripts/aws-iam-create-yubikey-mfa.sh) to automate the process. Note that this script requires your `$MFA_DEVICE_NAME` to be your IAM username as the `aws iam enable-mfa-device` command in the CLI does not yet offer specifying the name. When only one MFA device was allowed per IAM user, the `$MFA_DEVICE_NAME` would always be your IAM username. In case of TOTP being out of sync (AWS API doesn't accept MFA codes), a yubikey resync script can be found at [contrib/scripts/aws-iam-resync-yubikey-mfa.sh](contrib/scripts/aws-iam-resync-yubikey-mfa.sh) to resync the yubikey with AWS. As above, this script requires your `$MFA_DEVICE_NAME` to be your IAM username. Note that each `[profile ]` in your `~/.aws/config` only supports one `mfa_serial` entry. If you wish to use multiple Yubikeys, or mix and match MFA devices, you'll need to add a profile for each method. ### Usage Using the `ykman` prompt driver, aws-vault will execute `ykman` to generate tokens for any profile in your `.aws/config` using an `mfa_device`. ```shell aws-vault exec --prompt ykman ${AWS_VAULT_PROFILE_USING_MFA} -- aws s3 ls ``` An alternative to manually supplying the prompt driver as a CLI argument to `aws-vault` is setting the [`mfa_process`](#mfa_process) parameter in your `.aws/config` for the profiles that should use a YubiKey to generate tokens. Example: (Note: Remember to swap out the name of the OATH account used in `mfa_process` below with the name you gave it during [YubiKey setup](#setup)) ```ini [profile jon] mfa_serial = arn:aws:iam::123456789012:mfa/jonsmith mfa_process = ykman oath accounts code --single arn:aws:iam::123456789012:mfa/jonsmith ``` Further config: - `AWS_VAULT_PROMPT=ykman`: to avoid specifying `--prompt` each time - `YKMAN_OATH_CREDENTIAL_NAME`: to use an alternative ykman credential - `AWS_VAULT_YKMAN_VERSION`: to set the major version of the ykman cli being used. Defaults to "4" - `YKMAN_OATH_DEVICE_SERIAL`: to set the device serial of a specific Yubikey if you have multiple Yubikeys plugged into your computer. ## Shell completion You can generate shell completions for - bash: `eval "$(curl -fs https://raw.githubusercontent.com/99designs/aws-vault/master/contrib/completions/bash/aws-vault.bash)"` - zsh: `eval "$(curl -fs https://raw.githubusercontent.com/99designs/aws-vault/master/contrib/completions/zsh/aws-vault.zsh)"` - fish: `eval "$(curl -fs https://raw.githubusercontent.com/99designs/aws-vault/master/contrib/completions/fish/aws-vault.fish)"` Find the completion scripts at [contrib/completions](contrib/completions). ## Desktop apps You can use desktop apps with temporary credentials from AWS Vault too! For example on macOS run ```shell aws-vault exec --server jonsmith -- open -W -a Lens ``` * `--server`: starts the background server so that temporary credentials get refreshed automatically * `open -W -a Lens`: run the applications, waiting for it to exit ## Docker It's possible for Docker containers to retrieve credentials from aws-vault running on the host. ![Screen Shot 2022-03-03 at 12 16 15 pm](https://user-images.githubusercontent.com/980499/156477380-423f4eb9-f10e-4568-afa8-7fa525a1f3a3.png) The ECS server responds to requests on `/role-arn/YOUR_ROLE_ARN` with the role credentials, making it usable with the `AWS_CONTAINER_CREDENTIALS_FULL_URI` or `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` environment variables. These environment variables are used by the AWS SDKs as part of the [default credential provider chain](https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/credentials.html#credentials-default). In particular, this is designed to allow aws-vault to run on your local host while docker images access role credentials dynamically. This is achieved via a reverse-proxy container (started with `aws-vault exec --ecs-server --lazy PROFILE -- docker-compose up ...`) using the default ECS IP address `169.254.170.2`. Docker containers no longer need AWS keys at all - instead they can specify the role they want to assume with `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI`. This use-case is similar to the goal of [amazon-ecs-local-container-endpoints](https://github.com/awslabs/amazon-ecs-local-container-endpoints/blob/mainline/docs/features.md#vend-credentials-to-containers), however the difference here is that the long-lived AWS credentials are getting sourced from your keychain via aws-vault. To test it out: 1. Add a base role to your `~/.aws/config` (replacing with valid values) ```ini [profile base-role] source_profile=myprofile role_arn=arn:aws:iam::222222222222:role/aws-vault-test mfa_serial=arn:aws:iam::222222222222:mfa/ ``` 2. Start a reverse proxy: ```shell $ cd contrib/_aws-vault-proxy $ aws-vault --debug exec --server --lazy base-role -- docker compose up --build aws-vault-proxy ``` 3. In a new terminal, assume a new role ```shell $ export AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/role-arn/arn:aws:iam::222222222222:role/another-role-that-can-be-assumed-by-base-role $ docker-compose run testapp testapp $ aws sts get-caller-identity ``` ================================================ FILE: bin/create-dmg ================================================ #!/bin/bash # # create-dmg packages the aws-vault CLI binary for macOS # using Apple's signing and notorizing process # # # As per https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow # AC_PASSWORD can be set in your keychain with: # xcrun notarytool store-credentials "AC_PASSWORD" # --apple-id "AC_USERNAME" # --team-id # --password # set -euo pipefail BIN_PATH="$1" DMG_PATH="${2:-$1.dmg}" CERT_ID="${CERT_ID:-"Developer ID Application: 99designs Inc (NRM9HVJ62Z)"}" KEYCHAIN_PROFILE="${KEYCHAIN_PROFILE:-AC_PASSWORD}" if [[ -f "$DMG_PATH" ]] ; then echo "File '$DMG_PATH' already exists. Remove it and try again" exit 1 fi tmpdir="$(mktemp -d)" trap "rm -rf $tmpdir" EXIT cp -a $BIN_PATH $tmpdir/aws-vault src_path="$tmpdir/aws-vault" echo "Signing binary" codesign --options runtime --timestamp --sign "$CERT_ID" "$src_path" echo "Creating dmg" hdiutil create -quiet -srcfolder "$src_path" "$DMG_PATH" echo "Signing dmg" codesign --timestamp --sign "$CERT_ID" "$DMG_PATH" echo "Submitting notorization request" xcrun notarytool submit $DMG_PATH --keychain-profile "$KEYCHAIN_PROFILE" --wait echo "Stapling" xcrun stapler staple -q $DMG_PATH ================================================ FILE: cli/add.go ================================================ package cli import ( "fmt" "log" "os" "github.com/99designs/aws-vault/v7/prompt" "github.com/99designs/aws-vault/v7/vault" "github.com/99designs/keyring" "github.com/alecthomas/kingpin/v2" "github.com/aws/aws-sdk-go-v2/aws" ) type AddCommandInput struct { ProfileName string FromEnv bool AddConfig bool } func ConfigureAddCommand(app *kingpin.Application, a *AwsVault) { input := AddCommandInput{} cmd := app.Command("add", "Add credentials to the secure keystore.") cmd.Arg("profile", "Name of the profile"). Required(). StringVar(&input.ProfileName) cmd.Flag("env", "Read the credentials from the environment (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)"). BoolVar(&input.FromEnv) cmd.Flag("add-config", "Add a profile to ~/.aws/config if one doesn't exist"). Default("true"). BoolVar(&input.AddConfig) cmd.Action(func(c *kingpin.ParseContext) error { keyring, err := a.Keyring() if err != nil { return err } awsConfigFile, err := a.AwsConfigFile() if err != nil { return err } err = AddCommand(input, keyring, awsConfigFile) app.FatalIfError(err, "add") return nil }) } func AddCommand(input AddCommandInput, keyring keyring.Keyring, awsConfigFile *vault.ConfigFile) error { var accessKeyID, secretKey string p, _ := awsConfigFile.ProfileSection(input.ProfileName) if p.SourceProfile != "" { return fmt.Errorf("Your profile has a source_profile of %s, adding credentials to %s won't have any effect", p.SourceProfile, input.ProfileName) } if input.FromEnv { if accessKeyID = os.Getenv("AWS_ACCESS_KEY_ID"); accessKeyID == "" { return fmt.Errorf("Missing value for AWS_ACCESS_KEY_ID") } if secretKey = os.Getenv("AWS_SECRET_ACCESS_KEY"); secretKey == "" { return fmt.Errorf("Missing value for AWS_SECRET_ACCESS_KEY") } } else { var err error if accessKeyID, err = prompt.TerminalPrompt("Enter Access Key ID: "); err != nil { return err } if secretKey, err = prompt.TerminalSecretPrompt("Enter Secret Access Key: "); err != nil { return err } } creds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey} ckr := &vault.CredentialKeyring{Keyring: keyring} if err := ckr.Set(input.ProfileName, creds); err != nil { return err } fmt.Printf("Added credentials to profile %q in vault\n", input.ProfileName) sk := &vault.SessionKeyring{Keyring: keyring} if n, _ := sk.RemoveForProfile(input.ProfileName); n > 0 { fmt.Printf("Deleted %d existing sessions.\n", n) } if _, hasProfile := awsConfigFile.ProfileSection(input.ProfileName); !hasProfile { if input.AddConfig { newProfileSection := vault.ProfileSection{ Name: input.ProfileName, } log.Printf("Adding profile %s to config at %s", input.ProfileName, awsConfigFile.Path) if err := awsConfigFile.Add(newProfileSection); err != nil { return fmt.Errorf("Error adding profile: %w", err) } } } return nil } ================================================ FILE: cli/add_test.go ================================================ package cli import ( "log" "os" "github.com/alecthomas/kingpin/v2" ) func ExampleAddCommand() { f, err := os.CreateTemp("", "aws-config") if err != nil { log.Fatal(err) } defer os.Remove(f.Name()) os.Setenv("AWS_CONFIG_FILE", f.Name()) os.Setenv("AWS_ACCESS_KEY_ID", "llamas") os.Setenv("AWS_SECRET_ACCESS_KEY", "rock") os.Setenv("AWS_VAULT_BACKEND", "file") os.Setenv("AWS_VAULT_FILE_PASSPHRASE", "password") defer os.Unsetenv("AWS_ACCESS_KEY_ID") defer os.Unsetenv("AWS_SECRET_ACCESS_KEY") defer os.Unsetenv("AWS_VAULT_BACKEND") defer os.Unsetenv("AWS_VAULT_FILE_PASSPHRASE") app := kingpin.New(`aws-vault`, ``) ConfigureAddCommand(app, ConfigureGlobals(app)) kingpin.MustParse(app.Parse([]string{"add", "--debug", "--env", "foo"})) // Output: // Added credentials to profile "foo" in vault } ================================================ FILE: cli/clear.go ================================================ package cli import ( "fmt" "github.com/99designs/aws-vault/v7/vault" "github.com/99designs/keyring" "github.com/alecthomas/kingpin/v2" ) type ClearCommandInput struct { ProfileName string } func ConfigureClearCommand(app *kingpin.Application, a *AwsVault) { input := ClearCommandInput{} cmd := app.Command("clear", "Clear temporary credentials from the secure keystore.") cmd.Arg("profile", "Name of the profile"). HintAction(a.MustGetProfileNames). StringVar(&input.ProfileName) cmd.Action(func(c *kingpin.ParseContext) (err error) { keyring, err := a.Keyring() if err != nil { return err } awsConfigFile, err := a.AwsConfigFile() if err != nil { return err } err = ClearCommand(input, awsConfigFile, keyring) app.FatalIfError(err, "clear") return nil }) } func ClearCommand(input ClearCommandInput, awsConfigFile *vault.ConfigFile, keyring keyring.Keyring) error { sessions := &vault.SessionKeyring{Keyring: keyring} oidcTokens := &vault.OIDCTokenKeyring{Keyring: keyring} var oldSessionsRemoved, numSessionsRemoved, numTokensRemoved int var err error if input.ProfileName == "" { oldSessionsRemoved, err = sessions.RemoveOldSessions() if err != nil { return err } numSessionsRemoved, err = sessions.RemoveAll() if err != nil { return err } numTokensRemoved, err = oidcTokens.RemoveAll() if err != nil { return err } } else { numSessionsRemoved, err = sessions.RemoveForProfile(input.ProfileName) if err != nil { return err } if profileSection, ok := awsConfigFile.ProfileSection(input.ProfileName); ok { if exists, _ := oidcTokens.Has(profileSection.SSOStartURL); exists { err = oidcTokens.Remove(profileSection.SSOStartURL) if err != nil { return err } numTokensRemoved = 1 } } } fmt.Printf("Cleared %d sessions.\n", oldSessionsRemoved+numSessionsRemoved+numTokensRemoved) return nil } ================================================ FILE: cli/exec.go ================================================ package cli import ( "context" "fmt" "log" "net/http" "os" osexec "os/exec" "os/signal" "runtime" "strings" "syscall" "time" "github.com/99designs/aws-vault/v7/iso8601" "github.com/99designs/aws-vault/v7/server" "github.com/99designs/aws-vault/v7/vault" "github.com/99designs/keyring" "github.com/alecthomas/kingpin/v2" "github.com/aws/aws-sdk-go-v2/aws" ) type ExecCommandInput struct { ProfileName string Command string Args []string StartEc2Server bool StartEcsServer bool Lazy bool JSONDeprecated bool Config vault.ProfileConfig SessionDuration time.Duration NoSession bool UseStdout bool ShowHelpMessages bool } func (input ExecCommandInput) validate() error { if input.StartEc2Server && input.StartEcsServer { return fmt.Errorf("Can't use --ec2-server with --ecs-server") } if input.StartEc2Server && input.JSONDeprecated { return fmt.Errorf("Can't use --ec2-server with --json") } if input.StartEc2Server && input.NoSession { return fmt.Errorf("Can't use --ec2-server with --no-session") } if input.StartEcsServer && input.JSONDeprecated { return fmt.Errorf("Can't use --ecs-server with --json") } if input.StartEcsServer && input.NoSession { return fmt.Errorf("Can't use --ecs-server with --no-session") } if input.StartEcsServer && input.Config.MfaPromptMethod == "terminal" { return fmt.Errorf("Can't use --prompt=terminal with --ecs-server. Specify a different prompt driver") } if input.StartEc2Server && input.Config.MfaPromptMethod == "terminal" { return fmt.Errorf("Can't use --prompt=terminal with --ec2-server. Specify a different prompt driver") } return nil } func hasBackgroundServer(input ExecCommandInput) bool { return input.StartEcsServer || input.StartEc2Server } func ConfigureExecCommand(app *kingpin.Application, a *AwsVault) { input := ExecCommandInput{} cmd := app.Command("exec", "Execute a command with AWS credentials.") cmd.Flag("duration", "Duration of the temporary or assume-role session. Defaults to 1h"). Short('d'). DurationVar(&input.SessionDuration) cmd.Flag("no-session", "Skip creating STS session with GetSessionToken"). Short('n'). BoolVar(&input.NoSession) cmd.Flag("region", "The AWS region"). StringVar(&input.Config.Region) cmd.Flag("mfa-token", "The MFA token to use"). Short('t'). StringVar(&input.Config.MfaToken) cmd.Flag("json", "Output credentials in JSON that can be used by credential_process"). Short('j'). Hidden(). BoolVar(&input.JSONDeprecated) cmd.Flag("server", "Alias for --ecs-server"). Short('s'). BoolVar(&input.StartEcsServer) cmd.Flag("ec2-server", "Run a EC2 metadata server in the background for credentials"). BoolVar(&input.StartEc2Server) cmd.Flag("ecs-server", "Run a ECS credential server in the background for credentials (the SDK or app must support AWS_CONTAINER_CREDENTIALS_FULL_URI)"). BoolVar(&input.StartEcsServer) cmd.Flag("lazy", "When using --ecs-server, lazily fetch credentials"). BoolVar(&input.Lazy) cmd.Flag("stdout", "Print the SSO link to the terminal without automatically opening the browser"). BoolVar(&input.UseStdout) cmd.Arg("profile", "Name of the profile"). Required(). HintAction(a.MustGetProfileNames). StringVar(&input.ProfileName) cmd.Arg("cmd", "Command to execute, defaults to $SHELL"). StringVar(&input.Command) cmd.Arg("args", "Command arguments"). StringsVar(&input.Args) cmd.Action(func(c *kingpin.ParseContext) (err error) { input.Config.MfaPromptMethod = a.PromptDriver(hasBackgroundServer(input)) input.Config.NonChainedGetSessionTokenDuration = input.SessionDuration input.Config.AssumeRoleDuration = input.SessionDuration input.Config.SSOUseStdout = input.UseStdout input.ShowHelpMessages = !a.Debug && input.Command == "" && isATerminal() && os.Getenv("AWS_VAULT_DISABLE_HELP_MESSAGE") != "1" f, err := a.AwsConfigFile() if err != nil { return err } keyring, err := a.Keyring() if err != nil { return err } exitcode := 0 if input.JSONDeprecated { exportCommandInput := ExportCommandInput{ ProfileName: input.ProfileName, Format: "json", Config: input.Config, SessionDuration: input.SessionDuration, NoSession: input.NoSession, } err = ExportCommand(exportCommandInput, f, keyring) } else { exitcode, err = ExecCommand(input, f, keyring) } app.FatalIfError(err, "exec") // override exit code if not err os.Exit(exitcode) return nil }) } func ExecCommand(input ExecCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) (exitcode int, err error) { if os.Getenv("AWS_VAULT") != "" { return 0, fmt.Errorf("running in an existing aws-vault subshell; 'exit' from the subshell or unset AWS_VAULT to force") } if err := input.validate(); err != nil { return 0, err } config, err := vault.NewConfigLoader(input.Config, f, input.ProfileName).GetProfileConfig(input.ProfileName) if err != nil { return 0, fmt.Errorf("Error loading config: %w", err) } credsProvider, err := vault.NewTempCredentialsProvider(config, &vault.CredentialKeyring{Keyring: keyring}, input.NoSession, false) if err != nil { return 0, fmt.Errorf("Error getting temporary credentials: %w", err) } subshellHelp := "" if input.Command == "" { input.Command = getDefaultShell() subshellHelp = fmt.Sprintf("Starting subshell %s, use `exit` to exit the subshell", input.Command) } cmdEnv := createEnv(input.ProfileName, config.Region) if input.StartEc2Server { if server.IsProxyRunning() { return 0, fmt.Errorf("Another process is already bound to 169.254.169.254:80") } printHelpMessage("Warning: Starting a local EC2 credential server on 169.254.169.254:80; AWS credentials will be accessible to any process while it is running", input.ShowHelpMessages) if err := server.StartEc2EndpointProxyServerProcess(); err != nil { return 0, err } defer server.StopProxy() if err = server.StartEc2CredentialsServer(context.TODO(), credsProvider, config.Region); err != nil { return 0, fmt.Errorf("Failed to start credential server: %w", err) } printHelpMessage(subshellHelp, input.ShowHelpMessages) } else if input.StartEcsServer { printHelpMessage("Starting a local ECS credential server; your app's AWS sdk must support AWS_CONTAINER_CREDENTIALS_FULL_URI.", input.ShowHelpMessages) if err = startEcsServerAndSetEnv(credsProvider, config, input.Lazy, &cmdEnv); err != nil { return 0, err } printHelpMessage(subshellHelp, input.ShowHelpMessages) } else { if err = addCredsToEnv(credsProvider, input.ProfileName, &cmdEnv); err != nil { return 0, err } printHelpMessage(subshellHelp, input.ShowHelpMessages) err = doExecSyscall(input.Command, input.Args, cmdEnv) // will not return if exec syscall succeeds if err != nil { log.Println("Error doing execve syscall:", err.Error()) log.Println("Falling back to running a subprocess") } } return runSubProcess(input.Command, input.Args, cmdEnv) } func printHelpMessage(helpMsg string, showHelpMessages bool) { if helpMsg != "" { if showHelpMessages { printToStderr(helpMsg) } else { log.Println(helpMsg) } } } func printToStderr(helpMsg string) { fmt.Fprint(os.Stderr, helpMsg, "\n") } func createEnv(profileName string, region string) environ { env := environ(os.Environ()) env.Unset("AWS_ACCESS_KEY_ID") env.Unset("AWS_SECRET_ACCESS_KEY") env.Unset("AWS_SESSION_TOKEN") env.Unset("AWS_SECURITY_TOKEN") env.Unset("AWS_CREDENTIAL_FILE") env.Unset("AWS_DEFAULT_PROFILE") env.Unset("AWS_PROFILE") env.Unset("AWS_SDK_LOAD_CONFIG") env.Set("AWS_VAULT", profileName) if region != "" { // AWS_REGION is used by most SDKs. But boto3 (Python SDK) uses AWS_DEFAULT_REGION // See https://docs.aws.amazon.com/sdkref/latest/guide/feature-region.html log.Printf("Setting subprocess env: AWS_REGION=%s, AWS_DEFAULT_REGION=%s", region, region) env.Set("AWS_REGION", region) env.Set("AWS_DEFAULT_REGION", region) } return env } func startEcsServerAndSetEnv(credsProvider aws.CredentialsProvider, config *vault.ProfileConfig, lazy bool, cmdEnv *environ) error { ecsServer, err := server.NewEcsServer(context.TODO(), credsProvider, config, "", 0, lazy) if err != nil { return err } go func() { err = ecsServer.Serve() if err != http.ErrServerClosed { // ErrServerClosed is a graceful close log.Fatalf("ecs server: %s", err.Error()) } }() log.Println("Setting subprocess env AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN") cmdEnv.Set("AWS_CONTAINER_CREDENTIALS_FULL_URI", ecsServer.BaseURL()) cmdEnv.Set("AWS_CONTAINER_AUTHORIZATION_TOKEN", ecsServer.AuthToken()) return nil } func addCredsToEnv(credsProvider aws.CredentialsProvider, profileName string, cmdEnv *environ) error { creds, err := credsProvider.Retrieve(context.TODO()) if err != nil { return fmt.Errorf("Failed to get credentials for %s: %w", profileName, err) } log.Println("Setting subprocess env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY") cmdEnv.Set("AWS_ACCESS_KEY_ID", creds.AccessKeyID) cmdEnv.Set("AWS_SECRET_ACCESS_KEY", creds.SecretAccessKey) if creds.SessionToken != "" { log.Println("Setting subprocess env: AWS_SESSION_TOKEN") cmdEnv.Set("AWS_SESSION_TOKEN", creds.SessionToken) } if creds.CanExpire { log.Println("Setting subprocess env: AWS_CREDENTIAL_EXPIRATION") cmdEnv.Set("AWS_CREDENTIAL_EXPIRATION", iso8601.Format(creds.Expires)) } return nil } // environ is a slice of strings representing the environment, in the form "key=value". type environ []string // Unset an environment variable by key func (e *environ) Unset(key string) { for i := range *e { if strings.HasPrefix((*e)[i], key+"=") { (*e)[i] = (*e)[len(*e)-1] *e = (*e)[:len(*e)-1] break } } } // Set adds an environment variable, replacing any existing ones of the same key func (e *environ) Set(key, val string) { e.Unset(key) *e = append(*e, key+"="+val) } func getDefaultShell() string { command := os.Getenv("SHELL") if command == "" { if runtime.GOOS == "windows" { command = "cmd.exe" } else { command = "/bin/sh" } } return command } func runSubProcess(command string, args []string, env []string) (int, error) { log.Printf("Starting a subprocess: %s %s", command, strings.Join(args, " ")) cmd := osexec.Command(command, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = env sigChan := make(chan os.Signal, 1) signal.Notify(sigChan) if err := cmd.Start(); err != nil { return 0, err } // proxy signals to process go func() { for { sig := <-sigChan _ = cmd.Process.Signal(sig) } }() if err := cmd.Wait(); err != nil { _ = cmd.Process.Signal(os.Kill) return 0, fmt.Errorf("Failed to wait for command termination: %v", err) } waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus) return waitStatus.ExitStatus(), nil } func doExecSyscall(command string, args []string, env []string) error { log.Printf("Exec command %s %s", command, strings.Join(args, " ")) argv0, err := osexec.LookPath(command) if err != nil { return fmt.Errorf("Couldn't find the executable '%s': %w", command, err) } log.Printf("Found executable %s", argv0) argv := make([]string, 0, 1+len(args)) argv = append(argv, command) argv = append(argv, args...) return syscall.Exec(argv0, argv, env) } ================================================ FILE: cli/exec_test.go ================================================ package cli import ( "github.com/alecthomas/kingpin/v2" "github.com/99designs/keyring" ) func ExampleExecCommand() { app := kingpin.New("aws-vault", "") awsVault := ConfigureGlobals(app) awsVault.keyringImpl = keyring.NewArrayKeyring([]keyring.Item{ {Key: "llamas", Data: []byte(`{"AccessKeyID":"ABC","SecretAccessKey":"XYZ"}`)}, }) ConfigureExecCommand(app, awsVault) kingpin.MustParse(app.Parse([]string{ "--debug", "exec", "--no-session", "llamas", "--", "sh", "-c", "echo $AWS_ACCESS_KEY_ID", })) // Output: // ABC } ================================================ FILE: cli/export.go ================================================ package cli import ( "context" "encoding/json" "fmt" "log" "os" "time" "github.com/99designs/aws-vault/v7/iso8601" "github.com/99designs/aws-vault/v7/vault" "github.com/99designs/keyring" "github.com/alecthomas/kingpin/v2" "github.com/aws/aws-sdk-go-v2/aws" ini "gopkg.in/ini.v1" ) type ExportCommandInput struct { ProfileName string Format string Config vault.ProfileConfig SessionDuration time.Duration NoSession bool UseStdout bool } var ( FormatTypeEnv = "env" FormatTypeExportEnv = "export-env" FormatTypeExportJSON = "json" FormatTypeExportINI = "ini" ) func ConfigureExportCommand(app *kingpin.Application, a *AwsVault) { input := ExportCommandInput{} cmd := app.Command("export", "Export AWS credentials.") cmd.Flag("duration", "Duration of the temporary or assume-role session. Defaults to 1h"). Short('d'). DurationVar(&input.SessionDuration) cmd.Flag("no-session", "Skip creating STS session with GetSessionToken"). Short('n'). BoolVar(&input.NoSession) cmd.Flag("region", "The AWS region"). StringVar(&input.Config.Region) cmd.Flag("mfa-token", "The MFA token to use"). Short('t'). StringVar(&input.Config.MfaToken) cmd.Flag("format", fmt.Sprintf("Format to output credentials. Valid formats: %s, %s, %s, %s", FormatTypeEnv, FormatTypeExportEnv, FormatTypeExportJSON, FormatTypeExportINI)). Default(FormatTypeEnv). EnumVar(&input.Format, FormatTypeEnv, FormatTypeExportEnv, FormatTypeExportJSON, FormatTypeExportINI) cmd.Flag("stdout", "Print the SSO link to the terminal without automatically opening the browser"). BoolVar(&input.UseStdout) cmd.Arg("profile", "Name of the profile"). Required(). HintAction(a.MustGetProfileNames). StringVar(&input.ProfileName) cmd.Action(func(c *kingpin.ParseContext) (err error) { input.Config.MfaPromptMethod = a.PromptDriver(false) input.Config.NonChainedGetSessionTokenDuration = input.SessionDuration input.Config.AssumeRoleDuration = input.SessionDuration input.Config.SSOUseStdout = input.UseStdout f, err := a.AwsConfigFile() if err != nil { return err } keyring, err := a.Keyring() if err != nil { return err } err = ExportCommand(input, f, keyring) app.FatalIfError(err, "exec") return nil }) } func ExportCommand(input ExportCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) error { if os.Getenv("AWS_VAULT") != "" { return fmt.Errorf("in an existing aws-vault subshell; 'exit' from the subshell or unset AWS_VAULT to force") } config, err := vault.NewConfigLoader(input.Config, f, input.ProfileName).GetProfileConfig(input.ProfileName) if err != nil { return fmt.Errorf("Error loading config: %w", err) } ckr := &vault.CredentialKeyring{Keyring: keyring} credsProvider, err := vault.NewTempCredentialsProvider(config, ckr, input.NoSession, false) if err != nil { return fmt.Errorf("Error getting temporary credentials: %w", err) } if input.Format == FormatTypeExportJSON { return printJSON(input, credsProvider) } else if input.Format == FormatTypeExportINI { return printINI(credsProvider, input.ProfileName, config.Region) } else if input.Format == FormatTypeExportEnv { return printEnv(input, credsProvider, config.Region, "export ") } else { return printEnv(input, credsProvider, config.Region, "") } } func printJSON(input ExportCommandInput, credsProvider aws.CredentialsProvider) error { // AwsCredentialHelperData is metadata for AWS CLI credential process // See https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes type AwsCredentialHelperData struct { Version int `json:"Version"` AccessKeyID string `json:"AccessKeyId"` SecretAccessKey string `json:"SecretAccessKey"` SessionToken string `json:"SessionToken,omitempty"` Expiration string `json:"Expiration,omitempty"` } creds, err := credsProvider.Retrieve(context.TODO()) if err != nil { return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err) } credentialData := AwsCredentialHelperData{ Version: 1, AccessKeyID: creds.AccessKeyID, SecretAccessKey: creds.SecretAccessKey, SessionToken: creds.SessionToken, } if creds.CanExpire { credentialData.Expiration = iso8601.Format(creds.Expires) } json, err := json.MarshalIndent(&credentialData, "", " ") if err != nil { return fmt.Errorf("Error creating credential json: %w", err) } fmt.Print(string(json) + "\n") return nil } func mustNewKey(s *ini.Section, name, val string) { if val != "" { _, err := s.NewKey(name, val) if err != nil { log.Fatalln("Failed to create ini key:", err.Error()) } } } func printINI(credsProvider aws.CredentialsProvider, profilename, region string) error { creds, err := credsProvider.Retrieve(context.TODO()) if err != nil { return fmt.Errorf("Failed to get credentials for %s: %w", profilename, err) } f := ini.Empty() s, err := f.NewSection(profilename) if err != nil { return fmt.Errorf("Failed to create ini section: %w", err) } mustNewKey(s, "aws_access_key_id", creds.AccessKeyID) mustNewKey(s, "aws_secret_access_key", creds.SecretAccessKey) mustNewKey(s, "aws_session_token", creds.SessionToken) if creds.CanExpire { mustNewKey(s, "aws_credential_expiration", iso8601.Format(creds.Expires)) } mustNewKey(s, "region", region) _, err = f.WriteTo(os.Stdout) if err != nil { return fmt.Errorf("Failed to output ini: %w", err) } return nil } func printEnv(input ExportCommandInput, credsProvider aws.CredentialsProvider, region, prefix string) error { creds, err := credsProvider.Retrieve(context.TODO()) if err != nil { return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err) } fmt.Printf("%sAWS_ACCESS_KEY_ID=%s\n", prefix, creds.AccessKeyID) fmt.Printf("%sAWS_SECRET_ACCESS_KEY=%s\n", prefix, creds.SecretAccessKey) if creds.SessionToken != "" { fmt.Printf("%sAWS_SESSION_TOKEN=%s\n", prefix, creds.SessionToken) } if creds.CanExpire { fmt.Printf("%sAWS_CREDENTIAL_EXPIRATION=%s\n", prefix, iso8601.Format(creds.Expires)) } if region != "" { fmt.Printf("%sAWS_REGION=%s\n", prefix, region) fmt.Printf("%sAWS_DEFAULT_REGION=%s\n", prefix, region) } return nil } ================================================ FILE: cli/export_test.go ================================================ package cli import ( "github.com/alecthomas/kingpin/v2" "github.com/99designs/keyring" ) func ExampleExportCommand() { app := kingpin.New("aws-vault", "") awsVault := ConfigureGlobals(app) awsVault.keyringImpl = keyring.NewArrayKeyring([]keyring.Item{ {Key: "llamas", Data: []byte(`{"AccessKeyID":"ABC","SecretAccessKey":"XYZ"}`)}, }) ConfigureExportCommand(app, awsVault) kingpin.MustParse(app.Parse([]string{ "export", "--format=ini", "--no-session", "llamas", })) // Output: // [llamas] // aws_access_key_id=ABC // aws_secret_access_key=XYZ // region=us-east-1 } ================================================ FILE: cli/global.go ================================================ package cli import ( "fmt" "io" "log" "os" "strings" "github.com/99designs/aws-vault/v7/prompt" "github.com/99designs/aws-vault/v7/vault" "github.com/99designs/keyring" "github.com/alecthomas/kingpin/v2" isatty "github.com/mattn/go-isatty" "golang.org/x/term" ) var keyringConfigDefaults = keyring.Config{ ServiceName: "aws-vault", FilePasswordFunc: fileKeyringPassphrasePrompt, LibSecretCollectionName: "awsvault", KWalletAppID: "aws-vault", KWalletFolder: "aws-vault", KeychainTrustApplication: true, WinCredPrefix: "aws-vault", } type AwsVault struct { Debug bool KeyringConfig keyring.Config KeyringBackend string promptDriver string keyringImpl keyring.Keyring awsConfigFile *vault.ConfigFile } func isATerminal() bool { fd := os.Stdout.Fd() return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) } func (a *AwsVault) PromptDriver(avoidTerminalPrompt bool) string { if a.promptDriver == "" { a.promptDriver = "terminal" if !isATerminal() || avoidTerminalPrompt { for _, driver := range prompt.Available() { a.promptDriver = driver if driver != "terminal" { break } } } } log.Println("Using prompt driver: " + a.promptDriver) return a.promptDriver } func (a *AwsVault) Keyring() (keyring.Keyring, error) { if a.keyringImpl == nil { if a.KeyringBackend != "" { a.KeyringConfig.AllowedBackends = []keyring.BackendType{keyring.BackendType(a.KeyringBackend)} } var err error a.keyringImpl, err = keyring.Open(a.KeyringConfig) if err != nil { return nil, err } } return a.keyringImpl, nil } func (a *AwsVault) AwsConfigFile() (*vault.ConfigFile, error) { if a.awsConfigFile == nil { var err error a.awsConfigFile, err = vault.LoadConfigFromEnv() if err != nil { return nil, err } } return a.awsConfigFile, nil } func (a *AwsVault) MustGetProfileNames() []string { config, err := a.AwsConfigFile() if err != nil { log.Fatalf("Error loading AWS config: %s", err.Error()) } return config.ProfileNames() } func ConfigureGlobals(app *kingpin.Application) *AwsVault { a := &AwsVault{ KeyringConfig: keyringConfigDefaults, } backendsAvailable := []string{} for _, backendType := range keyring.AvailableBackends() { backendsAvailable = append(backendsAvailable, string(backendType)) } promptsAvailable := prompt.Available() app.Flag("debug", "Show debugging output"). BoolVar(&a.Debug) app.Flag("backend", fmt.Sprintf("Secret backend to use %v", backendsAvailable)). Default(backendsAvailable[0]). Envar("AWS_VAULT_BACKEND"). EnumVar(&a.KeyringBackend, backendsAvailable...) app.Flag("prompt", fmt.Sprintf("Prompt driver to use %v", promptsAvailable)). Envar("AWS_VAULT_PROMPT"). StringVar(&a.promptDriver) app.Validate(func(app *kingpin.Application) error { if a.promptDriver == "" { return nil } if a.promptDriver == "pass" { kingpin.Fatalf("--prompt=pass (or AWS_VAULT_PROMPT=pass) has been removed from aws-vault as using TOTPs without " + "a dedicated device goes against security best practices. If you wish to continue using pass, " + "add `mfa_process = pass otp ` to profiles in your ~/.aws/config file.") } for _, v := range promptsAvailable { if v == a.promptDriver { return nil } } return fmt.Errorf("--prompt value must be one of %s, got '%s'", strings.Join(promptsAvailable, ","), a.promptDriver) }) app.Flag("keychain", "Name of macOS keychain to use, if it doesn't exist it will be created"). Default("aws-vault"). Envar("AWS_VAULT_KEYCHAIN_NAME"). StringVar(&a.KeyringConfig.KeychainName) app.Flag("secret-service-collection", "Name of secret-service collection to use, if it doesn't exist it will be created"). Default("awsvault"). Envar("AWS_VAULT_SECRET_SERVICE_COLLECTION_NAME"). StringVar(&a.KeyringConfig.LibSecretCollectionName) app.Flag("pass-dir", "Pass password store directory"). Envar("AWS_VAULT_PASS_PASSWORD_STORE_DIR"). StringVar(&a.KeyringConfig.PassDir) app.Flag("pass-cmd", "Name of the pass executable"). Envar("AWS_VAULT_PASS_CMD"). StringVar(&a.KeyringConfig.PassCmd) app.Flag("pass-prefix", "Prefix to prepend to the item path stored in pass"). Envar("AWS_VAULT_PASS_PREFIX"). StringVar(&a.KeyringConfig.PassPrefix) app.Flag("file-dir", "Directory for the \"file\" password store"). Default("~/.awsvault/keys/"). Envar("AWS_VAULT_FILE_DIR"). StringVar(&a.KeyringConfig.FileDir) app.PreAction(func(c *kingpin.ParseContext) error { if !a.Debug { log.SetOutput(io.Discard) } keyring.Debug = a.Debug log.Printf("aws-vault %s", app.Model().Version) return nil }) return a } func fileKeyringPassphrasePrompt(prompt string) (string, error) { if password, ok := os.LookupEnv("AWS_VAULT_FILE_PASSPHRASE"); ok { return password, nil } fmt.Fprintf(os.Stderr, "%s: ", prompt) b, err := term.ReadPassword(int(os.Stdin.Fd())) if err != nil { return "", err } fmt.Println() return string(b), nil } ================================================ FILE: cli/list.go ================================================ package cli import ( "fmt" "os" "strings" "text/tabwriter" "time" "github.com/99designs/aws-vault/v7/vault" "github.com/99designs/keyring" "github.com/alecthomas/kingpin/v2" ) type ListCommandInput struct { OnlyProfiles bool OnlySessions bool OnlyCredentials bool } func ConfigureListCommand(app *kingpin.Application, a *AwsVault) { input := ListCommandInput{} cmd := app.Command("list", "List profiles, along with their credentials and sessions.") cmd.Alias("ls") cmd.Flag("profiles", "Show only the profile names"). BoolVar(&input.OnlyProfiles) cmd.Flag("sessions", "Show only the session names"). BoolVar(&input.OnlySessions) cmd.Flag("credentials", "Show only the profiles with stored credential"). BoolVar(&input.OnlyCredentials) cmd.Action(func(c *kingpin.ParseContext) (err error) { keyring, err := a.Keyring() if err != nil { return err } awsConfigFile, err := a.AwsConfigFile() if err != nil { return err } err = ListCommand(input, awsConfigFile, keyring) app.FatalIfError(err, "list") return nil }) } type stringslice []string func (ss stringslice) remove(stringsToRemove []string) (newSS []string) { xx := stringslice(stringsToRemove) for _, s := range ss { if !xx.has(s) { newSS = append(newSS, s) } } return } func (ss stringslice) has(s string) bool { for _, t := range ss { if s == t { return true } } return false } func sessionLabel(sess vault.SessionMetadata) string { return fmt.Sprintf("%s:%s", sess.Type, time.Until(sess.Expiration).Truncate(time.Second)) } func ListCommand(input ListCommandInput, awsConfigFile *vault.ConfigFile, keyring keyring.Keyring) (err error) { credentialKeyring := &vault.CredentialKeyring{Keyring: keyring} oidcTokenKeyring := &vault.OIDCTokenKeyring{Keyring: credentialKeyring.Keyring} sessionKeyring := &vault.SessionKeyring{Keyring: credentialKeyring.Keyring} credentialsNames, err := credentialKeyring.Keys() if err != nil { return err } tokens, err := oidcTokenKeyring.Keys() if err != nil { return err } sessions, err := sessionKeyring.GetAllMetadata() if err != nil { return err } allSessionLabels := []string{} for _, t := range tokens { allSessionLabels = append(allSessionLabels, fmt.Sprintf("oidc:%s", t)) } for _, sess := range sessions { allSessionLabels = append(allSessionLabels, sessionLabel(sess)) } if input.OnlyCredentials { for _, c := range credentialsNames { fmt.Println(c) } return nil } if input.OnlyProfiles { for _, profileName := range awsConfigFile.ProfileNames() { fmt.Println(profileName) } return nil } if input.OnlySessions { for _, l := range allSessionLabels { fmt.Println(l) } return nil } displayedSessionLabels := []string{} w := tabwriter.NewWriter(os.Stdout, 25, 4, 2, ' ', 0) fmt.Fprintln(w, "Profile\tCredentials\tSessions\t") fmt.Fprintln(w, "=======\t===========\t========\t") // list out known profiles first for _, profileName := range awsConfigFile.ProfileNames() { fmt.Fprintf(w, "%s\t", profileName) hasCred, err := credentialKeyring.Has(profileName) if err != nil { return err } if hasCred { fmt.Fprintf(w, "%s\t", profileName) } else { fmt.Fprintf(w, "-\t") } var sessionLabels []string // check oidc keyring if profileSection, ok := awsConfigFile.ProfileSection(profileName); ok { if exists, _ := oidcTokenKeyring.Has(profileSection.SSOStartURL); exists { sessionLabels = append(sessionLabels, fmt.Sprintf("oidc:%s", profileSection.SSOStartURL)) } } // check session keyring for _, sess := range sessions { if profileName == sess.ProfileName { sessionLabels = append(sessionLabels, sessionLabel(sess)) } } if len(sessionLabels) > 0 { fmt.Fprintf(w, "%s\t\n", strings.Join(sessionLabels, ", ")) } else { fmt.Fprintf(w, "-\t\n") } displayedSessionLabels = append(displayedSessionLabels, sessionLabels...) } // show credentials that don't have profiles for _, credentialName := range credentialsNames { _, ok := awsConfigFile.ProfileSection(credentialName) if !ok { fmt.Fprintf(w, "-\t%s\t-\t\n", credentialName) } } // show sessions that don't have profiles sessionsWithoutProfiles := stringslice(allSessionLabels).remove(displayedSessionLabels) for _, s := range sessionsWithoutProfiles { fmt.Fprintf(w, "-\t-\t%s\t\n", s) } return w.Flush() } ================================================ FILE: cli/list_test.go ================================================ package cli import ( "github.com/alecthomas/kingpin/v2" "github.com/99designs/keyring" ) func ExampleListCommand() { app := kingpin.New("aws-vault", "") awsVault := ConfigureGlobals(app) awsVault.keyringImpl = keyring.NewArrayKeyring([]keyring.Item{ {Key: "llamas", Data: []byte(`{"AccessKeyID":"ABC","SecretAccessKey":"XYZ"}`)}, }) ConfigureListCommand(app, awsVault) kingpin.MustParse(app.Parse([]string{ "list", "--credentials", })) // Output: // llamas } ================================================ FILE: cli/login.go ================================================ package cli import ( "context" "encoding/json" "fmt" "io" "log" "net/http" "net/url" "strings" "time" "github.com/99designs/aws-vault/v7/vault" "github.com/99designs/keyring" "github.com/alecthomas/kingpin/v2" "github.com/aws/aws-sdk-go-v2/aws" awsconfig "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/sts" "github.com/skratchdot/open-golang/open" ) type LoginCommandInput struct { ProfileName string UseStdout bool Path string Config vault.ProfileConfig SessionDuration time.Duration NoSession bool } func ConfigureLoginCommand(app *kingpin.Application, a *AwsVault) { input := LoginCommandInput{} cmd := app.Command("login", "Generate a login link for the AWS Console.") cmd.Flag("duration", "Duration of the assume-role or federated session. Defaults to 1h"). Short('d'). DurationVar(&input.SessionDuration) cmd.Flag("no-session", "Skip creating STS session with GetSessionToken"). Short('n'). BoolVar(&input.NoSession) cmd.Flag("mfa-token", "The MFA token to use"). Short('t'). StringVar(&input.Config.MfaToken) cmd.Flag("path", "The AWS service you would like access"). StringVar(&input.Path) cmd.Flag("region", "The AWS region"). StringVar(&input.Config.Region) cmd.Flag("stdout", "Print login URL to stdout instead of opening in default browser"). Short('s'). BoolVar(&input.UseStdout) cmd.Arg("profile", "Name of the profile. If none given, credentials will be sourced from env vars"). HintAction(a.MustGetProfileNames). StringVar(&input.ProfileName) cmd.Action(func(c *kingpin.ParseContext) (err error) { input.Config.MfaPromptMethod = a.PromptDriver(false) input.Config.NonChainedGetSessionTokenDuration = input.SessionDuration input.Config.AssumeRoleDuration = input.SessionDuration input.Config.GetFederationTokenDuration = input.SessionDuration keyring, err := a.Keyring() if err != nil { return err } f, err := a.AwsConfigFile() if err != nil { return err } err = LoginCommand(context.Background(), input, f, keyring) app.FatalIfError(err, "login") return nil }) } func getCredsProvider(input LoginCommandInput, config *vault.ProfileConfig, keyring keyring.Keyring) (credsProvider aws.CredentialsProvider, err error) { if input.ProfileName == "" { // When no profile is specified, source credentials from the environment configFromEnv, err := awsconfig.NewEnvConfig() if err != nil { return nil, fmt.Errorf("unable to authenticate to AWS through your environment variables: %w", err) } if configFromEnv.Credentials.AccessKeyID == "" { return nil, fmt.Errorf("argument 'profile' not provided, nor any AWS env vars found. Try --help") } credsProvider = credentials.StaticCredentialsProvider{Value: configFromEnv.Credentials} } else { // Use a profile from the AWS config file ckr := &vault.CredentialKeyring{Keyring: keyring} t := vault.TempCredentialsCreator{ Keyring: ckr, DisableSessions: input.NoSession, DisableSessionsForProfile: config.ProfileName, } credsProvider, err = t.GetProviderForProfile(config) if err != nil { return nil, fmt.Errorf("profile %s: %w", input.ProfileName, err) } } return credsProvider, err } // LoginCommand creates a login URL for the AWS Management Console using the method described at // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html func LoginCommand(ctx context.Context, input LoginCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) error { config, err := vault.NewConfigLoader(input.Config, f, input.ProfileName).GetProfileConfig(input.ProfileName) if err != nil { return fmt.Errorf("Error loading config: %w", err) } credsProvider, err := getCredsProvider(input, config, keyring) if err != nil { return err } // if we already know the type of credentials being created, avoid calling isCallerIdentityAssumedRole canCredsBeUsedInLoginURL, err := canProviderBeUsedForLogin(credsProvider) if err != nil { return err } if !canCredsBeUsedInLoginURL { // use a static creds provider so that we don't request credentials from AWS more than once credsProvider, err = createStaticCredentialsProvider(ctx, credsProvider) if err != nil { return err } // if the credentials have come from an unknown source like credential_process, check the // caller identity to see if it's an assumed role isAssumedRole, err := isCallerIdentityAssumedRole(ctx, credsProvider, config) if err != nil { return err } if !isAssumedRole { log.Println("Creating a federated session") credsProvider, err = vault.NewFederationTokenProvider(ctx, credsProvider, config) if err != nil { return err } } } creds, err := credsProvider.Retrieve(ctx) if err != nil { return err } if creds.CanExpire { log.Printf("Requesting a signin token for session expiring in %s", time.Until(creds.Expires)) } loginURLPrefix, destination := generateLoginURL(config.Region, input.Path) signinToken, err := requestSigninToken(ctx, creds, loginURLPrefix) if err != nil { return err } loginURL := fmt.Sprintf("%s?Action=login&Issuer=aws-vault&Destination=%s&SigninToken=%s", loginURLPrefix, url.QueryEscape(destination), url.QueryEscape(signinToken)) if input.UseStdout { fmt.Println(loginURL) } else if err = open.Run(loginURL); err != nil { return fmt.Errorf("Failed to open %s: %w", loginURL, err) } return nil } func generateLoginURL(region string, path string) (string, string) { loginURLPrefix := "https://signin.aws.amazon.com/federation" destination := "https://console.aws.amazon.com/" if region != "" { destinationDomain := "console.aws.amazon.com" switch { case strings.HasPrefix(region, "cn-"): loginURLPrefix = "https://signin.amazonaws.cn/federation" destinationDomain = "console.amazonaws.cn" case strings.HasPrefix(region, "us-gov-"): loginURLPrefix = "https://signin.amazonaws-us-gov.com/federation" destinationDomain = "console.amazonaws-us-gov.com" } if path != "" { destination = fmt.Sprintf("https://%s.%s/%s?region=%s", region, destinationDomain, path, region) } else { destination = fmt.Sprintf("https://%s.%s/console/home?region=%s", region, destinationDomain, region) } } return loginURLPrefix, destination } func isCallerIdentityAssumedRole(ctx context.Context, credsProvider aws.CredentialsProvider, config *vault.ProfileConfig) (bool, error) { cfg := vault.NewAwsConfigWithCredsProvider(credsProvider, config.Region, config.STSRegionalEndpoints) client := sts.NewFromConfig(cfg) id, err := client.GetCallerIdentity(ctx, nil) if err != nil { return false, err } arn := aws.ToString(id.Arn) arnParts := strings.Split(arn, ":") if len(arnParts) < 6 { return false, fmt.Errorf("unable to parse ARN: %s", arn) } if strings.HasPrefix(arnParts[5], "assumed-role") { return true, nil } return false, nil } func createStaticCredentialsProvider(ctx context.Context, credsProvider aws.CredentialsProvider) (sc credentials.StaticCredentialsProvider, err error) { creds, err := credsProvider.Retrieve(ctx) if err != nil { return sc, err } return credentials.StaticCredentialsProvider{Value: creds}, nil } // canProviderBeUsedForLogin returns true if the credentials produced by the provider is known to be usable by the login URL endpoint func canProviderBeUsedForLogin(credsProvider aws.CredentialsProvider) (bool, error) { if _, ok := credsProvider.(*vault.AssumeRoleProvider); ok { return true, nil } if _, ok := credsProvider.(*vault.SSORoleCredentialsProvider); ok { return true, nil } if _, ok := credsProvider.(*vault.AssumeRoleWithWebIdentityProvider); ok { return true, nil } if c, ok := credsProvider.(*vault.CachedSessionProvider); ok { return canProviderBeUsedForLogin(c.SessionProvider) } return false, nil } // Create a signin token func requestSigninToken(ctx context.Context, creds aws.Credentials, loginURLPrefix string) (string, error) { jsonSession, err := json.Marshal(map[string]string{ "sessionId": creds.AccessKeyID, "sessionKey": creds.SecretAccessKey, "sessionToken": creds.SessionToken, }) if err != nil { return "", err } req, err := http.NewRequestWithContext(ctx, "GET", loginURLPrefix, nil) if err != nil { return "", err } q := req.URL.Query() q.Add("Action", "getSigninToken") q.Add("Session", string(jsonSession)) req.URL.RawQuery = q.Encode() resp, err := http.DefaultClient.Do(req) if err != nil { return "", err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return "", err } if resp.StatusCode != http.StatusOK { log.Printf("Response body was %s", body) return "", fmt.Errorf("Call to getSigninToken failed with %v", resp.Status) } var respParsed map[string]string err = json.Unmarshal(body, &respParsed) if err != nil { return "", err } signinToken, ok := respParsed["SigninToken"] if !ok { return "", fmt.Errorf("Expected a response with SigninToken") } return signinToken, nil } ================================================ FILE: cli/proxy.go ================================================ package cli import ( "os" "os/signal" "syscall" "github.com/99designs/aws-vault/v7/server" "github.com/alecthomas/kingpin/v2" ) func ConfigureProxyCommand(app *kingpin.Application) { stop := false cmd := app.Command("proxy", "Start a proxy for the ec2 instance role server locally."). Alias("server"). Hidden() cmd.Flag("stop", "Stop the proxy"). BoolVar(&stop) cmd.Action(func(*kingpin.ParseContext) error { if stop { server.StopProxy() return nil } handleSigTerm() return server.StartProxy() }) } func handleSigTerm() { // shutdown c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c server.Shutdown() os.Exit(1) }() } ================================================ FILE: cli/remove.go ================================================ package cli import ( "fmt" "strings" "github.com/99designs/aws-vault/v7/prompt" "github.com/99designs/aws-vault/v7/vault" "github.com/99designs/keyring" "github.com/alecthomas/kingpin/v2" ) type RemoveCommandInput struct { ProfileName string SessionsOnly bool Force bool } func ConfigureRemoveCommand(app *kingpin.Application, a *AwsVault) { input := RemoveCommandInput{} cmd := app.Command("remove", "Remove credentials from the secure keystore.") cmd.Alias("rm") cmd.Arg("profile", "Name of the profile"). Required(). HintAction(a.MustGetProfileNames). StringVar(&input.ProfileName) cmd.Flag("sessions-only", "Only remove sessions, leave credentials intact"). Short('s'). Hidden(). BoolVar(&input.SessionsOnly) cmd.Flag("force", "Force-remove the profile without a prompt"). Short('f'). BoolVar(&input.Force) cmd.Action(func(c *kingpin.ParseContext) error { keyring, err := a.Keyring() if err != nil { return err } err = RemoveCommand(input, keyring) app.FatalIfError(err, "remove") return nil }) } func RemoveCommand(input RemoveCommandInput, keyring keyring.Keyring) error { ckr := &vault.CredentialKeyring{Keyring: keyring} // Legacy --sessions-only option for backwards compatibility, use aws-vault clear instead if input.SessionsOnly { sk := &vault.SessionKeyring{Keyring: ckr.Keyring} n, err := sk.RemoveForProfile(input.ProfileName) if err != nil { return err } fmt.Printf("Deleted %d sessions.\n", n) return nil } if !input.Force { r, err := prompt.TerminalPrompt(fmt.Sprintf("Delete credentials for profile %q? (y|N) ", input.ProfileName)) if err != nil { return err } if !strings.EqualFold(r, "y") && !strings.EqualFold(r, "yes") { return nil } } if err := ckr.Remove(input.ProfileName); err != nil { return err } fmt.Printf("Deleted credentials.\n") return nil } ================================================ FILE: cli/rotate.go ================================================ package cli import ( "context" "fmt" "log" "time" "github.com/99designs/aws-vault/v7/vault" "github.com/99designs/keyring" "github.com/alecthomas/kingpin/v2" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" ) type RotateCommandInput struct { NoSession bool ProfileName string Config vault.ProfileConfig } func ConfigureRotateCommand(app *kingpin.Application, a *AwsVault) { input := RotateCommandInput{} cmd := app.Command("rotate", "Rotate credentials.") cmd.Flag("no-session", "Use master credentials, no session or role used"). Short('n'). BoolVar(&input.NoSession) cmd.Arg("profile", "Name of the profile"). Required(). HintAction(a.MustGetProfileNames). StringVar(&input.ProfileName) cmd.Action(func(c *kingpin.ParseContext) (err error) { input.Config.MfaPromptMethod = a.PromptDriver(false) keyring, err := a.Keyring() if err != nil { return err } f, err := a.AwsConfigFile() if err != nil { return err } err = RotateCommand(input, f, keyring) app.FatalIfError(err, "rotate") return nil }) } func RotateCommand(input RotateCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) error { configLoader := vault.NewConfigLoader(input.Config, f, input.ProfileName) config, err := configLoader.GetProfileConfig(input.ProfileName) if err != nil { return fmt.Errorf("Error loading config: %w", err) } ckr := &vault.CredentialKeyring{Keyring: keyring} masterCredentialsName, err := vault.FindMasterCredentialsNameFor(input.ProfileName, ckr, config) if err != nil { return fmt.Errorf("Error determining credential name for '%s': %w", input.ProfileName, err) } if input.NoSession { fmt.Printf("Rotating credentials stored for profile '%s' using master credentials (takes 10-20 seconds)\n", masterCredentialsName) } else { fmt.Printf("Rotating credentials stored for profile '%s' using a session from profile '%s' (takes 10-20 seconds)\n", masterCredentialsName, input.ProfileName) } // Get the existing credentials access key ID oldMasterCreds, err := vault.NewMasterCredentialsProvider(ckr, masterCredentialsName).Retrieve(context.TODO()) if err != nil { return fmt.Errorf("Error loading source credentials for '%s': %w", masterCredentialsName, err) } oldMasterCredsAccessKeyID := vault.FormatKeyForDisplay(oldMasterCreds.AccessKeyID) log.Printf("Rotating access key %s\n", oldMasterCredsAccessKeyID) fmt.Println("Creating a new access key") // create a session to rotate the credentials var credsProvider aws.CredentialsProvider if input.NoSession { credsProvider = vault.NewMasterCredentialsProvider(ckr, config.ProfileName) } else { // Can't always disable sessions completely, might need to use session for MFA-Protected API Access credsProvider, err = vault.NewTempCredentialsProvider(config, ckr, input.NoSession, true) if err != nil { return fmt.Errorf("Error getting temporary credentials: %w", err) } } cfg := vault.NewAwsConfigWithCredsProvider(credsProvider, config.Region, config.STSRegionalEndpoints) // A username is needed for some IAM calls if the credentials have assumed a role iamUserName, err := getUsernameIfAssumingRole(context.TODO(), cfg, config) if err != nil { return err } iamClient := iam.NewFromConfig(cfg) // Create a new access key createOut, err := iamClient.CreateAccessKey(context.TODO(), &iam.CreateAccessKeyInput{ UserName: iamUserName, }) if err != nil { return fmt.Errorf("Error creating a new access key: %w", err) } fmt.Printf("Created new access key %s\n", vault.FormatKeyForDisplay(*createOut.AccessKey.AccessKeyId)) newMasterCreds := aws.Credentials{ AccessKeyID: *createOut.AccessKey.AccessKeyId, SecretAccessKey: *createOut.AccessKey.SecretAccessKey, } err = ckr.Set(masterCredentialsName, newMasterCreds) if err != nil { return fmt.Errorf("Error storing new access key %s: %w", vault.FormatKeyForDisplay(newMasterCreds.AccessKeyID), err) } // Delete old sessions sk := &vault.SessionKeyring{Keyring: ckr.Keyring} profileNames, err := getProfilesInChain(input.ProfileName, configLoader) for _, profileName := range profileNames { if n, _ := sk.RemoveForProfile(profileName); n > 0 { fmt.Printf("Deleted %d sessions for %s\n", n, profileName) } } // Use new credentials to delete old access key fmt.Printf("Deleting old access key %s\n", oldMasterCredsAccessKeyID) err = retry(time.Second*20, time.Second*2, func() error { _, err = iamClient.DeleteAccessKey(context.TODO(), &iam.DeleteAccessKeyInput{ AccessKeyId: &oldMasterCreds.AccessKeyID, UserName: iamUserName, }) return err }) if err != nil { return fmt.Errorf("Can't delete old access key %s: %w", oldMasterCredsAccessKeyID, err) } fmt.Printf("Deleted old access key %s\n", oldMasterCredsAccessKeyID) fmt.Println("Finished rotating access key") return nil } func retry(maxTime time.Duration, sleep time.Duration, f func() error) (err error) { t0 := time.Now() i := 0 for { i++ err = f() if err == nil { return // nolint } elapsed := time.Since(t0) if elapsed > maxTime { return fmt.Errorf("After %d attempts, last error: %s", i, err) } time.Sleep(sleep) log.Println("Retrying after error:", err) } } func getUsernameIfAssumingRole(ctx context.Context, awsCfg aws.Config, config *vault.ProfileConfig) (*string, error) { if config.RoleARN != "" { n, err := vault.GetUsernameFromSession(ctx, awsCfg) if err != nil { return nil, fmt.Errorf("Error getting IAM username from session: %w", err) } log.Printf("Found IAM username '%s'", n) return &n, nil } return nil, nil //nolint } func getProfilesInChain(profileName string, configLoader *vault.ConfigLoader) (profileNames []string, err error) { profileNames = append(profileNames, profileName) config, err := configLoader.GetProfileConfig(profileName) if err != nil { return profileNames, err } if config.SourceProfile != nil { newProfileNames, err := getProfilesInChain(config.SourceProfileName, configLoader) if err != nil { return profileNames, err } profileNames = append(profileNames, newProfileNames...) } return profileNames, nil } ================================================ FILE: contrib/_aws-vault-proxy/Dockerfile ================================================ FROM golang:1.17 WORKDIR /usr/src/aws-vault-proxy COPY . /usr/src/aws-vault-proxy RUN go build -v -o /usr/local/bin/aws-vault-proxy ./... CMD ["/usr/local/bin/aws-vault-proxy"] ================================================ FILE: contrib/_aws-vault-proxy/docker-compose.yml ================================================ version: "2.4" networks: aws-vault: driver: bridge ipam: config: - subnet: "169.254.170.0/24" gateway: "169.254.170.1" services: aws-vault-proxy: build: . environment: - AWS_CONTAINER_CREDENTIALS_FULL_URI - AWS_CONTAINER_AUTHORIZATION_TOKEN networks: aws-vault: ipv4_address: "169.254.170.2" # This special IP address is recognized by the AWS SDKs and AWS CLI healthcheck: test: pgrep aws-vault-proxy testapp: image: amazon/aws-cli entrypoint: "" command: /bin/bash environment: - AWS_CONTAINER_CREDENTIALS_RELATIVE_URI networks: aws-vault: {} default: {} ================================================ FILE: contrib/_aws-vault-proxy/go.mod ================================================ module aws-vault-ecs-server-reverse-proxy go 1.17 require github.com/gorilla/handlers v1.5.1 require github.com/felixge/httpsnoop v1.0.1 // indirect ================================================ FILE: contrib/_aws-vault-proxy/go.sum ================================================ github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= ================================================ FILE: contrib/_aws-vault-proxy/main.go ================================================ package main import ( "log" "net/http" "net/http/httputil" "net/url" "os" "github.com/gorilla/handlers" ) func GetReverseProxyTarget() *url.URL { url, err := url.Parse(os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")) if err != nil { log.Fatalln("Bad AWS_CONTAINER_CREDENTIALS_FULL_URI:", err.Error()) } url.Host = "host.docker.internal:" + url.Port() return url } func addAuthorizationHeader(authToken string, next http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { r.Header.Add("Authorization", authToken) next.ServeHTTP(w, r) } } func main() { target := GetReverseProxyTarget() authToken := os.Getenv("AWS_CONTAINER_AUTHORIZATION_TOKEN") log.Printf("reverse proxying target:%s auth:%s\n", target, authToken) handler := handlers.LoggingHandler(os.Stderr, addAuthorizationHeader(authToken, httputil.NewSingleHostReverseProxy(target))) _ = http.ListenAndServe(":80", handler) } ================================================ FILE: contrib/completions/bash/aws-vault.bash ================================================ _aws-vault_bash_autocomplete() { local i cur prev opts base for (( i=1; i < COMP_CWORD; i++ )); do if [[ ${COMP_WORDS[i]} == -- ]]; then _command_offset $i+1 return fi done COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" opts=$( ${COMP_WORDS[0]} --completion-bash "${COMP_WORDS[@]:1:$COMP_CWORD}" ) COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) return 0 } complete -F _aws-vault_bash_autocomplete -o default aws-vault ================================================ FILE: contrib/completions/fish/aws-vault.fish ================================================ if status --is-interactive complete -ec aws-vault # switch based on seeing a `--` complete -c aws-vault -n 'not __fish_aws_vault_is_commandline' -xa '(__fish_aws_vault_complete_arg)' complete -c aws-vault -n '__fish_aws_vault_is_commandline' -xa '(__fish_aws_vault_complete_commandline)' function __fish_aws_vault_is_commandline string match -q -r '^--$' -- (commandline -opc) end function __fish_aws_vault_complete_arg set -l parts (commandline -opc) set -e parts[1] aws-vault --completion-bash $parts end function __fish_aws_vault_complete_commandline set -l parts (string split --max 1 '--' -- (commandline -pc)) complete "-C$parts[2]" end end ================================================ FILE: contrib/completions/zsh/aws-vault.zsh ================================================ #compdef aws-vault _aws-vault() { local i for (( i=2; i < CURRENT; i++ )); do if [[ ${words[i]} == -- ]]; then shift $i words (( CURRENT -= i )) _normal return fi done local matches=($(${words[1]} --completion-bash ${(@)words[2,$CURRENT]})) compadd -a matches if [[ $compstate[nmatches] -eq 0 && $words[$CURRENT] != -* ]]; then _files fi } if [[ "$(basename -- ${(%):-%x})" != "_aws-vault" ]]; then compdef _aws-vault aws-vault fi ================================================ FILE: contrib/docker/Dockerfile ================================================ FROM debian:bullseye-slim RUN apt update && apt install -y curl RUN curl -fLs -o /usr/local/bin/aws-vault https://github.com/99designs/aws-vault/releases/download/v6.3.1/aws-vault-linux-amd64 && chmod 755 /usr/local/bin/aws-vault ENV AWS_VAULT_BACKEND=file ENTRYPOINT ["/usr/local/bin/aws-vault"] # Example usage: # docker build -t aws-vault . # docker run -it -e COLUMNS=$(tput cols) -v ~/.aws/config:/root/.aws/config -v ~/.awsvault:/root/.awsvault aws-vault ================================================ FILE: contrib/scripts/aws-configure-with-env-vars.sh ================================================ #!/bin/sh # Configure aws-cli using the AWS env vars created with aws-vault # # Usage: aws-vault exec -- aws-configure-with-env-vars.sh [TARGET_PROFILE] # set -eu aws configure --profile "${1:-$AWS_VAULT}" set region "$AWS_REGION" aws configure --profile "${1:-$AWS_VAULT}" set aws_access_key_id "$AWS_ACCESS_KEY_ID" aws configure --profile "${1:-$AWS_VAULT}" set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY" aws configure --profile "${1:-$AWS_VAULT}" set aws_session_token "${AWS_SESSION_TOKEN:-}" ================================================ FILE: contrib/scripts/aws-iam-create-yubikey-mfa.sh ================================================ #!/bin/sh # Adds a Yubikey TOTP device to IAM using your IAM User as the $MFA_DEVICE_NAME # Currently, aws iam enable-mfa-device doesn't support specifying your MFA Device Name. set -eu if [ -n "${AWS_SESSION_TOKEN:-}" ]; then echo "aws-vault must be run without a STS session, please run it with the --no-session flag" >&2 exit 1 fi ACCOUNT_ARN=$(aws sts get-caller-identity --query Arn --output text) # Assume that the final portion of the ARN is the username # Works for ARNs like `users/` and `users/engineers/` USERNAME=$(echo "$ACCOUNT_ARN" | rev | cut -d/ -f1 | rev) OUTFILE=$(mktemp) trap 'rm -f "$OUTFILE"' EXIT SERIAL_NUMBER=$(aws iam create-virtual-mfa-device \ --virtual-mfa-device-name "$USERNAME" \ --bootstrap-method Base32StringSeed \ --outfile "$OUTFILE" \ --query VirtualMFADevice.SerialNumber \ --output text) ykman oath accounts add -ft "$SERIAL_NUMBER" < "$OUTFILE" 2> /dev/null CODE1=$(ykman oath accounts code -s "$SERIAL_NUMBER") WAIT_TIME=$((30-$(date +%s)%30)) echo "Waiting $WAIT_TIME seconds before generating a second code" >&2 sleep $WAIT_TIME CODE2=$(ykman oath accounts code -s "$SERIAL_NUMBER") aws iam enable-mfa-device \ --user-name "$USERNAME" \ --serial-number "$SERIAL_NUMBER" \ --authentication-code1 "$CODE1" \ --authentication-code2 "$CODE2" ================================================ FILE: contrib/scripts/aws-iam-resync-yubikey-mfa.sh ================================================ #!/bin/sh # Resync a Yubikey TOTP device to IAM using your IAM User as the $MFA_DEVICE_NAME # Currently, aws iam resync-mfa-device doesn't support specifying your MFA Device Name. set -eu ACCOUNT_ARN=$(aws sts get-caller-identity --query Arn --output text) # Assume that the final portion of the ARN is the username # Works for ARNs like `users/` and `users/engineers/` USERNAME=$(echo "$ACCOUNT_ARN" | rev | cut -d/ -f1 | rev) ACCOUNT_ID=$(echo "$ACCOUNT_ARN" | cut -d: -f5) SERIAL_NUMBER="arn:aws:iam::${ACCOUNT_ID}:mfa/${USERNAME}" CODE1=$(ykman oath accounts code -s "$SERIAL_NUMBER") WAIT_TIME=$((30-$(date +%s)%30)) echo "Waiting $WAIT_TIME seconds before generating a second code" >&2 sleep $WAIT_TIME CODE2=$(ykman oath accounts code -s "$SERIAL_NUMBER") aws iam resync-mfa-device \ --user-name "$USERNAME" \ --serial-number "$SERIAL_NUMBER" \ --authentication-code1 "$CODE1" \ --authentication-code2 "$CODE2" ================================================ FILE: go.mod ================================================ module github.com/99designs/aws-vault/v7 go 1.20 require ( github.com/99designs/keyring v1.2.2 github.com/alecthomas/kingpin/v2 v2.3.2 github.com/aws/aws-sdk-go-v2 v1.17.7 github.com/aws/aws-sdk-go-v2/config v1.18.19 github.com/aws/aws-sdk-go-v2/credentials v1.13.18 github.com/aws/aws-sdk-go-v2/service/iam v1.19.8 github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 github.com/google/go-cmp v0.5.9 github.com/mattn/go-isatty v0.0.18 github.com/mattn/go-tty v0.0.4 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 golang.org/x/term v0.6.0 gopkg.in/ini.v1 v1.67.0 ) require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 // indirect github.com/aws/smithy-go v1.13.5 // indirect github.com/danieljoos/wincred v1.1.2 // indirect github.com/dvsekhvalnov/jose2go v1.5.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect golang.org/x/sys v0.6.0 // indirect ) ================================================ FILE: go.sum ================================================ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg= github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw= github.com/aws/aws-sdk-go-v2/config v1.18.19/go.mod h1:XvTmGMY8d52ougvakOv1RpiTLPz9dlG/OQHsKU/cMmY= github.com/aws/aws-sdk-go-v2/credentials v1.13.18 h1:EQMdtHwz0ILTW1hoP+EwuWhwCG1hD6l3+RWFQABET4c= github.com/aws/aws-sdk-go-v2/credentials v1.13.18/go.mod h1:vnwlwjIe+3XJPBYKu1et30ZPABG3VaXJYr8ryohpIyM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 h1:gt57MN3liKiyGopcqgNzJb2+d9MJaKT/q1OksHNXVE4= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1/go.mod h1:lfUx8puBRdM5lVVMQlwt2v+ofiG/X6Ms+dy0UkG/kXw= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 h1:sJLYcS+eZn5EeNINGHSCRAwUJMFVqklwkH36Vbyai7M= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 h1:1mnRASEKnkqsntcxHaysxwgVoUUp5dkiB+l3llKnqyg= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 h1:p5luUImdIqywn6JpQsW3tq5GNOxKmOnEpybzPx+d1lk= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32/go.mod h1:XGhIBZDEgfqmFIugclZ6FU7v75nHhBDtzuB4xB/tEi4= github.com/aws/aws-sdk-go-v2/service/iam v1.19.8 h1:kQsBeGgm68kT0xc90spgC5qEOQGH74V2bFqgBgG21Bo= github.com/aws/aws-sdk-go-v2/service/iam v1.19.8/go.mod h1:lf/oAjt//UvPsmnOgPT61F+q4K6U0q4zDd1s1yx2NZs= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 h1:5LHn8JQ0qvjD9L9JhMtylnkcw7j05GDZqM9Oin6hpr0= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25/go.mod h1:/95IA+0lMnzW6XzqYJRpjjsAbKEORVeO0anQqjd2CNU= github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 h1:5V7DWLBd7wTELVz5bPpwzYy/sikk0gsgZfj40X+l5OI= github.com/aws/aws-sdk-go-v2/service/sso v1.12.6/go.mod h1:Y1VOmit/Fn6Tz1uFAeCO6Q7M2fmfXSCLeL5INVYsLuY= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 h1:B8cauxOH1W1v7rd8RdI/MWnoR4Ze0wIHWrb90qczxj4= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6/go.mod h1:Lh/bc9XUf8CfOY6Jp5aIkQtN+j1mc+nExc+KXj9jx2s= github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 h1:bWNgNdRko2x6gqa0blfATqAZKZokPIeM1vfmQt2pnvM= github.com/aws/aws-sdk-go-v2/service/sts v1.18.7/go.mod h1:JuTnSoeePXmMVe9G8NcjjwgOKEfZ4cOjMuT2IBT/2eI= github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-tty v0.0.4 h1:NVikla9X8MN0SQAqCYzpGyXv0jY7MNl3HOWD2dkle7E= github.com/mattn/go-tty v0.0.4/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= ================================================ FILE: iso8601/iso8601.go ================================================ package iso8601 import "time" // Format outputs an ISO-8601 datetime string from the given time, // in a format compatible with all of the AWS SDKs func Format(t time.Time) string { return t.UTC().Format(time.RFC3339) } ================================================ FILE: iso8601/iso8601_test.go ================================================ package iso8601 import ( "testing" "time" ) func TestFormat(t *testing.T) { input, _ := time.Parse(time.RFC3339, "2009-02-04T21:00:57-08:00") want := "2009-02-05T05:00:57Z" result := Format(input) if result != want { t.Errorf("expected %s for %q got %s", want, input, result) } } func TestFormatForIssue655(t *testing.T) { input, _ := time.Parse(time.RFC3339, "2020-09-10T18:16:52+02:00") want := "2020-09-10T16:16:52Z" result := Format(input) if result != want { t.Errorf("expected %s for %q got %s", want, input, result) } } ================================================ FILE: main.go ================================================ package main import ( "os" "github.com/99designs/aws-vault/v7/cli" "github.com/alecthomas/kingpin/v2" ) // Version is provided at compile time var Version = "dev" func main() { app := kingpin.New("aws-vault", "A vault for securely storing and accessing AWS credentials in development environments.") app.Version(Version) a := cli.ConfigureGlobals(app) cli.ConfigureAddCommand(app, a) cli.ConfigureRemoveCommand(app, a) cli.ConfigureListCommand(app, a) cli.ConfigureRotateCommand(app, a) cli.ConfigureExecCommand(app, a) cli.ConfigureExportCommand(app, a) cli.ConfigureClearCommand(app, a) cli.ConfigureLoginCommand(app, a) cli.ConfigureProxyCommand(app) kingpin.MustParse(app.Parse(os.Args[1:])) } ================================================ FILE: prompt/kdialog.go ================================================ package prompt import ( "os/exec" "strings" ) func KDialogMfaPrompt(mfaSerial string) (string, error) { cmd := exec.Command("kdialog", "--inputbox", mfaPromptMessage(mfaSerial), "--title", "aws-vault") out, err := cmd.Output() if err != nil { return "", err } return strings.TrimSpace(string(out)), nil } func init() { if _, err := exec.LookPath("kdialog"); err == nil { Methods["kdialog"] = KDialogMfaPrompt } } ================================================ FILE: prompt/osascript.go ================================================ package prompt import ( "fmt" "os/exec" "strings" ) func OSAScriptMfaPrompt(mfaSerial string) (string, error) { cmd := exec.Command("osascript", "-e", fmt.Sprintf(` display dialog %q default answer "" buttons {"OK", "Cancel"} default button 1 text returned of the result return result`, mfaPromptMessage(mfaSerial))) out, err := cmd.Output() if err != nil { return "", err } return strings.TrimSpace(string(out)), nil } func init() { if _, err := exec.LookPath("osascript"); err == nil { Methods["osascript"] = OSAScriptMfaPrompt } } ================================================ FILE: prompt/prompt.go ================================================ package prompt import ( "fmt" "sort" ) type Func func(string) (string, error) var Methods = map[string]Func{} func Available() []string { methods := []string{} for k := range Methods { methods = append(methods, k) } sort.Strings(methods) return methods } func Method(s string) Func { m, ok := Methods[s] if !ok { panic(fmt.Sprintf("Prompt method %q doesn't exist", s)) } return m } func mfaPromptMessage(mfaSerial string) string { return fmt.Sprintf("Enter MFA code for %s: ", mfaSerial) } ================================================ FILE: prompt/terminal.go ================================================ package prompt import ( "fmt" "strings" "github.com/mattn/go-tty" ) func TerminalPrompt(message string) (string, error) { tty, err := tty.Open() if err != nil { return "", err } defer tty.Close() fmt.Fprint(tty.Output(), message) text, err := tty.ReadString() if err != nil { return "", err } return strings.TrimSpace(text), nil } func TerminalSecretPrompt(message string) (string, error) { tty, err := tty.Open() if err != nil { return "", err } defer tty.Close() fmt.Fprint(tty.Output(), message) text, err := tty.ReadPassword() if err != nil { return "", err } return strings.TrimSpace(text), nil } func TerminalMfaPrompt(mfaSerial string) (string, error) { return TerminalPrompt(mfaPromptMessage(mfaSerial)) } func init() { Methods["terminal"] = TerminalMfaPrompt } ================================================ FILE: prompt/wincredui_windows.go ================================================ package prompt import ( "errors" "strings" "syscall" "unsafe" ) const ( CREDUI_FLAGS_ALWAYS_SHOW_UI = 0x00080 CREDUI_FLAGS_GENERIC_CREDENTIALS = 0x40000 CREDUI_FLAGS_KEEP_USERNAME = 0x100000 ) type creduiInfoA struct { cbSize uint32 hwndParent uintptr pszMessageText *uint16 pszCaptionText *uint16 hbmBanner uintptr } func WinCredUiPrompt(mfaSerial string) (string, error) { info := &creduiInfoA{ hwndParent: 0, pszCaptionText: syscall.StringToUTF16Ptr("Enter MFA code for aws-vault"), pszMessageText: syscall.StringToUTF16Ptr(mfaPromptMessage(mfaSerial)), hbmBanner: 0, } info.cbSize = uint32(unsafe.Sizeof(*info)) passwordBuf := make([]uint16, 64) save := false flags := CREDUI_FLAGS_ALWAYS_SHOW_UI | CREDUI_FLAGS_KEEP_USERNAME | CREDUI_FLAGS_GENERIC_CREDENTIALS shortSerial := strings.ReplaceAll(strings.ReplaceAll(mfaSerial, "arn:aws:iam::", ""), ":mfa", "") ret, _, _ := syscall.NewLazyDLL("credui.dll").NewProc("CredUIPromptForCredentialsW").Call( uintptr(unsafe.Pointer(info)), uintptr(unsafe.Pointer(syscall.StringBytePtr("aws-vault"))), 0, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(shortSerial))), uintptr(len(shortSerial)+1), uintptr(unsafe.Pointer(&passwordBuf[0])), 64, uintptr(unsafe.Pointer(&save)), uintptr(flags), ) if ret != 0 { return "", errors.New("wincredui: call to CredUIPromptForCredentialsW failed") } return strings.TrimSpace(syscall.UTF16ToString(passwordBuf)), nil } func init() { Methods["wincredui"] = WinCredUiPrompt } ================================================ FILE: prompt/ykman.go ================================================ package prompt import ( "fmt" "log" "os" "os/exec" "strings" ) // YkmanProvider runs ykman to generate a OATH-TOTP token from the Yubikey device // To set up ykman, first run `ykman oath accounts add` func YkmanMfaProvider(mfaSerial string) (string, error) { args := []string{} yubikeyOathCredName := os.Getenv("YKMAN_OATH_CREDENTIAL_NAME") if yubikeyOathCredName == "" { yubikeyOathCredName = mfaSerial } // Get the serial number of the yubikey device to use. yubikeyDeviceSerial := os.Getenv("YKMAN_OATH_DEVICE_SERIAL") if yubikeyDeviceSerial != "" { // If the env var was set, extend args to support passing the serial. args = append(args, "--device", yubikeyDeviceSerial) } // default to v4 and above switch os.Getenv("AWS_VAULT_YKMAN_VERSION") { case "1", "2", "3": args = append(args, "oath", "code", "--single", yubikeyOathCredName) default: args = append(args, "oath", "accounts", "code", "--single", yubikeyOathCredName) } log.Printf("Fetching MFA code using `ykman %s`", strings.Join(args, " ")) cmd := exec.Command("ykman", args...) cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { return "", fmt.Errorf("ykman: %w", err) } return strings.TrimSpace(string(out)), nil } func init() { if _, err := exec.LookPath("ykman"); err == nil { Methods["ykman"] = YkmanMfaProvider } } ================================================ FILE: prompt/zenity.go ================================================ package prompt import ( "os/exec" "strings" ) func ZenityMfaPrompt(mfaSerial string) (string, error) { cmd := exec.Command("zenity", "--entry", "--title", "aws-vault", "--text", mfaPromptMessage(mfaSerial)) out, err := cmd.Output() if err != nil { return "", err } return strings.TrimSpace(string(out)), nil } func init() { if _, err := exec.LookPath("zenity"); err == nil { Methods["zenity"] = ZenityMfaPrompt } } ================================================ FILE: server/ec2alias_bsd.go ================================================ //go:build darwin || freebsd || openbsd // +build darwin freebsd openbsd package server import "os/exec" func installEc2EndpointNetworkAlias() ([]byte, error) { return exec.Command("ifconfig", "lo0", "alias", "169.254.169.254").CombinedOutput() } func removeEc2EndpointNetworkAlias() ([]byte, error) { return exec.Command("ifconfig", "lo0", "-alias", "169.254.169.254").CombinedOutput() } ================================================ FILE: server/ec2alias_linux.go ================================================ //go:build linux // +build linux package server import "os/exec" func installEc2EndpointNetworkAlias() ([]byte, error) { return exec.Command("ip", "addr", "add", "169.254.169.254/24", "dev", "lo", "label", "lo:0").CombinedOutput() } func removeEc2EndpointNetworkAlias() ([]byte, error) { return exec.Command("ip", "addr", "del", "169.254.169.254/24", "dev", "lo", "label", "lo:0").CombinedOutput() } ================================================ FILE: server/ec2alias_windows.go ================================================ //go:build windows // +build windows package server import ( "fmt" "os/exec" "strings" ) var alreadyRegisteredLocalised = []string{ "The object already exists", "Das Objekt ist bereits vorhanden", "El objeto ya existe", } var runAsAdministratorLocalised = []string{ "Run as administrator", // truncate before 'Umlaut' to avoid encoding problems coming from Windows cmd "Als Administrator ausf", "Ejecutar como administrador", } func msgFound(localised []string, toTest string) bool { for _, value := range localised { if strings.Contains(toTest, value) { return true } } return false } func runAndWrapAdminErrors(name string, arg ...string) ([]byte, error) { out, err := exec.Command(name, arg...).CombinedOutput() if msgFound(runAsAdministratorLocalised, string(out)) { err = fmt.Errorf("Creation of network alias for server mode requires elevated permissions, run as administrator", err) } return out, err } func installEc2EndpointNetworkAlias() ([]byte, error) { out, err := runAndWrapAdminErrors("netsh", "interface", "ipv4", "add", "address", "Loopback Pseudo-Interface 1", "169.254.169.254", "255.255.0.0") if msgFound(alreadyRegisteredLocalised, string(out)) { return []byte{}, nil } return out, err } func removeEc2EndpointNetworkAlias() ([]byte, error) { return runAndWrapAdminErrors("netsh", "interface", "ipv4", "delete", "address", "Loopback Pseudo-Interface 1", "169.254.169.254", "255.255.0.0") } ================================================ FILE: server/ec2proxy.go ================================================ package server import ( "fmt" "log" "net" "net/http" "net/http/httputil" "net/url" "os" "strings" "time" ) const ( ec2MetadataEndpointIP = "169.254.169.254" ec2MetadataEndpointAddr = "169.254.169.254:80" ) // StartProxy starts a http proxy server that listens on the standard EC2 Instance Metadata endpoint http://169.254.169.254:80/ // and forwards requests through to the running `aws-vault exec` command func StartProxy() error { var localServerURL, err = url.Parse(fmt.Sprintf("http://%s/", ec2CredentialsServerAddr)) if err != nil { return err } if output, err := installEc2EndpointNetworkAlias(); err != nil { return fmt.Errorf("%s: %s", strings.TrimSpace(string(output)), err.Error()) } l, err := net.Listen("tcp", ec2MetadataEndpointAddr) if err != nil { return err } handler := http.NewServeMux() handler.HandleFunc("/stop", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) go Shutdown() }) handler.Handle("/", httputil.NewSingleHostReverseProxy(localServerURL)) log.Printf("EC2 Instance Metadata endpoint proxy server running on %s", l.Addr()) return http.Serve(l, handler) } func IsProxyRunning() bool { _, err := net.DialTimeout("tcp", ec2MetadataEndpointAddr, time.Millisecond*10) return err == nil } func Shutdown() { _, err := removeEc2EndpointNetworkAlias() if err != nil { log.Fatalln(err) } os.Exit(0) } // StopProxy stops the http proxy server on the standard EC2 Instance Metadata endpoint func StopProxy() { _, _ = http.Get(fmt.Sprintf("http://%s/stop", ec2MetadataEndpointAddr)) //nolint } func awsVaultExecutable() string { awsVaultPath, err := os.Executable() if err != nil { return awsVaultPath } return os.Args[0] } ================================================ FILE: server/ec2proxy_default.go ================================================ //go:build !darwin && !freebsd && !openbsd && !linux // +build !darwin,!freebsd,!openbsd,!linux package server import ( "errors" "log" "os" "os/exec" "time" ) // StartEc2EndpointProxyServerProcess starts a `aws-vault proxy` process func StartEc2EndpointProxyServerProcess() error { log.Println("Starting `aws-vault proxy`") cmd := exec.Command(awsVaultExecutable(), "proxy") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { return err } time.Sleep(time.Second * 1) if !IsProxyRunning() { return errors.New("The EC2 Instance Metadata endpoint proxy server isn't running. Run `aws-vault proxy` as Administrator or root in the background and then try this command again") } return nil } ================================================ FILE: server/ec2proxy_unix.go ================================================ //go:build darwin || freebsd || openbsd || linux // +build darwin freebsd openbsd linux package server import ( "log" "os" "os/exec" ) // StartEc2EndpointProxyServerProcess starts a `aws-vault proxy` process func StartEc2EndpointProxyServerProcess() error { log.Println("Starting `aws-vault proxy` as root in the background") cmd := exec.Command("sudo", "-b", awsVaultExecutable(), "proxy") cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() } ================================================ FILE: server/ec2server.go ================================================ package server import ( "context" "encoding/json" "fmt" "log" "net" "net/http" "time" "github.com/99designs/aws-vault/v7/iso8601" "github.com/aws/aws-sdk-go-v2/aws" ) const ec2CredentialsServerAddr = "127.0.0.1:9099" // StartEc2CredentialsServer starts a EC2 Instance Metadata server and endpoint proxy func StartEc2CredentialsServer(ctx context.Context, credsProvider aws.CredentialsProvider, region string) error { credsCache := aws.NewCredentialsCache(credsProvider) // pre-fetch credentials so that we can respond quickly to the first request // SDKs seem to very aggressively timeout _, _ = credsCache.Retrieve(ctx) go startEc2CredentialsServer(credsCache, region) return nil } func startEc2CredentialsServer(credsProvider aws.CredentialsProvider, region string) { log.Printf("Starting EC2 Instance Metadata server on %s", ec2CredentialsServerAddr) router := http.NewServeMux() router.HandleFunc("/latest/meta-data/iam/security-credentials/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "local-credentials") }) // The AWS Go SDK checks the instance-id endpoint to validate the existence of EC2 Metadata router.HandleFunc("/latest/meta-data/instance-id/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "aws-vault") }) // The AWS .NET SDK checks this endpoint during obtaining credentials/refreshing them router.HandleFunc("/latest/meta-data/iam/info/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"Code" : "Success"}`) }) // used by AWS SDK to determine region router.HandleFunc("/latest/dynamic/instance-identity/document", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, `{"region": "`+region+`"}`) }) router.HandleFunc("/latest/meta-data/iam/security-credentials/local-credentials", credsHandler(credsProvider)) log.Fatalln(http.ListenAndServe(ec2CredentialsServerAddr, withLogging(withSecurityChecks(router)))) } // withSecurityChecks is middleware to protect the server from attack vectors func withSecurityChecks(next *http.ServeMux) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { // Check the remote ip is from the loopback, otherwise clients on the same network segment could // potentially route traffic via 169.254.169.254:80 // See https://developer.apple.com/library/content/qa/qa1357/_index.html ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } if !net.ParseIP(ip).IsLoopback() { http.Error(w, "Access denied from non-localhost address", http.StatusUnauthorized) return } // Check that the request is to 169.254.169.254 // Without this it's possible for an attacker to mount a DNS rebinding attack // See https://github.com/99designs/aws-vault/issues/578 if r.Host != ec2MetadataEndpointIP && r.Host != ec2MetadataEndpointAddr { http.Error(w, fmt.Sprintf("Access denied for host '%s'", r.Host), http.StatusUnauthorized) return } next.ServeHTTP(w, r) } } func credsHandler(credsProvider aws.CredentialsProvider) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { creds, err := credsProvider.Retrieve(r.Context()) if err != nil { http.Error(w, err.Error(), http.StatusGatewayTimeout) return } log.Printf("Serving credentials via http ****************%s, expiration of %s (%s)", creds.AccessKeyID[len(creds.AccessKeyID)-4:], creds.Expires.Format(time.RFC3339), time.Until(creds.Expires).String()) err = json.NewEncoder(w).Encode(map[string]interface{}{ "Code": "Success", "LastUpdated": iso8601.Format(time.Now()), "Type": "AWS-HMAC", "AccessKeyId": creds.AccessKeyID, "SecretAccessKey": creds.SecretAccessKey, "Token": creds.SessionToken, "Expiration": iso8601.Format(creds.Expires), }) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } } } ================================================ FILE: server/ecsserver.go ================================================ package server import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "log" "net" "net/http" "strings" "sync" "github.com/99designs/aws-vault/v7/iso8601" "github.com/99designs/aws-vault/v7/vault" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" ) func writeErrorMessage(w http.ResponseWriter, msg string, statusCode int) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(statusCode) if err := json.NewEncoder(w).Encode(map[string]string{"Message": msg}); err != nil { log.Println(err.Error()) } } func withAuthorizationCheck(authToken string, next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("Authorization") != authToken { writeErrorMessage(w, "invalid Authorization token", http.StatusForbidden) return } next.ServeHTTP(w, r) } } func writeCredsToResponse(creds aws.Credentials, w http.ResponseWriter) { err := json.NewEncoder(w).Encode(map[string]string{ "AccessKeyId": creds.AccessKeyID, "SecretAccessKey": creds.SecretAccessKey, "Token": creds.SessionToken, "Expiration": iso8601.Format(creds.Expires), }) if err != nil { writeErrorMessage(w, err.Error(), http.StatusInternalServerError) return } } func generateRandomString() string { b := make([]byte, 30) if _, err := rand.Read(b); err != nil { panic(err) } return base64.RawURLEncoding.EncodeToString(b) } type EcsServer struct { listener net.Listener authToken string server http.Server cache sync.Map baseCredsProvider aws.CredentialsProvider config *vault.ProfileConfig } func NewEcsServer(ctx context.Context, baseCredsProvider aws.CredentialsProvider, config *vault.ProfileConfig, authToken string, port int, lazyLoadBaseCreds bool) (*EcsServer, error) { listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) if err != nil { return nil, err } if authToken == "" { authToken = generateRandomString() } credsCache := aws.NewCredentialsCache(baseCredsProvider) if !lazyLoadBaseCreds { _, err := credsCache.Retrieve(ctx) if err != nil { return nil, fmt.Errorf("Retrieving creds: %w", err) } } e := &EcsServer{ listener: listener, authToken: authToken, baseCredsProvider: credsCache, config: config, } router := http.NewServeMux() router.HandleFunc("/", e.DefaultRoute) router.HandleFunc("/role-arn/", e.AssumeRoleArnRoute) e.server.Handler = withLogging(withAuthorizationCheck(e.authToken, router.ServeHTTP)) return e, nil } func (e *EcsServer) BaseURL() string { return fmt.Sprintf("http://%s", e.listener.Addr().String()) } func (e *EcsServer) AuthToken() string { return e.authToken } func (e *EcsServer) Serve() error { return e.server.Serve(e.listener) } func (e *EcsServer) DefaultRoute(w http.ResponseWriter, r *http.Request) { creds, err := e.baseCredsProvider.Retrieve(r.Context()) if err != nil { writeErrorMessage(w, err.Error(), http.StatusInternalServerError) return } writeCredsToResponse(creds, w) } func (e *EcsServer) getRoleProvider(roleArn string) aws.CredentialsProvider { var roleProviderCache *aws.CredentialsCache v, ok := e.cache.Load(roleArn) if ok { roleProviderCache = v.(*aws.CredentialsCache) } else { cfg := vault.NewAwsConfigWithCredsProvider(e.baseCredsProvider, e.config.Region, e.config.STSRegionalEndpoints) roleProvider := &vault.AssumeRoleProvider{ StsClient: sts.NewFromConfig(cfg), RoleARN: roleArn, Duration: e.config.AssumeRoleDuration, } roleProviderCache = aws.NewCredentialsCache(roleProvider) e.cache.Store(roleArn, roleProviderCache) } return roleProviderCache } func (e *EcsServer) AssumeRoleArnRoute(w http.ResponseWriter, r *http.Request) { roleArn := strings.TrimPrefix(r.URL.Path, "/role-arn/") roleProvider := e.getRoleProvider(roleArn) creds, err := roleProvider.Retrieve(r.Context()) if err != nil { writeErrorMessage(w, err.Error(), http.StatusInternalServerError) return } writeCredsToResponse(creds, w) } ================================================ FILE: server/httplog.go ================================================ package server import ( "log" "net/http" "time" ) type loggingMiddlewareResponseWriter struct { http.ResponseWriter Code int } func (w *loggingMiddlewareResponseWriter) WriteHeader(statusCode int) { w.Code = statusCode w.ResponseWriter.WriteHeader(statusCode) } func withLogging(handler http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestStart := time.Now() w2 := &loggingMiddlewareResponseWriter{w, http.StatusOK} handler.ServeHTTP(w2, r) log.Printf("http: %s: %d %s %s (%s)", r.RemoteAddr, w2.Code, r.Method, r.URL, time.Since(requestStart)) }) } ================================================ FILE: vault/assumeroleprovider.go ================================================ package vault import ( "context" "fmt" "log" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" ) // AssumeRoleProvider retrieves temporary credentials from STS using AssumeRole type AssumeRoleProvider struct { StsClient *sts.Client RoleARN string RoleSessionName string ExternalID string Duration time.Duration Tags map[string]string TransitiveTagKeys []string SourceIdentity string Mfa } // Retrieve generates a new set of temporary credentials using STS AssumeRole func (p *AssumeRoleProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { role, err := p.RetrieveStsCredentials(ctx) if err != nil { return aws.Credentials{}, err } return aws.Credentials{ AccessKeyID: *role.AccessKeyId, SecretAccessKey: *role.SecretAccessKey, SessionToken: *role.SessionToken, CanExpire: true, Expires: *role.Expiration, }, nil } func (p *AssumeRoleProvider) roleSessionName() string { if p.RoleSessionName == "" { // Try to work out a role name that will hopefully end up unique. return fmt.Sprintf("%d", time.Now().UTC().UnixNano()) } return p.RoleSessionName } func (p *AssumeRoleProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { var err error input := &sts.AssumeRoleInput{ RoleArn: aws.String(p.RoleARN), RoleSessionName: aws.String(p.roleSessionName()), DurationSeconds: aws.Int32(int32(p.Duration.Seconds())), } if p.ExternalID != "" { input.ExternalId = aws.String(p.ExternalID) } if p.MfaSerial != "" { input.SerialNumber = aws.String(p.MfaSerial) input.TokenCode, err = p.GetMfaToken() if err != nil { return nil, err } } if len(p.Tags) > 0 { input.Tags = make([]ststypes.Tag, 0) for key, value := range p.Tags { tag := ststypes.Tag{ Key: aws.String(key), Value: aws.String(value), } input.Tags = append(input.Tags, tag) } } if len(p.TransitiveTagKeys) > 0 { input.TransitiveTagKeys = p.TransitiveTagKeys } if p.SourceIdentity != "" { input.SourceIdentity = aws.String(p.SourceIdentity) } resp, err := p.StsClient.AssumeRole(ctx, input) if err != nil { return nil, err } log.Printf("Generated credentials %s using AssumeRole, expires in %s", FormatKeyForDisplay(*resp.Credentials.AccessKeyId), time.Until(*resp.Credentials.Expiration).String()) return resp.Credentials, nil } ================================================ FILE: vault/assumerolewithwebidentityprovider.go ================================================ package vault import ( "context" "fmt" "log" "os" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" ) // AssumeRoleWithWebIdentityProvider retrieves temporary credentials from STS using AssumeRoleWithWebIdentity type AssumeRoleWithWebIdentityProvider struct { StsClient *sts.Client RoleARN string RoleSessionName string WebIdentityTokenFile string WebIdentityTokenProcess string ExternalID string Duration time.Duration } // Retrieve generates a new set of temporary credentials using STS AssumeRoleWithWebIdentity func (p *AssumeRoleWithWebIdentityProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { creds, err := p.RetrieveStsCredentials(ctx) if err != nil { return aws.Credentials{}, err } return aws.Credentials{ AccessKeyID: aws.ToString(creds.AccessKeyId), SecretAccessKey: aws.ToString(creds.SecretAccessKey), SessionToken: aws.ToString(creds.SessionToken), CanExpire: true, Expires: aws.ToTime(creds.Expiration), }, nil } func (p *AssumeRoleWithWebIdentityProvider) roleSessionName() string { if p.RoleSessionName == "" { // Try to work out a role name that will hopefully end up unique. return fmt.Sprintf("%d", time.Now().UTC().UnixNano()) } return p.RoleSessionName } func (p *AssumeRoleWithWebIdentityProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { var err error webIdentityToken, err := p.webIdentityToken() if err != nil { return nil, err } resp, err := p.StsClient.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityInput{ RoleArn: aws.String(p.RoleARN), RoleSessionName: aws.String(p.roleSessionName()), DurationSeconds: aws.Int32(int32(p.Duration.Seconds())), WebIdentityToken: aws.String(webIdentityToken), }) if err != nil { return nil, err } log.Printf("Generated credentials %s using AssumeRoleWithWebIdentity, expires in %s", FormatKeyForDisplay(*resp.Credentials.AccessKeyId), time.Until(*resp.Credentials.Expiration).String()) return resp.Credentials, nil } func (p *AssumeRoleWithWebIdentityProvider) webIdentityToken() (string, error) { // Read OpenID Connect token from WebIdentityTokenFile if p.WebIdentityTokenFile != "" { b, err := os.ReadFile(p.WebIdentityTokenFile) if err != nil { return "", fmt.Errorf("unable to read file at %s: %v", p.WebIdentityTokenFile, err) } return string(b), nil } // Exec WebIdentityTokenProcess to retrieve OpenID Connect token return executeProcess(p.WebIdentityTokenProcess) } ================================================ FILE: vault/cachedsessionprovider.go ================================================ package vault import ( "context" "log" "time" "github.com/aws/aws-sdk-go-v2/aws" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" ) type StsSessionProvider interface { aws.CredentialsProvider RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) } // CachedSessionProvider retrieves cached credentials from the keyring, or if no credentials are cached // retrieves temporary credentials using the CredentialsFunc type CachedSessionProvider struct { SessionKey SessionMetadata SessionProvider StsSessionProvider Keyring *SessionKeyring ExpiryWindow time.Duration } func (p *CachedSessionProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { creds, err := p.Keyring.Get(p.SessionKey) if err != nil || time.Until(*creds.Expiration) < p.ExpiryWindow { // lookup missed, we need to create a new one. creds, err = p.SessionProvider.RetrieveStsCredentials(ctx) if err != nil { return nil, err } err = p.Keyring.Set(p.SessionKey, creds) if err != nil { return nil, err } } else { log.Printf("Re-using cached credentials %s from %s, expires in %s", FormatKeyForDisplay(*creds.AccessKeyId), p.SessionKey.Type, time.Until(*creds.Expiration).String()) } return creds, nil } // Retrieve returns cached credentials from the keyring, or if no credentials are cached // generates a new set of temporary credentials using the CredentialsFunc func (p *CachedSessionProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { creds, err := p.RetrieveStsCredentials(ctx) if err != nil { return aws.Credentials{}, err } return aws.Credentials{ AccessKeyID: aws.ToString(creds.AccessKeyId), SecretAccessKey: aws.ToString(creds.SecretAccessKey), SessionToken: aws.ToString(creds.SessionToken), CanExpire: true, Expires: aws.ToTime(creds.Expiration), }, nil } ================================================ FILE: vault/config.go ================================================ package vault import ( "errors" "fmt" "log" "os" "path/filepath" "strings" "time" ini "gopkg.in/ini.v1" ) const ( // DefaultSessionDuration is the default duration for GetSessionToken or AssumeRole sessions DefaultSessionDuration = time.Hour * 1 // DefaultChainedSessionDuration is the default duration for GetSessionToken sessions when chaining DefaultChainedSessionDuration = time.Hour * 8 defaultSectionName = "default" roleChainingMaximumDuration = 1 * time.Hour ) func init() { ini.PrettyFormat = false } // ConfigFile is an abstraction over what is in ~/.aws/config type ConfigFile struct { Path string iniFile *ini.File } // configPath returns either $AWS_CONFIG_FILE or ~/.aws/config func configPath() (string, error) { file := os.Getenv("AWS_CONFIG_FILE") if file == "" { home, err := os.UserHomeDir() if err != nil { return "", err } file = filepath.Join(home, "/.aws/config") } else { log.Printf("Using AWS_CONFIG_FILE value: %s", file) } return file, nil } // createConfigFilesIfMissing will create the config directory and file if they do not exist func createConfigFilesIfMissing() error { file, err := configPath() if err != nil { return err } dir := filepath.Dir(file) if _, err := os.Stat(dir); os.IsNotExist(err) { err = os.Mkdir(dir, 0700) if err != nil { return err } log.Printf("Config directory %s created", dir) } if _, err := os.Stat(file); os.IsNotExist(err) { newFile, err := os.Create(file) if err != nil { log.Printf("Config file %s not created", file) return err } newFile.Close() log.Printf("Config file %s created", file) } return nil } // LoadConfig loads and parses a config file. No error is returned if the file doesn't exist func LoadConfig(path string) (*ConfigFile, error) { config := &ConfigFile{ Path: path, } if _, err := os.Stat(path); err == nil { if parseErr := config.parseFile(); parseErr != nil { return nil, parseErr } } else { log.Printf("Config file %s doesn't exist so lets create it", path) err := createConfigFilesIfMissing() if err != nil { return nil, err } if parseErr := config.parseFile(); parseErr != nil { return nil, parseErr } } return config, nil } // LoadConfigFromEnv finds the config file from the environment func LoadConfigFromEnv() (*ConfigFile, error) { file, err := configPath() if err != nil { return nil, err } log.Printf("Loading config file %s", file) return LoadConfig(file) } func (c *ConfigFile) parseFile() error { log.Printf("Parsing config file %s", c.Path) f, err := ini.LoadSources(ini.LoadOptions{ AllowNestedValues: true, InsensitiveSections: false, InsensitiveKeys: true, }, c.Path) if err != nil { return fmt.Errorf("Error parsing config file %s: %w", c.Path, err) } c.iniFile = f return nil } // ProfileSection is a profile section of the config file type ProfileSection struct { Name string `ini:"-"` MfaSerial string `ini:"mfa_serial,omitempty"` RoleARN string `ini:"role_arn,omitempty"` ExternalID string `ini:"external_id,omitempty"` Region string `ini:"region,omitempty"` RoleSessionName string `ini:"role_session_name,omitempty"` DurationSeconds uint `ini:"duration_seconds,omitempty"` SourceProfile string `ini:"source_profile,omitempty"` IncludeProfile string `ini:"include_profile,omitempty"` SSOSession string `ini:"sso_session,omitempty"` SSOStartURL string `ini:"sso_start_url,omitempty"` SSORegion string `ini:"sso_region,omitempty"` SSOAccountID string `ini:"sso_account_id,omitempty"` SSORoleName string `ini:"sso_role_name,omitempty"` WebIdentityTokenFile string `ini:"web_identity_token_file,omitempty"` WebIdentityTokenProcess string `ini:"web_identity_token_process,omitempty"` STSRegionalEndpoints string `ini:"sts_regional_endpoints,omitempty"` SessionTags string `ini:"session_tags,omitempty"` TransitiveSessionTags string `ini:"transitive_session_tags,omitempty"` SourceIdentity string `ini:"source_identity,omitempty"` CredentialProcess string `ini:"credential_process,omitempty"` MfaProcess string `ini:"mfa_process,omitempty"` } // SSOSessionSection is a [sso-session] section of the config file type SSOSessionSection struct { Name string `ini:"-"` SSOStartURL string `ini:"sso_start_url,omitempty"` SSORegion string `ini:"sso_region,omitempty"` SSORegistrationScopes string `ini:"sso_registration_scopes,omitempty"` } func (s ProfileSection) IsEmpty() bool { s.Name = "" return s == ProfileSection{} } // ProfileSections returns all the profile sections in the config func (c *ConfigFile) ProfileSections() []ProfileSection { result := []ProfileSection{} if c.iniFile == nil { return result } for _, section := range c.iniFile.SectionStrings() { if section == defaultSectionName || strings.HasPrefix(section, "profile ") { profile, _ := c.ProfileSection(strings.TrimPrefix(section, "profile ")) // ignore the default profile if it's empty if section == defaultSectionName && profile.IsEmpty() { continue } result = append(result, profile) } else if strings.HasPrefix(section, "sso-session ") { // Not a profile continue } else { log.Printf("Unrecognised ini file section: %s", section) continue } } return result } // ProfileSection returns the profile section with the matching name. If there isn't any, // an empty profile with the provided name is returned, along with false. func (c *ConfigFile) ProfileSection(name string) (ProfileSection, bool) { profile := ProfileSection{ Name: name, } if c.iniFile == nil { return profile, false } // default profile name has a slightly different section format sectionName := "profile " + name if name == defaultSectionName { sectionName = defaultSectionName } section, err := c.iniFile.GetSection(sectionName) if err != nil { return profile, false } if err = section.MapTo(&profile); err != nil { panic(err) } return profile, true } // SSOSessionSection returns the [sso-session] section with the matching name. If there isn't any, // an empty sso-session with the provided name is returned, along with false. func (c *ConfigFile) SSOSessionSection(name string) (SSOSessionSection, bool) { ssoSession := SSOSessionSection{ Name: name, } if c.iniFile == nil { return ssoSession, false } sectionName := "sso-session " + name section, err := c.iniFile.GetSection(sectionName) if err != nil { return ssoSession, false } if err = section.MapTo(&ssoSession); err != nil { panic(err) } return ssoSession, true } func (c *ConfigFile) Save() error { return c.iniFile.SaveTo(c.Path) } // Add the profile to the configuration file func (c *ConfigFile) Add(profile ProfileSection) error { if c.iniFile == nil { return errors.New("No iniFile to add to") } // default profile name has a slightly different section format sectionName := "profile " + profile.Name if profile.Name == defaultSectionName { sectionName = defaultSectionName } section, err := c.iniFile.NewSection(sectionName) if err != nil { return fmt.Errorf("Error creating section %q: %v", profile.Name, err) } if err = section.ReflectFrom(&profile); err != nil { return fmt.Errorf("Error mapping profile to ini file: %v", err) } return c.Save() } // ProfileNames returns a slice of profile names from the AWS config func (c *ConfigFile) ProfileNames() []string { profileNames := []string{} for _, profile := range c.ProfileSections() { profileNames = append(profileNames, profile.Name) } return profileNames } // ConfigLoader loads config from configfile and environment variables type ConfigLoader struct { BaseConfig ProfileConfig File *ConfigFile ActiveProfile string visitedProfiles []string } func NewConfigLoader(baseConfig ProfileConfig, file *ConfigFile, activeProfile string) *ConfigLoader { return &ConfigLoader{ BaseConfig: baseConfig, File: file, ActiveProfile: activeProfile, } } func (cl *ConfigLoader) visitProfile(name string) bool { for _, p := range cl.visitedProfiles { if p == name { return false } } cl.visitedProfiles = append(cl.visitedProfiles, name) return true } func (cl *ConfigLoader) resetLoopDetection() { cl.visitedProfiles = []string{} } func (cl *ConfigLoader) populateFromDefaults(config *ProfileConfig) { if config.AssumeRoleDuration == 0 { config.AssumeRoleDuration = DefaultSessionDuration } if config.GetFederationTokenDuration == 0 { config.GetFederationTokenDuration = DefaultSessionDuration } if config.NonChainedGetSessionTokenDuration == 0 { config.NonChainedGetSessionTokenDuration = DefaultSessionDuration } if config.ChainedGetSessionTokenDuration == 0 { config.ChainedGetSessionTokenDuration = DefaultChainedSessionDuration } } func (cl *ConfigLoader) populateFromConfigFile(config *ProfileConfig, profileName string) error { if !cl.visitProfile(profileName) { return fmt.Errorf("Loop detected in config file for profile '%s'", profileName) } psection, ok := cl.File.ProfileSection(profileName) if !ok { // ignore missing profiles log.Printf("Profile '%s' missing in config file", profileName) } if config.MfaSerial == "" { config.MfaSerial = psection.MfaSerial } if config.RoleARN == "" { config.RoleARN = psection.RoleARN } if config.ExternalID == "" { config.ExternalID = psection.ExternalID } if config.Region == "" { config.Region = psection.Region } if config.RoleSessionName == "" { config.RoleSessionName = psection.RoleSessionName } if config.AssumeRoleDuration == 0 { config.AssumeRoleDuration = time.Duration(psection.DurationSeconds) * time.Second } if config.SourceProfileName == "" { config.SourceProfileName = psection.SourceProfile } if config.SSOSession == "" { config.SSOSession = psection.SSOSession if psection.SSOSession != "" { // Populate profile with values from [sso-session]. ssoSection, ok := cl.File.SSOSessionSection(psection.SSOSession) if ok { config.SSOStartURL = ssoSection.SSOStartURL config.SSORegion = ssoSection.SSORegion config.SSORegistrationScopes = ssoSection.SSORegistrationScopes } else { // ignore missing profiles log.Printf("[sso-session] '%s' missing in config file", psection.SSOSession) } } } if config.SSOStartURL == "" { config.SSOStartURL = psection.SSOStartURL } if config.SSORegion == "" { config.SSORegion = psection.SSORegion } if config.SSOAccountID == "" { config.SSOAccountID = psection.SSOAccountID } if config.SSORoleName == "" { config.SSORoleName = psection.SSORoleName } if config.WebIdentityTokenFile == "" { config.WebIdentityTokenFile = psection.WebIdentityTokenFile } if config.WebIdentityTokenProcess == "" { config.WebIdentityTokenProcess = psection.WebIdentityTokenProcess } if config.STSRegionalEndpoints == "" { config.STSRegionalEndpoints = psection.STSRegionalEndpoints } if config.SourceIdentity == "" { config.SourceIdentity = psection.SourceIdentity } if config.CredentialProcess == "" { config.CredentialProcess = psection.CredentialProcess } if config.MfaProcess == "" { config.MfaProcess = psection.MfaProcess } if sessionTags := psection.SessionTags; sessionTags != "" && config.SessionTags == nil { err := config.SetSessionTags(sessionTags) if err != nil { return fmt.Errorf("Failed to parse session_tags profile setting: %s", err) } } if transitiveSessionTags := psection.TransitiveSessionTags; transitiveSessionTags != "" && config.TransitiveSessionTags == nil { config.SetTransitiveSessionTags(transitiveSessionTags) } if psection.IncludeProfile != "" { err := cl.populateFromConfigFile(config, psection.IncludeProfile) if err != nil { return err } } else if profileName != defaultSectionName { err := cl.populateFromConfigFile(config, defaultSectionName) if err != nil { return err } } // Ignore source_profile if it recursively refers to the profile if config.SourceProfileName == config.ProfileName { config.SourceProfileName = "" } return nil } func (cl *ConfigLoader) populateFromEnv(profile *ProfileConfig) { if region := os.Getenv("AWS_REGION"); region != "" && profile.Region == "" { log.Printf("Using region %q from AWS_REGION", region) profile.Region = region } if region := os.Getenv("AWS_DEFAULT_REGION"); region != "" && profile.Region == "" { log.Printf("Using region %q from AWS_DEFAULT_REGION", region) profile.Region = region } if stsRegionalEndpoints := os.Getenv("AWS_STS_REGIONAL_ENDPOINTS"); stsRegionalEndpoints != "" && profile.STSRegionalEndpoints == "" { log.Printf("Using %q from AWS_STS_REGIONAL_ENDPOINTS", stsRegionalEndpoints) profile.STSRegionalEndpoints = stsRegionalEndpoints } if mfaSerial := os.Getenv("AWS_MFA_SERIAL"); mfaSerial != "" && profile.MfaSerial == "" { log.Printf("Using mfa_serial %q from AWS_MFA_SERIAL", mfaSerial) profile.MfaSerial = mfaSerial } var err error if assumeRoleTTL := os.Getenv("AWS_ASSUME_ROLE_TTL"); assumeRoleTTL != "" && profile.AssumeRoleDuration == 0 { profile.AssumeRoleDuration, err = time.ParseDuration(assumeRoleTTL) if err == nil { log.Printf("Using duration_seconds %q from AWS_ASSUME_ROLE_TTL", profile.AssumeRoleDuration) } } if sessionTTL := os.Getenv("AWS_SESSION_TOKEN_TTL"); sessionTTL != "" && profile.NonChainedGetSessionTokenDuration == 0 { profile.NonChainedGetSessionTokenDuration, err = time.ParseDuration(sessionTTL) if err == nil { log.Printf("Using a session duration of %q from AWS_SESSION_TOKEN_TTL", profile.NonChainedGetSessionTokenDuration) } } if sessionTTL := os.Getenv("AWS_CHAINED_SESSION_TOKEN_TTL"); sessionTTL != "" && profile.ChainedGetSessionTokenDuration == 0 { profile.ChainedGetSessionTokenDuration, err = time.ParseDuration(sessionTTL) if err == nil { log.Printf("Using a cached MFA session duration of %q from AWS_CACHED_SESSION_TOKEN_TTL", profile.ChainedGetSessionTokenDuration) } } if federationTokenTTL := os.Getenv("AWS_FEDERATION_TOKEN_TTL"); federationTokenTTL != "" && profile.GetFederationTokenDuration == 0 { profile.GetFederationTokenDuration, err = time.ParseDuration(federationTokenTTL) if err == nil { log.Printf("Using a session duration of %q from AWS_FEDERATION_TOKEN_TTL", profile.GetFederationTokenDuration) } } // AWS_ROLE_ARN, AWS_ROLE_SESSION_NAME, AWS_SESSION_TAGS, AWS_TRANSITIVE_TAGS and AWS_SOURCE_IDENTITY only apply to the target profile if profile.ProfileName == cl.ActiveProfile { if roleARN := os.Getenv("AWS_ROLE_ARN"); roleARN != "" && profile.RoleARN == "" { log.Printf("Using role_arn %q from AWS_ROLE_ARN", roleARN) profile.RoleARN = roleARN } if roleSessionName := os.Getenv("AWS_ROLE_SESSION_NAME"); roleSessionName != "" && profile.RoleSessionName == "" { log.Printf("Using role_session_name %q from AWS_ROLE_SESSION_NAME", roleSessionName) profile.RoleSessionName = roleSessionName } if sessionTags := os.Getenv("AWS_SESSION_TAGS"); sessionTags != "" && profile.SessionTags == nil { err := profile.SetSessionTags(sessionTags) if err != nil { log.Fatalf("Failed to parse AWS_SESSION_TAGS environment variable: %s", err) } log.Printf("Using session_tags %v from AWS_SESSION_TAGS", profile.SessionTags) } if transitiveSessionTags := os.Getenv("AWS_TRANSITIVE_TAGS"); transitiveSessionTags != "" && profile.TransitiveSessionTags == nil { profile.SetTransitiveSessionTags(transitiveSessionTags) log.Printf("Using transitive_session_tags %v from AWS_TRANSITIVE_TAGS", profile.TransitiveSessionTags) } if sourceIdentity := os.Getenv("AWS_SOURCE_IDENTITY"); sourceIdentity != "" && profile.SourceIdentity == "" { profile.SourceIdentity = sourceIdentity log.Printf("Using source_identity %v from AWS_SOURCE_IDENTITY", profile.SourceIdentity) } } } func (cl *ConfigLoader) hydrateSourceConfig(config *ProfileConfig) error { if config.SourceProfileName != "" { sc, err := cl.GetProfileConfig(config.SourceProfileName) if err != nil { return err } sc.ChainedFromProfile = config config.SourceProfile = sc } return nil } // GetProfileConfig loads the profile from the config file and environment variables into config func (cl *ConfigLoader) GetProfileConfig(profileName string) (*ProfileConfig, error) { config := cl.BaseConfig config.ProfileName = profileName cl.populateFromEnv(&config) cl.resetLoopDetection() err := cl.populateFromConfigFile(&config, profileName) if err != nil { return nil, err } cl.populateFromDefaults(&config) err = cl.hydrateSourceConfig(&config) if err != nil { return nil, err } return &config, nil } // ProfileConfig is a collection of configuration options for creating temporary credentials type ProfileConfig struct { // ProfileName specifies the name of the profile config ProfileName string // SourceProfile is the profile where credentials come from SourceProfileName string // SourceProfile is the profile where credentials come from SourceProfile *ProfileConfig // ChainedFromProfile is the profile that used this profile as its source profile ChainedFromProfile *ProfileConfig // Region is the AWS region Region string // STSRegionalEndpoints sets STS endpoint resolution logic, must be "regional" or "legacy" STSRegionalEndpoints string // Mfa config MfaSerial string MfaToken string MfaPromptMethod string // MfaProcess specifies external command to run to get an MFA token MfaProcess string // AssumeRole config RoleARN string RoleSessionName string ExternalID string // AssumeRoleWithWebIdentity config WebIdentityTokenFile string WebIdentityTokenProcess string // GetSessionTokenDuration specifies the wanted duration for credentials generated with AssumeRole AssumeRoleDuration time.Duration // NonChainedGetSessionTokenDuration specifies the wanted duration for credentials generated with GetSessionToken NonChainedGetSessionTokenDuration time.Duration // ChainedGetSessionTokenDuration specifies the wanted duration for credentials generated with GetSessionToken when chaining ChainedGetSessionTokenDuration time.Duration // GetFederationTokenDuration specifies the wanted duration for credentials generated with GetFederationToken GetFederationTokenDuration time.Duration // SSOSession specifies the [sso-session] section name. SSOSession string // SSOStartURL specifies the URL for the AWS IAM Identity Center user portal, legacy option. SSOStartURL string // SSORegion specifies the region for the AWS IAM Identity Center user portal, legacy option. SSORegion string // SSORegistrationScopes specifies registration scopes for the AWS IAM Identity Center user portal. SSORegistrationScopes string // SSOAccountID specifies the AWS account ID for the profile. SSOAccountID string // SSORoleName specifies the AWS IAM Role name to target. SSORoleName string // SSOUseStdout specifies that the system browser should not be automatically opened SSOUseStdout bool // SessionTags specifies assumed role Session Tags SessionTags map[string]string // TransitiveSessionTags specifies assumed role Transitive Session Tags keys TransitiveSessionTags []string // SourceIdentity specifies assumed role Source Identity SourceIdentity string // CredentialProcess specifies external command to run to get an AWS credential CredentialProcess string } // SetSessionTags parses a comma separated key=vaue string and sets Config.SessionTags map func (c *ProfileConfig) SetSessionTags(s string) error { c.SessionTags = make(map[string]string) for _, tag := range strings.Split(s, ",") { kvPair := strings.SplitN(tag, "=", 2) if len(kvPair) != 2 { return errors.New("session tags string must be =,[=[,...]]") } c.SessionTags[strings.TrimSpace(kvPair[0])] = strings.TrimSpace(kvPair[1]) } return nil } // SetTransitiveSessionTags parses a comma separated string and sets Config.TransitiveSessionTags func (c *ProfileConfig) SetTransitiveSessionTags(s string) { for _, tag := range strings.Split(s, ",") { if tag = strings.TrimSpace(tag); tag != "" { c.TransitiveSessionTags = append(c.TransitiveSessionTags, tag) } } } func (c *ProfileConfig) IsChained() bool { return c.ChainedFromProfile != nil } func (c *ProfileConfig) HasSourceProfile() bool { return c.SourceProfile != nil } func (c *ProfileConfig) HasMfaSerial() bool { return c.MfaSerial != "" } func (c *ProfileConfig) HasRole() bool { return c.RoleARN != "" } func (c *ProfileConfig) HasSSOSession() bool { return c.SSOSession != "" } func (c *ProfileConfig) HasSSOStartURL() bool { return c.SSOStartURL != "" } func (c *ProfileConfig) HasWebIdentity() bool { return c.WebIdentityTokenFile != "" || c.WebIdentityTokenProcess != "" } func (c *ProfileConfig) HasCredentialProcess() bool { return c.CredentialProcess != "" } func (c *ProfileConfig) GetSessionTokenDuration() time.Duration { if c.IsChained() { return c.ChainedGetSessionTokenDuration } return c.NonChainedGetSessionTokenDuration } ================================================ FILE: vault/config_test.go ================================================ package vault_test import ( "bytes" "fmt" "os" "reflect" "testing" "github.com/99designs/aws-vault/v7/vault" "github.com/google/go-cmp/cmp" ) // see http://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html var exampleConfig = []byte(`# an example profile file [default] region=us-west-2 output=json [profile user2] REGION=us-east-1 output=text [profile withsource] source_profile=user2 region=us-east-1 [profile withMFA] source_profile=user2 Role_Arn=arn:aws:iam::4451234513441615400570:role/aws_admin mfa_Serial=arn:aws:iam::1234513441:mfa/blah Region=us-east-1 duration_seconds=1200 sts_regional_endpoints=legacy [profile testincludeprofile1] region=us-east-1 [profile testincludeprofile2] include_profile=testincludeprofile1 [profile with-sso-session] sso_session = moon-sso sso_account_id=123456 region = moon-1 # Different from sso region [sso-session moon-sso] sso_start_url = https://d-123456789.example.com/start sso_region = moon-2 # Different from profile region sso_registration_scopes = sso:account:access `) var nestedConfig = []byte(`[default] [profile testing] aws_access_key_id=foo aws_secret_access_key=bar region=us-west-2 s3= max_concurrent_requests=10 max_queue_size=1000 `) var defaultsOnlyConfigWithHeader = []byte(`[default] region=us-west-2 output=json `) func newConfigFile(t *testing.T, b []byte) string { t.Helper() f, err := os.CreateTemp("", "aws-config") if err != nil { t.Fatal(err) } if err := os.WriteFile(f.Name(), b, 0600); err != nil { t.Fatal(err) } return f.Name() } func TestProfileNameCaseSensitivity(t *testing.T) { f := newConfigFile(t, exampleConfig) defer os.Remove(f) cfg, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } def, ok := cfg.ProfileSection("withMFA") if !ok { t.Fatalf("Expected to match profile withMFA") } expectedMfaSerial := "arn:aws:iam::1234513441:mfa/blah" if def.MfaSerial != expectedMfaSerial { t.Fatalf("Expected %s, got %s", expectedMfaSerial, def.MfaSerial) } } func TestConfigParsingProfiles(t *testing.T) { f := newConfigFile(t, exampleConfig) defer os.Remove(f) cfg, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } var testCases = []struct { expected vault.ProfileSection ok bool }{ {vault.ProfileSection{Name: "user2", Region: "us-east-1"}, true}, {vault.ProfileSection{Name: "withsource", SourceProfile: "user2", Region: "us-east-1"}, true}, {vault.ProfileSection{Name: "withMFA", MfaSerial: "arn:aws:iam::1234513441:mfa/blah", RoleARN: "arn:aws:iam::4451234513441615400570:role/aws_admin", Region: "us-east-1", DurationSeconds: 1200, SourceProfile: "user2", STSRegionalEndpoints: "legacy"}, true}, {vault.ProfileSection{Name: "nopenotthere"}, false}, } for _, tc := range testCases { t.Run(fmt.Sprintf("profile_%s", tc.expected.Name), func(t *testing.T) { actual, ok := cfg.ProfileSection(tc.expected.Name) if ok != tc.ok { t.Fatalf("Expected second param to be %v, got %v", tc.ok, ok) } if diff := cmp.Diff(tc.expected, actual); diff != "" { t.Errorf("ProfileSection() mismatch (-expected +actual):\n%s", diff) } }) } } func TestConfigParsingDefault(t *testing.T) { f := newConfigFile(t, exampleConfig) defer os.Remove(f) cfg, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } def, ok := cfg.ProfileSection("default") if !ok { t.Fatalf("Expected to find default profile") } expected := vault.ProfileSection{ Name: "default", Region: "us-west-2", } if !reflect.DeepEqual(def, expected) { t.Fatalf("Expected %+v, got %+v", expected, def) } } func TestProfilesFromConfig(t *testing.T) { f := newConfigFile(t, exampleConfig) defer os.Remove(f) cfg, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } expected := []vault.ProfileSection{ {Name: "default", Region: "us-west-2"}, {Name: "user2", Region: "us-east-1"}, {Name: "withsource", Region: "us-east-1", SourceProfile: "user2"}, {Name: "withMFA", MfaSerial: "arn:aws:iam::1234513441:mfa/blah", RoleARN: "arn:aws:iam::4451234513441615400570:role/aws_admin", Region: "us-east-1", DurationSeconds: 1200, SourceProfile: "user2", STSRegionalEndpoints: "legacy"}, {Name: "testincludeprofile1", Region: "us-east-1"}, {Name: "testincludeprofile2", IncludeProfile: "testincludeprofile1"}, {Name: "with-sso-session", SSOSession: "moon-sso", Region: "moon-1", SSOAccountID: "123456"}, } actual := cfg.ProfileSections() if diff := cmp.Diff(expected, actual); diff != "" { t.Errorf("ProfileSections() mismatch (-expected +actual):\n%s", diff) } } func TestAddProfileToExistingConfig(t *testing.T) { f := newConfigFile(t, exampleConfig) defer os.Remove(f) cfg, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } err = cfg.Add(vault.ProfileSection{ Name: "llamas", MfaSerial: "testserial", Region: "us-east-1", SourceProfile: "default", }) if err != nil { t.Fatalf("Error adding profile: %#v", err) } expected := []vault.ProfileSection{ {Name: "default", Region: "us-west-2"}, {Name: "user2", Region: "us-east-1"}, {Name: "withsource", Region: "us-east-1", SourceProfile: "user2"}, {Name: "withMFA", MfaSerial: "arn:aws:iam::1234513441:mfa/blah", RoleARN: "arn:aws:iam::4451234513441615400570:role/aws_admin", Region: "us-east-1", DurationSeconds: 1200, SourceProfile: "user2", STSRegionalEndpoints: "legacy"}, {Name: "testincludeprofile1", Region: "us-east-1"}, {Name: "testincludeprofile2", IncludeProfile: "testincludeprofile1"}, {Name: "with-sso-session", SSOSession: "moon-sso", Region: "moon-1", SSOAccountID: "123456"}, {Name: "llamas", MfaSerial: "testserial", Region: "us-east-1", SourceProfile: "default"}, } actual := cfg.ProfileSections() if diff := cmp.Diff(expected, actual); diff != "" { t.Errorf("ProfileSections() mismatch (-expected +actual):\n%s", diff) } } func TestAddProfileToExistingNestedConfig(t *testing.T) { f := newConfigFile(t, nestedConfig) defer os.Remove(f) cfg, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } err = cfg.Add(vault.ProfileSection{ Name: "llamas", MfaSerial: "testserial", Region: "us-east-1", }) if err != nil { t.Fatalf("Error adding profile: %#v", err) } expected := append(nestedConfig, []byte( "\n[profile llamas]\nmfa_serial=testserial\nregion=us-east-1\n", )...) b, _ := os.ReadFile(f) if !bytes.Equal(expected, b) { t.Fatalf("Expected:\n%q\nGot:\n%q", expected, b) } } func TestIncludeProfile(t *testing.T) { f := newConfigFile(t, exampleConfig) defer os.Remove(f) configFile, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } configLoader := &vault.ConfigLoader{File: configFile} config, err := configLoader.GetProfileConfig("testincludeprofile2") if err != nil { t.Fatalf("Should have found a profile: %v", err) } if config.Region != "us-east-1" { t.Fatalf("Expected region %q, got %q", "us-east-1", config.Region) } } func TestIncludeSsoSession(t *testing.T) { f := newConfigFile(t, exampleConfig) defer os.Remove(f) configFile, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } configLoader := &vault.ConfigLoader{File: configFile} config, err := configLoader.GetProfileConfig("with-sso-session") if err != nil { t.Fatalf("Should have found a profile: %v", err) } if config.Region != "moon-1" { // Test not the same as SSO region t.Fatalf("Expected region %q, got %q", "moon-1", config.Region) } ssoStartURL := "https://d-123456789.example.com/start" if config.SSOStartURL != ssoStartURL { t.Fatalf("Expected sso_start_url %q, got %q", ssoStartURL, config.Region) } if config.SSORegion != "moon-2" { // Test not the same as profile region t.Fatalf("Expected sso_region %q, got %q", "moon-2", config.Region) } // Not checking sso_registration_scopes as it seems to be unused by aws-cli. } func TestProfileIsEmpty(t *testing.T) { p := vault.ProfileSection{Name: "foo"} if !p.IsEmpty() { t.Errorf("Expected p to be empty") } } func TestIniWithHeaderSavesWithHeader(t *testing.T) { f := newConfigFile(t, defaultsOnlyConfigWithHeader) defer os.Remove(f) cfg, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } err = cfg.Save() if err != nil { t.Fatal(err) } expected := defaultsOnlyConfigWithHeader b, _ := os.ReadFile(f) if !bytes.Equal(expected, b) { t.Fatalf("Expected:\n%q\nGot:\n%q", expected, b) } } func TestIniWithDEFAULTHeader(t *testing.T) { f := newConfigFile(t, []byte(`[DEFAULT] region=us-east-1 [default] region=us-west-2 `)) defer os.Remove(f) cfg, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } expected := []vault.ProfileSection{ {Name: "default", Region: "us-west-2"}, } actual := cfg.ProfileSections() if diff := cmp.Diff(expected, actual); diff != "" { t.Errorf("ProfileSections() mismatch (-expected +actual):\n%s", diff) } } func TestLoadedProfileDoesntReferToItself(t *testing.T) { f := newConfigFile(t, []byte(` [profile foo] source_profile=foo `)) defer os.Remove(f) configFile, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } def, ok := configFile.ProfileSection("foo") if !ok { t.Fatalf("Couldn't load profile foo") } expectedSourceProfile := "foo" if def.SourceProfile != expectedSourceProfile { t.Fatalf("Expected '%s', got '%s'", expectedSourceProfile, def.SourceProfile) } configLoader := &vault.ConfigLoader{File: configFile} config, err := configLoader.GetProfileConfig("foo") if err != nil { t.Fatalf("Should have found a profile: %v", err) } expectedSourceProfileName := "" if config.SourceProfileName != expectedSourceProfileName { t.Fatalf("Expected '%s', got '%s'", expectedSourceProfileName, config.SourceProfileName) } } func TestSourceProfileCanReferToParent(t *testing.T) { f := newConfigFile(t, []byte(` [profile root] [profile foo] include_profile=root source_profile=root `)) defer os.Remove(f) configFile, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } def, ok := configFile.ProfileSection("foo") if !ok { t.Fatalf("Couldn't load profile foo") } expectedSourceProfile := "root" if def.SourceProfile != expectedSourceProfile { t.Fatalf("Expected '%s', got '%s'", expectedSourceProfile, def.SourceProfile) } configLoader := &vault.ConfigLoader{File: configFile} config, err := configLoader.GetProfileConfig("foo") if err != nil { t.Fatalf("Should have found a profile: %v", err) } expectedSourceProfileName := "root" if config.SourceProfileName != expectedSourceProfileName { t.Fatalf("Expected '%s', got '%s'", expectedSourceProfileName, config.SourceProfileName) } } func TestSetSessionTags(t *testing.T) { var testCases = []struct { stringValue string expected map[string]string ok bool }{ {"tag1=value1", map[string]string{"tag1": "value1"}, true}, { "tag2=value2,tag3=value3,tag4=value4", map[string]string{"tag2": "value2", "tag3": "value3", "tag4": "value4"}, true, }, {" tagA = valueA , tagB = valueB , tagC = valueC ", map[string]string{"tagA": "valueA", "tagB": "valueB", "tagC": "valueC"}, true, }, {"", nil, false}, {"tag1=value1,", nil, false}, {"tagA=valueA,tagB", nil, false}, {"tagOne,tagTwo=valueTwo", nil, false}, {"tagI=valueI,tagII,tagIII=valueIII", nil, false}, } for _, tc := range testCases { config := vault.ProfileConfig{} err := config.SetSessionTags(tc.stringValue) if tc.ok { if err != nil { t.Fatalf("Unsexpected parsing error: %s", err) } if !reflect.DeepEqual(tc.expected, config.SessionTags) { t.Fatalf("Expected SessionTags: %+v, got %+v", tc.expected, config.SessionTags) } } else { if err == nil { t.Fatalf("Expected an error parsing %#v, but got none", tc.stringValue) } } } } func TestSetTransitiveSessionTags(t *testing.T) { var testCases = []struct { stringValue string expected []string }{ {"tag1", []string{"tag1"}}, {"tag2,tag3,tag4", []string{"tag2", "tag3", "tag4"}}, {" tagA , tagB , tagC ", []string{"tagA", "tagB", "tagC"}}, {"tag1,", []string{"tag1"}}, {",tagA", []string{"tagA"}}, {"", nil}, {",", nil}, } for _, tc := range testCases { config := vault.ProfileConfig{} config.SetTransitiveSessionTags(tc.stringValue) if !reflect.DeepEqual(tc.expected, config.TransitiveSessionTags) { t.Fatalf("Expected TransitiveSessionTags: %+v, got %+v", tc.expected, config.TransitiveSessionTags) } } } func TestSessionTaggingFromIni(t *testing.T) { os.Unsetenv("AWS_SESSION_TAGS") os.Unsetenv("AWS_TRANSITIVE_TAGS") f := newConfigFile(t, []byte(` [profile tagged] session_tags = tag1 = value1 , tag2=value2 ,tag3=value3 transitive_session_tags = tagOne ,tagTwo,tagThree `)) defer os.Remove(f) configFile, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "tagged"} config, err := configLoader.GetProfileConfig("tagged") if err != nil { t.Fatalf("Should have found a profile: %v", err) } expectedSessionTags := map[string]string{ "tag1": "value1", "tag2": "value2", "tag3": "value3", } if !reflect.DeepEqual(expectedSessionTags, config.SessionTags) { t.Fatalf("Expected session_tags: %+v, got %+v", expectedSessionTags, config.SessionTags) } expectedTransitiveSessionTags := []string{"tagOne", "tagTwo", "tagThree"} if !reflect.DeepEqual(expectedTransitiveSessionTags, config.TransitiveSessionTags) { t.Fatalf("Expected transitive_session_tags: %+v, got %+v", expectedTransitiveSessionTags, config.TransitiveSessionTags) } } func TestSessionTaggingFromEnvironment(t *testing.T) { os.Setenv("AWS_SESSION_TAGS", " tagA = val1 , tagB=val2 ,tagC=val3") os.Setenv("AWS_TRANSITIVE_TAGS", " tagD ,tagE") defer os.Unsetenv("AWS_SESSION_TAGS") defer os.Unsetenv("AWS_TRANSITIVE_TAGS") f := newConfigFile(t, []byte(` [profile tagged] session_tags = tag1 = value1 , tag2=value2 ,tag3=value3 transitive_session_tags = tagOne ,tagTwo,tagThree `)) defer os.Remove(f) configFile, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "tagged"} config, err := configLoader.GetProfileConfig("tagged") if err != nil { t.Fatalf("Should have found a profile: %v", err) } expectedSessionTags := map[string]string{ "tagA": "val1", "tagB": "val2", "tagC": "val3", } if !reflect.DeepEqual(expectedSessionTags, config.SessionTags) { t.Fatalf("Expected session_tags: %+v, got %+v", expectedSessionTags, config.SessionTags) } expectedTransitiveSessionTags := []string{"tagD", "tagE"} if !reflect.DeepEqual(expectedTransitiveSessionTags, config.TransitiveSessionTags) { t.Fatalf("Expected transitive_session_tags: %+v, got %+v", expectedTransitiveSessionTags, config.TransitiveSessionTags) } } func TestSessionTaggingFromEnvironmentChainedRoles(t *testing.T) { os.Setenv("AWS_SESSION_TAGS", "tagI=valI") os.Setenv("AWS_TRANSITIVE_TAGS", " tagII") defer os.Unsetenv("AWS_SESSION_TAGS") defer os.Unsetenv("AWS_TRANSITIVE_TAGS") f := newConfigFile(t, []byte(` [profile base] [profile interim] session_tags=tag1=value1 transitive_session_tags=tag2 source_profile = base [profile target] session_tags=tagA=valueA transitive_session_tags=tagB source_profile = interim `)) defer os.Remove(f) configFile, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "target"} config, err := configLoader.GetProfileConfig("target") if err != nil { t.Fatalf("Should have found a profile: %v", err) } // Testing target profile, should have values populated from environment variables expectedSessionTags := map[string]string{"tagI": "valI"} if !reflect.DeepEqual(expectedSessionTags, config.SessionTags) { t.Fatalf("Expected session_tags: %+v, got %+v", expectedSessionTags, config.SessionTags) } expectedTransitiveSessionTags := []string{"tagII"} if !reflect.DeepEqual(expectedTransitiveSessionTags, config.TransitiveSessionTags) { t.Fatalf("Expected transitive_session_tags: %+v, got %+v", expectedTransitiveSessionTags, config.TransitiveSessionTags) } // Testing interim profile, parameters should come from the config, not environment interimConfig := config.SourceProfile expectedSessionTags = map[string]string{"tag1": "value1"} if !reflect.DeepEqual(expectedSessionTags, interimConfig.SessionTags) { t.Fatalf("Expected session_tags: %+v, got %+v", expectedSessionTags, interimConfig.SessionTags) } expectedTransitiveSessionTags = []string{"tag2"} if !reflect.DeepEqual(expectedTransitiveSessionTags, interimConfig.TransitiveSessionTags) { t.Fatalf("Expected transitive_session_tags: %+v, got %+v", expectedTransitiveSessionTags, interimConfig.TransitiveSessionTags) } // Testing base profile, should have empty parameters baseConfig := interimConfig.SourceProfile if len(baseConfig.SessionTags) > 0 { t.Fatalf("Expected session_tags to be empty, got %+v", baseConfig.SessionTags) } if len(baseConfig.TransitiveSessionTags) > 0 { t.Fatalf("Expected transitive_session_tags to be empty, got %+v", baseConfig.TransitiveSessionTags) } } ================================================ FILE: vault/credentialkeyring.go ================================================ package vault import ( "encoding/json" "fmt" "github.com/99designs/keyring" "github.com/aws/aws-sdk-go-v2/aws" ) type CredentialKeyring struct { Keyring keyring.Keyring } func (ck *CredentialKeyring) Keys() (credentialsNames []string, err error) { allKeys, err := ck.Keyring.Keys() if err != nil { return credentialsNames, err } for _, keyName := range allKeys { if !IsSessionKey(keyName) && !IsOIDCTokenKey(keyName) { credentialsNames = append(credentialsNames, keyName) } } return credentialsNames, nil } func (ck *CredentialKeyring) Has(credentialsName string) (bool, error) { allKeys, err := ck.Keyring.Keys() if err != nil { return false, err } for _, keyName := range allKeys { if keyName == credentialsName { return true, nil } } return false, nil } func (ck *CredentialKeyring) Get(credentialsName string) (creds aws.Credentials, err error) { item, err := ck.Keyring.Get(credentialsName) if err != nil { return creds, err } if err = json.Unmarshal(item.Data, &creds); err != nil { return creds, fmt.Errorf("Invalid data in keyring: %v", err) } return creds, err } func (ck *CredentialKeyring) Set(credentialsName string, creds aws.Credentials) error { bytes, err := json.Marshal(creds) if err != nil { return err } return ck.Keyring.Set(keyring.Item{ Key: credentialsName, Label: fmt.Sprintf("aws-vault (%s)", credentialsName), Data: bytes, // specific Keychain settings KeychainNotTrustApplication: true, }) } func (ck *CredentialKeyring) Remove(credentialsName string) error { return ck.Keyring.Remove(credentialsName) } ================================================ FILE: vault/credentialprocessprovider.go ================================================ package vault import ( "context" "encoding/json" "fmt" "reflect" "github.com/aws/aws-sdk-go-v2/aws" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" ) var credentialProcessRequiredFields = []string{"AccessKeyId", "Expiration", "SecretAccessKey", "SessionToken"} // CredentialProcessProvider implements interface aws.CredentialsProvider to retrieve credentials from an external executable // as described in https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes type CredentialProcessProvider struct { CredentialProcess string } func (p *CredentialProcessProvider) validateJSONCredential(cred *ststypes.Credentials) error { var missing []string h := reflect.ValueOf(cred).Elem() for _, requiredField := range credentialProcessRequiredFields { if h.FieldByName(requiredField).IsNil() { missing = append(missing, requiredField) } } if len(missing) > 0 { return fmt.Errorf("JSON credential from command %q missing the following fields: %v", p.CredentialProcess, missing) } return nil } // Retrieve obtains a new set of temporary credentials using an external process, required to satisfy interface aws.CredentialsProvider func (p *CredentialProcessProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { return p.retrieveWith(ctx, executeProcess) } func (p *CredentialProcessProvider) retrieveWith(ctx context.Context, fn func(string) (string, error)) (aws.Credentials, error) { creds, err := p.callCredentialProcessWith(ctx, fn) if err != nil { return aws.Credentials{}, err } return aws.Credentials{ AccessKeyID: aws.ToString(creds.AccessKeyId), SecretAccessKey: aws.ToString(creds.SecretAccessKey), SessionToken: aws.ToString(creds.SessionToken), CanExpire: true, Expires: aws.ToTime(creds.Expiration), }, nil } func (p *CredentialProcessProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { return p.callCredentialProcessWith(ctx, executeProcess) } func (p *CredentialProcessProvider) callCredentialProcessWith(_ context.Context, fn func(string) (string, error)) (*ststypes.Credentials, error) { // Exec CredentialProcess to retrieve AWS creds in JSON format as described in // https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes output, err := fn(p.CredentialProcess) if err != nil { return nil, err } // Unmarshal the JSON into a ststypes.Credentials object var value ststypes.Credentials if err := json.Unmarshal([]byte(output), &value); err != nil { return &ststypes.Credentials{}, fmt.Errorf("invalid JSON format from command %q: %v", p.CredentialProcess, err) } // Validate that all required fields were present in JSON before returning return &value, p.validateJSONCredential(&value) } ================================================ FILE: vault/credentialprocessprovider_test.go ================================================ package vault import ( "context" "encoding/json" "errors" "reflect" "strings" "testing" "time" "github.com/aws/aws-sdk-go-v2/aws" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" ) func executeFail(_ string) (string, error) { return "", errors.New("executing process failed") } func executeGetBadJSON(_ string) (string, error) { return "Junk", nil } func executeGetCredential(accessKeyID *string, expiration *time.Time, secretAccesKey *string, sessionToken *string) (string, error) { v, err := json.Marshal(ststypes.Credentials{ AccessKeyId: accessKeyID, Expiration: expiration, SecretAccessKey: secretAccesKey, SessionToken: sessionToken, }) return string(v), err } func TestCredentialProcessProvider_Retrieve(t *testing.T) { accessKeyID := "abcd" expiration := time.Time{} secretAccessKey := "0123" sessionToken := "4567" want := aws.Credentials{ AccessKeyID: accessKeyID, Expires: expiration, CanExpire: true, SecretAccessKey: secretAccessKey, SessionToken: sessionToken, } tests := []struct { name string execFunc func(string) (string, error) wantErr bool expectMissingFields bool }{ { name: "process execution fails", execFunc: executeFail, wantErr: true, expectMissingFields: false, }, { name: "bad json", execFunc: executeGetBadJSON, wantErr: true, expectMissingFields: false, }, { name: "successful execution, good cred", execFunc: func(string) (string, error) { return executeGetCredential(&accessKeyID, &expiration, &secretAccessKey, &sessionToken) }, wantErr: false, expectMissingFields: false, }, { name: "fields missing", execFunc: func(string) (string, error) { return executeGetCredential(nil, nil, nil, nil) }, wantErr: true, expectMissingFields: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() provider := CredentialProcessProvider{ CredentialProcess: "", } got, err := provider.retrieveWith(ctx, tt.execFunc) if (err != nil) != tt.wantErr { t.Errorf("CredentialProcessProvider.Retrieve() error = %v, wantErr %v", err, tt.wantErr) return } if !tt.wantErr && !reflect.DeepEqual(got, want) { t.Errorf("CredentialProcessProvider.Retrieve() = %v, want %v", got, want) } if tt.wantErr && tt.expectMissingFields { for _, expectedMissingField := range credentialProcessRequiredFields { if !strings.Contains(err.Error(), expectedMissingField) { t.Errorf("expected field '%v' not present in error: %v'", expectedMissingField, err) } } } }) } } ================================================ FILE: vault/executeprocess.go ================================================ package vault import ( "fmt" "os" "os/exec" "runtime" ) func executeProcess(process string) (string, error) { var cmdArgs []string if runtime.GOOS == "windows" { cmdArgs = []string{"cmd.exe", "/C", process} } else { cmdArgs = []string{"/bin/sh", "-c", process} } cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) cmd.Env = os.Environ() cmd.Stdin = os.Stdin cmd.Stderr = os.Stderr output, err := cmd.Output() if err != nil { return "", fmt.Errorf("running command %q: %v", process, err) } return string(output), nil } ================================================ FILE: vault/federationtokenprovider.go ================================================ package vault import ( "context" "log" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" ) const allowAllIAMPolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}` // FederationTokenProvider retrieves temporary credentials from STS using GetFederationToken type FederationTokenProvider struct { StsClient *sts.Client Name string Duration time.Duration } func (f *FederationTokenProvider) name() string { // truncate the username if it's longer than 32 characters or else GetFederationToken will fail. see: https://docs.aws.amazon.com/STS/latest/APIReference/API_GetFederationToken.html if len(f.Name) > 32 { return f.Name[0:32] } return f.Name } // Retrieve generates a new set of temporary credentials using STS GetFederationToken func (f *FederationTokenProvider) Retrieve(ctx context.Context) (creds aws.Credentials, err error) { resp, err := f.StsClient.GetFederationToken(ctx, &sts.GetFederationTokenInput{ Name: aws.String(f.name()), DurationSeconds: aws.Int32(int32(f.Duration.Seconds())), Policy: aws.String(allowAllIAMPolicy), }) if err != nil { return creds, err } log.Printf("Generated credentials %s using GetFederationToken, expires in %s", FormatKeyForDisplay(*resp.Credentials.AccessKeyId), time.Until(*resp.Credentials.Expiration).String()) return aws.Credentials{ AccessKeyID: aws.ToString(resp.Credentials.AccessKeyId), SecretAccessKey: aws.ToString(resp.Credentials.SecretAccessKey), SessionToken: aws.ToString(resp.Credentials.SessionToken), CanExpire: true, Expires: aws.ToTime(resp.Credentials.Expiration), }, nil } ================================================ FILE: vault/getuser.go ================================================ package vault import ( "context" "fmt" "regexp" "strings" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/iam" ) var getUserErrorRegexp = regexp.MustCompile(`^AccessDenied: User: arn:aws:iam::(\d+):user/(.+) is not`) // GetUsernameFromSession returns the IAM username (or root) associated with the current aws session func GetUsernameFromSession(ctx context.Context, cfg aws.Config) (string, error) { iamClient := iam.NewFromConfig(cfg) resp, err := iamClient.GetUser(ctx, &iam.GetUserInput{}) if err != nil { // Even if GetUser fails, the current user is included in the error. This happens when you have o IAM permissions // on the master credentials, but have permission to use assumeRole later matches := getUserErrorRegexp.FindStringSubmatch(err.Error()) if len(matches) > 0 { pathParts := strings.Split(matches[2], "/") return pathParts[len(pathParts)-1], nil } return "", err } if resp.User.UserName != nil { return *resp.User.UserName, nil } if resp.User.Arn != nil { arnParts := strings.Split(*resp.User.Arn, ":") return arnParts[len(arnParts)-1], nil } return "", fmt.Errorf("Couldn't determine current username") } ================================================ FILE: vault/keyringprovider.go ================================================ package vault import ( "context" "log" "github.com/aws/aws-sdk-go-v2/aws" ) // KeyringProvider stores and retrieves master credentials type KeyringProvider struct { Keyring *CredentialKeyring CredentialsName string } func (p *KeyringProvider) Retrieve(_ context.Context) (aws.Credentials, error) { log.Printf("Looking up keyring for '%s'", p.CredentialsName) return p.Keyring.Get(p.CredentialsName) } ================================================ FILE: vault/mfa.go ================================================ package vault import ( "errors" "fmt" "log" "os" "os/exec" "strings" "github.com/99designs/aws-vault/v7/prompt" "github.com/aws/aws-sdk-go-v2/aws" ) // Mfa contains options for an MFA device type Mfa struct { MfaSerial string mfaPromptFunc prompt.Func } // GetMfaToken returns the MFA token func (m Mfa) GetMfaToken() (*string, error) { if m.mfaPromptFunc != nil { token, err := m.mfaPromptFunc(m.MfaSerial) return aws.String(token), err } return nil, errors.New("No prompt found") } func NewMfa(config *ProfileConfig) Mfa { m := Mfa{ MfaSerial: config.MfaSerial, } if config.MfaToken != "" { m.mfaPromptFunc = func(_ string) (string, error) { return config.MfaToken, nil } } else if config.MfaProcess != "" { m.mfaPromptFunc = func(_ string) (string, error) { log.Println("Executing mfa_process") return ProcessMfaProvider(config.MfaProcess) } } else { m.mfaPromptFunc = prompt.Method(config.MfaPromptMethod) } return m } func ProcessMfaProvider(processCmd string) (string, error) { cmd := exec.Command("/bin/sh", "-c", processCmd) cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { return "", fmt.Errorf("process provider: %w", err) } return strings.TrimSpace(string(out)), nil } ================================================ FILE: vault/oidctokenkeyring.go ================================================ package vault import ( "encoding/json" "fmt" "log" "strings" "time" "github.com/99designs/keyring" "github.com/aws/aws-sdk-go-v2/service/ssooidc" ) type OIDCTokenKeyring struct { Keyring keyring.Keyring } type OIDCTokenData struct { Token ssooidc.CreateTokenOutput Expiration time.Time } const oidcTokenKeyPrefix = "oidc:" func (o *OIDCTokenKeyring) fmtKey(startURL string) string { return oidcTokenKeyPrefix + startURL } func IsOIDCTokenKey(k string) bool { return strings.HasPrefix(k, oidcTokenKeyPrefix) } func (o OIDCTokenKeyring) Has(startURL string) (bool, error) { kk, err := o.Keyring.Keys() if err != nil { return false, err } for _, k := range kk { if startURL == k { return true, nil } } return false, nil } func (o OIDCTokenKeyring) Get(startURL string) (*ssooidc.CreateTokenOutput, error) { item, err := o.Keyring.Get(o.fmtKey(startURL)) if err != nil { return nil, err } val := OIDCTokenData{} if err = json.Unmarshal(item.Data, &val); err != nil { log.Printf("Invalid data in keyring: %s", err.Error()) return nil, keyring.ErrKeyNotFound } if time.Now().After(val.Expiration) { log.Printf("OIDC token for '%s' expired, removing", startURL) _ = o.Remove(startURL) return nil, keyring.ErrKeyNotFound } secondsLeft := time.Until(val.Expiration) / time.Second val.Token.ExpiresIn = int32(secondsLeft) return &val.Token, err } func (o OIDCTokenKeyring) Set(startURL string, token *ssooidc.CreateTokenOutput) error { val := OIDCTokenData{ Token: *token, Expiration: time.Now().Add(time.Duration(token.ExpiresIn) * time.Second), } valJSON, err := json.Marshal(val) if err != nil { return err } return o.Keyring.Set(keyring.Item{ Key: o.fmtKey(startURL), Data: valJSON, Label: fmt.Sprintf("aws-vault oidc token for %s (expires %s)", startURL, val.Expiration.Format(time.RFC3339)), Description: "aws-vault oidc token", }) } func (o OIDCTokenKeyring) Remove(startURL string) error { return o.Keyring.Remove(o.fmtKey(startURL)) } func (o *OIDCTokenKeyring) RemoveAll() (n int, err error) { allKeys, err := o.Keys() if err != nil { return 0, err } for _, key := range allKeys { if err = o.Remove(key); err != nil { return n, err } n++ } return n, nil } func (o *OIDCTokenKeyring) Keys() (kk []string, err error) { allKeys, err := o.Keyring.Keys() if err != nil { return nil, err } for _, k := range allKeys { if IsOIDCTokenKey(k) { kk = append(kk, strings.TrimPrefix(k, oidcTokenKeyPrefix)) } } return kk, nil } ================================================ FILE: vault/sessionkeyring.go ================================================ package vault import ( "encoding/base64" "encoding/json" "fmt" "log" "regexp" "strconv" "strings" "time" "github.com/99designs/keyring" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" ) var sessionKeyPattern = regexp.MustCompile(`^(?P[^,]+),(?P[^,]+),(?P[^,]*),(?P[0-9]{1,})$`) var oldSessionKeyPatterns = []*regexp.Regexp{ regexp.MustCompile(`^session,(?P[^,]+),(?P[^,]*),(?P[0-9]{2,})$`), regexp.MustCompile(`^session:(?P[^ ]+):(?P[^ ]*):(?P[^:]+)$`), regexp.MustCompile(`^(.+?) session \((\d+)\)$`), } var base64URLEncodingNoPadding = base64.URLEncoding.WithPadding(base64.NoPadding) func IsOldSessionKey(s string) bool { for _, pattern := range oldSessionKeyPatterns { if pattern.MatchString(s) { return true } } return false } func IsCurrentSessionKey(s string) bool { _, err := NewSessionKeyFromString(s) return err == nil } func IsSessionKey(s string) bool { return IsCurrentSessionKey(s) || IsOldSessionKey(s) } type SessionMetadata struct { Type string ProfileName string MfaSerial string Expiration time.Time } func (k *SessionMetadata) String() string { return fmt.Sprintf( "%s,%s,%s,%d", k.Type, base64URLEncodingNoPadding.EncodeToString([]byte(k.ProfileName)), base64URLEncodingNoPadding.EncodeToString([]byte(k.MfaSerial)), k.Expiration.Unix(), ) } func (k *SessionMetadata) StringForMatching() string { return fmt.Sprintf( "%s,%s,%s,", k.Type, base64URLEncodingNoPadding.EncodeToString([]byte(k.ProfileName)), base64URLEncodingNoPadding.EncodeToString([]byte(k.MfaSerial)), ) } func NewSessionKeyFromString(s string) (SessionMetadata, error) { matches := sessionKeyPattern.FindStringSubmatch(s) if len(matches) == 0 { return SessionMetadata{}, fmt.Errorf("failed to parse session name: %s", s) } profileName, err := base64URLEncodingNoPadding.DecodeString(matches[2]) if err != nil { return SessionMetadata{}, err } mfaSerial, err := base64URLEncodingNoPadding.DecodeString(matches[3]) if err != nil { return SessionMetadata{}, err } expiryUnixtime, err := strconv.Atoi(matches[4]) if err != nil { return SessionMetadata{}, err } return SessionMetadata{ Type: matches[1], ProfileName: string(profileName), MfaSerial: string(mfaSerial), Expiration: time.Unix(int64(expiryUnixtime), 0), }, nil } type SessionKeyring struct { Keyring keyring.Keyring } var ErrNotFound = keyring.ErrKeyNotFound func (sk *SessionKeyring) lookupKeyName(key SessionMetadata) (string, error) { allKeys, err := sk.Keyring.Keys() if err != nil { return key.String(), err } for _, keyName := range allKeys { if strings.HasPrefix(keyName, key.StringForMatching()) { return keyName, nil } } return key.String(), ErrNotFound } func (sk *SessionKeyring) Has(key SessionMetadata) (bool, error) { _, err := sk.lookupKeyName(key) if err == ErrNotFound { return false, nil } if err == nil { return true, nil } return false, err } func (sk *SessionKeyring) Get(key SessionMetadata) (creds *ststypes.Credentials, err error) { _, _ = sk.RemoveOldSessions() keyName, err := sk.lookupKeyName(key) if err != nil && err != ErrNotFound { return nil, err } item, err := sk.Keyring.Get(keyName) if err != nil { return creds, err } if err = json.Unmarshal(item.Data, &creds); err != nil { log.Printf("SessionKeyring: Ignoring invalid data: %s", err.Error()) return creds, ErrNotFound } return creds, err } func (sk *SessionKeyring) Set(key SessionMetadata, creds *ststypes.Credentials) error { _, _ = sk.RemoveOldSessions() key.Expiration = *creds.Expiration valJSON, err := json.Marshal(creds) if err != nil { return err } keyName, err := sk.lookupKeyName(key) if err != ErrNotFound { if err != nil { return err } if keyName != key.String() { err = sk.Keyring.Remove(keyName) if err != nil { return err } } } return sk.Keyring.Set(keyring.Item{ Key: key.String(), Data: valJSON, Label: fmt.Sprintf("aws-vault session for %s (expires %s)", key.ProfileName, creds.Expiration.Format(time.RFC3339)), Description: "aws-vault session", }) } func (sk *SessionKeyring) Remove(key SessionMetadata) error { keyName, err := sk.lookupKeyName(key) if err != nil && err != ErrNotFound { return err } return sk.Keyring.Remove(keyName) } func (sk *SessionKeyring) RemoveAll() (n int, err error) { allKeys, err := sk.Keys() if err != nil { return 0, err } for _, key := range allKeys { if err = sk.Remove(key); err != nil { return n, err } n++ } return n, nil } func (sk *SessionKeyring) Keys() (kk []SessionMetadata, err error) { allKeys, err := sk.Keyring.Keys() if err != nil { return nil, err } for _, s := range allKeys { if k, err := NewSessionKeyFromString(s); err == nil { kk = append(kk, k) } } return kk, nil } func (sk *SessionKeyring) realSessionKey(key SessionMetadata) (m SessionMetadata, err error) { keyName, err := sk.lookupKeyName(key) if err != nil { return m, err } sessKey, err := NewSessionKeyFromString(keyName) if err != nil { return m, err } return sessKey, nil } func (sk *SessionKeyring) GetAllMetadata() (mm []SessionMetadata, err error) { allKeys, err := sk.Keys() if err != nil { return nil, err } for _, k := range allKeys { m, err := sk.realSessionKey(k) if err != nil { return nil, fmt.Errorf("GetAllMetadata: %w", err) } mm = append(mm, m) } return mm, nil } func (sk *SessionKeyring) RemoveForProfile(profileName string) (n int, err error) { sessions, err := sk.GetAllMetadata() if err != nil { return n, err } for _, s := range sessions { if s.ProfileName == profileName { err = sk.Remove(s) if err != nil { return n, err } n++ } } return n, nil } func (sk *SessionKeyring) RemoveOldSessions() (n int, err error) { allKeys, err := sk.Keyring.Keys() if err != nil { log.Printf("Error while deleting old session: %s", err.Error()) } for _, k := range allKeys { if IsOldSessionKey(k) { err = sk.Keyring.Remove(k) if err != nil { log.Printf("Error while deleting old session: %s", err.Error()) continue } n++ } else { stsk, err := NewSessionKeyFromString(k) if err != nil { continue } if time.Now().After(stsk.Expiration) { err = sk.Keyring.Remove(k) if err != nil { log.Printf("Error while deleting old session: %s", err.Error()) continue } n++ } } } return n, nil } ================================================ FILE: vault/sessionkeyring_test.go ================================================ package vault_test import ( "testing" "github.com/99designs/aws-vault/v7/vault" ) func TestIsSessionKey(t *testing.T) { var testCases = []struct { Key string IsSession bool }{ {"blah", false}, {"blah session (61633665646639303539)", true}, {"blah-iam session (32383863333237616430)", true}, {"session,c2Vzc2lvbg,,1572281751", true}, {"session,c2Vzc2lvbg,YXJuOmF3czppYW06OjEyMzQ1Njc4OTA6bWZhL2pzdGV3bW9u,1572281751", true}, } for _, tc := range testCases { if tc.IsSession && !vault.IsSessionKey(tc.Key) { t.Fatalf("%q is a session key, but wasn't detected as one", tc.Key) } else if !tc.IsSession && vault.IsSessionKey(tc.Key) { t.Fatalf("%q isn't a session key, but was detected as one", tc.Key) } } } ================================================ FILE: vault/sessiontokenprovider.go ================================================ package vault import ( "context" "log" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" ) // SessionTokenProvider retrieves temporary credentials from STS using GetSessionToken type SessionTokenProvider struct { StsClient *sts.Client Duration time.Duration Mfa } // Retrieve generates a new set of temporary credentials using STS GetSessionToken func (p *SessionTokenProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { creds, err := p.RetrieveStsCredentials(ctx) if err != nil { return aws.Credentials{}, err } return aws.Credentials{ AccessKeyID: aws.ToString(creds.AccessKeyId), SecretAccessKey: aws.ToString(creds.SecretAccessKey), SessionToken: aws.ToString(creds.SessionToken), CanExpire: true, Expires: aws.ToTime(creds.Expiration), }, nil } // GetSessionToken generates a new set of temporary credentials using STS GetSessionToken func (p *SessionTokenProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { var err error input := &sts.GetSessionTokenInput{ DurationSeconds: aws.Int32(int32(p.Duration.Seconds())), } if p.MfaSerial != "" { input.SerialNumber = aws.String(p.MfaSerial) input.TokenCode, err = p.GetMfaToken() if err != nil { return nil, err } } resp, err := p.StsClient.GetSessionToken(ctx, input) if err != nil { return nil, err } log.Printf("Generated credentials %s using GetSessionToken, expires in %s", FormatKeyForDisplay(*resp.Credentials.AccessKeyId), time.Until(*resp.Credentials.Expiration).String()) return resp.Credentials, nil } ================================================ FILE: vault/ssorolecredentialsprovider.go ================================================ package vault import ( "context" "errors" "fmt" "log" "net/http" "os" "time" "github.com/99designs/keyring" "github.com/aws/aws-sdk-go-v2/aws" awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" "github.com/aws/aws-sdk-go-v2/service/sso" ssotypes "github.com/aws/aws-sdk-go-v2/service/sso/types" "github.com/aws/aws-sdk-go-v2/service/ssooidc" ssooidctypes "github.com/aws/aws-sdk-go-v2/service/ssooidc/types" ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" "github.com/skratchdot/open-golang/open" ) type OIDCTokenCacher interface { Get(string) (*ssooidc.CreateTokenOutput, error) Set(string, *ssooidc.CreateTokenOutput) error Remove(string) error } // SSORoleCredentialsProvider creates temporary credentials for an SSO Role. type SSORoleCredentialsProvider struct { OIDCClient *ssooidc.Client OIDCTokenCache OIDCTokenCacher StartURL string SSOClient *sso.Client AccountID string RoleName string UseStdout bool } func millisecondsTimeValue(v int64) time.Time { return time.Unix(0, v*int64(time.Millisecond)) } // Retrieve generates a new set of temporary credentials using SSO GetRoleCredentials. func (p *SSORoleCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { creds, err := p.getRoleCredentials(ctx) if err != nil { return aws.Credentials{}, err } return aws.Credentials{ AccessKeyID: aws.ToString(creds.AccessKeyId), SecretAccessKey: aws.ToString(creds.SecretAccessKey), SessionToken: aws.ToString(creds.SessionToken), CanExpire: true, Expires: millisecondsTimeValue(creds.Expiration), }, nil } func (p *SSORoleCredentialsProvider) getRoleCredentials(ctx context.Context) (*ssotypes.RoleCredentials, error) { token, cached, err := p.getOIDCToken(ctx) if err != nil { return nil, err } resp, err := p.SSOClient.GetRoleCredentials(ctx, &sso.GetRoleCredentialsInput{ AccessToken: token.AccessToken, AccountId: aws.String(p.AccountID), RoleName: aws.String(p.RoleName), }) if err != nil { if cached && p.OIDCTokenCache != nil { var rspError *awshttp.ResponseError if !errors.As(err, &rspError) { return nil, err } // If the error is a 401, remove the cached oidc token and try // again. This is a recursive call but it should only happen once // due to the cache being cleared before retrying. if rspError.HTTPStatusCode() == http.StatusUnauthorized { err = p.OIDCTokenCache.Remove(p.StartURL) if err != nil { return nil, err } return p.getRoleCredentials(ctx) } } return nil, err } log.Printf("Got credentials %s for SSO role %s (account: %s), expires in %s", FormatKeyForDisplay(*resp.RoleCredentials.AccessKeyId), p.RoleName, p.AccountID, time.Until(millisecondsTimeValue(resp.RoleCredentials.Expiration)).String()) return resp.RoleCredentials, nil } func (p *SSORoleCredentialsProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { return p.getRoleCredentialsAsStsCredemtials(ctx) } // getRoleCredentialsAsStsCredemtials returns getRoleCredentials as sts.Credentials because sessions.Store expects it func (p *SSORoleCredentialsProvider) getRoleCredentialsAsStsCredemtials(ctx context.Context) (*ststypes.Credentials, error) { creds, err := p.getRoleCredentials(ctx) if err != nil { return nil, err } return &ststypes.Credentials{ AccessKeyId: creds.AccessKeyId, SecretAccessKey: creds.SecretAccessKey, SessionToken: creds.SessionToken, Expiration: aws.Time(millisecondsTimeValue(creds.Expiration)), }, nil } func (p *SSORoleCredentialsProvider) getOIDCToken(ctx context.Context) (token *ssooidc.CreateTokenOutput, cached bool, err error) { if p.OIDCTokenCache != nil { token, err = p.OIDCTokenCache.Get(p.StartURL) if err != nil && err != keyring.ErrKeyNotFound { return nil, false, err } if token != nil { return token, true, nil } } token, err = p.newOIDCToken(ctx) if err != nil { return nil, false, err } if p.OIDCTokenCache != nil { err = p.OIDCTokenCache.Set(p.StartURL, token) if err != nil { return nil, false, err } } return token, false, err } func (p *SSORoleCredentialsProvider) newOIDCToken(ctx context.Context) (*ssooidc.CreateTokenOutput, error) { clientCreds, err := p.OIDCClient.RegisterClient(ctx, &ssooidc.RegisterClientInput{ ClientName: aws.String("aws-vault"), ClientType: aws.String("public"), }) if err != nil { return nil, err } log.Printf("Created new OIDC client (expires at: %s)", time.Unix(clientCreds.ClientSecretExpiresAt, 0)) deviceCreds, err := p.OIDCClient.StartDeviceAuthorization(ctx, &ssooidc.StartDeviceAuthorizationInput{ ClientId: clientCreds.ClientId, ClientSecret: clientCreds.ClientSecret, StartUrl: aws.String(p.StartURL), }) if err != nil { return nil, err } log.Printf("Created OIDC device code for %s (expires in: %ds)", p.StartURL, deviceCreds.ExpiresIn) if p.UseStdout { fmt.Fprintf(os.Stderr, "Open the SSO authorization page in a browser (use Ctrl-C to abort)\n%s\n", aws.ToString(deviceCreds.VerificationUriComplete)) } else { log.Println("Opening SSO authorization page in browser") fmt.Fprintf(os.Stderr, "Opening the SSO authorization page in your default browser (use Ctrl-C to abort)\n%s\n", aws.ToString(deviceCreds.VerificationUriComplete)) if err := open.Run(aws.ToString(deviceCreds.VerificationUriComplete)); err != nil { log.Printf("Failed to open browser: %s", err) } } // These are the default values defined in the following RFC: // https://tools.ietf.org/html/draft-ietf-oauth-device-flow-15#section-3.5 var slowDownDelay = 5 * time.Second var retryInterval = 5 * time.Second if i := deviceCreds.Interval; i > 0 { retryInterval = time.Duration(i) * time.Second } for { t, err := p.OIDCClient.CreateToken(ctx, &ssooidc.CreateTokenInput{ ClientId: clientCreds.ClientId, ClientSecret: clientCreds.ClientSecret, DeviceCode: deviceCreds.DeviceCode, GrantType: aws.String("urn:ietf:params:oauth:grant-type:device_code"), }) if err != nil { var sde *ssooidctypes.SlowDownException if errors.As(err, &sde) { retryInterval += slowDownDelay } var ape *ssooidctypes.AuthorizationPendingException if errors.As(err, &ape) { time.Sleep(retryInterval) continue } return nil, err } log.Printf("Created new OIDC access token for %s (expires in: %ds)", p.StartURL, t.ExpiresIn) return t, nil } } ================================================ FILE: vault/stsendpointresolver.go ================================================ package vault import ( "log" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sts" ) // getEndpointResolver resolves endpoints in accordance with // https://docs.aws.amazon.com/credref/latest/refdocs/setting-global-sts_regional_endpoints.html func getSTSEndpointResolver(stsRegionalEndpoints string) aws.EndpointResolverWithOptionsFunc { return func(service, region string, options ...interface{}) (aws.Endpoint, error) { if stsRegionalEndpoints == "legacy" && service == sts.ServiceID { if region == "ap-northeast-1" || region == "ap-south-1" || region == "ap-southeast-1" || region == "ap-southeast-2" || region == "aws-global" || region == "ca-central-1" || region == "eu-central-1" || region == "eu-north-1" || region == "eu-west-1" || region == "eu-west-2" || region == "eu-west-3" || region == "sa-east-1" || region == "us-east-1" || region == "us-east-2" || region == "us-west-1" || region == "us-west-2" { log.Println("Using legacy STS endpoint sts.amazonaws.com") return aws.Endpoint{ URL: "https://sts.amazonaws.com", SigningRegion: region, }, nil } } return aws.Endpoint{}, &aws.EndpointNotFoundError{} } } ================================================ FILE: vault/vault.go ================================================ package vault import ( "context" "fmt" "log" "os" "time" "github.com/99designs/keyring" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/service/sso" "github.com/aws/aws-sdk-go-v2/service/ssooidc" "github.com/aws/aws-sdk-go-v2/service/sts" ) var defaultExpirationWindow = 5 * time.Minute func init() { if d, err := time.ParseDuration(os.Getenv("AWS_MIN_TTL")); err == nil { defaultExpirationWindow = d } } func NewAwsConfig(region, stsRegionalEndpoints string) aws.Config { return aws.Config{ Region: region, EndpointResolverWithOptions: getSTSEndpointResolver(stsRegionalEndpoints), } } func NewAwsConfigWithCredsProvider(credsProvider aws.CredentialsProvider, region, stsRegionalEndpoints string) aws.Config { return aws.Config{ Region: region, Credentials: credsProvider, EndpointResolverWithOptions: getSTSEndpointResolver(stsRegionalEndpoints), } } func FormatKeyForDisplay(k string) string { return fmt.Sprintf("****************%s", k[len(k)-4:]) } func isMasterCredentialsProvider(credsProvider aws.CredentialsProvider) bool { _, ok := credsProvider.(*KeyringProvider) return ok } // NewMasterCredentialsProvider creates a provider for the master credentials func NewMasterCredentialsProvider(k *CredentialKeyring, credentialsName string) *KeyringProvider { return &KeyringProvider{k, credentialsName} } func NewSessionTokenProvider(credsProvider aws.CredentialsProvider, k keyring.Keyring, config *ProfileConfig, useSessionCache bool) (aws.CredentialsProvider, error) { cfg := NewAwsConfigWithCredsProvider(credsProvider, config.Region, config.STSRegionalEndpoints) sessionTokenProvider := &SessionTokenProvider{ StsClient: sts.NewFromConfig(cfg), Duration: config.GetSessionTokenDuration(), Mfa: NewMfa(config), } if useSessionCache { return &CachedSessionProvider{ SessionKey: SessionMetadata{ Type: "sts.GetSessionToken", ProfileName: config.ProfileName, MfaSerial: config.MfaSerial, }, Keyring: &SessionKeyring{Keyring: k}, ExpiryWindow: defaultExpirationWindow, SessionProvider: sessionTokenProvider, }, nil } return sessionTokenProvider, nil } // NewAssumeRoleProvider returns a provider that generates credentials using AssumeRole func NewAssumeRoleProvider(credsProvider aws.CredentialsProvider, k keyring.Keyring, config *ProfileConfig, useSessionCache bool) (aws.CredentialsProvider, error) { cfg := NewAwsConfigWithCredsProvider(credsProvider, config.Region, config.STSRegionalEndpoints) p := &AssumeRoleProvider{ StsClient: sts.NewFromConfig(cfg), RoleARN: config.RoleARN, RoleSessionName: config.RoleSessionName, ExternalID: config.ExternalID, Duration: config.AssumeRoleDuration, Tags: config.SessionTags, TransitiveTagKeys: config.TransitiveSessionTags, SourceIdentity: config.SourceIdentity, Mfa: NewMfa(config), } if useSessionCache && config.MfaSerial != "" { return &CachedSessionProvider{ SessionKey: SessionMetadata{ Type: "sts.AssumeRole", ProfileName: config.ProfileName, MfaSerial: config.MfaSerial, }, Keyring: &SessionKeyring{Keyring: k}, ExpiryWindow: defaultExpirationWindow, SessionProvider: p, }, nil } return p, nil } // NewAssumeRoleWithWebIdentityProvider returns a provider that generates // credentials using AssumeRoleWithWebIdentity func NewAssumeRoleWithWebIdentityProvider(k keyring.Keyring, config *ProfileConfig, useSessionCache bool) (aws.CredentialsProvider, error) { cfg := NewAwsConfig(config.Region, config.STSRegionalEndpoints) p := &AssumeRoleWithWebIdentityProvider{ StsClient: sts.NewFromConfig(cfg), RoleARN: config.RoleARN, RoleSessionName: config.RoleSessionName, WebIdentityTokenFile: config.WebIdentityTokenFile, WebIdentityTokenProcess: config.WebIdentityTokenProcess, Duration: config.AssumeRoleDuration, } if useSessionCache { return &CachedSessionProvider{ SessionKey: SessionMetadata{ Type: "sts.AssumeRoleWithWebIdentity", ProfileName: config.ProfileName, }, Keyring: &SessionKeyring{Keyring: k}, ExpiryWindow: defaultExpirationWindow, SessionProvider: p, }, nil } return p, nil } // NewSSORoleCredentialsProvider creates a provider for SSO credentials func NewSSORoleCredentialsProvider(k keyring.Keyring, config *ProfileConfig, useSessionCache bool) (aws.CredentialsProvider, error) { cfg := NewAwsConfig(config.SSORegion, config.STSRegionalEndpoints) ssoRoleCredentialsProvider := &SSORoleCredentialsProvider{ OIDCClient: ssooidc.NewFromConfig(cfg), StartURL: config.SSOStartURL, SSOClient: sso.NewFromConfig(cfg), AccountID: config.SSOAccountID, RoleName: config.SSORoleName, UseStdout: config.SSOUseStdout, } if useSessionCache { ssoRoleCredentialsProvider.OIDCTokenCache = OIDCTokenKeyring{Keyring: k} return &CachedSessionProvider{ SessionKey: SessionMetadata{ Type: "sso.GetRoleCredentials", ProfileName: config.ProfileName, MfaSerial: config.SSOStartURL, }, Keyring: &SessionKeyring{Keyring: k}, ExpiryWindow: defaultExpirationWindow, SessionProvider: ssoRoleCredentialsProvider, }, nil } return ssoRoleCredentialsProvider, nil } // NewCredentialProcessProvider creates a provider to retrieve credentials from an external // executable as described in https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes func NewCredentialProcessProvider(k keyring.Keyring, config *ProfileConfig, useSessionCache bool) (aws.CredentialsProvider, error) { credentialProcessProvider := &CredentialProcessProvider{ CredentialProcess: config.CredentialProcess, } if useSessionCache { return &CachedSessionProvider{ SessionKey: SessionMetadata{ Type: "credential_process", ProfileName: config.ProfileName, }, Keyring: &SessionKeyring{Keyring: k}, ExpiryWindow: defaultExpirationWindow, SessionProvider: credentialProcessProvider, }, nil } return credentialProcessProvider, nil } func NewFederationTokenProvider(ctx context.Context, credsProvider aws.CredentialsProvider, config *ProfileConfig) (*FederationTokenProvider, error) { cfg := NewAwsConfigWithCredsProvider(credsProvider, config.Region, config.STSRegionalEndpoints) name, err := GetUsernameFromSession(ctx, cfg) if err != nil { return nil, err } log.Printf("Using GetFederationToken for credentials") return &FederationTokenProvider{ StsClient: sts.NewFromConfig(cfg), Name: name, Duration: config.GetFederationTokenDuration, }, nil } func FindMasterCredentialsNameFor(profileName string, keyring *CredentialKeyring, config *ProfileConfig) (string, error) { hasMasterCreds, err := keyring.Has(profileName) if err != nil { return "", err } if hasMasterCreds { return profileName, nil } if profileName == config.SourceProfileName { return "", fmt.Errorf("No master credentials found") } return FindMasterCredentialsNameFor(config.SourceProfileName, keyring, config) } type TempCredentialsCreator struct { Keyring *CredentialKeyring // DisableSessions will disable the use of GetSessionToken DisableSessions bool // DisableCache will disable the use of the session cache DisableCache bool // DisableSessionsForProfile is a profile for which sessions should not be used DisableSessionsForProfile string chainedMfa string } func (t *TempCredentialsCreator) getSourceCreds(config *ProfileConfig, hasStoredCredentials bool) (sourcecredsProvider aws.CredentialsProvider, err error) { if hasStoredCredentials { log.Printf("profile %s: using stored credentials", config.ProfileName) return NewMasterCredentialsProvider(t.Keyring, config.ProfileName), nil } if config.HasSourceProfile() { log.Printf("profile %s: sourcing credentials from profile %s", config.ProfileName, config.SourceProfile.ProfileName) return t.GetProviderForProfile(config.SourceProfile) } return nil, fmt.Errorf("profile %s: credentials missing", config.ProfileName) } func (t *TempCredentialsCreator) getSourceCredWithSession(config *ProfileConfig, hasStoredCredentials bool) (sourcecredsProvider aws.CredentialsProvider, err error) { sourcecredsProvider, err = t.getSourceCreds(config, hasStoredCredentials) if err != nil { return nil, err } if config.HasRole() { isMfaChained := config.MfaSerial != "" && config.MfaSerial == t.chainedMfa if isMfaChained { config.MfaSerial = "" } log.Printf("profile %s: using AssumeRole %s", config.ProfileName, mfaDetails(isMfaChained, config)) return NewAssumeRoleProvider(sourcecredsProvider, t.Keyring.Keyring, config, !t.DisableCache) } if isMasterCredentialsProvider(sourcecredsProvider) { canUseGetSessionToken, reason := t.canUseGetSessionToken(config) if canUseGetSessionToken { t.chainedMfa = config.MfaSerial log.Printf("profile %s: using GetSessionToken %s", config.ProfileName, mfaDetails(false, config)) return NewSessionTokenProvider(sourcecredsProvider, t.Keyring.Keyring, config, !t.DisableCache) } log.Printf("profile %s: skipping GetSessionToken because %s", config.ProfileName, reason) } return sourcecredsProvider, nil } func (t *TempCredentialsCreator) GetProviderForProfile(config *ProfileConfig) (aws.CredentialsProvider, error) { hasStoredCredentials, err := t.Keyring.Has(config.ProfileName) if err != nil { return nil, err } if hasStoredCredentials || config.HasSourceProfile() { return t.getSourceCredWithSession(config, hasStoredCredentials) } if config.HasSSOStartURL() { log.Printf("profile %s: using SSO role credentials", config.ProfileName) return NewSSORoleCredentialsProvider(t.Keyring.Keyring, config, !t.DisableCache) } if config.HasWebIdentity() { log.Printf("profile %s: using web identity", config.ProfileName) return NewAssumeRoleWithWebIdentityProvider(t.Keyring.Keyring, config, !t.DisableCache) } if config.HasCredentialProcess() { log.Printf("profile %s: using credential process", config.ProfileName) return NewCredentialProcessProvider(t.Keyring.Keyring, config, !t.DisableCache) } return nil, fmt.Errorf("profile %s: credentials missing", config.ProfileName) } // canUseGetSessionToken determines if GetSessionToken should be used, and if not returns a reason func (t *TempCredentialsCreator) canUseGetSessionToken(c *ProfileConfig) (bool, string) { if t.DisableSessions { return false, "sessions are disabled" } if t.DisableSessionsForProfile == c.ProfileName { return false, "sessions are disabled for this profile" } if c.IsChained() { if !c.ChainedFromProfile.HasMfaSerial() { return false, fmt.Sprintf("profile '%s' has no MFA serial defined", c.ChainedFromProfile.ProfileName) } if !c.HasMfaSerial() && c.ChainedFromProfile.HasMfaSerial() { return false, fmt.Sprintf("profile '%s' has no MFA serial defined", c.ProfileName) } if c.ChainedFromProfile.MfaSerial != c.MfaSerial { return false, fmt.Sprintf("MFA serial doesn't match profile '%s'", c.ChainedFromProfile.ProfileName) } if c.ChainedFromProfile.AssumeRoleDuration > roleChainingMaximumDuration { return false, fmt.Sprintf("duration %s in profile '%s' is greater than the AWS maximum %s for chaining MFA", c.ChainedFromProfile.AssumeRoleDuration, c.ChainedFromProfile.ProfileName, roleChainingMaximumDuration) } } return true, "" } func mfaDetails(mfaChained bool, config *ProfileConfig) string { if mfaChained { return "(chained MFA)" } if config.HasMfaSerial() { return "(with MFA)" } return "" } // NewTempCredentialsProvider creates a credential provider for the given config func NewTempCredentialsProvider(config *ProfileConfig, keyring *CredentialKeyring, disableSessions bool, disableCache bool) (aws.CredentialsProvider, error) { t := TempCredentialsCreator{ Keyring: keyring, DisableSessions: disableSessions, DisableCache: disableCache, } return t.GetProviderForProfile(config) } ================================================ FILE: vault/vault_test.go ================================================ package vault_test import ( "os" "testing" "github.com/99designs/aws-vault/v7/vault" "github.com/99designs/keyring" ) func TestUsageWebIdentityExample(t *testing.T) { f := newConfigFile(t, []byte(` [profile role2] role_arn = arn:aws:iam::33333333333:role/role2 web_identity_token_process = oidccli raw `)) defer os.Remove(f) configFile, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "role2"} config, err := configLoader.GetProfileConfig("role2") if err != nil { t.Fatalf("Should have found a profile: %v", err) } ckr := &vault.CredentialKeyring{Keyring: keyring.NewArrayKeyring([]keyring.Item{})} p, err := vault.NewTempCredentialsProvider(config, ckr, true, true) if err != nil { t.Fatal(err) } _, ok := p.(*vault.AssumeRoleWithWebIdentityProvider) if !ok { t.Fatalf("Expected AssumeRoleWithWebIdentityProvider, got %T", p) } } func TestIssue1176(t *testing.T) { f := newConfigFile(t, []byte(` [profile my-shared-base-profile] credential_process=aws-vault exec my-shared-base-profile -j mfa_serial=arn:aws:iam::1234567890:mfa/danielholz region=eu-west-1 [profile profile-with-role] source_profile=my-shared-base-profile include_profile=my-shared-base-profile region=eu-west-1 role_arn=arn:aws:iam::12345678901:role/allow-view-only-access-from-other-accounts `)) defer os.Remove(f) configFile, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "my-shared-base-profile"} config, err := configLoader.GetProfileConfig("my-shared-base-profile") if err != nil { t.Fatalf("Should have found a profile: %v", err) } ckr := &vault.CredentialKeyring{Keyring: keyring.NewArrayKeyring([]keyring.Item{})} p, err := vault.NewTempCredentialsProvider(config, ckr, true, true) if err != nil { t.Fatal(err) } _, ok := p.(*vault.CredentialProcessProvider) if !ok { t.Fatalf("Expected CredentialProcessProvider, got %T", p) } } func TestIssue1195(t *testing.T) { f := newConfigFile(t, []byte(` [profile test] source_profile=dev region=ap-northeast-2 [profile dev] sso_session=common sso_account_id=2160xxxx sso_role_name=AdministratorAccess region=ap-northeast-2 output=json [default] sso_session=common sso_account_id=3701xxxx sso_role_name=AdministratorAccess region=ap-northeast-2 output=json [sso-session common] sso_start_url=https://xxxx.awsapps.com/start sso_region=ap-northeast-2 sso_registration_scopes=sso:account:access `)) defer os.Remove(f) configFile, err := vault.LoadConfig(f) if err != nil { t.Fatal(err) } configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "test"} config, err := configLoader.GetProfileConfig("test") if err != nil { t.Fatalf("Should have found a profile: %v", err) } ckr := &vault.CredentialKeyring{Keyring: keyring.NewArrayKeyring([]keyring.Item{})} p, err := vault.NewTempCredentialsProvider(config, ckr, true, true) if err != nil { t.Fatal(err) } ssoProvider, ok := p.(*vault.SSORoleCredentialsProvider) if !ok { t.Fatalf("Expected SSORoleCredentialsProvider, got %T", p) } if ssoProvider.AccountID != "2160xxxx" { t.Fatalf("Expected AccountID to be 2160xxxx, got %s", ssoProvider.AccountID) } }