├── .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 /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | - [ ] I am using the latest release of AWS Vault 11 | - [ ] I have provided my `.aws/config` (redacted if necessary) 12 | - [ ] I have provided the debug output using `aws-vault --debug` (redacted if necessary) 13 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | on: 3 | push: 4 | pull_request: 5 | branches: 6 | - master 7 | permissions: 8 | contents: read 9 | 10 | jobs: 11 | test: 12 | name: test 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, macos-latest] 16 | runs-on: ${{ matrix.os }} 17 | steps: 18 | - uses: actions/setup-go@v3 19 | with: 20 | go-version: '1.20' 21 | - uses: actions/checkout@v3 22 | - name: Run tests 23 | run: go test -race ./... 24 | lint: 25 | permissions: 26 | contents: read # for actions/checkout to fetch code 27 | pull-requests: read # for golangci/golangci-lint-action to fetch pull requests 28 | name: lint 29 | strategy: 30 | matrix: 31 | os: [macos-latest, ubuntu-latest] 32 | runs-on: ${{ matrix.os }} 33 | steps: 34 | - uses: actions/setup-go@v3 35 | with: 36 | go-version: '1.20' 37 | - uses: actions/checkout@v3 38 | - name: golangci-lint 39 | uses: golangci/golangci-lint-action@v3.4.0 40 | with: 41 | version: v1.52.0 42 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # See https://github.com/actions/stale 2 | name: Mark and close stale issues 3 | on: 4 | schedule: 5 | - cron: '15 10 * * *' 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | permissions: 10 | issues: write 11 | steps: 12 | - uses: actions/stale@v7 13 | with: 14 | days-before-stale: 180 15 | days-before-close: 7 16 | 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.' 17 | exempt-issue-labels: pinned,security,feature 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /aws-vault 2 | /aws-vault-* 3 | /SHA256SUMS 4 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - bodyclose 4 | - contextcheck 5 | - depguard 6 | - durationcheck 7 | - dupl 8 | - errchkjson 9 | - errname 10 | - exhaustive 11 | - exportloopref 12 | - gofmt 13 | - goimports 14 | - makezero 15 | - misspell 16 | - nakedret 17 | - nilerr 18 | - nilnil 19 | - noctx 20 | - prealloc 21 | - revive 22 | # - rowserrcheck 23 | - thelper 24 | - tparallel 25 | - unconvert 26 | - unparam 27 | # - wastedassign 28 | - whitespace 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 99designs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell git describe --tags --candidates=1 --dirty) 2 | BUILD_FLAGS=-ldflags="-X main.Version=$(VERSION)" -trimpath 3 | CERT_ID ?= Developer ID Application: 99designs Inc (NRM9HVJ62Z) 4 | SRC=$(shell find . -name '*.go') go.mod 5 | INSTALL_DIR ?= ~/bin 6 | .PHONY: binaries clean release install 7 | 8 | ifeq ($(shell uname), Darwin) 9 | aws-vault: $(SRC) 10 | go build -ldflags="-X main.Version=$(VERSION)" -o $@ . 11 | codesign --options runtime --timestamp --sign "$(CERT_ID)" $@ 12 | else 13 | aws-vault: $(SRC) 14 | go build -ldflags="-X main.Version=$(VERSION)" -o $@ . 15 | endif 16 | 17 | install: aws-vault 18 | mkdir -p $(INSTALL_DIR) 19 | rm -f $(INSTALL_DIR)/aws-vault 20 | cp -a ./aws-vault $(INSTALL_DIR)/aws-vault 21 | 22 | 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 23 | dmgs: aws-vault-darwin-amd64.dmg aws-vault-darwin-arm64.dmg 24 | 25 | clean: 26 | rm -f ./aws-vault ./aws-vault-*-* ./SHA256SUMS 27 | 28 | release: binaries dmgs SHA256SUMS 29 | 30 | @echo "\nTo create a new release run:\n\n gh release create --title $(VERSION) $(VERSION) \ 31 | aws-vault-darwin-amd64.dmg \ 32 | aws-vault-darwin-arm64.dmg \ 33 | aws-vault-freebsd-amd64 \ 34 | aws-vault-linux-amd64 \ 35 | aws-vault-linux-arm64 \ 36 | aws-vault-linux-arm7 \ 37 | aws-vault-linux-ppc64le \ 38 | aws-vault-windows-386.exe \ 39 | aws-vault-windows-arm64.exe \ 40 | SHA256SUMS\n" 41 | 42 | @echo "\nTo update homebrew-cask run:\n\n brew bump-cask-pr --version $(shell echo $(VERSION) | sed 's/v\(.*\)/\1/') aws-vault\n" 43 | 44 | aws-vault-darwin-amd64: $(SRC) 45 | GOOS=darwin GOARCH=amd64 CGO_ENABLED=1 SDKROOT=$(shell xcrun --sdk macosx --show-sdk-path) go build $(BUILD_FLAGS) -o $@ . 46 | 47 | aws-vault-darwin-arm64: $(SRC) 48 | GOOS=darwin GOARCH=arm64 CGO_ENABLED=1 SDKROOT=$(shell xcrun --sdk macosx --show-sdk-path) go build $(BUILD_FLAGS) -o $@ . 49 | 50 | aws-vault-freebsd-amd64: $(SRC) 51 | GOOS=freebsd GOARCH=amd64 go build $(BUILD_FLAGS) -o $@ . 52 | 53 | aws-vault-linux-amd64: $(SRC) 54 | GOOS=linux GOARCH=amd64 go build $(BUILD_FLAGS) -o $@ . 55 | 56 | aws-vault-linux-arm64: $(SRC) 57 | GOOS=linux GOARCH=arm64 go build $(BUILD_FLAGS) -o $@ . 58 | 59 | aws-vault-linux-ppc64le: $(SRC) 60 | GOOS=linux GOARCH=ppc64le go build $(BUILD_FLAGS) -o $@ . 61 | 62 | aws-vault-linux-arm7: $(SRC) 63 | GOOS=linux GOARCH=arm GOARM=7 go build $(BUILD_FLAGS) -o $@ . 64 | 65 | aws-vault-windows-386.exe: $(SRC) 66 | GOOS=windows GOARCH=386 go build $(BUILD_FLAGS) -o $@ . 67 | 68 | aws-vault-windows-arm64.exe: $(SRC) 69 | GOOS=windows GOARCH=arm64 go build $(BUILD_FLAGS) -o $@ . 70 | 71 | aws-vault-darwin-amd64.dmg: aws-vault-darwin-amd64 72 | ./bin/create-dmg aws-vault-darwin-amd64 $@ 73 | 74 | aws-vault-darwin-arm64.dmg: aws-vault-darwin-arm64 75 | ./bin/create-dmg aws-vault-darwin-arm64 $@ 76 | 77 | SHA256SUMS: binaries dmgs 78 | shasum -a 256 \ 79 | aws-vault-darwin-amd64.dmg \ 80 | aws-vault-darwin-arm64.dmg \ 81 | aws-vault-freebsd-amd64 \ 82 | aws-vault-linux-amd64 \ 83 | aws-vault-linux-arm64 \ 84 | aws-vault-linux-arm7 \ 85 | aws-vault-linux-ppc64le \ 86 | aws-vault-windows-386.exe \ 87 | aws-vault-windows-arm64.exe \ 88 | > $@ 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS Vault 2 | 3 | [![Downloads](https://img.shields.io/github/downloads/99designs/aws-vault/total.svg)](https://github.com/99designs/aws-vault/releases) 4 | [![Continuous Integration](https://github.com/99designs/aws-vault/workflows/Continuous%20Integration/badge.svg)](https://github.com/99designs/aws-vault/actions) 5 | 6 | AWS Vault is a tool to securely store and access AWS credentials in a development environment. 7 | 8 | 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). 9 | 10 | Check out the [announcement blog post](https://99designs.com.au/tech-blog/blog/2015/10/26/aws-vault/) for more details. 11 | 12 | ## Installing 13 | 14 | You can install AWS Vault: 15 | - by downloading the [latest release](https://github.com/99designs/aws-vault/releases/latest) 16 | - on macOS with [Homebrew Cask](https://formulae.brew.sh/cask/aws-vault): `brew install --cask aws-vault` 17 | - on macOS with [MacPorts](https://ports.macports.org/port/aws-vault/summary): `port install aws-vault` 18 | - on Windows with [Chocolatey](https://chocolatey.org/packages/aws-vault): `choco install aws-vault` 19 | - on Windows with [Scoop](https://scoop.sh/): `scoop install aws-vault` 20 | - on Linux with [Homebrew on Linux](https://formulae.brew.sh/formula/aws-vault): `brew install aws-vault` 21 | - on [Arch Linux](https://www.archlinux.org/packages/community/x86_64/aws-vault/): `pacman -S aws-vault` 22 | - 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)) 23 | - on [FreeBSD](https://www.freshports.org/security/aws-vault/): `pkg install aws-vault` 24 | - on [OpenSUSE](https://software.opensuse.org/package/aws-vault): enable devel:languages:go repo then `zypper install aws-vault` 25 | - with [Nix](https://search.nixos.org/packages?show=aws-vault&query=aws-vault): `nix-env -i aws-vault` 26 | - 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 ` 27 | 28 | ## Documentation 29 | 30 | Config, usage, tips and tricks are available in the [USAGE.md](./USAGE.md) file. 31 | 32 | ## Vaulting Backends 33 | 34 | The supported vaulting backends are: 35 | 36 | * [macOS Keychain](https://support.apple.com/en-au/guide/keychain-access/welcome/mac) 37 | * [Windows Credential Manager](https://support.microsoft.com/en-au/help/4026814/windows-accessing-credential-manager) 38 | * Secret Service ([Gnome Keyring](https://wiki.gnome.org/Projects/GnomeKeyring), [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5)) 39 | * [KWallet](https://kde.org/applications/system/org.kde.kwalletmanager5) 40 | * [Pass](https://www.passwordstore.org/) 41 | * Encrypted file 42 | 43 | Use the `--backend` flag or `AWS_VAULT_BACKEND` environment variable to specify. 44 | 45 | ## Quick start 46 | 47 | ```shell 48 | # Store AWS credentials for the "jonsmith" profile 49 | $ aws-vault add jonsmith 50 | Enter Access Key Id: ABDCDEFDASDASF 51 | Enter Secret Key: %%% 52 | 53 | # Execute a command (using temporary credentials) 54 | $ aws-vault exec jonsmith -- aws s3 ls 55 | bucket_1 56 | bucket_2 57 | 58 | # open a browser window and login to the AWS Console 59 | $ aws-vault login jonsmith 60 | 61 | # List credentials 62 | $ aws-vault list 63 | Profile Credentials Sessions 64 | ======= =========== ======== 65 | jonsmith jonsmith - 66 | 67 | # Start a subshell with temporary credentials 68 | $ aws-vault exec jonsmith 69 | Starting subshell /bin/zsh, use `exit` to exit the subshell 70 | $ aws s3 ls 71 | bucket_1 72 | bucket_2 73 | ``` 74 | 75 | ## How it works 76 | 77 | `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. 78 | 79 | AWS Vault then exposes the temporary credentials to the sub-process in one of two ways 80 | 81 | 1. **Environment variables** are written to the sub-process. Notice in the below example how the AWS credentials get written out 82 | ```shell 83 | $ aws-vault exec jonsmith -- env | grep AWS 84 | AWS_VAULT=jonsmith 85 | AWS_DEFAULT_REGION=us-east-1 86 | AWS_REGION=us-east-1 87 | AWS_ACCESS_KEY_ID=%%% 88 | AWS_SECRET_ACCESS_KEY=%%% 89 | AWS_SESSION_TOKEN=%%% 90 | AWS_CREDENTIAL_EXPIRATION=2020-04-16T11:16:27Z 91 | ``` 92 | 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. 93 | ```shell 94 | $ aws-vault exec --server jonsmith -- env | grep AWS 95 | AWS_VAULT=jonsmith 96 | AWS_DEFAULT_REGION=us-east-1 97 | AWS_REGION=us-east-1 98 | AWS_CONTAINER_CREDENTIALS_FULL_URI=%%% 99 | AWS_CONTAINER_AUTHORIZATION_TOKEN=%%% 100 | ``` 101 | 102 | 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. 103 | 104 | ## Roles and MFA 105 | 106 | [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. 107 | 108 | 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). 109 | 110 | Here's an example configuration using roles and MFA: 111 | 112 | ```ini 113 | [default] 114 | region = us-east-1 115 | 116 | [profile jonsmith] 117 | mfa_serial = arn:aws:iam::111111111111:mfa/jonsmith 118 | 119 | [profile foo-readonly] 120 | source_profile = jonsmith 121 | role_arn = arn:aws:iam::22222222222:role/ReadOnly 122 | 123 | [profile foo-admin] 124 | source_profile = jonsmith 125 | role_arn = arn:aws:iam::22222222222:role/Administrator 126 | mfa_serial = arn:aws:iam::111111111111:mfa/jonsmith 127 | 128 | [profile bar-role1] 129 | source_profile = jonsmith 130 | role_arn = arn:aws:iam::333333333333:role/Role1 131 | mfa_serial = arn:aws:iam::111111111111:mfa/jonsmith 132 | 133 | [profile bar-role2] 134 | source_profile = bar-role1 135 | role_arn = arn:aws:iam::333333333333:role/Role2 136 | mfa_serial = arn:aws:iam::111111111111:mfa/jonsmith 137 | ``` 138 | 139 | Here's what you can expect from aws-vault 140 | 141 | | Command | Credentials | Cached | MFA | 142 | |------------------------------------------|-----------------------------|---------------|-----| 143 | | `aws-vault exec jonsmith --no-session` | Long-term credentials | No | No | 144 | | `aws-vault exec jonsmith` | session-token | session-token | Yes | 145 | | `aws-vault exec foo-readonly` | role | No | No | 146 | | `aws-vault exec foo-admin` | session-token + role | session-token | Yes | 147 | | `aws-vault exec foo-admin --duration=2h` | role | role | Yes | 148 | | `aws-vault exec bar-role2` | session-token + role + role | session-token | Yes | 149 | | `aws-vault exec bar-role2 --no-session` | role + role | role | Yes | 150 | 151 | ## Development 152 | 153 | 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: 154 | ```shell 155 | $ codesign --verify --verbose $(which aws-vault) 156 | ``` 157 | 158 | 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: 159 | ```shell 160 | $ go build . 161 | $ codesign --sign ./aws-vault 162 | ``` 163 | 164 | ## References and Inspiration 165 | 166 | * https://github.com/pda/aws-keychain 167 | * https://docs.aws.amazon.com/IAM/latest/UserGuide/MFAProtectedAPI.html 168 | * https://docs.aws.amazon.com/IAM/latest/UserGuide/IAMBestPractices.html#create-iam-users 169 | * https://github.com/makethunder/awsudo 170 | * https://github.com/AdRoll/hologram 171 | * https://github.com/realestate-com-au/credulous 172 | * https://github.com/dump247/aws-mock-metadata 173 | * https://boto.readthedocs.org/en/latest/boto_config_tut.html 174 | -------------------------------------------------------------------------------- /bin/create-dmg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # create-dmg packages the aws-vault CLI binary for macOS 4 | # using Apple's signing and notorizing process 5 | # 6 | # 7 | # As per https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow 8 | # AC_PASSWORD can be set in your keychain with: 9 | # xcrun notarytool store-credentials "AC_PASSWORD" 10 | # --apple-id "AC_USERNAME" 11 | # --team-id 12 | # --password 13 | # 14 | 15 | set -euo pipefail 16 | 17 | BIN_PATH="$1" 18 | DMG_PATH="${2:-$1.dmg}" 19 | CERT_ID="${CERT_ID:-"Developer ID Application: 99designs Inc (NRM9HVJ62Z)"}" 20 | KEYCHAIN_PROFILE="${KEYCHAIN_PROFILE:-AC_PASSWORD}" 21 | 22 | if [[ -f "$DMG_PATH" ]] ; then 23 | echo "File '$DMG_PATH' already exists. Remove it and try again" 24 | exit 1 25 | fi 26 | 27 | tmpdir="$(mktemp -d)" 28 | trap "rm -rf $tmpdir" EXIT 29 | 30 | cp -a $BIN_PATH $tmpdir/aws-vault 31 | src_path="$tmpdir/aws-vault" 32 | 33 | echo "Signing binary" 34 | codesign --options runtime --timestamp --sign "$CERT_ID" "$src_path" 35 | 36 | echo "Creating dmg" 37 | hdiutil create -quiet -srcfolder "$src_path" "$DMG_PATH" 38 | 39 | echo "Signing dmg" 40 | codesign --timestamp --sign "$CERT_ID" "$DMG_PATH" 41 | 42 | echo "Submitting notorization request" 43 | xcrun notarytool submit $DMG_PATH --keychain-profile "$KEYCHAIN_PROFILE" --wait 44 | 45 | echo "Stapling" 46 | xcrun stapler staple -q $DMG_PATH 47 | -------------------------------------------------------------------------------- /cli/add.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | 8 | "github.com/99designs/aws-vault/v7/prompt" 9 | "github.com/99designs/aws-vault/v7/vault" 10 | "github.com/99designs/keyring" 11 | "github.com/alecthomas/kingpin/v2" 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | ) 14 | 15 | type AddCommandInput struct { 16 | ProfileName string 17 | FromEnv bool 18 | AddConfig bool 19 | } 20 | 21 | func ConfigureAddCommand(app *kingpin.Application, a *AwsVault) { 22 | input := AddCommandInput{} 23 | 24 | cmd := app.Command("add", "Add credentials to the secure keystore.") 25 | 26 | cmd.Arg("profile", "Name of the profile"). 27 | Required(). 28 | StringVar(&input.ProfileName) 29 | 30 | cmd.Flag("env", "Read the credentials from the environment (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY)"). 31 | BoolVar(&input.FromEnv) 32 | 33 | cmd.Flag("add-config", "Add a profile to ~/.aws/config if one doesn't exist"). 34 | Default("true"). 35 | BoolVar(&input.AddConfig) 36 | 37 | cmd.Action(func(c *kingpin.ParseContext) error { 38 | keyring, err := a.Keyring() 39 | if err != nil { 40 | return err 41 | } 42 | awsConfigFile, err := a.AwsConfigFile() 43 | if err != nil { 44 | return err 45 | } 46 | err = AddCommand(input, keyring, awsConfigFile) 47 | app.FatalIfError(err, "add") 48 | return nil 49 | }) 50 | } 51 | 52 | func AddCommand(input AddCommandInput, keyring keyring.Keyring, awsConfigFile *vault.ConfigFile) error { 53 | var accessKeyID, secretKey string 54 | 55 | p, _ := awsConfigFile.ProfileSection(input.ProfileName) 56 | if p.SourceProfile != "" { 57 | return fmt.Errorf("Your profile has a source_profile of %s, adding credentials to %s won't have any effect", 58 | p.SourceProfile, input.ProfileName) 59 | } 60 | 61 | if input.FromEnv { 62 | if accessKeyID = os.Getenv("AWS_ACCESS_KEY_ID"); accessKeyID == "" { 63 | return fmt.Errorf("Missing value for AWS_ACCESS_KEY_ID") 64 | } 65 | if secretKey = os.Getenv("AWS_SECRET_ACCESS_KEY"); secretKey == "" { 66 | return fmt.Errorf("Missing value for AWS_SECRET_ACCESS_KEY") 67 | } 68 | } else { 69 | var err error 70 | if accessKeyID, err = prompt.TerminalPrompt("Enter Access Key ID: "); err != nil { 71 | return err 72 | } 73 | if secretKey, err = prompt.TerminalSecretPrompt("Enter Secret Access Key: "); err != nil { 74 | return err 75 | } 76 | } 77 | 78 | creds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey} 79 | 80 | ckr := &vault.CredentialKeyring{Keyring: keyring} 81 | if err := ckr.Set(input.ProfileName, creds); err != nil { 82 | return err 83 | } 84 | 85 | fmt.Printf("Added credentials to profile %q in vault\n", input.ProfileName) 86 | 87 | sk := &vault.SessionKeyring{Keyring: keyring} 88 | if n, _ := sk.RemoveForProfile(input.ProfileName); n > 0 { 89 | fmt.Printf("Deleted %d existing sessions.\n", n) 90 | } 91 | 92 | if _, hasProfile := awsConfigFile.ProfileSection(input.ProfileName); !hasProfile { 93 | if input.AddConfig { 94 | newProfileSection := vault.ProfileSection{ 95 | Name: input.ProfileName, 96 | } 97 | log.Printf("Adding profile %s to config at %s", input.ProfileName, awsConfigFile.Path) 98 | if err := awsConfigFile.Add(newProfileSection); err != nil { 99 | return fmt.Errorf("Error adding profile: %w", err) 100 | } 101 | } 102 | } 103 | 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /cli/add_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/alecthomas/kingpin/v2" 8 | ) 9 | 10 | func ExampleAddCommand() { 11 | f, err := os.CreateTemp("", "aws-config") 12 | if err != nil { 13 | log.Fatal(err) 14 | } 15 | defer os.Remove(f.Name()) 16 | 17 | os.Setenv("AWS_CONFIG_FILE", f.Name()) 18 | os.Setenv("AWS_ACCESS_KEY_ID", "llamas") 19 | os.Setenv("AWS_SECRET_ACCESS_KEY", "rock") 20 | os.Setenv("AWS_VAULT_BACKEND", "file") 21 | os.Setenv("AWS_VAULT_FILE_PASSPHRASE", "password") 22 | 23 | defer os.Unsetenv("AWS_ACCESS_KEY_ID") 24 | defer os.Unsetenv("AWS_SECRET_ACCESS_KEY") 25 | defer os.Unsetenv("AWS_VAULT_BACKEND") 26 | defer os.Unsetenv("AWS_VAULT_FILE_PASSPHRASE") 27 | 28 | app := kingpin.New(`aws-vault`, ``) 29 | ConfigureAddCommand(app, ConfigureGlobals(app)) 30 | kingpin.MustParse(app.Parse([]string{"add", "--debug", "--env", "foo"})) 31 | 32 | // Output: 33 | // Added credentials to profile "foo" in vault 34 | } 35 | -------------------------------------------------------------------------------- /cli/clear.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/99designs/aws-vault/v7/vault" 7 | "github.com/99designs/keyring" 8 | "github.com/alecthomas/kingpin/v2" 9 | ) 10 | 11 | type ClearCommandInput struct { 12 | ProfileName string 13 | } 14 | 15 | func ConfigureClearCommand(app *kingpin.Application, a *AwsVault) { 16 | input := ClearCommandInput{} 17 | 18 | cmd := app.Command("clear", "Clear temporary credentials from the secure keystore.") 19 | 20 | cmd.Arg("profile", "Name of the profile"). 21 | HintAction(a.MustGetProfileNames). 22 | StringVar(&input.ProfileName) 23 | 24 | cmd.Action(func(c *kingpin.ParseContext) (err error) { 25 | keyring, err := a.Keyring() 26 | if err != nil { 27 | return err 28 | } 29 | awsConfigFile, err := a.AwsConfigFile() 30 | if err != nil { 31 | return err 32 | } 33 | 34 | err = ClearCommand(input, awsConfigFile, keyring) 35 | app.FatalIfError(err, "clear") 36 | return nil 37 | }) 38 | } 39 | 40 | func ClearCommand(input ClearCommandInput, awsConfigFile *vault.ConfigFile, keyring keyring.Keyring) error { 41 | sessions := &vault.SessionKeyring{Keyring: keyring} 42 | oidcTokens := &vault.OIDCTokenKeyring{Keyring: keyring} 43 | var oldSessionsRemoved, numSessionsRemoved, numTokensRemoved int 44 | var err error 45 | if input.ProfileName == "" { 46 | oldSessionsRemoved, err = sessions.RemoveOldSessions() 47 | if err != nil { 48 | return err 49 | } 50 | numSessionsRemoved, err = sessions.RemoveAll() 51 | if err != nil { 52 | return err 53 | } 54 | numTokensRemoved, err = oidcTokens.RemoveAll() 55 | if err != nil { 56 | return err 57 | } 58 | } else { 59 | numSessionsRemoved, err = sessions.RemoveForProfile(input.ProfileName) 60 | if err != nil { 61 | return err 62 | } 63 | 64 | if profileSection, ok := awsConfigFile.ProfileSection(input.ProfileName); ok { 65 | if exists, _ := oidcTokens.Has(profileSection.SSOStartURL); exists { 66 | err = oidcTokens.Remove(profileSection.SSOStartURL) 67 | if err != nil { 68 | return err 69 | } 70 | numTokensRemoved = 1 71 | } 72 | } 73 | } 74 | fmt.Printf("Cleared %d sessions.\n", oldSessionsRemoved+numSessionsRemoved+numTokensRemoved) 75 | 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /cli/exec.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | osexec "os/exec" 10 | "os/signal" 11 | "runtime" 12 | "strings" 13 | "syscall" 14 | "time" 15 | 16 | "github.com/99designs/aws-vault/v7/iso8601" 17 | "github.com/99designs/aws-vault/v7/server" 18 | "github.com/99designs/aws-vault/v7/vault" 19 | "github.com/99designs/keyring" 20 | "github.com/alecthomas/kingpin/v2" 21 | "github.com/aws/aws-sdk-go-v2/aws" 22 | ) 23 | 24 | type ExecCommandInput struct { 25 | ProfileName string 26 | Command string 27 | Args []string 28 | StartEc2Server bool 29 | StartEcsServer bool 30 | Lazy bool 31 | JSONDeprecated bool 32 | Config vault.ProfileConfig 33 | SessionDuration time.Duration 34 | NoSession bool 35 | UseStdout bool 36 | ShowHelpMessages bool 37 | } 38 | 39 | func (input ExecCommandInput) validate() error { 40 | if input.StartEc2Server && input.StartEcsServer { 41 | return fmt.Errorf("Can't use --ec2-server with --ecs-server") 42 | } 43 | if input.StartEc2Server && input.JSONDeprecated { 44 | return fmt.Errorf("Can't use --ec2-server with --json") 45 | } 46 | if input.StartEc2Server && input.NoSession { 47 | return fmt.Errorf("Can't use --ec2-server with --no-session") 48 | } 49 | if input.StartEcsServer && input.JSONDeprecated { 50 | return fmt.Errorf("Can't use --ecs-server with --json") 51 | } 52 | if input.StartEcsServer && input.NoSession { 53 | return fmt.Errorf("Can't use --ecs-server with --no-session") 54 | } 55 | if input.StartEcsServer && input.Config.MfaPromptMethod == "terminal" { 56 | return fmt.Errorf("Can't use --prompt=terminal with --ecs-server. Specify a different prompt driver") 57 | } 58 | if input.StartEc2Server && input.Config.MfaPromptMethod == "terminal" { 59 | return fmt.Errorf("Can't use --prompt=terminal with --ec2-server. Specify a different prompt driver") 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func hasBackgroundServer(input ExecCommandInput) bool { 66 | return input.StartEcsServer || input.StartEc2Server 67 | } 68 | 69 | func ConfigureExecCommand(app *kingpin.Application, a *AwsVault) { 70 | input := ExecCommandInput{} 71 | 72 | cmd := app.Command("exec", "Execute a command with AWS credentials.") 73 | 74 | cmd.Flag("duration", "Duration of the temporary or assume-role session. Defaults to 1h"). 75 | Short('d'). 76 | DurationVar(&input.SessionDuration) 77 | 78 | cmd.Flag("no-session", "Skip creating STS session with GetSessionToken"). 79 | Short('n'). 80 | BoolVar(&input.NoSession) 81 | 82 | cmd.Flag("region", "The AWS region"). 83 | StringVar(&input.Config.Region) 84 | 85 | cmd.Flag("mfa-token", "The MFA token to use"). 86 | Short('t'). 87 | StringVar(&input.Config.MfaToken) 88 | 89 | cmd.Flag("json", "Output credentials in JSON that can be used by credential_process"). 90 | Short('j'). 91 | Hidden(). 92 | BoolVar(&input.JSONDeprecated) 93 | 94 | cmd.Flag("server", "Alias for --ecs-server"). 95 | Short('s'). 96 | BoolVar(&input.StartEcsServer) 97 | 98 | cmd.Flag("ec2-server", "Run a EC2 metadata server in the background for credentials"). 99 | BoolVar(&input.StartEc2Server) 100 | 101 | 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)"). 102 | BoolVar(&input.StartEcsServer) 103 | 104 | cmd.Flag("lazy", "When using --ecs-server, lazily fetch credentials"). 105 | BoolVar(&input.Lazy) 106 | 107 | cmd.Flag("stdout", "Print the SSO link to the terminal without automatically opening the browser"). 108 | BoolVar(&input.UseStdout) 109 | 110 | cmd.Arg("profile", "Name of the profile"). 111 | Required(). 112 | HintAction(a.MustGetProfileNames). 113 | StringVar(&input.ProfileName) 114 | 115 | cmd.Arg("cmd", "Command to execute, defaults to $SHELL"). 116 | StringVar(&input.Command) 117 | 118 | cmd.Arg("args", "Command arguments"). 119 | StringsVar(&input.Args) 120 | 121 | cmd.Action(func(c *kingpin.ParseContext) (err error) { 122 | input.Config.MfaPromptMethod = a.PromptDriver(hasBackgroundServer(input)) 123 | input.Config.NonChainedGetSessionTokenDuration = input.SessionDuration 124 | input.Config.AssumeRoleDuration = input.SessionDuration 125 | input.Config.SSOUseStdout = input.UseStdout 126 | input.ShowHelpMessages = !a.Debug && input.Command == "" && isATerminal() && os.Getenv("AWS_VAULT_DISABLE_HELP_MESSAGE") != "1" 127 | 128 | f, err := a.AwsConfigFile() 129 | if err != nil { 130 | return err 131 | } 132 | keyring, err := a.Keyring() 133 | if err != nil { 134 | return err 135 | } 136 | 137 | exitcode := 0 138 | if input.JSONDeprecated { 139 | exportCommandInput := ExportCommandInput{ 140 | ProfileName: input.ProfileName, 141 | Format: "json", 142 | Config: input.Config, 143 | SessionDuration: input.SessionDuration, 144 | NoSession: input.NoSession, 145 | } 146 | 147 | err = ExportCommand(exportCommandInput, f, keyring) 148 | } else { 149 | exitcode, err = ExecCommand(input, f, keyring) 150 | } 151 | 152 | app.FatalIfError(err, "exec") 153 | 154 | // override exit code if not err 155 | os.Exit(exitcode) 156 | 157 | return nil 158 | }) 159 | } 160 | 161 | func ExecCommand(input ExecCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) (exitcode int, err error) { 162 | if os.Getenv("AWS_VAULT") != "" { 163 | return 0, fmt.Errorf("running in an existing aws-vault subshell; 'exit' from the subshell or unset AWS_VAULT to force") 164 | } 165 | 166 | if err := input.validate(); err != nil { 167 | return 0, err 168 | } 169 | 170 | config, err := vault.NewConfigLoader(input.Config, f, input.ProfileName).GetProfileConfig(input.ProfileName) 171 | if err != nil { 172 | return 0, fmt.Errorf("Error loading config: %w", err) 173 | } 174 | 175 | credsProvider, err := vault.NewTempCredentialsProvider(config, &vault.CredentialKeyring{Keyring: keyring}, input.NoSession, false) 176 | if err != nil { 177 | return 0, fmt.Errorf("Error getting temporary credentials: %w", err) 178 | } 179 | 180 | subshellHelp := "" 181 | if input.Command == "" { 182 | input.Command = getDefaultShell() 183 | subshellHelp = fmt.Sprintf("Starting subshell %s, use `exit` to exit the subshell", input.Command) 184 | } 185 | 186 | cmdEnv := createEnv(input.ProfileName, config.Region) 187 | 188 | if input.StartEc2Server { 189 | if server.IsProxyRunning() { 190 | return 0, fmt.Errorf("Another process is already bound to 169.254.169.254:80") 191 | } 192 | 193 | 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) 194 | if err := server.StartEc2EndpointProxyServerProcess(); err != nil { 195 | return 0, err 196 | } 197 | defer server.StopProxy() 198 | 199 | if err = server.StartEc2CredentialsServer(context.TODO(), credsProvider, config.Region); err != nil { 200 | return 0, fmt.Errorf("Failed to start credential server: %w", err) 201 | } 202 | printHelpMessage(subshellHelp, input.ShowHelpMessages) 203 | } else if input.StartEcsServer { 204 | printHelpMessage("Starting a local ECS credential server; your app's AWS sdk must support AWS_CONTAINER_CREDENTIALS_FULL_URI.", input.ShowHelpMessages) 205 | if err = startEcsServerAndSetEnv(credsProvider, config, input.Lazy, &cmdEnv); err != nil { 206 | return 0, err 207 | } 208 | printHelpMessage(subshellHelp, input.ShowHelpMessages) 209 | } else { 210 | if err = addCredsToEnv(credsProvider, input.ProfileName, &cmdEnv); err != nil { 211 | return 0, err 212 | } 213 | printHelpMessage(subshellHelp, input.ShowHelpMessages) 214 | 215 | err = doExecSyscall(input.Command, input.Args, cmdEnv) // will not return if exec syscall succeeds 216 | if err != nil { 217 | log.Println("Error doing execve syscall:", err.Error()) 218 | log.Println("Falling back to running a subprocess") 219 | } 220 | } 221 | 222 | return runSubProcess(input.Command, input.Args, cmdEnv) 223 | } 224 | 225 | func printHelpMessage(helpMsg string, showHelpMessages bool) { 226 | if helpMsg != "" { 227 | if showHelpMessages { 228 | printToStderr(helpMsg) 229 | } else { 230 | log.Println(helpMsg) 231 | } 232 | } 233 | } 234 | 235 | func printToStderr(helpMsg string) { 236 | fmt.Fprint(os.Stderr, helpMsg, "\n") 237 | } 238 | 239 | func createEnv(profileName string, region string) environ { 240 | env := environ(os.Environ()) 241 | env.Unset("AWS_ACCESS_KEY_ID") 242 | env.Unset("AWS_SECRET_ACCESS_KEY") 243 | env.Unset("AWS_SESSION_TOKEN") 244 | env.Unset("AWS_SECURITY_TOKEN") 245 | env.Unset("AWS_CREDENTIAL_FILE") 246 | env.Unset("AWS_DEFAULT_PROFILE") 247 | env.Unset("AWS_PROFILE") 248 | env.Unset("AWS_SDK_LOAD_CONFIG") 249 | 250 | env.Set("AWS_VAULT", profileName) 251 | 252 | if region != "" { 253 | // AWS_REGION is used by most SDKs. But boto3 (Python SDK) uses AWS_DEFAULT_REGION 254 | // See https://docs.aws.amazon.com/sdkref/latest/guide/feature-region.html 255 | log.Printf("Setting subprocess env: AWS_REGION=%s, AWS_DEFAULT_REGION=%s", region, region) 256 | env.Set("AWS_REGION", region) 257 | env.Set("AWS_DEFAULT_REGION", region) 258 | } 259 | 260 | return env 261 | } 262 | 263 | func startEcsServerAndSetEnv(credsProvider aws.CredentialsProvider, config *vault.ProfileConfig, lazy bool, cmdEnv *environ) error { 264 | ecsServer, err := server.NewEcsServer(context.TODO(), credsProvider, config, "", 0, lazy) 265 | if err != nil { 266 | return err 267 | } 268 | go func() { 269 | err = ecsServer.Serve() 270 | if err != http.ErrServerClosed { // ErrServerClosed is a graceful close 271 | log.Fatalf("ecs server: %s", err.Error()) 272 | } 273 | }() 274 | 275 | log.Println("Setting subprocess env AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN") 276 | cmdEnv.Set("AWS_CONTAINER_CREDENTIALS_FULL_URI", ecsServer.BaseURL()) 277 | cmdEnv.Set("AWS_CONTAINER_AUTHORIZATION_TOKEN", ecsServer.AuthToken()) 278 | 279 | return nil 280 | } 281 | 282 | func addCredsToEnv(credsProvider aws.CredentialsProvider, profileName string, cmdEnv *environ) error { 283 | creds, err := credsProvider.Retrieve(context.TODO()) 284 | if err != nil { 285 | return fmt.Errorf("Failed to get credentials for %s: %w", profileName, err) 286 | } 287 | 288 | log.Println("Setting subprocess env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY") 289 | cmdEnv.Set("AWS_ACCESS_KEY_ID", creds.AccessKeyID) 290 | cmdEnv.Set("AWS_SECRET_ACCESS_KEY", creds.SecretAccessKey) 291 | 292 | if creds.SessionToken != "" { 293 | log.Println("Setting subprocess env: AWS_SESSION_TOKEN") 294 | cmdEnv.Set("AWS_SESSION_TOKEN", creds.SessionToken) 295 | } 296 | if creds.CanExpire { 297 | log.Println("Setting subprocess env: AWS_CREDENTIAL_EXPIRATION") 298 | cmdEnv.Set("AWS_CREDENTIAL_EXPIRATION", iso8601.Format(creds.Expires)) 299 | } 300 | 301 | return nil 302 | } 303 | 304 | // environ is a slice of strings representing the environment, in the form "key=value". 305 | type environ []string 306 | 307 | // Unset an environment variable by key 308 | func (e *environ) Unset(key string) { 309 | for i := range *e { 310 | if strings.HasPrefix((*e)[i], key+"=") { 311 | (*e)[i] = (*e)[len(*e)-1] 312 | *e = (*e)[:len(*e)-1] 313 | break 314 | } 315 | } 316 | } 317 | 318 | // Set adds an environment variable, replacing any existing ones of the same key 319 | func (e *environ) Set(key, val string) { 320 | e.Unset(key) 321 | *e = append(*e, key+"="+val) 322 | } 323 | 324 | func getDefaultShell() string { 325 | command := os.Getenv("SHELL") 326 | if command == "" { 327 | if runtime.GOOS == "windows" { 328 | command = "cmd.exe" 329 | } else { 330 | command = "/bin/sh" 331 | } 332 | } 333 | return command 334 | } 335 | 336 | func runSubProcess(command string, args []string, env []string) (int, error) { 337 | log.Printf("Starting a subprocess: %s %s", command, strings.Join(args, " ")) 338 | 339 | cmd := osexec.Command(command, args...) 340 | cmd.Stdin = os.Stdin 341 | cmd.Stdout = os.Stdout 342 | cmd.Stderr = os.Stderr 343 | cmd.Env = env 344 | 345 | sigChan := make(chan os.Signal, 1) 346 | signal.Notify(sigChan) 347 | 348 | if err := cmd.Start(); err != nil { 349 | return 0, err 350 | } 351 | 352 | // proxy signals to process 353 | go func() { 354 | for { 355 | sig := <-sigChan 356 | _ = cmd.Process.Signal(sig) 357 | } 358 | }() 359 | 360 | if err := cmd.Wait(); err != nil { 361 | _ = cmd.Process.Signal(os.Kill) 362 | return 0, fmt.Errorf("Failed to wait for command termination: %v", err) 363 | } 364 | 365 | waitStatus := cmd.ProcessState.Sys().(syscall.WaitStatus) 366 | 367 | return waitStatus.ExitStatus(), nil 368 | } 369 | 370 | func doExecSyscall(command string, args []string, env []string) error { 371 | log.Printf("Exec command %s %s", command, strings.Join(args, " ")) 372 | 373 | argv0, err := osexec.LookPath(command) 374 | if err != nil { 375 | return fmt.Errorf("Couldn't find the executable '%s': %w", command, err) 376 | } 377 | 378 | log.Printf("Found executable %s", argv0) 379 | 380 | argv := make([]string, 0, 1+len(args)) 381 | argv = append(argv, command) 382 | argv = append(argv, args...) 383 | 384 | return syscall.Exec(argv0, argv, env) 385 | } 386 | -------------------------------------------------------------------------------- /cli/exec_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/alecthomas/kingpin/v2" 5 | 6 | "github.com/99designs/keyring" 7 | ) 8 | 9 | func ExampleExecCommand() { 10 | app := kingpin.New("aws-vault", "") 11 | awsVault := ConfigureGlobals(app) 12 | awsVault.keyringImpl = keyring.NewArrayKeyring([]keyring.Item{ 13 | {Key: "llamas", Data: []byte(`{"AccessKeyID":"ABC","SecretAccessKey":"XYZ"}`)}, 14 | }) 15 | ConfigureExecCommand(app, awsVault) 16 | kingpin.MustParse(app.Parse([]string{ 17 | "--debug", "exec", "--no-session", "llamas", "--", "sh", "-c", "echo $AWS_ACCESS_KEY_ID", 18 | })) 19 | 20 | // Output: 21 | // ABC 22 | } 23 | -------------------------------------------------------------------------------- /cli/export.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "os" 9 | "time" 10 | 11 | "github.com/99designs/aws-vault/v7/iso8601" 12 | "github.com/99designs/aws-vault/v7/vault" 13 | "github.com/99designs/keyring" 14 | "github.com/alecthomas/kingpin/v2" 15 | "github.com/aws/aws-sdk-go-v2/aws" 16 | ini "gopkg.in/ini.v1" 17 | ) 18 | 19 | type ExportCommandInput struct { 20 | ProfileName string 21 | Format string 22 | Config vault.ProfileConfig 23 | SessionDuration time.Duration 24 | NoSession bool 25 | UseStdout bool 26 | } 27 | 28 | var ( 29 | FormatTypeEnv = "env" 30 | FormatTypeExportEnv = "export-env" 31 | FormatTypeExportJSON = "json" 32 | FormatTypeExportINI = "ini" 33 | ) 34 | 35 | func ConfigureExportCommand(app *kingpin.Application, a *AwsVault) { 36 | input := ExportCommandInput{} 37 | 38 | cmd := app.Command("export", "Export AWS credentials.") 39 | 40 | cmd.Flag("duration", "Duration of the temporary or assume-role session. Defaults to 1h"). 41 | Short('d'). 42 | DurationVar(&input.SessionDuration) 43 | 44 | cmd.Flag("no-session", "Skip creating STS session with GetSessionToken"). 45 | Short('n'). 46 | BoolVar(&input.NoSession) 47 | 48 | cmd.Flag("region", "The AWS region"). 49 | StringVar(&input.Config.Region) 50 | 51 | cmd.Flag("mfa-token", "The MFA token to use"). 52 | Short('t'). 53 | StringVar(&input.Config.MfaToken) 54 | 55 | cmd.Flag("format", fmt.Sprintf("Format to output credentials. Valid formats: %s, %s, %s, %s", FormatTypeEnv, FormatTypeExportEnv, FormatTypeExportJSON, FormatTypeExportINI)). 56 | Default(FormatTypeEnv). 57 | EnumVar(&input.Format, FormatTypeEnv, FormatTypeExportEnv, FormatTypeExportJSON, FormatTypeExportINI) 58 | 59 | cmd.Flag("stdout", "Print the SSO link to the terminal without automatically opening the browser"). 60 | BoolVar(&input.UseStdout) 61 | 62 | cmd.Arg("profile", "Name of the profile"). 63 | Required(). 64 | HintAction(a.MustGetProfileNames). 65 | StringVar(&input.ProfileName) 66 | 67 | cmd.Action(func(c *kingpin.ParseContext) (err error) { 68 | input.Config.MfaPromptMethod = a.PromptDriver(false) 69 | input.Config.NonChainedGetSessionTokenDuration = input.SessionDuration 70 | input.Config.AssumeRoleDuration = input.SessionDuration 71 | input.Config.SSOUseStdout = input.UseStdout 72 | 73 | f, err := a.AwsConfigFile() 74 | if err != nil { 75 | return err 76 | } 77 | keyring, err := a.Keyring() 78 | if err != nil { 79 | return err 80 | } 81 | 82 | err = ExportCommand(input, f, keyring) 83 | app.FatalIfError(err, "exec") 84 | return nil 85 | }) 86 | } 87 | 88 | func ExportCommand(input ExportCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) error { 89 | if os.Getenv("AWS_VAULT") != "" { 90 | return fmt.Errorf("in an existing aws-vault subshell; 'exit' from the subshell or unset AWS_VAULT to force") 91 | } 92 | 93 | config, err := vault.NewConfigLoader(input.Config, f, input.ProfileName).GetProfileConfig(input.ProfileName) 94 | if err != nil { 95 | return fmt.Errorf("Error loading config: %w", err) 96 | } 97 | 98 | ckr := &vault.CredentialKeyring{Keyring: keyring} 99 | credsProvider, err := vault.NewTempCredentialsProvider(config, ckr, input.NoSession, false) 100 | if err != nil { 101 | return fmt.Errorf("Error getting temporary credentials: %w", err) 102 | } 103 | 104 | if input.Format == FormatTypeExportJSON { 105 | return printJSON(input, credsProvider) 106 | } else if input.Format == FormatTypeExportINI { 107 | return printINI(credsProvider, input.ProfileName, config.Region) 108 | } else if input.Format == FormatTypeExportEnv { 109 | return printEnv(input, credsProvider, config.Region, "export ") 110 | } else { 111 | return printEnv(input, credsProvider, config.Region, "") 112 | } 113 | } 114 | 115 | func printJSON(input ExportCommandInput, credsProvider aws.CredentialsProvider) error { 116 | // AwsCredentialHelperData is metadata for AWS CLI credential process 117 | // See https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes 118 | type AwsCredentialHelperData struct { 119 | Version int `json:"Version"` 120 | AccessKeyID string `json:"AccessKeyId"` 121 | SecretAccessKey string `json:"SecretAccessKey"` 122 | SessionToken string `json:"SessionToken,omitempty"` 123 | Expiration string `json:"Expiration,omitempty"` 124 | } 125 | 126 | creds, err := credsProvider.Retrieve(context.TODO()) 127 | if err != nil { 128 | return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err) 129 | } 130 | 131 | credentialData := AwsCredentialHelperData{ 132 | Version: 1, 133 | AccessKeyID: creds.AccessKeyID, 134 | SecretAccessKey: creds.SecretAccessKey, 135 | SessionToken: creds.SessionToken, 136 | } 137 | 138 | if creds.CanExpire { 139 | credentialData.Expiration = iso8601.Format(creds.Expires) 140 | } 141 | 142 | json, err := json.MarshalIndent(&credentialData, "", " ") 143 | if err != nil { 144 | return fmt.Errorf("Error creating credential json: %w", err) 145 | } 146 | 147 | fmt.Print(string(json) + "\n") 148 | 149 | return nil 150 | } 151 | 152 | func mustNewKey(s *ini.Section, name, val string) { 153 | if val != "" { 154 | _, err := s.NewKey(name, val) 155 | if err != nil { 156 | log.Fatalln("Failed to create ini key:", err.Error()) 157 | } 158 | } 159 | } 160 | 161 | func printINI(credsProvider aws.CredentialsProvider, profilename, region string) error { 162 | creds, err := credsProvider.Retrieve(context.TODO()) 163 | if err != nil { 164 | return fmt.Errorf("Failed to get credentials for %s: %w", profilename, err) 165 | } 166 | 167 | f := ini.Empty() 168 | s, err := f.NewSection(profilename) 169 | if err != nil { 170 | return fmt.Errorf("Failed to create ini section: %w", err) 171 | } 172 | 173 | mustNewKey(s, "aws_access_key_id", creds.AccessKeyID) 174 | mustNewKey(s, "aws_secret_access_key", creds.SecretAccessKey) 175 | mustNewKey(s, "aws_session_token", creds.SessionToken) 176 | if creds.CanExpire { 177 | mustNewKey(s, "aws_credential_expiration", iso8601.Format(creds.Expires)) 178 | } 179 | mustNewKey(s, "region", region) 180 | 181 | _, err = f.WriteTo(os.Stdout) 182 | if err != nil { 183 | return fmt.Errorf("Failed to output ini: %w", err) 184 | } 185 | 186 | return nil 187 | } 188 | 189 | func printEnv(input ExportCommandInput, credsProvider aws.CredentialsProvider, region, prefix string) error { 190 | creds, err := credsProvider.Retrieve(context.TODO()) 191 | if err != nil { 192 | return fmt.Errorf("Failed to get credentials for %s: %w", input.ProfileName, err) 193 | } 194 | 195 | fmt.Printf("%sAWS_ACCESS_KEY_ID=%s\n", prefix, creds.AccessKeyID) 196 | fmt.Printf("%sAWS_SECRET_ACCESS_KEY=%s\n", prefix, creds.SecretAccessKey) 197 | 198 | if creds.SessionToken != "" { 199 | fmt.Printf("%sAWS_SESSION_TOKEN=%s\n", prefix, creds.SessionToken) 200 | } 201 | if creds.CanExpire { 202 | fmt.Printf("%sAWS_CREDENTIAL_EXPIRATION=%s\n", prefix, iso8601.Format(creds.Expires)) 203 | } 204 | if region != "" { 205 | fmt.Printf("%sAWS_REGION=%s\n", prefix, region) 206 | fmt.Printf("%sAWS_DEFAULT_REGION=%s\n", prefix, region) 207 | } 208 | 209 | return nil 210 | } 211 | -------------------------------------------------------------------------------- /cli/export_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/alecthomas/kingpin/v2" 5 | 6 | "github.com/99designs/keyring" 7 | ) 8 | 9 | func ExampleExportCommand() { 10 | app := kingpin.New("aws-vault", "") 11 | awsVault := ConfigureGlobals(app) 12 | awsVault.keyringImpl = keyring.NewArrayKeyring([]keyring.Item{ 13 | {Key: "llamas", Data: []byte(`{"AccessKeyID":"ABC","SecretAccessKey":"XYZ"}`)}, 14 | }) 15 | ConfigureExportCommand(app, awsVault) 16 | kingpin.MustParse(app.Parse([]string{ 17 | "export", "--format=ini", "--no-session", "llamas", 18 | })) 19 | 20 | // Output: 21 | // [llamas] 22 | // aws_access_key_id=ABC 23 | // aws_secret_access_key=XYZ 24 | // region=us-east-1 25 | } 26 | -------------------------------------------------------------------------------- /cli/global.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | "strings" 9 | 10 | "github.com/99designs/aws-vault/v7/prompt" 11 | "github.com/99designs/aws-vault/v7/vault" 12 | "github.com/99designs/keyring" 13 | "github.com/alecthomas/kingpin/v2" 14 | isatty "github.com/mattn/go-isatty" 15 | "golang.org/x/term" 16 | ) 17 | 18 | var keyringConfigDefaults = keyring.Config{ 19 | ServiceName: "aws-vault", 20 | FilePasswordFunc: fileKeyringPassphrasePrompt, 21 | LibSecretCollectionName: "awsvault", 22 | KWalletAppID: "aws-vault", 23 | KWalletFolder: "aws-vault", 24 | KeychainTrustApplication: true, 25 | WinCredPrefix: "aws-vault", 26 | } 27 | 28 | type AwsVault struct { 29 | Debug bool 30 | KeyringConfig keyring.Config 31 | KeyringBackend string 32 | promptDriver string 33 | 34 | keyringImpl keyring.Keyring 35 | awsConfigFile *vault.ConfigFile 36 | } 37 | 38 | func isATerminal() bool { 39 | fd := os.Stdout.Fd() 40 | return isatty.IsTerminal(fd) || isatty.IsCygwinTerminal(fd) 41 | } 42 | 43 | func (a *AwsVault) PromptDriver(avoidTerminalPrompt bool) string { 44 | if a.promptDriver == "" { 45 | a.promptDriver = "terminal" 46 | 47 | if !isATerminal() || avoidTerminalPrompt { 48 | for _, driver := range prompt.Available() { 49 | a.promptDriver = driver 50 | if driver != "terminal" { 51 | break 52 | } 53 | } 54 | } 55 | } 56 | 57 | log.Println("Using prompt driver: " + a.promptDriver) 58 | 59 | return a.promptDriver 60 | } 61 | 62 | func (a *AwsVault) Keyring() (keyring.Keyring, error) { 63 | if a.keyringImpl == nil { 64 | if a.KeyringBackend != "" { 65 | a.KeyringConfig.AllowedBackends = []keyring.BackendType{keyring.BackendType(a.KeyringBackend)} 66 | } 67 | var err error 68 | a.keyringImpl, err = keyring.Open(a.KeyringConfig) 69 | if err != nil { 70 | return nil, err 71 | } 72 | } 73 | 74 | return a.keyringImpl, nil 75 | } 76 | 77 | func (a *AwsVault) AwsConfigFile() (*vault.ConfigFile, error) { 78 | if a.awsConfigFile == nil { 79 | var err error 80 | a.awsConfigFile, err = vault.LoadConfigFromEnv() 81 | if err != nil { 82 | return nil, err 83 | } 84 | } 85 | 86 | return a.awsConfigFile, nil 87 | } 88 | 89 | func (a *AwsVault) MustGetProfileNames() []string { 90 | config, err := a.AwsConfigFile() 91 | if err != nil { 92 | log.Fatalf("Error loading AWS config: %s", err.Error()) 93 | } 94 | return config.ProfileNames() 95 | } 96 | 97 | func ConfigureGlobals(app *kingpin.Application) *AwsVault { 98 | a := &AwsVault{ 99 | KeyringConfig: keyringConfigDefaults, 100 | } 101 | 102 | backendsAvailable := []string{} 103 | for _, backendType := range keyring.AvailableBackends() { 104 | backendsAvailable = append(backendsAvailable, string(backendType)) 105 | } 106 | 107 | promptsAvailable := prompt.Available() 108 | 109 | app.Flag("debug", "Show debugging output"). 110 | BoolVar(&a.Debug) 111 | 112 | app.Flag("backend", fmt.Sprintf("Secret backend to use %v", backendsAvailable)). 113 | Default(backendsAvailable[0]). 114 | Envar("AWS_VAULT_BACKEND"). 115 | EnumVar(&a.KeyringBackend, backendsAvailable...) 116 | 117 | app.Flag("prompt", fmt.Sprintf("Prompt driver to use %v", promptsAvailable)). 118 | Envar("AWS_VAULT_PROMPT"). 119 | StringVar(&a.promptDriver) 120 | 121 | app.Validate(func(app *kingpin.Application) error { 122 | if a.promptDriver == "" { 123 | return nil 124 | } 125 | if a.promptDriver == "pass" { 126 | kingpin.Fatalf("--prompt=pass (or AWS_VAULT_PROMPT=pass) has been removed from aws-vault as using TOTPs without " + 127 | "a dedicated device goes against security best practices. If you wish to continue using pass, " + 128 | "add `mfa_process = pass otp ` to profiles in your ~/.aws/config file.") 129 | } 130 | for _, v := range promptsAvailable { 131 | if v == a.promptDriver { 132 | return nil 133 | } 134 | } 135 | return fmt.Errorf("--prompt value must be one of %s, got '%s'", strings.Join(promptsAvailable, ","), a.promptDriver) 136 | }) 137 | 138 | app.Flag("keychain", "Name of macOS keychain to use, if it doesn't exist it will be created"). 139 | Default("aws-vault"). 140 | Envar("AWS_VAULT_KEYCHAIN_NAME"). 141 | StringVar(&a.KeyringConfig.KeychainName) 142 | 143 | app.Flag("secret-service-collection", "Name of secret-service collection to use, if it doesn't exist it will be created"). 144 | Default("awsvault"). 145 | Envar("AWS_VAULT_SECRET_SERVICE_COLLECTION_NAME"). 146 | StringVar(&a.KeyringConfig.LibSecretCollectionName) 147 | 148 | app.Flag("pass-dir", "Pass password store directory"). 149 | Envar("AWS_VAULT_PASS_PASSWORD_STORE_DIR"). 150 | StringVar(&a.KeyringConfig.PassDir) 151 | 152 | app.Flag("pass-cmd", "Name of the pass executable"). 153 | Envar("AWS_VAULT_PASS_CMD"). 154 | StringVar(&a.KeyringConfig.PassCmd) 155 | 156 | app.Flag("pass-prefix", "Prefix to prepend to the item path stored in pass"). 157 | Envar("AWS_VAULT_PASS_PREFIX"). 158 | StringVar(&a.KeyringConfig.PassPrefix) 159 | 160 | app.Flag("file-dir", "Directory for the \"file\" password store"). 161 | Default("~/.awsvault/keys/"). 162 | Envar("AWS_VAULT_FILE_DIR"). 163 | StringVar(&a.KeyringConfig.FileDir) 164 | 165 | app.PreAction(func(c *kingpin.ParseContext) error { 166 | if !a.Debug { 167 | log.SetOutput(io.Discard) 168 | } 169 | keyring.Debug = a.Debug 170 | log.Printf("aws-vault %s", app.Model().Version) 171 | return nil 172 | }) 173 | 174 | return a 175 | } 176 | 177 | func fileKeyringPassphrasePrompt(prompt string) (string, error) { 178 | if password, ok := os.LookupEnv("AWS_VAULT_FILE_PASSPHRASE"); ok { 179 | return password, nil 180 | } 181 | 182 | fmt.Fprintf(os.Stderr, "%s: ", prompt) 183 | b, err := term.ReadPassword(int(os.Stdin.Fd())) 184 | if err != nil { 185 | return "", err 186 | } 187 | fmt.Println() 188 | return string(b), nil 189 | } 190 | -------------------------------------------------------------------------------- /cli/list.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | "text/tabwriter" 8 | "time" 9 | 10 | "github.com/99designs/aws-vault/v7/vault" 11 | "github.com/99designs/keyring" 12 | "github.com/alecthomas/kingpin/v2" 13 | ) 14 | 15 | type ListCommandInput struct { 16 | OnlyProfiles bool 17 | OnlySessions bool 18 | OnlyCredentials bool 19 | } 20 | 21 | func ConfigureListCommand(app *kingpin.Application, a *AwsVault) { 22 | input := ListCommandInput{} 23 | 24 | cmd := app.Command("list", "List profiles, along with their credentials and sessions.") 25 | cmd.Alias("ls") 26 | 27 | cmd.Flag("profiles", "Show only the profile names"). 28 | BoolVar(&input.OnlyProfiles) 29 | 30 | cmd.Flag("sessions", "Show only the session names"). 31 | BoolVar(&input.OnlySessions) 32 | 33 | cmd.Flag("credentials", "Show only the profiles with stored credential"). 34 | BoolVar(&input.OnlyCredentials) 35 | 36 | cmd.Action(func(c *kingpin.ParseContext) (err error) { 37 | keyring, err := a.Keyring() 38 | if err != nil { 39 | return err 40 | } 41 | awsConfigFile, err := a.AwsConfigFile() 42 | if err != nil { 43 | return err 44 | } 45 | err = ListCommand(input, awsConfigFile, keyring) 46 | app.FatalIfError(err, "list") 47 | return nil 48 | }) 49 | } 50 | 51 | type stringslice []string 52 | 53 | func (ss stringslice) remove(stringsToRemove []string) (newSS []string) { 54 | xx := stringslice(stringsToRemove) 55 | for _, s := range ss { 56 | if !xx.has(s) { 57 | newSS = append(newSS, s) 58 | } 59 | } 60 | 61 | return 62 | } 63 | 64 | func (ss stringslice) has(s string) bool { 65 | for _, t := range ss { 66 | if s == t { 67 | return true 68 | } 69 | } 70 | return false 71 | } 72 | 73 | func sessionLabel(sess vault.SessionMetadata) string { 74 | return fmt.Sprintf("%s:%s", sess.Type, time.Until(sess.Expiration).Truncate(time.Second)) 75 | } 76 | 77 | func ListCommand(input ListCommandInput, awsConfigFile *vault.ConfigFile, keyring keyring.Keyring) (err error) { 78 | credentialKeyring := &vault.CredentialKeyring{Keyring: keyring} 79 | oidcTokenKeyring := &vault.OIDCTokenKeyring{Keyring: credentialKeyring.Keyring} 80 | sessionKeyring := &vault.SessionKeyring{Keyring: credentialKeyring.Keyring} 81 | 82 | credentialsNames, err := credentialKeyring.Keys() 83 | if err != nil { 84 | return err 85 | } 86 | 87 | tokens, err := oidcTokenKeyring.Keys() 88 | if err != nil { 89 | return err 90 | } 91 | 92 | sessions, err := sessionKeyring.GetAllMetadata() 93 | if err != nil { 94 | return err 95 | } 96 | 97 | allSessionLabels := []string{} 98 | for _, t := range tokens { 99 | allSessionLabels = append(allSessionLabels, fmt.Sprintf("oidc:%s", t)) 100 | } 101 | for _, sess := range sessions { 102 | allSessionLabels = append(allSessionLabels, sessionLabel(sess)) 103 | } 104 | 105 | if input.OnlyCredentials { 106 | for _, c := range credentialsNames { 107 | fmt.Println(c) 108 | } 109 | return nil 110 | } 111 | 112 | if input.OnlyProfiles { 113 | for _, profileName := range awsConfigFile.ProfileNames() { 114 | fmt.Println(profileName) 115 | } 116 | return nil 117 | } 118 | 119 | if input.OnlySessions { 120 | for _, l := range allSessionLabels { 121 | fmt.Println(l) 122 | } 123 | return nil 124 | } 125 | 126 | displayedSessionLabels := []string{} 127 | 128 | w := tabwriter.NewWriter(os.Stdout, 25, 4, 2, ' ', 0) 129 | 130 | fmt.Fprintln(w, "Profile\tCredentials\tSessions\t") 131 | fmt.Fprintln(w, "=======\t===========\t========\t") 132 | 133 | // list out known profiles first 134 | for _, profileName := range awsConfigFile.ProfileNames() { 135 | fmt.Fprintf(w, "%s\t", profileName) 136 | 137 | hasCred, err := credentialKeyring.Has(profileName) 138 | if err != nil { 139 | return err 140 | } 141 | 142 | if hasCred { 143 | fmt.Fprintf(w, "%s\t", profileName) 144 | } else { 145 | fmt.Fprintf(w, "-\t") 146 | } 147 | 148 | var sessionLabels []string 149 | 150 | // check oidc keyring 151 | if profileSection, ok := awsConfigFile.ProfileSection(profileName); ok { 152 | if exists, _ := oidcTokenKeyring.Has(profileSection.SSOStartURL); exists { 153 | sessionLabels = append(sessionLabels, fmt.Sprintf("oidc:%s", profileSection.SSOStartURL)) 154 | } 155 | } 156 | 157 | // check session keyring 158 | for _, sess := range sessions { 159 | if profileName == sess.ProfileName { 160 | sessionLabels = append(sessionLabels, sessionLabel(sess)) 161 | } 162 | } 163 | 164 | if len(sessionLabels) > 0 { 165 | fmt.Fprintf(w, "%s\t\n", strings.Join(sessionLabels, ", ")) 166 | } else { 167 | fmt.Fprintf(w, "-\t\n") 168 | } 169 | 170 | displayedSessionLabels = append(displayedSessionLabels, sessionLabels...) 171 | } 172 | 173 | // show credentials that don't have profiles 174 | for _, credentialName := range credentialsNames { 175 | _, ok := awsConfigFile.ProfileSection(credentialName) 176 | if !ok { 177 | fmt.Fprintf(w, "-\t%s\t-\t\n", credentialName) 178 | } 179 | } 180 | 181 | // show sessions that don't have profiles 182 | sessionsWithoutProfiles := stringslice(allSessionLabels).remove(displayedSessionLabels) 183 | for _, s := range sessionsWithoutProfiles { 184 | fmt.Fprintf(w, "-\t-\t%s\t\n", s) 185 | } 186 | 187 | return w.Flush() 188 | } 189 | -------------------------------------------------------------------------------- /cli/list_test.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "github.com/alecthomas/kingpin/v2" 5 | 6 | "github.com/99designs/keyring" 7 | ) 8 | 9 | func ExampleListCommand() { 10 | app := kingpin.New("aws-vault", "") 11 | awsVault := ConfigureGlobals(app) 12 | awsVault.keyringImpl = keyring.NewArrayKeyring([]keyring.Item{ 13 | {Key: "llamas", Data: []byte(`{"AccessKeyID":"ABC","SecretAccessKey":"XYZ"}`)}, 14 | }) 15 | ConfigureListCommand(app, awsVault) 16 | kingpin.MustParse(app.Parse([]string{ 17 | "list", "--credentials", 18 | })) 19 | 20 | // Output: 21 | // llamas 22 | } 23 | -------------------------------------------------------------------------------- /cli/login.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "log" 9 | "net/http" 10 | "net/url" 11 | "strings" 12 | "time" 13 | 14 | "github.com/99designs/aws-vault/v7/vault" 15 | "github.com/99designs/keyring" 16 | "github.com/alecthomas/kingpin/v2" 17 | "github.com/aws/aws-sdk-go-v2/aws" 18 | awsconfig "github.com/aws/aws-sdk-go-v2/config" 19 | "github.com/aws/aws-sdk-go-v2/credentials" 20 | "github.com/aws/aws-sdk-go-v2/service/sts" 21 | "github.com/skratchdot/open-golang/open" 22 | ) 23 | 24 | type LoginCommandInput struct { 25 | ProfileName string 26 | UseStdout bool 27 | Path string 28 | Config vault.ProfileConfig 29 | SessionDuration time.Duration 30 | NoSession bool 31 | } 32 | 33 | func ConfigureLoginCommand(app *kingpin.Application, a *AwsVault) { 34 | input := LoginCommandInput{} 35 | 36 | cmd := app.Command("login", "Generate a login link for the AWS Console.") 37 | 38 | cmd.Flag("duration", "Duration of the assume-role or federated session. Defaults to 1h"). 39 | Short('d'). 40 | DurationVar(&input.SessionDuration) 41 | 42 | cmd.Flag("no-session", "Skip creating STS session with GetSessionToken"). 43 | Short('n'). 44 | BoolVar(&input.NoSession) 45 | 46 | cmd.Flag("mfa-token", "The MFA token to use"). 47 | Short('t'). 48 | StringVar(&input.Config.MfaToken) 49 | 50 | cmd.Flag("path", "The AWS service you would like access"). 51 | StringVar(&input.Path) 52 | 53 | cmd.Flag("region", "The AWS region"). 54 | StringVar(&input.Config.Region) 55 | 56 | cmd.Flag("stdout", "Print login URL to stdout instead of opening in default browser"). 57 | Short('s'). 58 | BoolVar(&input.UseStdout) 59 | 60 | cmd.Arg("profile", "Name of the profile. If none given, credentials will be sourced from env vars"). 61 | HintAction(a.MustGetProfileNames). 62 | StringVar(&input.ProfileName) 63 | 64 | cmd.Action(func(c *kingpin.ParseContext) (err error) { 65 | input.Config.MfaPromptMethod = a.PromptDriver(false) 66 | input.Config.NonChainedGetSessionTokenDuration = input.SessionDuration 67 | input.Config.AssumeRoleDuration = input.SessionDuration 68 | input.Config.GetFederationTokenDuration = input.SessionDuration 69 | keyring, err := a.Keyring() 70 | if err != nil { 71 | return err 72 | } 73 | f, err := a.AwsConfigFile() 74 | if err != nil { 75 | return err 76 | } 77 | 78 | err = LoginCommand(context.Background(), input, f, keyring) 79 | app.FatalIfError(err, "login") 80 | return nil 81 | }) 82 | } 83 | 84 | func getCredsProvider(input LoginCommandInput, config *vault.ProfileConfig, keyring keyring.Keyring) (credsProvider aws.CredentialsProvider, err error) { 85 | if input.ProfileName == "" { 86 | // When no profile is specified, source credentials from the environment 87 | configFromEnv, err := awsconfig.NewEnvConfig() 88 | if err != nil { 89 | return nil, fmt.Errorf("unable to authenticate to AWS through your environment variables: %w", err) 90 | } 91 | 92 | if configFromEnv.Credentials.AccessKeyID == "" { 93 | return nil, fmt.Errorf("argument 'profile' not provided, nor any AWS env vars found. Try --help") 94 | } 95 | 96 | credsProvider = credentials.StaticCredentialsProvider{Value: configFromEnv.Credentials} 97 | } else { 98 | // Use a profile from the AWS config file 99 | ckr := &vault.CredentialKeyring{Keyring: keyring} 100 | t := vault.TempCredentialsCreator{ 101 | Keyring: ckr, 102 | DisableSessions: input.NoSession, 103 | DisableSessionsForProfile: config.ProfileName, 104 | } 105 | credsProvider, err = t.GetProviderForProfile(config) 106 | if err != nil { 107 | return nil, fmt.Errorf("profile %s: %w", input.ProfileName, err) 108 | } 109 | } 110 | 111 | return credsProvider, err 112 | } 113 | 114 | // LoginCommand creates a login URL for the AWS Management Console using the method described at 115 | // https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html 116 | func LoginCommand(ctx context.Context, input LoginCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) error { 117 | config, err := vault.NewConfigLoader(input.Config, f, input.ProfileName).GetProfileConfig(input.ProfileName) 118 | if err != nil { 119 | return fmt.Errorf("Error loading config: %w", err) 120 | } 121 | 122 | credsProvider, err := getCredsProvider(input, config, keyring) 123 | if err != nil { 124 | return err 125 | } 126 | 127 | // if we already know the type of credentials being created, avoid calling isCallerIdentityAssumedRole 128 | canCredsBeUsedInLoginURL, err := canProviderBeUsedForLogin(credsProvider) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | if !canCredsBeUsedInLoginURL { 134 | // use a static creds provider so that we don't request credentials from AWS more than once 135 | credsProvider, err = createStaticCredentialsProvider(ctx, credsProvider) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | // if the credentials have come from an unknown source like credential_process, check the 141 | // caller identity to see if it's an assumed role 142 | isAssumedRole, err := isCallerIdentityAssumedRole(ctx, credsProvider, config) 143 | if err != nil { 144 | return err 145 | } 146 | 147 | if !isAssumedRole { 148 | log.Println("Creating a federated session") 149 | credsProvider, err = vault.NewFederationTokenProvider(ctx, credsProvider, config) 150 | if err != nil { 151 | return err 152 | } 153 | } 154 | } 155 | 156 | creds, err := credsProvider.Retrieve(ctx) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | if creds.CanExpire { 162 | log.Printf("Requesting a signin token for session expiring in %s", time.Until(creds.Expires)) 163 | } 164 | 165 | loginURLPrefix, destination := generateLoginURL(config.Region, input.Path) 166 | signinToken, err := requestSigninToken(ctx, creds, loginURLPrefix) 167 | if err != nil { 168 | return err 169 | } 170 | 171 | loginURL := fmt.Sprintf("%s?Action=login&Issuer=aws-vault&Destination=%s&SigninToken=%s", 172 | loginURLPrefix, url.QueryEscape(destination), url.QueryEscape(signinToken)) 173 | 174 | if input.UseStdout { 175 | fmt.Println(loginURL) 176 | } else if err = open.Run(loginURL); err != nil { 177 | return fmt.Errorf("Failed to open %s: %w", loginURL, err) 178 | } 179 | 180 | return nil 181 | } 182 | 183 | func generateLoginURL(region string, path string) (string, string) { 184 | loginURLPrefix := "https://signin.aws.amazon.com/federation" 185 | destination := "https://console.aws.amazon.com/" 186 | 187 | if region != "" { 188 | destinationDomain := "console.aws.amazon.com" 189 | switch { 190 | case strings.HasPrefix(region, "cn-"): 191 | loginURLPrefix = "https://signin.amazonaws.cn/federation" 192 | destinationDomain = "console.amazonaws.cn" 193 | case strings.HasPrefix(region, "us-gov-"): 194 | loginURLPrefix = "https://signin.amazonaws-us-gov.com/federation" 195 | destinationDomain = "console.amazonaws-us-gov.com" 196 | } 197 | if path != "" { 198 | destination = fmt.Sprintf("https://%s.%s/%s?region=%s", 199 | region, destinationDomain, path, region) 200 | } else { 201 | destination = fmt.Sprintf("https://%s.%s/console/home?region=%s", 202 | region, destinationDomain, region) 203 | } 204 | } 205 | return loginURLPrefix, destination 206 | } 207 | 208 | func isCallerIdentityAssumedRole(ctx context.Context, credsProvider aws.CredentialsProvider, config *vault.ProfileConfig) (bool, error) { 209 | cfg := vault.NewAwsConfigWithCredsProvider(credsProvider, config.Region, config.STSRegionalEndpoints) 210 | client := sts.NewFromConfig(cfg) 211 | id, err := client.GetCallerIdentity(ctx, nil) 212 | if err != nil { 213 | return false, err 214 | } 215 | arn := aws.ToString(id.Arn) 216 | arnParts := strings.Split(arn, ":") 217 | if len(arnParts) < 6 { 218 | return false, fmt.Errorf("unable to parse ARN: %s", arn) 219 | } 220 | if strings.HasPrefix(arnParts[5], "assumed-role") { 221 | return true, nil 222 | } 223 | return false, nil 224 | } 225 | 226 | func createStaticCredentialsProvider(ctx context.Context, credsProvider aws.CredentialsProvider) (sc credentials.StaticCredentialsProvider, err error) { 227 | creds, err := credsProvider.Retrieve(ctx) 228 | if err != nil { 229 | return sc, err 230 | } 231 | return credentials.StaticCredentialsProvider{Value: creds}, nil 232 | } 233 | 234 | // canProviderBeUsedForLogin returns true if the credentials produced by the provider is known to be usable by the login URL endpoint 235 | func canProviderBeUsedForLogin(credsProvider aws.CredentialsProvider) (bool, error) { 236 | if _, ok := credsProvider.(*vault.AssumeRoleProvider); ok { 237 | return true, nil 238 | } 239 | if _, ok := credsProvider.(*vault.SSORoleCredentialsProvider); ok { 240 | return true, nil 241 | } 242 | if _, ok := credsProvider.(*vault.AssumeRoleWithWebIdentityProvider); ok { 243 | return true, nil 244 | } 245 | if c, ok := credsProvider.(*vault.CachedSessionProvider); ok { 246 | return canProviderBeUsedForLogin(c.SessionProvider) 247 | } 248 | 249 | return false, nil 250 | } 251 | 252 | // Create a signin token 253 | func requestSigninToken(ctx context.Context, creds aws.Credentials, loginURLPrefix string) (string, error) { 254 | jsonSession, err := json.Marshal(map[string]string{ 255 | "sessionId": creds.AccessKeyID, 256 | "sessionKey": creds.SecretAccessKey, 257 | "sessionToken": creds.SessionToken, 258 | }) 259 | if err != nil { 260 | return "", err 261 | } 262 | 263 | req, err := http.NewRequestWithContext(ctx, "GET", loginURLPrefix, nil) 264 | if err != nil { 265 | return "", err 266 | } 267 | 268 | q := req.URL.Query() 269 | q.Add("Action", "getSigninToken") 270 | q.Add("Session", string(jsonSession)) 271 | req.URL.RawQuery = q.Encode() 272 | 273 | resp, err := http.DefaultClient.Do(req) 274 | if err != nil { 275 | return "", err 276 | } 277 | 278 | defer resp.Body.Close() 279 | body, err := io.ReadAll(resp.Body) 280 | if err != nil { 281 | return "", err 282 | } 283 | 284 | if resp.StatusCode != http.StatusOK { 285 | log.Printf("Response body was %s", body) 286 | return "", fmt.Errorf("Call to getSigninToken failed with %v", resp.Status) 287 | } 288 | 289 | var respParsed map[string]string 290 | 291 | err = json.Unmarshal(body, &respParsed) 292 | if err != nil { 293 | return "", err 294 | } 295 | 296 | signinToken, ok := respParsed["SigninToken"] 297 | if !ok { 298 | return "", fmt.Errorf("Expected a response with SigninToken") 299 | } 300 | 301 | return signinToken, nil 302 | } 303 | -------------------------------------------------------------------------------- /cli/proxy.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "os" 5 | "os/signal" 6 | "syscall" 7 | 8 | "github.com/99designs/aws-vault/v7/server" 9 | "github.com/alecthomas/kingpin/v2" 10 | ) 11 | 12 | func ConfigureProxyCommand(app *kingpin.Application) { 13 | stop := false 14 | 15 | cmd := app.Command("proxy", "Start a proxy for the ec2 instance role server locally."). 16 | Alias("server"). 17 | Hidden() 18 | 19 | cmd.Flag("stop", "Stop the proxy"). 20 | BoolVar(&stop) 21 | 22 | cmd.Action(func(*kingpin.ParseContext) error { 23 | if stop { 24 | server.StopProxy() 25 | return nil 26 | } 27 | handleSigTerm() 28 | return server.StartProxy() 29 | }) 30 | } 31 | 32 | func handleSigTerm() { 33 | // shutdown 34 | c := make(chan os.Signal, 1) 35 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 36 | go func() { 37 | <-c 38 | server.Shutdown() 39 | os.Exit(1) 40 | }() 41 | } 42 | -------------------------------------------------------------------------------- /cli/remove.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/99designs/aws-vault/v7/prompt" 8 | "github.com/99designs/aws-vault/v7/vault" 9 | "github.com/99designs/keyring" 10 | "github.com/alecthomas/kingpin/v2" 11 | ) 12 | 13 | type RemoveCommandInput struct { 14 | ProfileName string 15 | SessionsOnly bool 16 | Force bool 17 | } 18 | 19 | func ConfigureRemoveCommand(app *kingpin.Application, a *AwsVault) { 20 | input := RemoveCommandInput{} 21 | 22 | cmd := app.Command("remove", "Remove credentials from the secure keystore.") 23 | cmd.Alias("rm") 24 | 25 | cmd.Arg("profile", "Name of the profile"). 26 | Required(). 27 | HintAction(a.MustGetProfileNames). 28 | StringVar(&input.ProfileName) 29 | 30 | cmd.Flag("sessions-only", "Only remove sessions, leave credentials intact"). 31 | Short('s'). 32 | Hidden(). 33 | BoolVar(&input.SessionsOnly) 34 | 35 | cmd.Flag("force", "Force-remove the profile without a prompt"). 36 | Short('f'). 37 | BoolVar(&input.Force) 38 | 39 | cmd.Action(func(c *kingpin.ParseContext) error { 40 | keyring, err := a.Keyring() 41 | if err != nil { 42 | return err 43 | } 44 | err = RemoveCommand(input, keyring) 45 | app.FatalIfError(err, "remove") 46 | return nil 47 | }) 48 | } 49 | 50 | func RemoveCommand(input RemoveCommandInput, keyring keyring.Keyring) error { 51 | ckr := &vault.CredentialKeyring{Keyring: keyring} 52 | 53 | // Legacy --sessions-only option for backwards compatibility, use aws-vault clear instead 54 | if input.SessionsOnly { 55 | sk := &vault.SessionKeyring{Keyring: ckr.Keyring} 56 | n, err := sk.RemoveForProfile(input.ProfileName) 57 | if err != nil { 58 | return err 59 | } 60 | fmt.Printf("Deleted %d sessions.\n", n) 61 | return nil 62 | } 63 | 64 | if !input.Force { 65 | r, err := prompt.TerminalPrompt(fmt.Sprintf("Delete credentials for profile %q? (y|N) ", input.ProfileName)) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | if !strings.EqualFold(r, "y") && !strings.EqualFold(r, "yes") { 71 | return nil 72 | } 73 | } 74 | 75 | if err := ckr.Remove(input.ProfileName); err != nil { 76 | return err 77 | } 78 | fmt.Printf("Deleted credentials.\n") 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /cli/rotate.go: -------------------------------------------------------------------------------- 1 | package cli 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/99designs/aws-vault/v7/vault" 10 | "github.com/99designs/keyring" 11 | "github.com/alecthomas/kingpin/v2" 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | "github.com/aws/aws-sdk-go-v2/service/iam" 14 | ) 15 | 16 | type RotateCommandInput struct { 17 | NoSession bool 18 | ProfileName string 19 | Config vault.ProfileConfig 20 | } 21 | 22 | func ConfigureRotateCommand(app *kingpin.Application, a *AwsVault) { 23 | input := RotateCommandInput{} 24 | 25 | cmd := app.Command("rotate", "Rotate credentials.") 26 | 27 | cmd.Flag("no-session", "Use master credentials, no session or role used"). 28 | Short('n'). 29 | BoolVar(&input.NoSession) 30 | 31 | cmd.Arg("profile", "Name of the profile"). 32 | Required(). 33 | HintAction(a.MustGetProfileNames). 34 | StringVar(&input.ProfileName) 35 | 36 | cmd.Action(func(c *kingpin.ParseContext) (err error) { 37 | input.Config.MfaPromptMethod = a.PromptDriver(false) 38 | keyring, err := a.Keyring() 39 | if err != nil { 40 | return err 41 | } 42 | f, err := a.AwsConfigFile() 43 | if err != nil { 44 | return err 45 | } 46 | 47 | err = RotateCommand(input, f, keyring) 48 | app.FatalIfError(err, "rotate") 49 | return nil 50 | }) 51 | } 52 | 53 | func RotateCommand(input RotateCommandInput, f *vault.ConfigFile, keyring keyring.Keyring) error { 54 | configLoader := vault.NewConfigLoader(input.Config, f, input.ProfileName) 55 | config, err := configLoader.GetProfileConfig(input.ProfileName) 56 | if err != nil { 57 | return fmt.Errorf("Error loading config: %w", err) 58 | } 59 | 60 | ckr := &vault.CredentialKeyring{Keyring: keyring} 61 | masterCredentialsName, err := vault.FindMasterCredentialsNameFor(input.ProfileName, ckr, config) 62 | if err != nil { 63 | return fmt.Errorf("Error determining credential name for '%s': %w", input.ProfileName, err) 64 | } 65 | 66 | if input.NoSession { 67 | fmt.Printf("Rotating credentials stored for profile '%s' using master credentials (takes 10-20 seconds)\n", masterCredentialsName) 68 | } else { 69 | fmt.Printf("Rotating credentials stored for profile '%s' using a session from profile '%s' (takes 10-20 seconds)\n", masterCredentialsName, input.ProfileName) 70 | } 71 | 72 | // Get the existing credentials access key ID 73 | oldMasterCreds, err := vault.NewMasterCredentialsProvider(ckr, masterCredentialsName).Retrieve(context.TODO()) 74 | if err != nil { 75 | return fmt.Errorf("Error loading source credentials for '%s': %w", masterCredentialsName, err) 76 | } 77 | oldMasterCredsAccessKeyID := vault.FormatKeyForDisplay(oldMasterCreds.AccessKeyID) 78 | log.Printf("Rotating access key %s\n", oldMasterCredsAccessKeyID) 79 | 80 | fmt.Println("Creating a new access key") 81 | 82 | // create a session to rotate the credentials 83 | var credsProvider aws.CredentialsProvider 84 | if input.NoSession { 85 | credsProvider = vault.NewMasterCredentialsProvider(ckr, config.ProfileName) 86 | } else { 87 | // Can't always disable sessions completely, might need to use session for MFA-Protected API Access 88 | credsProvider, err = vault.NewTempCredentialsProvider(config, ckr, input.NoSession, true) 89 | if err != nil { 90 | return fmt.Errorf("Error getting temporary credentials: %w", err) 91 | } 92 | } 93 | 94 | cfg := vault.NewAwsConfigWithCredsProvider(credsProvider, config.Region, config.STSRegionalEndpoints) 95 | 96 | // A username is needed for some IAM calls if the credentials have assumed a role 97 | iamUserName, err := getUsernameIfAssumingRole(context.TODO(), cfg, config) 98 | if err != nil { 99 | return err 100 | } 101 | 102 | iamClient := iam.NewFromConfig(cfg) 103 | // Create a new access key 104 | createOut, err := iamClient.CreateAccessKey(context.TODO(), &iam.CreateAccessKeyInput{ 105 | UserName: iamUserName, 106 | }) 107 | if err != nil { 108 | return fmt.Errorf("Error creating a new access key: %w", err) 109 | } 110 | fmt.Printf("Created new access key %s\n", vault.FormatKeyForDisplay(*createOut.AccessKey.AccessKeyId)) 111 | 112 | newMasterCreds := aws.Credentials{ 113 | AccessKeyID: *createOut.AccessKey.AccessKeyId, 114 | SecretAccessKey: *createOut.AccessKey.SecretAccessKey, 115 | } 116 | 117 | err = ckr.Set(masterCredentialsName, newMasterCreds) 118 | if err != nil { 119 | return fmt.Errorf("Error storing new access key %s: %w", vault.FormatKeyForDisplay(newMasterCreds.AccessKeyID), err) 120 | } 121 | 122 | // Delete old sessions 123 | sk := &vault.SessionKeyring{Keyring: ckr.Keyring} 124 | profileNames, err := getProfilesInChain(input.ProfileName, configLoader) 125 | for _, profileName := range profileNames { 126 | if n, _ := sk.RemoveForProfile(profileName); n > 0 { 127 | fmt.Printf("Deleted %d sessions for %s\n", n, profileName) 128 | } 129 | } 130 | 131 | // Use new credentials to delete old access key 132 | fmt.Printf("Deleting old access key %s\n", oldMasterCredsAccessKeyID) 133 | err = retry(time.Second*20, time.Second*2, func() error { 134 | _, err = iamClient.DeleteAccessKey(context.TODO(), &iam.DeleteAccessKeyInput{ 135 | AccessKeyId: &oldMasterCreds.AccessKeyID, 136 | UserName: iamUserName, 137 | }) 138 | return err 139 | }) 140 | if err != nil { 141 | return fmt.Errorf("Can't delete old access key %s: %w", oldMasterCredsAccessKeyID, err) 142 | } 143 | fmt.Printf("Deleted old access key %s\n", oldMasterCredsAccessKeyID) 144 | 145 | fmt.Println("Finished rotating access key") 146 | 147 | return nil 148 | } 149 | 150 | func retry(maxTime time.Duration, sleep time.Duration, f func() error) (err error) { 151 | t0 := time.Now() 152 | i := 0 153 | for { 154 | i++ 155 | 156 | err = f() 157 | if err == nil { 158 | return // nolint 159 | } 160 | 161 | elapsed := time.Since(t0) 162 | if elapsed > maxTime { 163 | return fmt.Errorf("After %d attempts, last error: %s", i, err) 164 | } 165 | 166 | time.Sleep(sleep) 167 | log.Println("Retrying after error:", err) 168 | } 169 | } 170 | 171 | func getUsernameIfAssumingRole(ctx context.Context, awsCfg aws.Config, config *vault.ProfileConfig) (*string, error) { 172 | if config.RoleARN != "" { 173 | n, err := vault.GetUsernameFromSession(ctx, awsCfg) 174 | if err != nil { 175 | return nil, fmt.Errorf("Error getting IAM username from session: %w", err) 176 | } 177 | log.Printf("Found IAM username '%s'", n) 178 | return &n, nil 179 | } 180 | return nil, nil //nolint 181 | } 182 | 183 | func getProfilesInChain(profileName string, configLoader *vault.ConfigLoader) (profileNames []string, err error) { 184 | profileNames = append(profileNames, profileName) 185 | 186 | config, err := configLoader.GetProfileConfig(profileName) 187 | if err != nil { 188 | return profileNames, err 189 | } 190 | 191 | if config.SourceProfile != nil { 192 | newProfileNames, err := getProfilesInChain(config.SourceProfileName, configLoader) 193 | if err != nil { 194 | return profileNames, err 195 | } 196 | profileNames = append(profileNames, newProfileNames...) 197 | } 198 | 199 | return profileNames, nil 200 | } 201 | -------------------------------------------------------------------------------- /contrib/_aws-vault-proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 2 | WORKDIR /usr/src/aws-vault-proxy 3 | COPY . /usr/src/aws-vault-proxy 4 | RUN go build -v -o /usr/local/bin/aws-vault-proxy ./... 5 | CMD ["/usr/local/bin/aws-vault-proxy"] 6 | -------------------------------------------------------------------------------- /contrib/_aws-vault-proxy/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2.4" 2 | networks: 3 | aws-vault: 4 | driver: bridge 5 | ipam: 6 | config: 7 | - subnet: "169.254.170.0/24" 8 | gateway: "169.254.170.1" 9 | services: 10 | aws-vault-proxy: 11 | build: . 12 | environment: 13 | - AWS_CONTAINER_CREDENTIALS_FULL_URI 14 | - AWS_CONTAINER_AUTHORIZATION_TOKEN 15 | networks: 16 | aws-vault: 17 | ipv4_address: "169.254.170.2" # This special IP address is recognized by the AWS SDKs and AWS CLI 18 | healthcheck: 19 | test: pgrep aws-vault-proxy 20 | testapp: 21 | image: amazon/aws-cli 22 | entrypoint: "" 23 | command: /bin/bash 24 | environment: 25 | - AWS_CONTAINER_CREDENTIALS_RELATIVE_URI 26 | networks: 27 | aws-vault: {} 28 | default: {} 29 | -------------------------------------------------------------------------------- /contrib/_aws-vault-proxy/go.mod: -------------------------------------------------------------------------------- 1 | module aws-vault-ecs-server-reverse-proxy 2 | 3 | go 1.17 4 | 5 | require github.com/gorilla/handlers v1.5.1 6 | 7 | require github.com/felixge/httpsnoop v1.0.1 // indirect 8 | -------------------------------------------------------------------------------- /contrib/_aws-vault-proxy/go.sum: -------------------------------------------------------------------------------- 1 | github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ= 2 | github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 3 | github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4= 4 | github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q= 5 | -------------------------------------------------------------------------------- /contrib/_aws-vault-proxy/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "net/http/httputil" 7 | "net/url" 8 | "os" 9 | 10 | "github.com/gorilla/handlers" 11 | ) 12 | 13 | func GetReverseProxyTarget() *url.URL { 14 | url, err := url.Parse(os.Getenv("AWS_CONTAINER_CREDENTIALS_FULL_URI")) 15 | if err != nil { 16 | log.Fatalln("Bad AWS_CONTAINER_CREDENTIALS_FULL_URI:", err.Error()) 17 | } 18 | url.Host = "host.docker.internal:" + url.Port() 19 | return url 20 | } 21 | 22 | func addAuthorizationHeader(authToken string, next http.Handler) http.HandlerFunc { 23 | return func(w http.ResponseWriter, r *http.Request) { 24 | r.Header.Add("Authorization", authToken) 25 | next.ServeHTTP(w, r) 26 | } 27 | } 28 | 29 | func main() { 30 | target := GetReverseProxyTarget() 31 | authToken := os.Getenv("AWS_CONTAINER_AUTHORIZATION_TOKEN") 32 | log.Printf("reverse proxying target:%s auth:%s\n", target, authToken) 33 | 34 | handler := handlers.LoggingHandler(os.Stderr, 35 | addAuthorizationHeader(authToken, 36 | httputil.NewSingleHostReverseProxy(target))) 37 | 38 | _ = http.ListenAndServe(":80", handler) 39 | } 40 | -------------------------------------------------------------------------------- /contrib/completions/bash/aws-vault.bash: -------------------------------------------------------------------------------- 1 | _aws-vault_bash_autocomplete() { 2 | local i cur prev opts base 3 | 4 | for (( i=1; i < COMP_CWORD; i++ )); do 5 | if [[ ${COMP_WORDS[i]} == -- ]]; then 6 | _command_offset $i+1 7 | return 8 | fi 9 | done 10 | 11 | COMPREPLY=() 12 | cur="${COMP_WORDS[COMP_CWORD]}" 13 | opts=$( ${COMP_WORDS[0]} --completion-bash "${COMP_WORDS[@]:1:$COMP_CWORD}" ) 14 | COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) ) 15 | return 0 16 | } 17 | complete -F _aws-vault_bash_autocomplete -o default aws-vault 18 | -------------------------------------------------------------------------------- /contrib/completions/fish/aws-vault.fish: -------------------------------------------------------------------------------- 1 | if status --is-interactive 2 | complete -ec aws-vault 3 | 4 | # switch based on seeing a `--` 5 | complete -c aws-vault -n 'not __fish_aws_vault_is_commandline' -xa '(__fish_aws_vault_complete_arg)' 6 | complete -c aws-vault -n '__fish_aws_vault_is_commandline' -xa '(__fish_aws_vault_complete_commandline)' 7 | 8 | function __fish_aws_vault_is_commandline 9 | string match -q -r '^--$' -- (commandline -opc) 10 | end 11 | 12 | function __fish_aws_vault_complete_arg 13 | set -l parts (commandline -opc) 14 | set -e parts[1] 15 | 16 | aws-vault --completion-bash $parts 17 | end 18 | 19 | function __fish_aws_vault_complete_commandline 20 | set -l parts (string split --max 1 '--' -- (commandline -pc)) 21 | 22 | complete "-C$parts[2]" 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /contrib/completions/zsh/aws-vault.zsh: -------------------------------------------------------------------------------- 1 | #compdef aws-vault 2 | 3 | _aws-vault() { 4 | local i 5 | for (( i=2; i < CURRENT; i++ )); do 6 | if [[ ${words[i]} == -- ]]; then 7 | shift $i words 8 | (( CURRENT -= i )) 9 | _normal 10 | return 11 | fi 12 | done 13 | 14 | local matches=($(${words[1]} --completion-bash ${(@)words[2,$CURRENT]})) 15 | compadd -a matches 16 | 17 | if [[ $compstate[nmatches] -eq 0 && $words[$CURRENT] != -* ]]; then 18 | _files 19 | fi 20 | } 21 | 22 | if [[ "$(basename -- ${(%):-%x})" != "_aws-vault" ]]; then 23 | compdef _aws-vault aws-vault 24 | fi 25 | -------------------------------------------------------------------------------- /contrib/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:bullseye-slim 2 | RUN apt update && apt install -y curl 3 | 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 4 | ENV AWS_VAULT_BACKEND=file 5 | ENTRYPOINT ["/usr/local/bin/aws-vault"] 6 | 7 | # Example usage: 8 | # docker build -t aws-vault . 9 | # docker run -it -e COLUMNS=$(tput cols) -v ~/.aws/config:/root/.aws/config -v ~/.awsvault:/root/.awsvault aws-vault 10 | -------------------------------------------------------------------------------- /contrib/scripts/aws-configure-with-env-vars.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Configure aws-cli using the AWS env vars created with aws-vault 3 | # 4 | # Usage: aws-vault exec -- aws-configure-with-env-vars.sh [TARGET_PROFILE] 5 | # 6 | 7 | set -eu 8 | 9 | aws configure --profile "${1:-$AWS_VAULT}" set region "$AWS_REGION" 10 | aws configure --profile "${1:-$AWS_VAULT}" set aws_access_key_id "$AWS_ACCESS_KEY_ID" 11 | aws configure --profile "${1:-$AWS_VAULT}" set aws_secret_access_key "$AWS_SECRET_ACCESS_KEY" 12 | aws configure --profile "${1:-$AWS_VAULT}" set aws_session_token "${AWS_SESSION_TOKEN:-}" 13 | -------------------------------------------------------------------------------- /contrib/scripts/aws-iam-create-yubikey-mfa.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Adds a Yubikey TOTP device to IAM using your IAM User as the $MFA_DEVICE_NAME 3 | # Currently, aws iam enable-mfa-device doesn't support specifying your MFA Device Name. 4 | 5 | set -eu 6 | 7 | if [ -n "${AWS_SESSION_TOKEN:-}" ]; then 8 | echo "aws-vault must be run without a STS session, please run it with the --no-session flag" >&2 9 | exit 1 10 | fi 11 | 12 | ACCOUNT_ARN=$(aws sts get-caller-identity --query Arn --output text) 13 | 14 | # Assume that the final portion of the ARN is the username 15 | # Works for ARNs like `users/` and `users/engineers/` 16 | USERNAME=$(echo "$ACCOUNT_ARN" | rev | cut -d/ -f1 | rev) 17 | 18 | OUTFILE=$(mktemp) 19 | trap 'rm -f "$OUTFILE"' EXIT 20 | 21 | SERIAL_NUMBER=$(aws iam create-virtual-mfa-device \ 22 | --virtual-mfa-device-name "$USERNAME" \ 23 | --bootstrap-method Base32StringSeed \ 24 | --outfile "$OUTFILE" \ 25 | --query VirtualMFADevice.SerialNumber \ 26 | --output text) 27 | 28 | ykman oath accounts add -ft "$SERIAL_NUMBER" < "$OUTFILE" 2> /dev/null 29 | 30 | CODE1=$(ykman oath accounts code -s "$SERIAL_NUMBER") 31 | 32 | WAIT_TIME=$((30-$(date +%s)%30)) 33 | echo "Waiting $WAIT_TIME seconds before generating a second code" >&2 34 | sleep $WAIT_TIME 35 | 36 | CODE2=$(ykman oath accounts code -s "$SERIAL_NUMBER") 37 | 38 | aws iam enable-mfa-device \ 39 | --user-name "$USERNAME" \ 40 | --serial-number "$SERIAL_NUMBER" \ 41 | --authentication-code1 "$CODE1" \ 42 | --authentication-code2 "$CODE2" 43 | -------------------------------------------------------------------------------- /contrib/scripts/aws-iam-resync-yubikey-mfa.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Resync a Yubikey TOTP device to IAM using your IAM User as the $MFA_DEVICE_NAME 3 | # Currently, aws iam resync-mfa-device doesn't support specifying your MFA Device Name. 4 | 5 | set -eu 6 | 7 | ACCOUNT_ARN=$(aws sts get-caller-identity --query Arn --output text) 8 | 9 | # Assume that the final portion of the ARN is the username 10 | # Works for ARNs like `users/` and `users/engineers/` 11 | USERNAME=$(echo "$ACCOUNT_ARN" | rev | cut -d/ -f1 | rev) 12 | 13 | ACCOUNT_ID=$(echo "$ACCOUNT_ARN" | cut -d: -f5) 14 | SERIAL_NUMBER="arn:aws:iam::${ACCOUNT_ID}:mfa/${USERNAME}" 15 | 16 | CODE1=$(ykman oath accounts code -s "$SERIAL_NUMBER") 17 | 18 | WAIT_TIME=$((30-$(date +%s)%30)) 19 | echo "Waiting $WAIT_TIME seconds before generating a second code" >&2 20 | sleep $WAIT_TIME 21 | 22 | CODE2=$(ykman oath accounts code -s "$SERIAL_NUMBER") 23 | 24 | aws iam resync-mfa-device \ 25 | --user-name "$USERNAME" \ 26 | --serial-number "$SERIAL_NUMBER" \ 27 | --authentication-code1 "$CODE1" \ 28 | --authentication-code2 "$CODE2" 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/99designs/aws-vault/v7 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/99designs/keyring v1.2.2 7 | github.com/alecthomas/kingpin/v2 v2.3.2 8 | github.com/aws/aws-sdk-go-v2 v1.17.7 9 | github.com/aws/aws-sdk-go-v2/config v1.18.19 10 | github.com/aws/aws-sdk-go-v2/credentials v1.13.18 11 | github.com/aws/aws-sdk-go-v2/service/iam v1.19.8 12 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 13 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 14 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 15 | github.com/google/go-cmp v0.5.9 16 | github.com/mattn/go-isatty v0.0.18 17 | github.com/mattn/go-tty v0.0.4 18 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 19 | golang.org/x/term v0.6.0 20 | gopkg.in/ini.v1 v1.67.0 21 | ) 22 | 23 | require ( 24 | github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect 25 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 26 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 // indirect 27 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 // indirect 28 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 // indirect 29 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 // indirect 30 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 // indirect 31 | github.com/aws/smithy-go v1.13.5 // indirect 32 | github.com/danieljoos/wincred v1.1.2 // indirect 33 | github.com/dvsekhvalnov/jose2go v1.5.0 // indirect 34 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect 35 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect 36 | github.com/mtibben/percent v0.2.1 // indirect 37 | github.com/xhit/go-str2duration/v2 v2.1.0 // indirect 38 | golang.org/x/sys v0.6.0 // indirect 39 | ) 40 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= 2 | github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= 3 | github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= 4 | github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= 5 | github.com/alecthomas/kingpin/v2 v2.3.2 h1:H0aULhgmSzN8xQ3nX1uxtdlTHYoPLu5AhHxWrKI6ocU= 6 | github.com/alecthomas/kingpin/v2 v2.3.2/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= 7 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 8 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 9 | github.com/aws/aws-sdk-go-v2 v1.17.7 h1:CLSjnhJSTSogvqUGhIC6LqFKATMRexcxLZ0i/Nzk9Eg= 10 | github.com/aws/aws-sdk-go-v2 v1.17.7/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= 11 | github.com/aws/aws-sdk-go-v2/config v1.18.19 h1:AqFK6zFNtq4i1EYu+eC7lcKHYnZagMn6SW171la0bGw= 12 | github.com/aws/aws-sdk-go-v2/config v1.18.19/go.mod h1:XvTmGMY8d52ougvakOv1RpiTLPz9dlG/OQHsKU/cMmY= 13 | github.com/aws/aws-sdk-go-v2/credentials v1.13.18 h1:EQMdtHwz0ILTW1hoP+EwuWhwCG1hD6l3+RWFQABET4c= 14 | github.com/aws/aws-sdk-go-v2/credentials v1.13.18/go.mod h1:vnwlwjIe+3XJPBYKu1et30ZPABG3VaXJYr8ryohpIyM= 15 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1 h1:gt57MN3liKiyGopcqgNzJb2+d9MJaKT/q1OksHNXVE4= 16 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.13.1/go.mod h1:lfUx8puBRdM5lVVMQlwt2v+ofiG/X6Ms+dy0UkG/kXw= 17 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31 h1:sJLYcS+eZn5EeNINGHSCRAwUJMFVqklwkH36Vbyai7M= 18 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.31/go.mod h1:QT0BqUvX1Bh2ABdTGnjqEjvjzrCfIniM9Sc8zn9Yndo= 19 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25 h1:1mnRASEKnkqsntcxHaysxwgVoUUp5dkiB+l3llKnqyg= 20 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.25/go.mod h1:zBHOPwhBc3FlQjQJE/D3IfPWiWaQmT06Vq9aNukDo0k= 21 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32 h1:p5luUImdIqywn6JpQsW3tq5GNOxKmOnEpybzPx+d1lk= 22 | github.com/aws/aws-sdk-go-v2/internal/ini v1.3.32/go.mod h1:XGhIBZDEgfqmFIugclZ6FU7v75nHhBDtzuB4xB/tEi4= 23 | github.com/aws/aws-sdk-go-v2/service/iam v1.19.8 h1:kQsBeGgm68kT0xc90spgC5qEOQGH74V2bFqgBgG21Bo= 24 | github.com/aws/aws-sdk-go-v2/service/iam v1.19.8/go.mod h1:lf/oAjt//UvPsmnOgPT61F+q4K6U0q4zDd1s1yx2NZs= 25 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25 h1:5LHn8JQ0qvjD9L9JhMtylnkcw7j05GDZqM9Oin6hpr0= 26 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.25/go.mod h1:/95IA+0lMnzW6XzqYJRpjjsAbKEORVeO0anQqjd2CNU= 27 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.6 h1:5V7DWLBd7wTELVz5bPpwzYy/sikk0gsgZfj40X+l5OI= 28 | github.com/aws/aws-sdk-go-v2/service/sso v1.12.6/go.mod h1:Y1VOmit/Fn6Tz1uFAeCO6Q7M2fmfXSCLeL5INVYsLuY= 29 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6 h1:B8cauxOH1W1v7rd8RdI/MWnoR4Ze0wIHWrb90qczxj4= 30 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.14.6/go.mod h1:Lh/bc9XUf8CfOY6Jp5aIkQtN+j1mc+nExc+KXj9jx2s= 31 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.7 h1:bWNgNdRko2x6gqa0blfATqAZKZokPIeM1vfmQt2pnvM= 32 | github.com/aws/aws-sdk-go-v2/service/sts v1.18.7/go.mod h1:JuTnSoeePXmMVe9G8NcjjwgOKEfZ4cOjMuT2IBT/2eI= 33 | github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8= 34 | github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= 35 | github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= 36 | github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= 37 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 38 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 39 | github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= 40 | github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= 41 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= 42 | github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= 43 | github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 44 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 45 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 46 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= 47 | github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= 48 | github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= 49 | github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= 50 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 51 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 52 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 53 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 54 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 55 | github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= 56 | github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= 57 | github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 58 | github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= 59 | github.com/mattn/go-tty v0.0.4 h1:NVikla9X8MN0SQAqCYzpGyXv0jY7MNl3HOWD2dkle7E= 60 | github.com/mattn/go-tty v0.0.4/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28= 61 | github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= 62 | github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= 63 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 64 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 65 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 66 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= 68 | github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= 69 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 70 | github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= 71 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 72 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 73 | github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= 74 | github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= 75 | github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= 76 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 77 | golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 80 | golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= 81 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 82 | golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= 83 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 84 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 85 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= 86 | gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= 88 | gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= 89 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 90 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 91 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 92 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 93 | -------------------------------------------------------------------------------- /iso8601/iso8601.go: -------------------------------------------------------------------------------- 1 | package iso8601 2 | 3 | import "time" 4 | 5 | // Format outputs an ISO-8601 datetime string from the given time, 6 | // in a format compatible with all of the AWS SDKs 7 | func Format(t time.Time) string { 8 | return t.UTC().Format(time.RFC3339) 9 | } 10 | -------------------------------------------------------------------------------- /iso8601/iso8601_test.go: -------------------------------------------------------------------------------- 1 | package iso8601 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestFormat(t *testing.T) { 9 | input, _ := time.Parse(time.RFC3339, "2009-02-04T21:00:57-08:00") 10 | want := "2009-02-05T05:00:57Z" 11 | result := Format(input) 12 | if result != want { 13 | t.Errorf("expected %s for %q got %s", want, input, result) 14 | } 15 | } 16 | 17 | func TestFormatForIssue655(t *testing.T) { 18 | input, _ := time.Parse(time.RFC3339, "2020-09-10T18:16:52+02:00") 19 | want := "2020-09-10T16:16:52Z" 20 | result := Format(input) 21 | if result != want { 22 | t.Errorf("expected %s for %q got %s", want, input, result) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/99designs/aws-vault/v7/cli" 7 | "github.com/alecthomas/kingpin/v2" 8 | ) 9 | 10 | // Version is provided at compile time 11 | var Version = "dev" 12 | 13 | func main() { 14 | app := kingpin.New("aws-vault", "A vault for securely storing and accessing AWS credentials in development environments.") 15 | app.Version(Version) 16 | 17 | a := cli.ConfigureGlobals(app) 18 | cli.ConfigureAddCommand(app, a) 19 | cli.ConfigureRemoveCommand(app, a) 20 | cli.ConfigureListCommand(app, a) 21 | cli.ConfigureRotateCommand(app, a) 22 | cli.ConfigureExecCommand(app, a) 23 | cli.ConfigureExportCommand(app, a) 24 | cli.ConfigureClearCommand(app, a) 25 | cli.ConfigureLoginCommand(app, a) 26 | cli.ConfigureProxyCommand(app) 27 | 28 | kingpin.MustParse(app.Parse(os.Args[1:])) 29 | } 30 | -------------------------------------------------------------------------------- /prompt/kdialog.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | ) 7 | 8 | func KDialogMfaPrompt(mfaSerial string) (string, error) { 9 | cmd := exec.Command("kdialog", "--inputbox", mfaPromptMessage(mfaSerial), "--title", "aws-vault") 10 | 11 | out, err := cmd.Output() 12 | if err != nil { 13 | return "", err 14 | } 15 | 16 | return strings.TrimSpace(string(out)), nil 17 | } 18 | 19 | func init() { 20 | if _, err := exec.LookPath("kdialog"); err == nil { 21 | Methods["kdialog"] = KDialogMfaPrompt 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /prompt/osascript.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "os/exec" 6 | "strings" 7 | ) 8 | 9 | func OSAScriptMfaPrompt(mfaSerial string) (string, error) { 10 | cmd := exec.Command("osascript", "-e", fmt.Sprintf(` 11 | display dialog %q default answer "" buttons {"OK", "Cancel"} default button 1 12 | text returned of the result 13 | return result`, 14 | mfaPromptMessage(mfaSerial))) 15 | 16 | out, err := cmd.Output() 17 | if err != nil { 18 | return "", err 19 | } 20 | 21 | return strings.TrimSpace(string(out)), nil 22 | } 23 | 24 | func init() { 25 | if _, err := exec.LookPath("osascript"); err == nil { 26 | Methods["osascript"] = OSAScriptMfaPrompt 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /prompt/prompt.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | ) 7 | 8 | type Func func(string) (string, error) 9 | 10 | var Methods = map[string]Func{} 11 | 12 | func Available() []string { 13 | methods := []string{} 14 | for k := range Methods { 15 | methods = append(methods, k) 16 | } 17 | sort.Strings(methods) 18 | return methods 19 | } 20 | 21 | func Method(s string) Func { 22 | m, ok := Methods[s] 23 | if !ok { 24 | panic(fmt.Sprintf("Prompt method %q doesn't exist", s)) 25 | } 26 | return m 27 | } 28 | 29 | func mfaPromptMessage(mfaSerial string) string { 30 | return fmt.Sprintf("Enter MFA code for %s: ", mfaSerial) 31 | } 32 | -------------------------------------------------------------------------------- /prompt/terminal.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/mattn/go-tty" 8 | ) 9 | 10 | func TerminalPrompt(message string) (string, error) { 11 | tty, err := tty.Open() 12 | if err != nil { 13 | return "", err 14 | } 15 | defer tty.Close() 16 | 17 | fmt.Fprint(tty.Output(), message) 18 | 19 | text, err := tty.ReadString() 20 | if err != nil { 21 | return "", err 22 | } 23 | 24 | return strings.TrimSpace(text), nil 25 | } 26 | 27 | func TerminalSecretPrompt(message string) (string, error) { 28 | tty, err := tty.Open() 29 | if err != nil { 30 | return "", err 31 | } 32 | defer tty.Close() 33 | 34 | fmt.Fprint(tty.Output(), message) 35 | 36 | text, err := tty.ReadPassword() 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | return strings.TrimSpace(text), nil 42 | } 43 | 44 | func TerminalMfaPrompt(mfaSerial string) (string, error) { 45 | return TerminalPrompt(mfaPromptMessage(mfaSerial)) 46 | } 47 | 48 | func init() { 49 | Methods["terminal"] = TerminalMfaPrompt 50 | } 51 | -------------------------------------------------------------------------------- /prompt/wincredui_windows.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "errors" 5 | "strings" 6 | "syscall" 7 | "unsafe" 8 | ) 9 | 10 | const ( 11 | CREDUI_FLAGS_ALWAYS_SHOW_UI = 0x00080 12 | CREDUI_FLAGS_GENERIC_CREDENTIALS = 0x40000 13 | CREDUI_FLAGS_KEEP_USERNAME = 0x100000 14 | ) 15 | 16 | type creduiInfoA struct { 17 | cbSize uint32 18 | hwndParent uintptr 19 | pszMessageText *uint16 20 | pszCaptionText *uint16 21 | hbmBanner uintptr 22 | } 23 | 24 | func WinCredUiPrompt(mfaSerial string) (string, error) { 25 | info := &creduiInfoA{ 26 | hwndParent: 0, 27 | pszCaptionText: syscall.StringToUTF16Ptr("Enter MFA code for aws-vault"), 28 | pszMessageText: syscall.StringToUTF16Ptr(mfaPromptMessage(mfaSerial)), 29 | hbmBanner: 0, 30 | } 31 | info.cbSize = uint32(unsafe.Sizeof(*info)) 32 | passwordBuf := make([]uint16, 64) 33 | save := false 34 | flags := CREDUI_FLAGS_ALWAYS_SHOW_UI | CREDUI_FLAGS_KEEP_USERNAME | CREDUI_FLAGS_GENERIC_CREDENTIALS 35 | shortSerial := strings.ReplaceAll(strings.ReplaceAll(mfaSerial, "arn:aws:iam::", ""), ":mfa", "") 36 | 37 | ret, _, _ := syscall.NewLazyDLL("credui.dll").NewProc("CredUIPromptForCredentialsW").Call( 38 | uintptr(unsafe.Pointer(info)), 39 | uintptr(unsafe.Pointer(syscall.StringBytePtr("aws-vault"))), 40 | 0, 41 | 0, 42 | uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(shortSerial))), 43 | uintptr(len(shortSerial)+1), 44 | uintptr(unsafe.Pointer(&passwordBuf[0])), 45 | 64, 46 | uintptr(unsafe.Pointer(&save)), 47 | uintptr(flags), 48 | ) 49 | if ret != 0 { 50 | return "", errors.New("wincredui: call to CredUIPromptForCredentialsW failed") 51 | } 52 | 53 | return strings.TrimSpace(syscall.UTF16ToString(passwordBuf)), nil 54 | } 55 | 56 | func init() { 57 | Methods["wincredui"] = WinCredUiPrompt 58 | } 59 | -------------------------------------------------------------------------------- /prompt/ykman.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "strings" 9 | ) 10 | 11 | // YkmanProvider runs ykman to generate a OATH-TOTP token from the Yubikey device 12 | // To set up ykman, first run `ykman oath accounts add` 13 | func YkmanMfaProvider(mfaSerial string) (string, error) { 14 | args := []string{} 15 | 16 | yubikeyOathCredName := os.Getenv("YKMAN_OATH_CREDENTIAL_NAME") 17 | if yubikeyOathCredName == "" { 18 | yubikeyOathCredName = mfaSerial 19 | } 20 | 21 | // Get the serial number of the yubikey device to use. 22 | yubikeyDeviceSerial := os.Getenv("YKMAN_OATH_DEVICE_SERIAL") 23 | if yubikeyDeviceSerial != "" { 24 | // If the env var was set, extend args to support passing the serial. 25 | args = append(args, "--device", yubikeyDeviceSerial) 26 | } 27 | 28 | // default to v4 and above 29 | switch os.Getenv("AWS_VAULT_YKMAN_VERSION") { 30 | case "1", "2", "3": 31 | args = append(args, "oath", "code", "--single", yubikeyOathCredName) 32 | default: 33 | args = append(args, "oath", "accounts", "code", "--single", yubikeyOathCredName) 34 | } 35 | 36 | log.Printf("Fetching MFA code using `ykman %s`", strings.Join(args, " ")) 37 | cmd := exec.Command("ykman", args...) 38 | cmd.Stderr = os.Stderr 39 | 40 | out, err := cmd.Output() 41 | if err != nil { 42 | return "", fmt.Errorf("ykman: %w", err) 43 | } 44 | 45 | return strings.TrimSpace(string(out)), nil 46 | } 47 | 48 | func init() { 49 | if _, err := exec.LookPath("ykman"); err == nil { 50 | Methods["ykman"] = YkmanMfaProvider 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /prompt/zenity.go: -------------------------------------------------------------------------------- 1 | package prompt 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | ) 7 | 8 | func ZenityMfaPrompt(mfaSerial string) (string, error) { 9 | cmd := exec.Command("zenity", "--entry", "--title", "aws-vault", "--text", mfaPromptMessage(mfaSerial)) 10 | 11 | out, err := cmd.Output() 12 | if err != nil { 13 | return "", err 14 | } 15 | 16 | return strings.TrimSpace(string(out)), nil 17 | } 18 | 19 | func init() { 20 | if _, err := exec.LookPath("zenity"); err == nil { 21 | Methods["zenity"] = ZenityMfaPrompt 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /server/ec2alias_bsd.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || openbsd 2 | // +build darwin freebsd openbsd 3 | 4 | package server 5 | 6 | import "os/exec" 7 | 8 | func installEc2EndpointNetworkAlias() ([]byte, error) { 9 | return exec.Command("ifconfig", "lo0", "alias", "169.254.169.254").CombinedOutput() 10 | } 11 | 12 | func removeEc2EndpointNetworkAlias() ([]byte, error) { 13 | return exec.Command("ifconfig", "lo0", "-alias", "169.254.169.254").CombinedOutput() 14 | } 15 | -------------------------------------------------------------------------------- /server/ec2alias_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package server 5 | 6 | import "os/exec" 7 | 8 | func installEc2EndpointNetworkAlias() ([]byte, error) { 9 | return exec.Command("ip", "addr", "add", "169.254.169.254/24", "dev", "lo", "label", "lo:0").CombinedOutput() 10 | } 11 | 12 | func removeEc2EndpointNetworkAlias() ([]byte, error) { 13 | return exec.Command("ip", "addr", "del", "169.254.169.254/24", "dev", "lo", "label", "lo:0").CombinedOutput() 14 | } 15 | -------------------------------------------------------------------------------- /server/ec2alias_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package server 5 | 6 | import ( 7 | "fmt" 8 | "os/exec" 9 | "strings" 10 | ) 11 | 12 | var alreadyRegisteredLocalised = []string{ 13 | "The object already exists", 14 | "Das Objekt ist bereits vorhanden", 15 | "El objeto ya existe", 16 | } 17 | 18 | var runAsAdministratorLocalised = []string{ 19 | "Run as administrator", 20 | // truncate before 'Umlaut' to avoid encoding problems coming from Windows cmd 21 | "Als Administrator ausf", 22 | "Ejecutar como administrador", 23 | } 24 | 25 | func msgFound(localised []string, toTest string) bool { 26 | for _, value := range localised { 27 | if strings.Contains(toTest, value) { 28 | return true 29 | } 30 | } 31 | 32 | return false 33 | } 34 | 35 | func runAndWrapAdminErrors(name string, arg ...string) ([]byte, error) { 36 | out, err := exec.Command(name, arg...).CombinedOutput() 37 | if msgFound(runAsAdministratorLocalised, string(out)) { 38 | err = fmt.Errorf("Creation of network alias for server mode requires elevated permissions, run as administrator", err) 39 | } 40 | 41 | return out, err 42 | } 43 | 44 | func installEc2EndpointNetworkAlias() ([]byte, error) { 45 | out, err := runAndWrapAdminErrors("netsh", "interface", "ipv4", "add", "address", "Loopback Pseudo-Interface 1", "169.254.169.254", "255.255.0.0") 46 | if msgFound(alreadyRegisteredLocalised, string(out)) { 47 | return []byte{}, nil 48 | } 49 | 50 | return out, err 51 | } 52 | 53 | func removeEc2EndpointNetworkAlias() ([]byte, error) { 54 | return runAndWrapAdminErrors("netsh", "interface", "ipv4", "delete", "address", "Loopback Pseudo-Interface 1", "169.254.169.254", "255.255.0.0") 55 | } 56 | -------------------------------------------------------------------------------- /server/ec2proxy.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net" 7 | "net/http" 8 | "net/http/httputil" 9 | "net/url" 10 | "os" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const ( 16 | ec2MetadataEndpointIP = "169.254.169.254" 17 | ec2MetadataEndpointAddr = "169.254.169.254:80" 18 | ) 19 | 20 | // StartProxy starts a http proxy server that listens on the standard EC2 Instance Metadata endpoint http://169.254.169.254:80/ 21 | // and forwards requests through to the running `aws-vault exec` command 22 | func StartProxy() error { 23 | var localServerURL, err = url.Parse(fmt.Sprintf("http://%s/", ec2CredentialsServerAddr)) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | if output, err := installEc2EndpointNetworkAlias(); err != nil { 29 | return fmt.Errorf("%s: %s", strings.TrimSpace(string(output)), err.Error()) 30 | } 31 | 32 | l, err := net.Listen("tcp", ec2MetadataEndpointAddr) 33 | if err != nil { 34 | return err 35 | } 36 | 37 | handler := http.NewServeMux() 38 | handler.HandleFunc("/stop", func(w http.ResponseWriter, r *http.Request) { 39 | w.WriteHeader(http.StatusOK) 40 | go Shutdown() 41 | }) 42 | handler.Handle("/", httputil.NewSingleHostReverseProxy(localServerURL)) 43 | 44 | log.Printf("EC2 Instance Metadata endpoint proxy server running on %s", l.Addr()) 45 | return http.Serve(l, handler) 46 | } 47 | 48 | func IsProxyRunning() bool { 49 | _, err := net.DialTimeout("tcp", ec2MetadataEndpointAddr, time.Millisecond*10) 50 | return err == nil 51 | } 52 | 53 | func Shutdown() { 54 | _, err := removeEc2EndpointNetworkAlias() 55 | if err != nil { 56 | log.Fatalln(err) 57 | } 58 | os.Exit(0) 59 | } 60 | 61 | // StopProxy stops the http proxy server on the standard EC2 Instance Metadata endpoint 62 | func StopProxy() { 63 | _, _ = http.Get(fmt.Sprintf("http://%s/stop", ec2MetadataEndpointAddr)) //nolint 64 | } 65 | 66 | func awsVaultExecutable() string { 67 | awsVaultPath, err := os.Executable() 68 | if err != nil { 69 | return awsVaultPath 70 | } 71 | 72 | return os.Args[0] 73 | } 74 | -------------------------------------------------------------------------------- /server/ec2proxy_default.go: -------------------------------------------------------------------------------- 1 | //go:build !darwin && !freebsd && !openbsd && !linux 2 | // +build !darwin,!freebsd,!openbsd,!linux 3 | 4 | package server 5 | 6 | import ( 7 | "errors" 8 | "log" 9 | "os" 10 | "os/exec" 11 | "time" 12 | ) 13 | 14 | // StartEc2EndpointProxyServerProcess starts a `aws-vault proxy` process 15 | func StartEc2EndpointProxyServerProcess() error { 16 | log.Println("Starting `aws-vault proxy`") 17 | cmd := exec.Command(awsVaultExecutable(), "proxy") 18 | cmd.Stdin = os.Stdin 19 | cmd.Stdout = os.Stdout 20 | cmd.Stderr = os.Stderr 21 | if err := cmd.Start(); err != nil { 22 | return err 23 | } 24 | time.Sleep(time.Second * 1) 25 | if !IsProxyRunning() { 26 | 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") 27 | } 28 | return nil 29 | } 30 | -------------------------------------------------------------------------------- /server/ec2proxy_unix.go: -------------------------------------------------------------------------------- 1 | //go:build darwin || freebsd || openbsd || linux 2 | // +build darwin freebsd openbsd linux 3 | 4 | package server 5 | 6 | import ( 7 | "log" 8 | "os" 9 | "os/exec" 10 | ) 11 | 12 | // StartEc2EndpointProxyServerProcess starts a `aws-vault proxy` process 13 | func StartEc2EndpointProxyServerProcess() error { 14 | log.Println("Starting `aws-vault proxy` as root in the background") 15 | cmd := exec.Command("sudo", "-b", awsVaultExecutable(), "proxy") 16 | cmd.Stdin = os.Stdin 17 | cmd.Stdout = os.Stdout 18 | cmd.Stderr = os.Stderr 19 | return cmd.Run() 20 | } 21 | -------------------------------------------------------------------------------- /server/ec2server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | "time" 11 | 12 | "github.com/99designs/aws-vault/v7/iso8601" 13 | "github.com/aws/aws-sdk-go-v2/aws" 14 | ) 15 | 16 | const ec2CredentialsServerAddr = "127.0.0.1:9099" 17 | 18 | // StartEc2CredentialsServer starts a EC2 Instance Metadata server and endpoint proxy 19 | func StartEc2CredentialsServer(ctx context.Context, credsProvider aws.CredentialsProvider, region string) error { 20 | credsCache := aws.NewCredentialsCache(credsProvider) 21 | 22 | // pre-fetch credentials so that we can respond quickly to the first request 23 | // SDKs seem to very aggressively timeout 24 | _, _ = credsCache.Retrieve(ctx) 25 | 26 | go startEc2CredentialsServer(credsCache, region) 27 | 28 | return nil 29 | } 30 | 31 | func startEc2CredentialsServer(credsProvider aws.CredentialsProvider, region string) { 32 | log.Printf("Starting EC2 Instance Metadata server on %s", ec2CredentialsServerAddr) 33 | router := http.NewServeMux() 34 | 35 | router.HandleFunc("/latest/meta-data/iam/security-credentials/", func(w http.ResponseWriter, r *http.Request) { 36 | fmt.Fprintf(w, "local-credentials") 37 | }) 38 | 39 | // The AWS Go SDK checks the instance-id endpoint to validate the existence of EC2 Metadata 40 | router.HandleFunc("/latest/meta-data/instance-id/", func(w http.ResponseWriter, r *http.Request) { 41 | fmt.Fprintf(w, "aws-vault") 42 | }) 43 | 44 | // The AWS .NET SDK checks this endpoint during obtaining credentials/refreshing them 45 | router.HandleFunc("/latest/meta-data/iam/info/", func(w http.ResponseWriter, r *http.Request) { 46 | fmt.Fprintf(w, `{"Code" : "Success"}`) 47 | }) 48 | 49 | // used by AWS SDK to determine region 50 | router.HandleFunc("/latest/dynamic/instance-identity/document", func(w http.ResponseWriter, r *http.Request) { 51 | fmt.Fprintf(w, `{"region": "`+region+`"}`) 52 | }) 53 | 54 | router.HandleFunc("/latest/meta-data/iam/security-credentials/local-credentials", credsHandler(credsProvider)) 55 | 56 | log.Fatalln(http.ListenAndServe(ec2CredentialsServerAddr, withLogging(withSecurityChecks(router)))) 57 | } 58 | 59 | // withSecurityChecks is middleware to protect the server from attack vectors 60 | func withSecurityChecks(next *http.ServeMux) http.HandlerFunc { 61 | return func(w http.ResponseWriter, r *http.Request) { 62 | // Check the remote ip is from the loopback, otherwise clients on the same network segment could 63 | // potentially route traffic via 169.254.169.254:80 64 | // See https://developer.apple.com/library/content/qa/qa1357/_index.html 65 | ip, _, err := net.SplitHostPort(r.RemoteAddr) 66 | if err != nil { 67 | http.Error(w, err.Error(), http.StatusBadRequest) 68 | return 69 | } 70 | if !net.ParseIP(ip).IsLoopback() { 71 | http.Error(w, "Access denied from non-localhost address", http.StatusUnauthorized) 72 | return 73 | } 74 | 75 | // Check that the request is to 169.254.169.254 76 | // Without this it's possible for an attacker to mount a DNS rebinding attack 77 | // See https://github.com/99designs/aws-vault/issues/578 78 | if r.Host != ec2MetadataEndpointIP && r.Host != ec2MetadataEndpointAddr { 79 | http.Error(w, fmt.Sprintf("Access denied for host '%s'", r.Host), http.StatusUnauthorized) 80 | return 81 | } 82 | 83 | next.ServeHTTP(w, r) 84 | } 85 | } 86 | 87 | func credsHandler(credsProvider aws.CredentialsProvider) http.HandlerFunc { 88 | return func(w http.ResponseWriter, r *http.Request) { 89 | creds, err := credsProvider.Retrieve(r.Context()) 90 | if err != nil { 91 | http.Error(w, err.Error(), http.StatusGatewayTimeout) 92 | return 93 | } 94 | 95 | log.Printf("Serving credentials via http ****************%s, expiration of %s (%s)", 96 | creds.AccessKeyID[len(creds.AccessKeyID)-4:], 97 | creds.Expires.Format(time.RFC3339), 98 | time.Until(creds.Expires).String()) 99 | 100 | err = json.NewEncoder(w).Encode(map[string]interface{}{ 101 | "Code": "Success", 102 | "LastUpdated": iso8601.Format(time.Now()), 103 | "Type": "AWS-HMAC", 104 | "AccessKeyId": creds.AccessKeyID, 105 | "SecretAccessKey": creds.SecretAccessKey, 106 | "Token": creds.SessionToken, 107 | "Expiration": iso8601.Format(creds.Expires), 108 | }) 109 | if err != nil { 110 | http.Error(w, err.Error(), http.StatusInternalServerError) 111 | return 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /server/ecsserver.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "context" 5 | "crypto/rand" 6 | "encoding/base64" 7 | "encoding/json" 8 | "fmt" 9 | "log" 10 | "net" 11 | "net/http" 12 | "strings" 13 | "sync" 14 | 15 | "github.com/99designs/aws-vault/v7/iso8601" 16 | "github.com/99designs/aws-vault/v7/vault" 17 | "github.com/aws/aws-sdk-go-v2/aws" 18 | "github.com/aws/aws-sdk-go-v2/service/sts" 19 | ) 20 | 21 | func writeErrorMessage(w http.ResponseWriter, msg string, statusCode int) { 22 | w.Header().Set("Content-Type", "application/json; charset=utf-8") 23 | w.WriteHeader(statusCode) 24 | if err := json.NewEncoder(w).Encode(map[string]string{"Message": msg}); err != nil { 25 | log.Println(err.Error()) 26 | } 27 | } 28 | 29 | func withAuthorizationCheck(authToken string, next http.HandlerFunc) http.HandlerFunc { 30 | return func(w http.ResponseWriter, r *http.Request) { 31 | if r.Header.Get("Authorization") != authToken { 32 | writeErrorMessage(w, "invalid Authorization token", http.StatusForbidden) 33 | return 34 | } 35 | next.ServeHTTP(w, r) 36 | } 37 | } 38 | 39 | func writeCredsToResponse(creds aws.Credentials, w http.ResponseWriter) { 40 | err := json.NewEncoder(w).Encode(map[string]string{ 41 | "AccessKeyId": creds.AccessKeyID, 42 | "SecretAccessKey": creds.SecretAccessKey, 43 | "Token": creds.SessionToken, 44 | "Expiration": iso8601.Format(creds.Expires), 45 | }) 46 | if err != nil { 47 | writeErrorMessage(w, err.Error(), http.StatusInternalServerError) 48 | return 49 | } 50 | } 51 | 52 | func generateRandomString() string { 53 | b := make([]byte, 30) 54 | if _, err := rand.Read(b); err != nil { 55 | panic(err) 56 | } 57 | return base64.RawURLEncoding.EncodeToString(b) 58 | } 59 | 60 | type EcsServer struct { 61 | listener net.Listener 62 | authToken string 63 | server http.Server 64 | cache sync.Map 65 | baseCredsProvider aws.CredentialsProvider 66 | config *vault.ProfileConfig 67 | } 68 | 69 | func NewEcsServer(ctx context.Context, baseCredsProvider aws.CredentialsProvider, config *vault.ProfileConfig, authToken string, port int, lazyLoadBaseCreds bool) (*EcsServer, error) { 70 | listener, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port)) 71 | if err != nil { 72 | return nil, err 73 | } 74 | if authToken == "" { 75 | authToken = generateRandomString() 76 | } 77 | 78 | credsCache := aws.NewCredentialsCache(baseCredsProvider) 79 | if !lazyLoadBaseCreds { 80 | _, err := credsCache.Retrieve(ctx) 81 | if err != nil { 82 | return nil, fmt.Errorf("Retrieving creds: %w", err) 83 | } 84 | } 85 | 86 | e := &EcsServer{ 87 | listener: listener, 88 | authToken: authToken, 89 | baseCredsProvider: credsCache, 90 | config: config, 91 | } 92 | 93 | router := http.NewServeMux() 94 | router.HandleFunc("/", e.DefaultRoute) 95 | router.HandleFunc("/role-arn/", e.AssumeRoleArnRoute) 96 | e.server.Handler = withLogging(withAuthorizationCheck(e.authToken, router.ServeHTTP)) 97 | 98 | return e, nil 99 | } 100 | 101 | func (e *EcsServer) BaseURL() string { 102 | return fmt.Sprintf("http://%s", e.listener.Addr().String()) 103 | } 104 | func (e *EcsServer) AuthToken() string { 105 | return e.authToken 106 | } 107 | 108 | func (e *EcsServer) Serve() error { 109 | return e.server.Serve(e.listener) 110 | } 111 | 112 | func (e *EcsServer) DefaultRoute(w http.ResponseWriter, r *http.Request) { 113 | creds, err := e.baseCredsProvider.Retrieve(r.Context()) 114 | if err != nil { 115 | writeErrorMessage(w, err.Error(), http.StatusInternalServerError) 116 | return 117 | } 118 | writeCredsToResponse(creds, w) 119 | } 120 | 121 | func (e *EcsServer) getRoleProvider(roleArn string) aws.CredentialsProvider { 122 | var roleProviderCache *aws.CredentialsCache 123 | 124 | v, ok := e.cache.Load(roleArn) 125 | if ok { 126 | roleProviderCache = v.(*aws.CredentialsCache) 127 | } else { 128 | cfg := vault.NewAwsConfigWithCredsProvider(e.baseCredsProvider, e.config.Region, e.config.STSRegionalEndpoints) 129 | roleProvider := &vault.AssumeRoleProvider{ 130 | StsClient: sts.NewFromConfig(cfg), 131 | RoleARN: roleArn, 132 | Duration: e.config.AssumeRoleDuration, 133 | } 134 | roleProviderCache = aws.NewCredentialsCache(roleProvider) 135 | e.cache.Store(roleArn, roleProviderCache) 136 | } 137 | return roleProviderCache 138 | } 139 | 140 | func (e *EcsServer) AssumeRoleArnRoute(w http.ResponseWriter, r *http.Request) { 141 | roleArn := strings.TrimPrefix(r.URL.Path, "/role-arn/") 142 | roleProvider := e.getRoleProvider(roleArn) 143 | creds, err := roleProvider.Retrieve(r.Context()) 144 | if err != nil { 145 | writeErrorMessage(w, err.Error(), http.StatusInternalServerError) 146 | return 147 | } 148 | writeCredsToResponse(creds, w) 149 | } 150 | -------------------------------------------------------------------------------- /server/httplog.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | type loggingMiddlewareResponseWriter struct { 10 | http.ResponseWriter 11 | Code int 12 | } 13 | 14 | func (w *loggingMiddlewareResponseWriter) WriteHeader(statusCode int) { 15 | w.Code = statusCode 16 | w.ResponseWriter.WriteHeader(statusCode) 17 | } 18 | 19 | func withLogging(handler http.Handler) http.Handler { 20 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 21 | requestStart := time.Now() 22 | w2 := &loggingMiddlewareResponseWriter{w, http.StatusOK} 23 | handler.ServeHTTP(w2, r) 24 | log.Printf("http: %s: %d %s %s (%s)", r.RemoteAddr, w2.Code, r.Method, r.URL, time.Since(requestStart)) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /vault/assumeroleprovider.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/service/sts" 11 | ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" 12 | ) 13 | 14 | // AssumeRoleProvider retrieves temporary credentials from STS using AssumeRole 15 | type AssumeRoleProvider struct { 16 | StsClient *sts.Client 17 | RoleARN string 18 | RoleSessionName string 19 | ExternalID string 20 | Duration time.Duration 21 | Tags map[string]string 22 | TransitiveTagKeys []string 23 | SourceIdentity string 24 | Mfa 25 | } 26 | 27 | // Retrieve generates a new set of temporary credentials using STS AssumeRole 28 | func (p *AssumeRoleProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { 29 | role, err := p.RetrieveStsCredentials(ctx) 30 | if err != nil { 31 | return aws.Credentials{}, err 32 | } 33 | 34 | return aws.Credentials{ 35 | AccessKeyID: *role.AccessKeyId, 36 | SecretAccessKey: *role.SecretAccessKey, 37 | SessionToken: *role.SessionToken, 38 | CanExpire: true, 39 | Expires: *role.Expiration, 40 | }, nil 41 | } 42 | 43 | func (p *AssumeRoleProvider) roleSessionName() string { 44 | if p.RoleSessionName == "" { 45 | // Try to work out a role name that will hopefully end up unique. 46 | return fmt.Sprintf("%d", time.Now().UTC().UnixNano()) 47 | } 48 | 49 | return p.RoleSessionName 50 | } 51 | 52 | func (p *AssumeRoleProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { 53 | var err error 54 | 55 | input := &sts.AssumeRoleInput{ 56 | RoleArn: aws.String(p.RoleARN), 57 | RoleSessionName: aws.String(p.roleSessionName()), 58 | DurationSeconds: aws.Int32(int32(p.Duration.Seconds())), 59 | } 60 | 61 | if p.ExternalID != "" { 62 | input.ExternalId = aws.String(p.ExternalID) 63 | } 64 | 65 | if p.MfaSerial != "" { 66 | input.SerialNumber = aws.String(p.MfaSerial) 67 | input.TokenCode, err = p.GetMfaToken() 68 | if err != nil { 69 | return nil, err 70 | } 71 | } 72 | 73 | if len(p.Tags) > 0 { 74 | input.Tags = make([]ststypes.Tag, 0) 75 | for key, value := range p.Tags { 76 | tag := ststypes.Tag{ 77 | Key: aws.String(key), 78 | Value: aws.String(value), 79 | } 80 | input.Tags = append(input.Tags, tag) 81 | } 82 | } 83 | 84 | if len(p.TransitiveTagKeys) > 0 { 85 | input.TransitiveTagKeys = p.TransitiveTagKeys 86 | } 87 | 88 | if p.SourceIdentity != "" { 89 | input.SourceIdentity = aws.String(p.SourceIdentity) 90 | } 91 | 92 | resp, err := p.StsClient.AssumeRole(ctx, input) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | log.Printf("Generated credentials %s using AssumeRole, expires in %s", FormatKeyForDisplay(*resp.Credentials.AccessKeyId), time.Until(*resp.Credentials.Expiration).String()) 98 | 99 | return resp.Credentials, nil 100 | } 101 | -------------------------------------------------------------------------------- /vault/assumerolewithwebidentityprovider.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/aws/aws-sdk-go-v2/aws" 11 | "github.com/aws/aws-sdk-go-v2/service/sts" 12 | ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" 13 | ) 14 | 15 | // AssumeRoleWithWebIdentityProvider retrieves temporary credentials from STS using AssumeRoleWithWebIdentity 16 | type AssumeRoleWithWebIdentityProvider struct { 17 | StsClient *sts.Client 18 | RoleARN string 19 | RoleSessionName string 20 | WebIdentityTokenFile string 21 | WebIdentityTokenProcess string 22 | ExternalID string 23 | Duration time.Duration 24 | } 25 | 26 | // Retrieve generates a new set of temporary credentials using STS AssumeRoleWithWebIdentity 27 | func (p *AssumeRoleWithWebIdentityProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { 28 | creds, err := p.RetrieveStsCredentials(ctx) 29 | if err != nil { 30 | return aws.Credentials{}, err 31 | } 32 | 33 | return aws.Credentials{ 34 | AccessKeyID: aws.ToString(creds.AccessKeyId), 35 | SecretAccessKey: aws.ToString(creds.SecretAccessKey), 36 | SessionToken: aws.ToString(creds.SessionToken), 37 | CanExpire: true, 38 | Expires: aws.ToTime(creds.Expiration), 39 | }, nil 40 | } 41 | 42 | func (p *AssumeRoleWithWebIdentityProvider) roleSessionName() string { 43 | if p.RoleSessionName == "" { 44 | // Try to work out a role name that will hopefully end up unique. 45 | return fmt.Sprintf("%d", time.Now().UTC().UnixNano()) 46 | } 47 | 48 | return p.RoleSessionName 49 | } 50 | 51 | func (p *AssumeRoleWithWebIdentityProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { 52 | var err error 53 | 54 | webIdentityToken, err := p.webIdentityToken() 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | resp, err := p.StsClient.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityInput{ 60 | RoleArn: aws.String(p.RoleARN), 61 | RoleSessionName: aws.String(p.roleSessionName()), 62 | DurationSeconds: aws.Int32(int32(p.Duration.Seconds())), 63 | WebIdentityToken: aws.String(webIdentityToken), 64 | }) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | log.Printf("Generated credentials %s using AssumeRoleWithWebIdentity, expires in %s", FormatKeyForDisplay(*resp.Credentials.AccessKeyId), time.Until(*resp.Credentials.Expiration).String()) 70 | 71 | return resp.Credentials, nil 72 | } 73 | 74 | func (p *AssumeRoleWithWebIdentityProvider) webIdentityToken() (string, error) { 75 | // Read OpenID Connect token from WebIdentityTokenFile 76 | if p.WebIdentityTokenFile != "" { 77 | b, err := os.ReadFile(p.WebIdentityTokenFile) 78 | if err != nil { 79 | return "", fmt.Errorf("unable to read file at %s: %v", p.WebIdentityTokenFile, err) 80 | } 81 | 82 | return string(b), nil 83 | } 84 | 85 | // Exec WebIdentityTokenProcess to retrieve OpenID Connect token 86 | return executeProcess(p.WebIdentityTokenProcess) 87 | } 88 | -------------------------------------------------------------------------------- /vault/cachedsessionprovider.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" 10 | ) 11 | 12 | type StsSessionProvider interface { 13 | aws.CredentialsProvider 14 | RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) 15 | } 16 | 17 | // CachedSessionProvider retrieves cached credentials from the keyring, or if no credentials are cached 18 | // retrieves temporary credentials using the CredentialsFunc 19 | type CachedSessionProvider struct { 20 | SessionKey SessionMetadata 21 | SessionProvider StsSessionProvider 22 | Keyring *SessionKeyring 23 | ExpiryWindow time.Duration 24 | } 25 | 26 | func (p *CachedSessionProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { 27 | creds, err := p.Keyring.Get(p.SessionKey) 28 | 29 | if err != nil || time.Until(*creds.Expiration) < p.ExpiryWindow { 30 | // lookup missed, we need to create a new one. 31 | creds, err = p.SessionProvider.RetrieveStsCredentials(ctx) 32 | if err != nil { 33 | return nil, err 34 | } 35 | err = p.Keyring.Set(p.SessionKey, creds) 36 | if err != nil { 37 | return nil, err 38 | } 39 | } else { 40 | log.Printf("Re-using cached credentials %s from %s, expires in %s", FormatKeyForDisplay(*creds.AccessKeyId), p.SessionKey.Type, time.Until(*creds.Expiration).String()) 41 | } 42 | 43 | return creds, nil 44 | } 45 | 46 | // Retrieve returns cached credentials from the keyring, or if no credentials are cached 47 | // generates a new set of temporary credentials using the CredentialsFunc 48 | func (p *CachedSessionProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { 49 | creds, err := p.RetrieveStsCredentials(ctx) 50 | if err != nil { 51 | return aws.Credentials{}, err 52 | } 53 | 54 | return aws.Credentials{ 55 | AccessKeyID: aws.ToString(creds.AccessKeyId), 56 | SecretAccessKey: aws.ToString(creds.SecretAccessKey), 57 | SessionToken: aws.ToString(creds.SessionToken), 58 | CanExpire: true, 59 | Expires: aws.ToTime(creds.Expiration), 60 | }, nil 61 | } 62 | -------------------------------------------------------------------------------- /vault/config_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/99designs/aws-vault/v7/vault" 11 | "github.com/google/go-cmp/cmp" 12 | ) 13 | 14 | // see http://docs.aws.amazon.com/cli/latest/userguide/cli-multiple-profiles.html 15 | var exampleConfig = []byte(`# an example profile file 16 | [default] 17 | region=us-west-2 18 | output=json 19 | 20 | [profile user2] 21 | REGION=us-east-1 22 | output=text 23 | 24 | [profile withsource] 25 | source_profile=user2 26 | region=us-east-1 27 | 28 | [profile withMFA] 29 | source_profile=user2 30 | Role_Arn=arn:aws:iam::4451234513441615400570:role/aws_admin 31 | mfa_Serial=arn:aws:iam::1234513441:mfa/blah 32 | Region=us-east-1 33 | duration_seconds=1200 34 | sts_regional_endpoints=legacy 35 | 36 | [profile testincludeprofile1] 37 | region=us-east-1 38 | 39 | [profile testincludeprofile2] 40 | include_profile=testincludeprofile1 41 | 42 | [profile with-sso-session] 43 | sso_session = moon-sso 44 | sso_account_id=123456 45 | region = moon-1 # Different from sso region 46 | 47 | [sso-session moon-sso] 48 | sso_start_url = https://d-123456789.example.com/start 49 | sso_region = moon-2 # Different from profile region 50 | sso_registration_scopes = sso:account:access 51 | `) 52 | 53 | var nestedConfig = []byte(`[default] 54 | 55 | [profile testing] 56 | aws_access_key_id=foo 57 | aws_secret_access_key=bar 58 | region=us-west-2 59 | s3= 60 | max_concurrent_requests=10 61 | max_queue_size=1000 62 | `) 63 | 64 | var defaultsOnlyConfigWithHeader = []byte(`[default] 65 | region=us-west-2 66 | output=json 67 | `) 68 | 69 | func newConfigFile(t *testing.T, b []byte) string { 70 | t.Helper() 71 | f, err := os.CreateTemp("", "aws-config") 72 | if err != nil { 73 | t.Fatal(err) 74 | } 75 | if err := os.WriteFile(f.Name(), b, 0600); err != nil { 76 | t.Fatal(err) 77 | } 78 | return f.Name() 79 | } 80 | 81 | func TestProfileNameCaseSensitivity(t *testing.T) { 82 | f := newConfigFile(t, exampleConfig) 83 | defer os.Remove(f) 84 | 85 | cfg, err := vault.LoadConfig(f) 86 | if err != nil { 87 | t.Fatal(err) 88 | } 89 | 90 | def, ok := cfg.ProfileSection("withMFA") 91 | if !ok { 92 | t.Fatalf("Expected to match profile withMFA") 93 | } 94 | 95 | expectedMfaSerial := "arn:aws:iam::1234513441:mfa/blah" 96 | if def.MfaSerial != expectedMfaSerial { 97 | t.Fatalf("Expected %s, got %s", expectedMfaSerial, def.MfaSerial) 98 | } 99 | } 100 | 101 | func TestConfigParsingProfiles(t *testing.T) { 102 | f := newConfigFile(t, exampleConfig) 103 | defer os.Remove(f) 104 | 105 | cfg, err := vault.LoadConfig(f) 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | 110 | var testCases = []struct { 111 | expected vault.ProfileSection 112 | ok bool 113 | }{ 114 | {vault.ProfileSection{Name: "user2", Region: "us-east-1"}, true}, 115 | {vault.ProfileSection{Name: "withsource", SourceProfile: "user2", Region: "us-east-1"}, true}, 116 | {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}, 117 | {vault.ProfileSection{Name: "nopenotthere"}, false}, 118 | } 119 | 120 | for _, tc := range testCases { 121 | t.Run(fmt.Sprintf("profile_%s", tc.expected.Name), func(t *testing.T) { 122 | actual, ok := cfg.ProfileSection(tc.expected.Name) 123 | if ok != tc.ok { 124 | t.Fatalf("Expected second param to be %v, got %v", tc.ok, ok) 125 | } 126 | if diff := cmp.Diff(tc.expected, actual); diff != "" { 127 | t.Errorf("ProfileSection() mismatch (-expected +actual):\n%s", diff) 128 | } 129 | }) 130 | } 131 | } 132 | 133 | func TestConfigParsingDefault(t *testing.T) { 134 | f := newConfigFile(t, exampleConfig) 135 | defer os.Remove(f) 136 | 137 | cfg, err := vault.LoadConfig(f) 138 | if err != nil { 139 | t.Fatal(err) 140 | } 141 | 142 | def, ok := cfg.ProfileSection("default") 143 | if !ok { 144 | t.Fatalf("Expected to find default profile") 145 | } 146 | 147 | expected := vault.ProfileSection{ 148 | Name: "default", 149 | Region: "us-west-2", 150 | } 151 | 152 | if !reflect.DeepEqual(def, expected) { 153 | t.Fatalf("Expected %+v, got %+v", expected, def) 154 | } 155 | } 156 | 157 | func TestProfilesFromConfig(t *testing.T) { 158 | f := newConfigFile(t, exampleConfig) 159 | defer os.Remove(f) 160 | 161 | cfg, err := vault.LoadConfig(f) 162 | if err != nil { 163 | t.Fatal(err) 164 | } 165 | 166 | expected := []vault.ProfileSection{ 167 | {Name: "default", Region: "us-west-2"}, 168 | {Name: "user2", Region: "us-east-1"}, 169 | {Name: "withsource", Region: "us-east-1", SourceProfile: "user2"}, 170 | {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"}, 171 | {Name: "testincludeprofile1", Region: "us-east-1"}, 172 | {Name: "testincludeprofile2", IncludeProfile: "testincludeprofile1"}, 173 | {Name: "with-sso-session", SSOSession: "moon-sso", Region: "moon-1", SSOAccountID: "123456"}, 174 | } 175 | actual := cfg.ProfileSections() 176 | 177 | if diff := cmp.Diff(expected, actual); diff != "" { 178 | t.Errorf("ProfileSections() mismatch (-expected +actual):\n%s", diff) 179 | } 180 | } 181 | 182 | func TestAddProfileToExistingConfig(t *testing.T) { 183 | f := newConfigFile(t, exampleConfig) 184 | defer os.Remove(f) 185 | 186 | cfg, err := vault.LoadConfig(f) 187 | if err != nil { 188 | t.Fatal(err) 189 | } 190 | 191 | err = cfg.Add(vault.ProfileSection{ 192 | Name: "llamas", 193 | MfaSerial: "testserial", 194 | Region: "us-east-1", 195 | SourceProfile: "default", 196 | }) 197 | if err != nil { 198 | t.Fatalf("Error adding profile: %#v", err) 199 | } 200 | 201 | expected := []vault.ProfileSection{ 202 | {Name: "default", Region: "us-west-2"}, 203 | {Name: "user2", Region: "us-east-1"}, 204 | {Name: "withsource", Region: "us-east-1", SourceProfile: "user2"}, 205 | {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"}, 206 | {Name: "testincludeprofile1", Region: "us-east-1"}, 207 | {Name: "testincludeprofile2", IncludeProfile: "testincludeprofile1"}, 208 | {Name: "with-sso-session", SSOSession: "moon-sso", Region: "moon-1", SSOAccountID: "123456"}, 209 | {Name: "llamas", MfaSerial: "testserial", Region: "us-east-1", SourceProfile: "default"}, 210 | } 211 | actual := cfg.ProfileSections() 212 | 213 | if diff := cmp.Diff(expected, actual); diff != "" { 214 | t.Errorf("ProfileSections() mismatch (-expected +actual):\n%s", diff) 215 | } 216 | } 217 | 218 | func TestAddProfileToExistingNestedConfig(t *testing.T) { 219 | f := newConfigFile(t, nestedConfig) 220 | defer os.Remove(f) 221 | 222 | cfg, err := vault.LoadConfig(f) 223 | if err != nil { 224 | t.Fatal(err) 225 | } 226 | 227 | err = cfg.Add(vault.ProfileSection{ 228 | Name: "llamas", 229 | MfaSerial: "testserial", 230 | Region: "us-east-1", 231 | }) 232 | if err != nil { 233 | t.Fatalf("Error adding profile: %#v", err) 234 | } 235 | 236 | expected := append(nestedConfig, []byte( 237 | "\n[profile llamas]\nmfa_serial=testserial\nregion=us-east-1\n", 238 | )...) 239 | 240 | b, _ := os.ReadFile(f) 241 | 242 | if !bytes.Equal(expected, b) { 243 | t.Fatalf("Expected:\n%q\nGot:\n%q", expected, b) 244 | } 245 | } 246 | 247 | func TestIncludeProfile(t *testing.T) { 248 | f := newConfigFile(t, exampleConfig) 249 | defer os.Remove(f) 250 | 251 | configFile, err := vault.LoadConfig(f) 252 | if err != nil { 253 | t.Fatal(err) 254 | } 255 | 256 | configLoader := &vault.ConfigLoader{File: configFile} 257 | config, err := configLoader.GetProfileConfig("testincludeprofile2") 258 | if err != nil { 259 | t.Fatalf("Should have found a profile: %v", err) 260 | } 261 | 262 | if config.Region != "us-east-1" { 263 | t.Fatalf("Expected region %q, got %q", "us-east-1", config.Region) 264 | } 265 | } 266 | 267 | func TestIncludeSsoSession(t *testing.T) { 268 | f := newConfigFile(t, exampleConfig) 269 | defer os.Remove(f) 270 | 271 | configFile, err := vault.LoadConfig(f) 272 | if err != nil { 273 | t.Fatal(err) 274 | } 275 | 276 | configLoader := &vault.ConfigLoader{File: configFile} 277 | config, err := configLoader.GetProfileConfig("with-sso-session") 278 | if err != nil { 279 | t.Fatalf("Should have found a profile: %v", err) 280 | } 281 | 282 | if config.Region != "moon-1" { // Test not the same as SSO region 283 | t.Fatalf("Expected region %q, got %q", "moon-1", config.Region) 284 | } 285 | 286 | ssoStartURL := "https://d-123456789.example.com/start" 287 | if config.SSOStartURL != ssoStartURL { 288 | t.Fatalf("Expected sso_start_url %q, got %q", ssoStartURL, config.Region) 289 | } 290 | 291 | if config.SSORegion != "moon-2" { // Test not the same as profile region 292 | t.Fatalf("Expected sso_region %q, got %q", "moon-2", config.Region) 293 | } 294 | // Not checking sso_registration_scopes as it seems to be unused by aws-cli. 295 | } 296 | 297 | func TestProfileIsEmpty(t *testing.T) { 298 | p := vault.ProfileSection{Name: "foo"} 299 | if !p.IsEmpty() { 300 | t.Errorf("Expected p to be empty") 301 | } 302 | } 303 | 304 | func TestIniWithHeaderSavesWithHeader(t *testing.T) { 305 | f := newConfigFile(t, defaultsOnlyConfigWithHeader) 306 | defer os.Remove(f) 307 | 308 | cfg, err := vault.LoadConfig(f) 309 | if err != nil { 310 | t.Fatal(err) 311 | } 312 | 313 | err = cfg.Save() 314 | if err != nil { 315 | t.Fatal(err) 316 | } 317 | 318 | expected := defaultsOnlyConfigWithHeader 319 | 320 | b, _ := os.ReadFile(f) 321 | 322 | if !bytes.Equal(expected, b) { 323 | t.Fatalf("Expected:\n%q\nGot:\n%q", expected, b) 324 | } 325 | } 326 | 327 | func TestIniWithDEFAULTHeader(t *testing.T) { 328 | f := newConfigFile(t, []byte(`[DEFAULT] 329 | region=us-east-1 330 | [default] 331 | region=us-west-2 332 | `)) 333 | defer os.Remove(f) 334 | 335 | cfg, err := vault.LoadConfig(f) 336 | if err != nil { 337 | t.Fatal(err) 338 | } 339 | expected := []vault.ProfileSection{ 340 | {Name: "default", Region: "us-west-2"}, 341 | } 342 | actual := cfg.ProfileSections() 343 | 344 | if diff := cmp.Diff(expected, actual); diff != "" { 345 | t.Errorf("ProfileSections() mismatch (-expected +actual):\n%s", diff) 346 | } 347 | } 348 | 349 | func TestLoadedProfileDoesntReferToItself(t *testing.T) { 350 | f := newConfigFile(t, []byte(` 351 | [profile foo] 352 | source_profile=foo 353 | `)) 354 | defer os.Remove(f) 355 | 356 | configFile, err := vault.LoadConfig(f) 357 | if err != nil { 358 | t.Fatal(err) 359 | } 360 | 361 | def, ok := configFile.ProfileSection("foo") 362 | if !ok { 363 | t.Fatalf("Couldn't load profile foo") 364 | } 365 | 366 | expectedSourceProfile := "foo" 367 | if def.SourceProfile != expectedSourceProfile { 368 | t.Fatalf("Expected '%s', got '%s'", expectedSourceProfile, def.SourceProfile) 369 | } 370 | 371 | configLoader := &vault.ConfigLoader{File: configFile} 372 | config, err := configLoader.GetProfileConfig("foo") 373 | if err != nil { 374 | t.Fatalf("Should have found a profile: %v", err) 375 | } 376 | 377 | expectedSourceProfileName := "" 378 | if config.SourceProfileName != expectedSourceProfileName { 379 | t.Fatalf("Expected '%s', got '%s'", expectedSourceProfileName, config.SourceProfileName) 380 | } 381 | } 382 | 383 | func TestSourceProfileCanReferToParent(t *testing.T) { 384 | f := newConfigFile(t, []byte(` 385 | [profile root] 386 | 387 | [profile foo] 388 | include_profile=root 389 | source_profile=root 390 | `)) 391 | defer os.Remove(f) 392 | 393 | configFile, err := vault.LoadConfig(f) 394 | if err != nil { 395 | t.Fatal(err) 396 | } 397 | 398 | def, ok := configFile.ProfileSection("foo") 399 | if !ok { 400 | t.Fatalf("Couldn't load profile foo") 401 | } 402 | 403 | expectedSourceProfile := "root" 404 | if def.SourceProfile != expectedSourceProfile { 405 | t.Fatalf("Expected '%s', got '%s'", expectedSourceProfile, def.SourceProfile) 406 | } 407 | 408 | configLoader := &vault.ConfigLoader{File: configFile} 409 | config, err := configLoader.GetProfileConfig("foo") 410 | if err != nil { 411 | t.Fatalf("Should have found a profile: %v", err) 412 | } 413 | 414 | expectedSourceProfileName := "root" 415 | if config.SourceProfileName != expectedSourceProfileName { 416 | t.Fatalf("Expected '%s', got '%s'", expectedSourceProfileName, config.SourceProfileName) 417 | } 418 | } 419 | 420 | func TestSetSessionTags(t *testing.T) { 421 | var testCases = []struct { 422 | stringValue string 423 | expected map[string]string 424 | ok bool 425 | }{ 426 | {"tag1=value1", map[string]string{"tag1": "value1"}, true}, 427 | { 428 | "tag2=value2,tag3=value3,tag4=value4", 429 | map[string]string{"tag2": "value2", "tag3": "value3", "tag4": "value4"}, 430 | true, 431 | }, 432 | {" tagA = valueA , tagB = valueB , tagC = valueC ", 433 | map[string]string{"tagA": "valueA", "tagB": "valueB", "tagC": "valueC"}, 434 | true, 435 | }, 436 | {"", nil, false}, 437 | {"tag1=value1,", nil, false}, 438 | {"tagA=valueA,tagB", nil, false}, 439 | {"tagOne,tagTwo=valueTwo", nil, false}, 440 | {"tagI=valueI,tagII,tagIII=valueIII", nil, false}, 441 | } 442 | 443 | for _, tc := range testCases { 444 | config := vault.ProfileConfig{} 445 | err := config.SetSessionTags(tc.stringValue) 446 | if tc.ok { 447 | if err != nil { 448 | t.Fatalf("Unsexpected parsing error: %s", err) 449 | } 450 | if !reflect.DeepEqual(tc.expected, config.SessionTags) { 451 | t.Fatalf("Expected SessionTags: %+v, got %+v", tc.expected, config.SessionTags) 452 | } 453 | } else { 454 | if err == nil { 455 | t.Fatalf("Expected an error parsing %#v, but got none", tc.stringValue) 456 | } 457 | } 458 | } 459 | } 460 | 461 | func TestSetTransitiveSessionTags(t *testing.T) { 462 | var testCases = []struct { 463 | stringValue string 464 | expected []string 465 | }{ 466 | {"tag1", []string{"tag1"}}, 467 | {"tag2,tag3,tag4", []string{"tag2", "tag3", "tag4"}}, 468 | {" tagA , tagB , tagC ", []string{"tagA", "tagB", "tagC"}}, 469 | {"tag1,", []string{"tag1"}}, 470 | {",tagA", []string{"tagA"}}, 471 | {"", nil}, 472 | {",", nil}, 473 | } 474 | 475 | for _, tc := range testCases { 476 | config := vault.ProfileConfig{} 477 | config.SetTransitiveSessionTags(tc.stringValue) 478 | if !reflect.DeepEqual(tc.expected, config.TransitiveSessionTags) { 479 | t.Fatalf("Expected TransitiveSessionTags: %+v, got %+v", tc.expected, config.TransitiveSessionTags) 480 | } 481 | } 482 | } 483 | 484 | func TestSessionTaggingFromIni(t *testing.T) { 485 | os.Unsetenv("AWS_SESSION_TAGS") 486 | os.Unsetenv("AWS_TRANSITIVE_TAGS") 487 | f := newConfigFile(t, []byte(` 488 | [profile tagged] 489 | session_tags = tag1 = value1 , tag2=value2 ,tag3=value3 490 | transitive_session_tags = tagOne ,tagTwo,tagThree 491 | `)) 492 | defer os.Remove(f) 493 | 494 | configFile, err := vault.LoadConfig(f) 495 | if err != nil { 496 | t.Fatal(err) 497 | } 498 | configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "tagged"} 499 | config, err := configLoader.GetProfileConfig("tagged") 500 | if err != nil { 501 | t.Fatalf("Should have found a profile: %v", err) 502 | } 503 | expectedSessionTags := map[string]string{ 504 | "tag1": "value1", 505 | "tag2": "value2", 506 | "tag3": "value3", 507 | } 508 | if !reflect.DeepEqual(expectedSessionTags, config.SessionTags) { 509 | t.Fatalf("Expected session_tags: %+v, got %+v", expectedSessionTags, config.SessionTags) 510 | } 511 | 512 | expectedTransitiveSessionTags := []string{"tagOne", "tagTwo", "tagThree"} 513 | if !reflect.DeepEqual(expectedTransitiveSessionTags, config.TransitiveSessionTags) { 514 | t.Fatalf("Expected transitive_session_tags: %+v, got %+v", expectedTransitiveSessionTags, config.TransitiveSessionTags) 515 | } 516 | } 517 | 518 | func TestSessionTaggingFromEnvironment(t *testing.T) { 519 | os.Setenv("AWS_SESSION_TAGS", " tagA = val1 , tagB=val2 ,tagC=val3") 520 | os.Setenv("AWS_TRANSITIVE_TAGS", " tagD ,tagE") 521 | defer os.Unsetenv("AWS_SESSION_TAGS") 522 | defer os.Unsetenv("AWS_TRANSITIVE_TAGS") 523 | 524 | f := newConfigFile(t, []byte(` 525 | [profile tagged] 526 | session_tags = tag1 = value1 , tag2=value2 ,tag3=value3 527 | transitive_session_tags = tagOne ,tagTwo,tagThree 528 | `)) 529 | defer os.Remove(f) 530 | 531 | configFile, err := vault.LoadConfig(f) 532 | if err != nil { 533 | t.Fatal(err) 534 | } 535 | configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "tagged"} 536 | config, err := configLoader.GetProfileConfig("tagged") 537 | if err != nil { 538 | t.Fatalf("Should have found a profile: %v", err) 539 | } 540 | expectedSessionTags := map[string]string{ 541 | "tagA": "val1", 542 | "tagB": "val2", 543 | "tagC": "val3", 544 | } 545 | if !reflect.DeepEqual(expectedSessionTags, config.SessionTags) { 546 | t.Fatalf("Expected session_tags: %+v, got %+v", expectedSessionTags, config.SessionTags) 547 | } 548 | 549 | expectedTransitiveSessionTags := []string{"tagD", "tagE"} 550 | if !reflect.DeepEqual(expectedTransitiveSessionTags, config.TransitiveSessionTags) { 551 | t.Fatalf("Expected transitive_session_tags: %+v, got %+v", expectedTransitiveSessionTags, config.TransitiveSessionTags) 552 | } 553 | } 554 | 555 | func TestSessionTaggingFromEnvironmentChainedRoles(t *testing.T) { 556 | os.Setenv("AWS_SESSION_TAGS", "tagI=valI") 557 | os.Setenv("AWS_TRANSITIVE_TAGS", " tagII") 558 | defer os.Unsetenv("AWS_SESSION_TAGS") 559 | defer os.Unsetenv("AWS_TRANSITIVE_TAGS") 560 | 561 | f := newConfigFile(t, []byte(` 562 | [profile base] 563 | 564 | [profile interim] 565 | session_tags=tag1=value1 566 | transitive_session_tags=tag2 567 | source_profile = base 568 | 569 | [profile target] 570 | session_tags=tagA=valueA 571 | transitive_session_tags=tagB 572 | source_profile = interim 573 | `)) 574 | defer os.Remove(f) 575 | 576 | configFile, err := vault.LoadConfig(f) 577 | if err != nil { 578 | t.Fatal(err) 579 | } 580 | configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "target"} 581 | config, err := configLoader.GetProfileConfig("target") 582 | if err != nil { 583 | t.Fatalf("Should have found a profile: %v", err) 584 | } 585 | 586 | // Testing target profile, should have values populated from environment variables 587 | expectedSessionTags := map[string]string{"tagI": "valI"} 588 | if !reflect.DeepEqual(expectedSessionTags, config.SessionTags) { 589 | t.Fatalf("Expected session_tags: %+v, got %+v", expectedSessionTags, config.SessionTags) 590 | } 591 | 592 | expectedTransitiveSessionTags := []string{"tagII"} 593 | if !reflect.DeepEqual(expectedTransitiveSessionTags, config.TransitiveSessionTags) { 594 | t.Fatalf("Expected transitive_session_tags: %+v, got %+v", expectedTransitiveSessionTags, config.TransitiveSessionTags) 595 | } 596 | 597 | // Testing interim profile, parameters should come from the config, not environment 598 | interimConfig := config.SourceProfile 599 | expectedSessionTags = map[string]string{"tag1": "value1"} 600 | if !reflect.DeepEqual(expectedSessionTags, interimConfig.SessionTags) { 601 | t.Fatalf("Expected session_tags: %+v, got %+v", expectedSessionTags, interimConfig.SessionTags) 602 | } 603 | 604 | expectedTransitiveSessionTags = []string{"tag2"} 605 | if !reflect.DeepEqual(expectedTransitiveSessionTags, interimConfig.TransitiveSessionTags) { 606 | t.Fatalf("Expected transitive_session_tags: %+v, got %+v", expectedTransitiveSessionTags, interimConfig.TransitiveSessionTags) 607 | } 608 | 609 | // Testing base profile, should have empty parameters 610 | baseConfig := interimConfig.SourceProfile 611 | if len(baseConfig.SessionTags) > 0 { 612 | t.Fatalf("Expected session_tags to be empty, got %+v", baseConfig.SessionTags) 613 | } 614 | 615 | if len(baseConfig.TransitiveSessionTags) > 0 { 616 | t.Fatalf("Expected transitive_session_tags to be empty, got %+v", baseConfig.TransitiveSessionTags) 617 | } 618 | } 619 | -------------------------------------------------------------------------------- /vault/credentialkeyring.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/99designs/keyring" 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | ) 10 | 11 | type CredentialKeyring struct { 12 | Keyring keyring.Keyring 13 | } 14 | 15 | func (ck *CredentialKeyring) Keys() (credentialsNames []string, err error) { 16 | allKeys, err := ck.Keyring.Keys() 17 | if err != nil { 18 | return credentialsNames, err 19 | } 20 | for _, keyName := range allKeys { 21 | if !IsSessionKey(keyName) && !IsOIDCTokenKey(keyName) { 22 | credentialsNames = append(credentialsNames, keyName) 23 | } 24 | } 25 | return credentialsNames, nil 26 | } 27 | 28 | func (ck *CredentialKeyring) Has(credentialsName string) (bool, error) { 29 | allKeys, err := ck.Keyring.Keys() 30 | if err != nil { 31 | return false, err 32 | } 33 | for _, keyName := range allKeys { 34 | if keyName == credentialsName { 35 | return true, nil 36 | } 37 | } 38 | return false, nil 39 | } 40 | 41 | func (ck *CredentialKeyring) Get(credentialsName string) (creds aws.Credentials, err error) { 42 | item, err := ck.Keyring.Get(credentialsName) 43 | if err != nil { 44 | return creds, err 45 | } 46 | if err = json.Unmarshal(item.Data, &creds); err != nil { 47 | return creds, fmt.Errorf("Invalid data in keyring: %v", err) 48 | } 49 | return creds, err 50 | } 51 | 52 | func (ck *CredentialKeyring) Set(credentialsName string, creds aws.Credentials) error { 53 | bytes, err := json.Marshal(creds) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | return ck.Keyring.Set(keyring.Item{ 59 | Key: credentialsName, 60 | Label: fmt.Sprintf("aws-vault (%s)", credentialsName), 61 | Data: bytes, 62 | 63 | // specific Keychain settings 64 | KeychainNotTrustApplication: true, 65 | }) 66 | } 67 | 68 | func (ck *CredentialKeyring) Remove(credentialsName string) error { 69 | return ck.Keyring.Remove(credentialsName) 70 | } 71 | -------------------------------------------------------------------------------- /vault/credentialprocessprovider.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "reflect" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" 11 | ) 12 | 13 | var credentialProcessRequiredFields = []string{"AccessKeyId", "Expiration", "SecretAccessKey", "SessionToken"} 14 | 15 | // CredentialProcessProvider implements interface aws.CredentialsProvider to retrieve credentials from an external executable 16 | // as described in https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes 17 | type CredentialProcessProvider struct { 18 | CredentialProcess string 19 | } 20 | 21 | func (p *CredentialProcessProvider) validateJSONCredential(cred *ststypes.Credentials) error { 22 | var missing []string 23 | 24 | h := reflect.ValueOf(cred).Elem() 25 | for _, requiredField := range credentialProcessRequiredFields { 26 | if h.FieldByName(requiredField).IsNil() { 27 | missing = append(missing, requiredField) 28 | } 29 | } 30 | 31 | if len(missing) > 0 { 32 | return fmt.Errorf("JSON credential from command %q missing the following fields: %v", p.CredentialProcess, missing) 33 | } 34 | 35 | return nil 36 | } 37 | 38 | // Retrieve obtains a new set of temporary credentials using an external process, required to satisfy interface aws.CredentialsProvider 39 | func (p *CredentialProcessProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { 40 | return p.retrieveWith(ctx, executeProcess) 41 | } 42 | 43 | func (p *CredentialProcessProvider) retrieveWith(ctx context.Context, fn func(string) (string, error)) (aws.Credentials, error) { 44 | creds, err := p.callCredentialProcessWith(ctx, fn) 45 | if err != nil { 46 | return aws.Credentials{}, err 47 | } 48 | 49 | return aws.Credentials{ 50 | AccessKeyID: aws.ToString(creds.AccessKeyId), 51 | SecretAccessKey: aws.ToString(creds.SecretAccessKey), 52 | SessionToken: aws.ToString(creds.SessionToken), 53 | CanExpire: true, 54 | Expires: aws.ToTime(creds.Expiration), 55 | }, nil 56 | } 57 | 58 | func (p *CredentialProcessProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { 59 | return p.callCredentialProcessWith(ctx, executeProcess) 60 | } 61 | 62 | func (p *CredentialProcessProvider) callCredentialProcessWith(_ context.Context, fn func(string) (string, error)) (*ststypes.Credentials, error) { 63 | // Exec CredentialProcess to retrieve AWS creds in JSON format as described in 64 | // https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes 65 | output, err := fn(p.CredentialProcess) 66 | 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | // Unmarshal the JSON into a ststypes.Credentials object 72 | var value ststypes.Credentials 73 | if err := json.Unmarshal([]byte(output), &value); err != nil { 74 | return &ststypes.Credentials{}, fmt.Errorf("invalid JSON format from command %q: %v", p.CredentialProcess, err) 75 | } 76 | 77 | // Validate that all required fields were present in JSON before returning 78 | return &value, p.validateJSONCredential(&value) 79 | } 80 | -------------------------------------------------------------------------------- /vault/credentialprocessprovider_test.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "reflect" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" 14 | ) 15 | 16 | func executeFail(_ string) (string, error) { 17 | return "", errors.New("executing process failed") 18 | } 19 | 20 | func executeGetBadJSON(_ string) (string, error) { 21 | return "Junk", nil 22 | } 23 | 24 | func executeGetCredential(accessKeyID *string, expiration *time.Time, secretAccesKey *string, sessionToken *string) (string, error) { 25 | v, err := json.Marshal(ststypes.Credentials{ 26 | AccessKeyId: accessKeyID, 27 | Expiration: expiration, 28 | SecretAccessKey: secretAccesKey, 29 | SessionToken: sessionToken, 30 | }) 31 | return string(v), err 32 | } 33 | 34 | func TestCredentialProcessProvider_Retrieve(t *testing.T) { 35 | accessKeyID := "abcd" 36 | expiration := time.Time{} 37 | secretAccessKey := "0123" 38 | sessionToken := "4567" 39 | 40 | want := aws.Credentials{ 41 | AccessKeyID: accessKeyID, 42 | Expires: expiration, 43 | CanExpire: true, 44 | SecretAccessKey: secretAccessKey, 45 | SessionToken: sessionToken, 46 | } 47 | 48 | tests := []struct { 49 | name string 50 | execFunc func(string) (string, error) 51 | wantErr bool 52 | expectMissingFields bool 53 | }{ 54 | { 55 | name: "process execution fails", 56 | execFunc: executeFail, 57 | wantErr: true, 58 | expectMissingFields: false, 59 | }, 60 | { 61 | name: "bad json", 62 | execFunc: executeGetBadJSON, 63 | wantErr: true, 64 | expectMissingFields: false, 65 | }, 66 | { 67 | name: "successful execution, good cred", 68 | execFunc: func(string) (string, error) { 69 | return executeGetCredential(&accessKeyID, &expiration, &secretAccessKey, &sessionToken) 70 | }, 71 | wantErr: false, 72 | expectMissingFields: false, 73 | }, 74 | { 75 | name: "fields missing", 76 | execFunc: func(string) (string, error) { 77 | return executeGetCredential(nil, nil, nil, nil) 78 | }, 79 | wantErr: true, 80 | expectMissingFields: true, 81 | }, 82 | } 83 | for _, tt := range tests { 84 | t.Run(tt.name, func(t *testing.T) { 85 | ctx := context.Background() 86 | provider := CredentialProcessProvider{ 87 | CredentialProcess: "", 88 | } 89 | got, err := provider.retrieveWith(ctx, tt.execFunc) 90 | 91 | if (err != nil) != tt.wantErr { 92 | t.Errorf("CredentialProcessProvider.Retrieve() error = %v, wantErr %v", err, tt.wantErr) 93 | return 94 | } 95 | 96 | if !tt.wantErr && !reflect.DeepEqual(got, want) { 97 | t.Errorf("CredentialProcessProvider.Retrieve() = %v, want %v", got, want) 98 | } 99 | 100 | if tt.wantErr && tt.expectMissingFields { 101 | for _, expectedMissingField := range credentialProcessRequiredFields { 102 | if !strings.Contains(err.Error(), expectedMissingField) { 103 | t.Errorf("expected field '%v' not present in error: %v'", expectedMissingField, err) 104 | } 105 | } 106 | } 107 | }) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /vault/executeprocess.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "runtime" 8 | ) 9 | 10 | func executeProcess(process string) (string, error) { 11 | var cmdArgs []string 12 | if runtime.GOOS == "windows" { 13 | cmdArgs = []string{"cmd.exe", "/C", process} 14 | } else { 15 | cmdArgs = []string{"/bin/sh", "-c", process} 16 | } 17 | 18 | cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) 19 | cmd.Env = os.Environ() 20 | cmd.Stdin = os.Stdin 21 | cmd.Stderr = os.Stderr 22 | 23 | output, err := cmd.Output() 24 | if err != nil { 25 | return "", fmt.Errorf("running command %q: %v", process, err) 26 | } 27 | return string(output), nil 28 | } 29 | -------------------------------------------------------------------------------- /vault/federationtokenprovider.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/sts" 10 | ) 11 | 12 | const allowAllIAMPolicy = `{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"*","Resource":"*"}]}` 13 | 14 | // FederationTokenProvider retrieves temporary credentials from STS using GetFederationToken 15 | type FederationTokenProvider struct { 16 | StsClient *sts.Client 17 | Name string 18 | Duration time.Duration 19 | } 20 | 21 | func (f *FederationTokenProvider) name() string { 22 | // 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 23 | if len(f.Name) > 32 { 24 | return f.Name[0:32] 25 | } 26 | return f.Name 27 | } 28 | 29 | // Retrieve generates a new set of temporary credentials using STS GetFederationToken 30 | func (f *FederationTokenProvider) Retrieve(ctx context.Context) (creds aws.Credentials, err error) { 31 | resp, err := f.StsClient.GetFederationToken(ctx, &sts.GetFederationTokenInput{ 32 | Name: aws.String(f.name()), 33 | DurationSeconds: aws.Int32(int32(f.Duration.Seconds())), 34 | Policy: aws.String(allowAllIAMPolicy), 35 | }) 36 | if err != nil { 37 | return creds, err 38 | } 39 | 40 | log.Printf("Generated credentials %s using GetFederationToken, expires in %s", FormatKeyForDisplay(*resp.Credentials.AccessKeyId), time.Until(*resp.Credentials.Expiration).String()) 41 | 42 | return aws.Credentials{ 43 | AccessKeyID: aws.ToString(resp.Credentials.AccessKeyId), 44 | SecretAccessKey: aws.ToString(resp.Credentials.SecretAccessKey), 45 | SessionToken: aws.ToString(resp.Credentials.SessionToken), 46 | CanExpire: true, 47 | Expires: aws.ToTime(resp.Credentials.Expiration), 48 | }, nil 49 | } 50 | -------------------------------------------------------------------------------- /vault/getuser.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "regexp" 7 | "strings" 8 | 9 | "github.com/aws/aws-sdk-go-v2/aws" 10 | "github.com/aws/aws-sdk-go-v2/service/iam" 11 | ) 12 | 13 | var getUserErrorRegexp = regexp.MustCompile(`^AccessDenied: User: arn:aws:iam::(\d+):user/(.+) is not`) 14 | 15 | // GetUsernameFromSession returns the IAM username (or root) associated with the current aws session 16 | func GetUsernameFromSession(ctx context.Context, cfg aws.Config) (string, error) { 17 | iamClient := iam.NewFromConfig(cfg) 18 | resp, err := iamClient.GetUser(ctx, &iam.GetUserInput{}) 19 | if err != nil { 20 | // Even if GetUser fails, the current user is included in the error. This happens when you have o IAM permissions 21 | // on the master credentials, but have permission to use assumeRole later 22 | matches := getUserErrorRegexp.FindStringSubmatch(err.Error()) 23 | if len(matches) > 0 { 24 | pathParts := strings.Split(matches[2], "/") 25 | return pathParts[len(pathParts)-1], nil 26 | } 27 | 28 | return "", err 29 | } 30 | 31 | if resp.User.UserName != nil { 32 | return *resp.User.UserName, nil 33 | } 34 | 35 | if resp.User.Arn != nil { 36 | arnParts := strings.Split(*resp.User.Arn, ":") 37 | return arnParts[len(arnParts)-1], nil 38 | } 39 | 40 | return "", fmt.Errorf("Couldn't determine current username") 41 | } 42 | -------------------------------------------------------------------------------- /vault/keyringprovider.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/aws/aws-sdk-go-v2/aws" 8 | ) 9 | 10 | // KeyringProvider stores and retrieves master credentials 11 | type KeyringProvider struct { 12 | Keyring *CredentialKeyring 13 | CredentialsName string 14 | } 15 | 16 | func (p *KeyringProvider) Retrieve(_ context.Context) (aws.Credentials, error) { 17 | log.Printf("Looking up keyring for '%s'", p.CredentialsName) 18 | return p.Keyring.Get(p.CredentialsName) 19 | } 20 | -------------------------------------------------------------------------------- /vault/mfa.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "os" 8 | "os/exec" 9 | "strings" 10 | 11 | "github.com/99designs/aws-vault/v7/prompt" 12 | "github.com/aws/aws-sdk-go-v2/aws" 13 | ) 14 | 15 | // Mfa contains options for an MFA device 16 | type Mfa struct { 17 | MfaSerial string 18 | mfaPromptFunc prompt.Func 19 | } 20 | 21 | // GetMfaToken returns the MFA token 22 | func (m Mfa) GetMfaToken() (*string, error) { 23 | if m.mfaPromptFunc != nil { 24 | token, err := m.mfaPromptFunc(m.MfaSerial) 25 | return aws.String(token), err 26 | } 27 | 28 | return nil, errors.New("No prompt found") 29 | } 30 | 31 | func NewMfa(config *ProfileConfig) Mfa { 32 | m := Mfa{ 33 | MfaSerial: config.MfaSerial, 34 | } 35 | if config.MfaToken != "" { 36 | m.mfaPromptFunc = func(_ string) (string, error) { return config.MfaToken, nil } 37 | } else if config.MfaProcess != "" { 38 | m.mfaPromptFunc = func(_ string) (string, error) { 39 | log.Println("Executing mfa_process") 40 | return ProcessMfaProvider(config.MfaProcess) 41 | } 42 | } else { 43 | m.mfaPromptFunc = prompt.Method(config.MfaPromptMethod) 44 | } 45 | 46 | return m 47 | } 48 | 49 | func ProcessMfaProvider(processCmd string) (string, error) { 50 | cmd := exec.Command("/bin/sh", "-c", processCmd) 51 | cmd.Stderr = os.Stderr 52 | 53 | out, err := cmd.Output() 54 | if err != nil { 55 | return "", fmt.Errorf("process provider: %w", err) 56 | } 57 | 58 | return strings.TrimSpace(string(out)), nil 59 | } 60 | -------------------------------------------------------------------------------- /vault/oidctokenkeyring.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "time" 9 | 10 | "github.com/99designs/keyring" 11 | "github.com/aws/aws-sdk-go-v2/service/ssooidc" 12 | ) 13 | 14 | type OIDCTokenKeyring struct { 15 | Keyring keyring.Keyring 16 | } 17 | 18 | type OIDCTokenData struct { 19 | Token ssooidc.CreateTokenOutput 20 | Expiration time.Time 21 | } 22 | 23 | const oidcTokenKeyPrefix = "oidc:" 24 | 25 | func (o *OIDCTokenKeyring) fmtKey(startURL string) string { 26 | return oidcTokenKeyPrefix + startURL 27 | } 28 | 29 | func IsOIDCTokenKey(k string) bool { 30 | return strings.HasPrefix(k, oidcTokenKeyPrefix) 31 | } 32 | 33 | func (o OIDCTokenKeyring) Has(startURL string) (bool, error) { 34 | kk, err := o.Keyring.Keys() 35 | if err != nil { 36 | return false, err 37 | } 38 | 39 | for _, k := range kk { 40 | if startURL == k { 41 | return true, nil 42 | } 43 | } 44 | 45 | return false, nil 46 | } 47 | 48 | func (o OIDCTokenKeyring) Get(startURL string) (*ssooidc.CreateTokenOutput, error) { 49 | item, err := o.Keyring.Get(o.fmtKey(startURL)) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | val := OIDCTokenData{} 55 | 56 | if err = json.Unmarshal(item.Data, &val); err != nil { 57 | log.Printf("Invalid data in keyring: %s", err.Error()) 58 | return nil, keyring.ErrKeyNotFound 59 | } 60 | if time.Now().After(val.Expiration) { 61 | log.Printf("OIDC token for '%s' expired, removing", startURL) 62 | _ = o.Remove(startURL) 63 | return nil, keyring.ErrKeyNotFound 64 | } 65 | 66 | secondsLeft := time.Until(val.Expiration) / time.Second 67 | 68 | val.Token.ExpiresIn = int32(secondsLeft) 69 | 70 | return &val.Token, err 71 | } 72 | 73 | func (o OIDCTokenKeyring) Set(startURL string, token *ssooidc.CreateTokenOutput) error { 74 | val := OIDCTokenData{ 75 | Token: *token, 76 | Expiration: time.Now().Add(time.Duration(token.ExpiresIn) * time.Second), 77 | } 78 | 79 | valJSON, err := json.Marshal(val) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | return o.Keyring.Set(keyring.Item{ 85 | Key: o.fmtKey(startURL), 86 | Data: valJSON, 87 | Label: fmt.Sprintf("aws-vault oidc token for %s (expires %s)", startURL, val.Expiration.Format(time.RFC3339)), 88 | Description: "aws-vault oidc token", 89 | }) 90 | } 91 | 92 | func (o OIDCTokenKeyring) Remove(startURL string) error { 93 | return o.Keyring.Remove(o.fmtKey(startURL)) 94 | } 95 | 96 | func (o *OIDCTokenKeyring) RemoveAll() (n int, err error) { 97 | allKeys, err := o.Keys() 98 | if err != nil { 99 | return 0, err 100 | } 101 | for _, key := range allKeys { 102 | if err = o.Remove(key); err != nil { 103 | return n, err 104 | } 105 | n++ 106 | } 107 | return n, nil 108 | } 109 | 110 | func (o *OIDCTokenKeyring) Keys() (kk []string, err error) { 111 | allKeys, err := o.Keyring.Keys() 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | for _, k := range allKeys { 117 | if IsOIDCTokenKey(k) { 118 | kk = append(kk, strings.TrimPrefix(k, oidcTokenKeyPrefix)) 119 | } 120 | } 121 | 122 | return kk, nil 123 | } 124 | -------------------------------------------------------------------------------- /vault/sessionkeyring.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "regexp" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/99designs/keyring" 14 | ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" 15 | ) 16 | 17 | var sessionKeyPattern = regexp.MustCompile(`^(?P[^,]+),(?P[^,]+),(?P[^,]*),(?P[0-9]{1,})$`) 18 | 19 | var oldSessionKeyPatterns = []*regexp.Regexp{ 20 | regexp.MustCompile(`^session,(?P[^,]+),(?P[^,]*),(?P[0-9]{2,})$`), 21 | regexp.MustCompile(`^session:(?P[^ ]+):(?P[^ ]*):(?P[^:]+)$`), 22 | regexp.MustCompile(`^(.+?) session \((\d+)\)$`), 23 | } 24 | var base64URLEncodingNoPadding = base64.URLEncoding.WithPadding(base64.NoPadding) 25 | 26 | func IsOldSessionKey(s string) bool { 27 | for _, pattern := range oldSessionKeyPatterns { 28 | if pattern.MatchString(s) { 29 | return true 30 | } 31 | } 32 | return false 33 | } 34 | 35 | func IsCurrentSessionKey(s string) bool { 36 | _, err := NewSessionKeyFromString(s) 37 | return err == nil 38 | } 39 | 40 | func IsSessionKey(s string) bool { 41 | return IsCurrentSessionKey(s) || IsOldSessionKey(s) 42 | } 43 | 44 | type SessionMetadata struct { 45 | Type string 46 | ProfileName string 47 | MfaSerial string 48 | Expiration time.Time 49 | } 50 | 51 | func (k *SessionMetadata) String() string { 52 | return fmt.Sprintf( 53 | "%s,%s,%s,%d", 54 | k.Type, 55 | base64URLEncodingNoPadding.EncodeToString([]byte(k.ProfileName)), 56 | base64URLEncodingNoPadding.EncodeToString([]byte(k.MfaSerial)), 57 | k.Expiration.Unix(), 58 | ) 59 | } 60 | 61 | func (k *SessionMetadata) StringForMatching() string { 62 | return fmt.Sprintf( 63 | "%s,%s,%s,", 64 | k.Type, 65 | base64URLEncodingNoPadding.EncodeToString([]byte(k.ProfileName)), 66 | base64URLEncodingNoPadding.EncodeToString([]byte(k.MfaSerial)), 67 | ) 68 | } 69 | 70 | func NewSessionKeyFromString(s string) (SessionMetadata, error) { 71 | matches := sessionKeyPattern.FindStringSubmatch(s) 72 | if len(matches) == 0 { 73 | return SessionMetadata{}, fmt.Errorf("failed to parse session name: %s", s) 74 | } 75 | 76 | profileName, err := base64URLEncodingNoPadding.DecodeString(matches[2]) 77 | if err != nil { 78 | return SessionMetadata{}, err 79 | } 80 | mfaSerial, err := base64URLEncodingNoPadding.DecodeString(matches[3]) 81 | if err != nil { 82 | return SessionMetadata{}, err 83 | } 84 | expiryUnixtime, err := strconv.Atoi(matches[4]) 85 | if err != nil { 86 | return SessionMetadata{}, err 87 | } 88 | 89 | return SessionMetadata{ 90 | Type: matches[1], 91 | ProfileName: string(profileName), 92 | MfaSerial: string(mfaSerial), 93 | Expiration: time.Unix(int64(expiryUnixtime), 0), 94 | }, nil 95 | } 96 | 97 | type SessionKeyring struct { 98 | Keyring keyring.Keyring 99 | } 100 | 101 | var ErrNotFound = keyring.ErrKeyNotFound 102 | 103 | func (sk *SessionKeyring) lookupKeyName(key SessionMetadata) (string, error) { 104 | allKeys, err := sk.Keyring.Keys() 105 | if err != nil { 106 | return key.String(), err 107 | } 108 | for _, keyName := range allKeys { 109 | if strings.HasPrefix(keyName, key.StringForMatching()) { 110 | return keyName, nil 111 | } 112 | } 113 | return key.String(), ErrNotFound 114 | } 115 | 116 | func (sk *SessionKeyring) Has(key SessionMetadata) (bool, error) { 117 | _, err := sk.lookupKeyName(key) 118 | if err == ErrNotFound { 119 | return false, nil 120 | } 121 | if err == nil { 122 | return true, nil 123 | } 124 | 125 | return false, err 126 | } 127 | 128 | func (sk *SessionKeyring) Get(key SessionMetadata) (creds *ststypes.Credentials, err error) { 129 | _, _ = sk.RemoveOldSessions() 130 | 131 | keyName, err := sk.lookupKeyName(key) 132 | if err != nil && err != ErrNotFound { 133 | return nil, err 134 | } 135 | item, err := sk.Keyring.Get(keyName) 136 | if err != nil { 137 | return creds, err 138 | } 139 | if err = json.Unmarshal(item.Data, &creds); err != nil { 140 | log.Printf("SessionKeyring: Ignoring invalid data: %s", err.Error()) 141 | return creds, ErrNotFound 142 | } 143 | return creds, err 144 | } 145 | 146 | func (sk *SessionKeyring) Set(key SessionMetadata, creds *ststypes.Credentials) error { 147 | _, _ = sk.RemoveOldSessions() 148 | 149 | key.Expiration = *creds.Expiration 150 | 151 | valJSON, err := json.Marshal(creds) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | keyName, err := sk.lookupKeyName(key) 157 | if err != ErrNotFound { 158 | if err != nil { 159 | return err 160 | } 161 | if keyName != key.String() { 162 | err = sk.Keyring.Remove(keyName) 163 | if err != nil { 164 | return err 165 | } 166 | } 167 | } 168 | 169 | return sk.Keyring.Set(keyring.Item{ 170 | Key: key.String(), 171 | Data: valJSON, 172 | Label: fmt.Sprintf("aws-vault session for %s (expires %s)", key.ProfileName, creds.Expiration.Format(time.RFC3339)), 173 | Description: "aws-vault session", 174 | }) 175 | } 176 | 177 | func (sk *SessionKeyring) Remove(key SessionMetadata) error { 178 | keyName, err := sk.lookupKeyName(key) 179 | if err != nil && err != ErrNotFound { 180 | return err 181 | } 182 | 183 | return sk.Keyring.Remove(keyName) 184 | } 185 | 186 | func (sk *SessionKeyring) RemoveAll() (n int, err error) { 187 | allKeys, err := sk.Keys() 188 | if err != nil { 189 | return 0, err 190 | } 191 | for _, key := range allKeys { 192 | if err = sk.Remove(key); err != nil { 193 | return n, err 194 | } 195 | n++ 196 | } 197 | return n, nil 198 | } 199 | 200 | func (sk *SessionKeyring) Keys() (kk []SessionMetadata, err error) { 201 | allKeys, err := sk.Keyring.Keys() 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | for _, s := range allKeys { 207 | if k, err := NewSessionKeyFromString(s); err == nil { 208 | kk = append(kk, k) 209 | } 210 | } 211 | 212 | return kk, nil 213 | } 214 | 215 | func (sk *SessionKeyring) realSessionKey(key SessionMetadata) (m SessionMetadata, err error) { 216 | keyName, err := sk.lookupKeyName(key) 217 | if err != nil { 218 | return m, err 219 | } 220 | sessKey, err := NewSessionKeyFromString(keyName) 221 | if err != nil { 222 | return m, err 223 | } 224 | return sessKey, nil 225 | } 226 | 227 | func (sk *SessionKeyring) GetAllMetadata() (mm []SessionMetadata, err error) { 228 | allKeys, err := sk.Keys() 229 | if err != nil { 230 | return nil, err 231 | } 232 | 233 | for _, k := range allKeys { 234 | m, err := sk.realSessionKey(k) 235 | if err != nil { 236 | return nil, fmt.Errorf("GetAllMetadata: %w", err) 237 | } 238 | 239 | mm = append(mm, m) 240 | } 241 | 242 | return mm, nil 243 | } 244 | 245 | func (sk *SessionKeyring) RemoveForProfile(profileName string) (n int, err error) { 246 | sessions, err := sk.GetAllMetadata() 247 | if err != nil { 248 | return n, err 249 | } 250 | for _, s := range sessions { 251 | if s.ProfileName == profileName { 252 | err = sk.Remove(s) 253 | if err != nil { 254 | return n, err 255 | } 256 | n++ 257 | } 258 | } 259 | 260 | return n, nil 261 | } 262 | 263 | func (sk *SessionKeyring) RemoveOldSessions() (n int, err error) { 264 | allKeys, err := sk.Keyring.Keys() 265 | if err != nil { 266 | log.Printf("Error while deleting old session: %s", err.Error()) 267 | } 268 | 269 | for _, k := range allKeys { 270 | if IsOldSessionKey(k) { 271 | err = sk.Keyring.Remove(k) 272 | if err != nil { 273 | log.Printf("Error while deleting old session: %s", err.Error()) 274 | continue 275 | } 276 | n++ 277 | } else { 278 | stsk, err := NewSessionKeyFromString(k) 279 | if err != nil { 280 | continue 281 | } 282 | if time.Now().After(stsk.Expiration) { 283 | err = sk.Keyring.Remove(k) 284 | if err != nil { 285 | log.Printf("Error while deleting old session: %s", err.Error()) 286 | continue 287 | } 288 | n++ 289 | } 290 | } 291 | } 292 | 293 | return n, nil 294 | } 295 | -------------------------------------------------------------------------------- /vault/sessionkeyring_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/99designs/aws-vault/v7/vault" 7 | ) 8 | 9 | func TestIsSessionKey(t *testing.T) { 10 | var testCases = []struct { 11 | Key string 12 | IsSession bool 13 | }{ 14 | {"blah", false}, 15 | {"blah session (61633665646639303539)", true}, 16 | {"blah-iam session (32383863333237616430)", true}, 17 | {"session,c2Vzc2lvbg,,1572281751", true}, 18 | {"session,c2Vzc2lvbg,YXJuOmF3czppYW06OjEyMzQ1Njc4OTA6bWZhL2pzdGV3bW9u,1572281751", true}, 19 | } 20 | 21 | for _, tc := range testCases { 22 | if tc.IsSession && !vault.IsSessionKey(tc.Key) { 23 | t.Fatalf("%q is a session key, but wasn't detected as one", tc.Key) 24 | } else if !tc.IsSession && vault.IsSessionKey(tc.Key) { 25 | t.Fatalf("%q isn't a session key, but was detected as one", tc.Key) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /vault/sessiontokenprovider.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "time" 7 | 8 | "github.com/aws/aws-sdk-go-v2/aws" 9 | "github.com/aws/aws-sdk-go-v2/service/sts" 10 | ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" 11 | ) 12 | 13 | // SessionTokenProvider retrieves temporary credentials from STS using GetSessionToken 14 | type SessionTokenProvider struct { 15 | StsClient *sts.Client 16 | Duration time.Duration 17 | Mfa 18 | } 19 | 20 | // Retrieve generates a new set of temporary credentials using STS GetSessionToken 21 | func (p *SessionTokenProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { 22 | creds, err := p.RetrieveStsCredentials(ctx) 23 | if err != nil { 24 | return aws.Credentials{}, err 25 | } 26 | 27 | return aws.Credentials{ 28 | AccessKeyID: aws.ToString(creds.AccessKeyId), 29 | SecretAccessKey: aws.ToString(creds.SecretAccessKey), 30 | SessionToken: aws.ToString(creds.SessionToken), 31 | CanExpire: true, 32 | Expires: aws.ToTime(creds.Expiration), 33 | }, nil 34 | } 35 | 36 | // GetSessionToken generates a new set of temporary credentials using STS GetSessionToken 37 | func (p *SessionTokenProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { 38 | var err error 39 | 40 | input := &sts.GetSessionTokenInput{ 41 | DurationSeconds: aws.Int32(int32(p.Duration.Seconds())), 42 | } 43 | 44 | if p.MfaSerial != "" { 45 | input.SerialNumber = aws.String(p.MfaSerial) 46 | input.TokenCode, err = p.GetMfaToken() 47 | if err != nil { 48 | return nil, err 49 | } 50 | } 51 | 52 | resp, err := p.StsClient.GetSessionToken(ctx, input) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | log.Printf("Generated credentials %s using GetSessionToken, expires in %s", FormatKeyForDisplay(*resp.Credentials.AccessKeyId), time.Until(*resp.Credentials.Expiration).String()) 58 | 59 | return resp.Credentials, nil 60 | } 61 | -------------------------------------------------------------------------------- /vault/ssorolecredentialsprovider.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | "github.com/99designs/keyring" 13 | "github.com/aws/aws-sdk-go-v2/aws" 14 | awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http" 15 | "github.com/aws/aws-sdk-go-v2/service/sso" 16 | ssotypes "github.com/aws/aws-sdk-go-v2/service/sso/types" 17 | "github.com/aws/aws-sdk-go-v2/service/ssooidc" 18 | ssooidctypes "github.com/aws/aws-sdk-go-v2/service/ssooidc/types" 19 | ststypes "github.com/aws/aws-sdk-go-v2/service/sts/types" 20 | "github.com/skratchdot/open-golang/open" 21 | ) 22 | 23 | type OIDCTokenCacher interface { 24 | Get(string) (*ssooidc.CreateTokenOutput, error) 25 | Set(string, *ssooidc.CreateTokenOutput) error 26 | Remove(string) error 27 | } 28 | 29 | // SSORoleCredentialsProvider creates temporary credentials for an SSO Role. 30 | type SSORoleCredentialsProvider struct { 31 | OIDCClient *ssooidc.Client 32 | OIDCTokenCache OIDCTokenCacher 33 | StartURL string 34 | SSOClient *sso.Client 35 | AccountID string 36 | RoleName string 37 | UseStdout bool 38 | } 39 | 40 | func millisecondsTimeValue(v int64) time.Time { 41 | return time.Unix(0, v*int64(time.Millisecond)) 42 | } 43 | 44 | // Retrieve generates a new set of temporary credentials using SSO GetRoleCredentials. 45 | func (p *SSORoleCredentialsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) { 46 | creds, err := p.getRoleCredentials(ctx) 47 | if err != nil { 48 | return aws.Credentials{}, err 49 | } 50 | 51 | return aws.Credentials{ 52 | AccessKeyID: aws.ToString(creds.AccessKeyId), 53 | SecretAccessKey: aws.ToString(creds.SecretAccessKey), 54 | SessionToken: aws.ToString(creds.SessionToken), 55 | CanExpire: true, 56 | Expires: millisecondsTimeValue(creds.Expiration), 57 | }, nil 58 | } 59 | 60 | func (p *SSORoleCredentialsProvider) getRoleCredentials(ctx context.Context) (*ssotypes.RoleCredentials, error) { 61 | token, cached, err := p.getOIDCToken(ctx) 62 | if err != nil { 63 | return nil, err 64 | } 65 | 66 | resp, err := p.SSOClient.GetRoleCredentials(ctx, &sso.GetRoleCredentialsInput{ 67 | AccessToken: token.AccessToken, 68 | AccountId: aws.String(p.AccountID), 69 | RoleName: aws.String(p.RoleName), 70 | }) 71 | if err != nil { 72 | if cached && p.OIDCTokenCache != nil { 73 | var rspError *awshttp.ResponseError 74 | if !errors.As(err, &rspError) { 75 | return nil, err 76 | } 77 | 78 | // If the error is a 401, remove the cached oidc token and try 79 | // again. This is a recursive call but it should only happen once 80 | // due to the cache being cleared before retrying. 81 | if rspError.HTTPStatusCode() == http.StatusUnauthorized { 82 | err = p.OIDCTokenCache.Remove(p.StartURL) 83 | if err != nil { 84 | return nil, err 85 | } 86 | return p.getRoleCredentials(ctx) 87 | } 88 | } 89 | return nil, err 90 | } 91 | 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()) 92 | 93 | return resp.RoleCredentials, nil 94 | } 95 | 96 | func (p *SSORoleCredentialsProvider) RetrieveStsCredentials(ctx context.Context) (*ststypes.Credentials, error) { 97 | return p.getRoleCredentialsAsStsCredemtials(ctx) 98 | } 99 | 100 | // getRoleCredentialsAsStsCredemtials returns getRoleCredentials as sts.Credentials because sessions.Store expects it 101 | func (p *SSORoleCredentialsProvider) getRoleCredentialsAsStsCredemtials(ctx context.Context) (*ststypes.Credentials, error) { 102 | creds, err := p.getRoleCredentials(ctx) 103 | if err != nil { 104 | return nil, err 105 | } 106 | 107 | return &ststypes.Credentials{ 108 | AccessKeyId: creds.AccessKeyId, 109 | SecretAccessKey: creds.SecretAccessKey, 110 | SessionToken: creds.SessionToken, 111 | Expiration: aws.Time(millisecondsTimeValue(creds.Expiration)), 112 | }, nil 113 | } 114 | 115 | func (p *SSORoleCredentialsProvider) getOIDCToken(ctx context.Context) (token *ssooidc.CreateTokenOutput, cached bool, err error) { 116 | if p.OIDCTokenCache != nil { 117 | token, err = p.OIDCTokenCache.Get(p.StartURL) 118 | if err != nil && err != keyring.ErrKeyNotFound { 119 | return nil, false, err 120 | } 121 | if token != nil { 122 | return token, true, nil 123 | } 124 | } 125 | token, err = p.newOIDCToken(ctx) 126 | if err != nil { 127 | return nil, false, err 128 | } 129 | 130 | if p.OIDCTokenCache != nil { 131 | err = p.OIDCTokenCache.Set(p.StartURL, token) 132 | if err != nil { 133 | return nil, false, err 134 | } 135 | } 136 | return token, false, err 137 | } 138 | 139 | func (p *SSORoleCredentialsProvider) newOIDCToken(ctx context.Context) (*ssooidc.CreateTokenOutput, error) { 140 | clientCreds, err := p.OIDCClient.RegisterClient(ctx, &ssooidc.RegisterClientInput{ 141 | ClientName: aws.String("aws-vault"), 142 | ClientType: aws.String("public"), 143 | }) 144 | if err != nil { 145 | return nil, err 146 | } 147 | log.Printf("Created new OIDC client (expires at: %s)", time.Unix(clientCreds.ClientSecretExpiresAt, 0)) 148 | 149 | deviceCreds, err := p.OIDCClient.StartDeviceAuthorization(ctx, &ssooidc.StartDeviceAuthorizationInput{ 150 | ClientId: clientCreds.ClientId, 151 | ClientSecret: clientCreds.ClientSecret, 152 | StartUrl: aws.String(p.StartURL), 153 | }) 154 | if err != nil { 155 | return nil, err 156 | } 157 | log.Printf("Created OIDC device code for %s (expires in: %ds)", p.StartURL, deviceCreds.ExpiresIn) 158 | 159 | if p.UseStdout { 160 | fmt.Fprintf(os.Stderr, "Open the SSO authorization page in a browser (use Ctrl-C to abort)\n%s\n", aws.ToString(deviceCreds.VerificationUriComplete)) 161 | } else { 162 | log.Println("Opening SSO authorization page in browser") 163 | 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)) 164 | if err := open.Run(aws.ToString(deviceCreds.VerificationUriComplete)); err != nil { 165 | log.Printf("Failed to open browser: %s", err) 166 | } 167 | } 168 | 169 | // These are the default values defined in the following RFC: 170 | // https://tools.ietf.org/html/draft-ietf-oauth-device-flow-15#section-3.5 171 | var slowDownDelay = 5 * time.Second 172 | var retryInterval = 5 * time.Second 173 | 174 | if i := deviceCreds.Interval; i > 0 { 175 | retryInterval = time.Duration(i) * time.Second 176 | } 177 | 178 | for { 179 | t, err := p.OIDCClient.CreateToken(ctx, &ssooidc.CreateTokenInput{ 180 | ClientId: clientCreds.ClientId, 181 | ClientSecret: clientCreds.ClientSecret, 182 | DeviceCode: deviceCreds.DeviceCode, 183 | GrantType: aws.String("urn:ietf:params:oauth:grant-type:device_code"), 184 | }) 185 | if err != nil { 186 | var sde *ssooidctypes.SlowDownException 187 | if errors.As(err, &sde) { 188 | retryInterval += slowDownDelay 189 | } 190 | 191 | var ape *ssooidctypes.AuthorizationPendingException 192 | if errors.As(err, &ape) { 193 | time.Sleep(retryInterval) 194 | continue 195 | } 196 | 197 | return nil, err 198 | } 199 | 200 | log.Printf("Created new OIDC access token for %s (expires in: %ds)", p.StartURL, t.ExpiresIn) 201 | return t, nil 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /vault/stsendpointresolver.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "log" 5 | 6 | "github.com/aws/aws-sdk-go-v2/aws" 7 | "github.com/aws/aws-sdk-go-v2/service/sts" 8 | ) 9 | 10 | // getEndpointResolver resolves endpoints in accordance with 11 | // https://docs.aws.amazon.com/credref/latest/refdocs/setting-global-sts_regional_endpoints.html 12 | func getSTSEndpointResolver(stsRegionalEndpoints string) aws.EndpointResolverWithOptionsFunc { 13 | return func(service, region string, options ...interface{}) (aws.Endpoint, error) { 14 | if stsRegionalEndpoints == "legacy" && service == sts.ServiceID { 15 | if region == "ap-northeast-1" || 16 | region == "ap-south-1" || 17 | region == "ap-southeast-1" || 18 | region == "ap-southeast-2" || 19 | region == "aws-global" || 20 | region == "ca-central-1" || 21 | region == "eu-central-1" || 22 | region == "eu-north-1" || 23 | region == "eu-west-1" || 24 | region == "eu-west-2" || 25 | region == "eu-west-3" || 26 | region == "sa-east-1" || 27 | region == "us-east-1" || 28 | region == "us-east-2" || 29 | region == "us-west-1" || 30 | region == "us-west-2" { 31 | log.Println("Using legacy STS endpoint sts.amazonaws.com") 32 | 33 | return aws.Endpoint{ 34 | URL: "https://sts.amazonaws.com", 35 | SigningRegion: region, 36 | }, nil 37 | } 38 | } 39 | 40 | return aws.Endpoint{}, &aws.EndpointNotFoundError{} 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /vault/vault.go: -------------------------------------------------------------------------------- 1 | package vault 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "log" 7 | "os" 8 | "time" 9 | 10 | "github.com/99designs/keyring" 11 | "github.com/aws/aws-sdk-go-v2/aws" 12 | "github.com/aws/aws-sdk-go-v2/service/sso" 13 | "github.com/aws/aws-sdk-go-v2/service/ssooidc" 14 | "github.com/aws/aws-sdk-go-v2/service/sts" 15 | ) 16 | 17 | var defaultExpirationWindow = 5 * time.Minute 18 | 19 | func init() { 20 | if d, err := time.ParseDuration(os.Getenv("AWS_MIN_TTL")); err == nil { 21 | defaultExpirationWindow = d 22 | } 23 | } 24 | 25 | func NewAwsConfig(region, stsRegionalEndpoints string) aws.Config { 26 | return aws.Config{ 27 | Region: region, 28 | EndpointResolverWithOptions: getSTSEndpointResolver(stsRegionalEndpoints), 29 | } 30 | } 31 | 32 | func NewAwsConfigWithCredsProvider(credsProvider aws.CredentialsProvider, region, stsRegionalEndpoints string) aws.Config { 33 | return aws.Config{ 34 | Region: region, 35 | Credentials: credsProvider, 36 | EndpointResolverWithOptions: getSTSEndpointResolver(stsRegionalEndpoints), 37 | } 38 | } 39 | 40 | func FormatKeyForDisplay(k string) string { 41 | return fmt.Sprintf("****************%s", k[len(k)-4:]) 42 | } 43 | 44 | func isMasterCredentialsProvider(credsProvider aws.CredentialsProvider) bool { 45 | _, ok := credsProvider.(*KeyringProvider) 46 | return ok 47 | } 48 | 49 | // NewMasterCredentialsProvider creates a provider for the master credentials 50 | func NewMasterCredentialsProvider(k *CredentialKeyring, credentialsName string) *KeyringProvider { 51 | return &KeyringProvider{k, credentialsName} 52 | } 53 | 54 | func NewSessionTokenProvider(credsProvider aws.CredentialsProvider, k keyring.Keyring, config *ProfileConfig, useSessionCache bool) (aws.CredentialsProvider, error) { 55 | cfg := NewAwsConfigWithCredsProvider(credsProvider, config.Region, config.STSRegionalEndpoints) 56 | 57 | sessionTokenProvider := &SessionTokenProvider{ 58 | StsClient: sts.NewFromConfig(cfg), 59 | Duration: config.GetSessionTokenDuration(), 60 | Mfa: NewMfa(config), 61 | } 62 | 63 | if useSessionCache { 64 | return &CachedSessionProvider{ 65 | SessionKey: SessionMetadata{ 66 | Type: "sts.GetSessionToken", 67 | ProfileName: config.ProfileName, 68 | MfaSerial: config.MfaSerial, 69 | }, 70 | Keyring: &SessionKeyring{Keyring: k}, 71 | ExpiryWindow: defaultExpirationWindow, 72 | SessionProvider: sessionTokenProvider, 73 | }, nil 74 | } 75 | 76 | return sessionTokenProvider, nil 77 | } 78 | 79 | // NewAssumeRoleProvider returns a provider that generates credentials using AssumeRole 80 | func NewAssumeRoleProvider(credsProvider aws.CredentialsProvider, k keyring.Keyring, config *ProfileConfig, useSessionCache bool) (aws.CredentialsProvider, error) { 81 | cfg := NewAwsConfigWithCredsProvider(credsProvider, config.Region, config.STSRegionalEndpoints) 82 | 83 | p := &AssumeRoleProvider{ 84 | StsClient: sts.NewFromConfig(cfg), 85 | RoleARN: config.RoleARN, 86 | RoleSessionName: config.RoleSessionName, 87 | ExternalID: config.ExternalID, 88 | Duration: config.AssumeRoleDuration, 89 | Tags: config.SessionTags, 90 | TransitiveTagKeys: config.TransitiveSessionTags, 91 | SourceIdentity: config.SourceIdentity, 92 | Mfa: NewMfa(config), 93 | } 94 | 95 | if useSessionCache && config.MfaSerial != "" { 96 | return &CachedSessionProvider{ 97 | SessionKey: SessionMetadata{ 98 | Type: "sts.AssumeRole", 99 | ProfileName: config.ProfileName, 100 | MfaSerial: config.MfaSerial, 101 | }, 102 | Keyring: &SessionKeyring{Keyring: k}, 103 | ExpiryWindow: defaultExpirationWindow, 104 | SessionProvider: p, 105 | }, nil 106 | } 107 | 108 | return p, nil 109 | } 110 | 111 | // NewAssumeRoleWithWebIdentityProvider returns a provider that generates 112 | // credentials using AssumeRoleWithWebIdentity 113 | func NewAssumeRoleWithWebIdentityProvider(k keyring.Keyring, config *ProfileConfig, useSessionCache bool) (aws.CredentialsProvider, error) { 114 | cfg := NewAwsConfig(config.Region, config.STSRegionalEndpoints) 115 | 116 | p := &AssumeRoleWithWebIdentityProvider{ 117 | StsClient: sts.NewFromConfig(cfg), 118 | RoleARN: config.RoleARN, 119 | RoleSessionName: config.RoleSessionName, 120 | WebIdentityTokenFile: config.WebIdentityTokenFile, 121 | WebIdentityTokenProcess: config.WebIdentityTokenProcess, 122 | Duration: config.AssumeRoleDuration, 123 | } 124 | 125 | if useSessionCache { 126 | return &CachedSessionProvider{ 127 | SessionKey: SessionMetadata{ 128 | Type: "sts.AssumeRoleWithWebIdentity", 129 | ProfileName: config.ProfileName, 130 | }, 131 | Keyring: &SessionKeyring{Keyring: k}, 132 | ExpiryWindow: defaultExpirationWindow, 133 | SessionProvider: p, 134 | }, nil 135 | } 136 | 137 | return p, nil 138 | } 139 | 140 | // NewSSORoleCredentialsProvider creates a provider for SSO credentials 141 | func NewSSORoleCredentialsProvider(k keyring.Keyring, config *ProfileConfig, useSessionCache bool) (aws.CredentialsProvider, error) { 142 | cfg := NewAwsConfig(config.SSORegion, config.STSRegionalEndpoints) 143 | 144 | ssoRoleCredentialsProvider := &SSORoleCredentialsProvider{ 145 | OIDCClient: ssooidc.NewFromConfig(cfg), 146 | StartURL: config.SSOStartURL, 147 | SSOClient: sso.NewFromConfig(cfg), 148 | AccountID: config.SSOAccountID, 149 | RoleName: config.SSORoleName, 150 | UseStdout: config.SSOUseStdout, 151 | } 152 | 153 | if useSessionCache { 154 | ssoRoleCredentialsProvider.OIDCTokenCache = OIDCTokenKeyring{Keyring: k} 155 | return &CachedSessionProvider{ 156 | SessionKey: SessionMetadata{ 157 | Type: "sso.GetRoleCredentials", 158 | ProfileName: config.ProfileName, 159 | MfaSerial: config.SSOStartURL, 160 | }, 161 | Keyring: &SessionKeyring{Keyring: k}, 162 | ExpiryWindow: defaultExpirationWindow, 163 | SessionProvider: ssoRoleCredentialsProvider, 164 | }, nil 165 | } 166 | 167 | return ssoRoleCredentialsProvider, nil 168 | } 169 | 170 | // NewCredentialProcessProvider creates a provider to retrieve credentials from an external 171 | // executable as described in https://docs.aws.amazon.com/cli/latest/topic/config-vars.html#sourcing-credentials-from-external-processes 172 | func NewCredentialProcessProvider(k keyring.Keyring, config *ProfileConfig, useSessionCache bool) (aws.CredentialsProvider, error) { 173 | credentialProcessProvider := &CredentialProcessProvider{ 174 | CredentialProcess: config.CredentialProcess, 175 | } 176 | 177 | if useSessionCache { 178 | return &CachedSessionProvider{ 179 | SessionKey: SessionMetadata{ 180 | Type: "credential_process", 181 | ProfileName: config.ProfileName, 182 | }, 183 | Keyring: &SessionKeyring{Keyring: k}, 184 | ExpiryWindow: defaultExpirationWindow, 185 | SessionProvider: credentialProcessProvider, 186 | }, nil 187 | } 188 | 189 | return credentialProcessProvider, nil 190 | } 191 | 192 | func NewFederationTokenProvider(ctx context.Context, credsProvider aws.CredentialsProvider, config *ProfileConfig) (*FederationTokenProvider, error) { 193 | cfg := NewAwsConfigWithCredsProvider(credsProvider, config.Region, config.STSRegionalEndpoints) 194 | 195 | name, err := GetUsernameFromSession(ctx, cfg) 196 | if err != nil { 197 | return nil, err 198 | } 199 | 200 | log.Printf("Using GetFederationToken for credentials") 201 | return &FederationTokenProvider{ 202 | StsClient: sts.NewFromConfig(cfg), 203 | Name: name, 204 | Duration: config.GetFederationTokenDuration, 205 | }, nil 206 | } 207 | 208 | func FindMasterCredentialsNameFor(profileName string, keyring *CredentialKeyring, config *ProfileConfig) (string, error) { 209 | hasMasterCreds, err := keyring.Has(profileName) 210 | if err != nil { 211 | return "", err 212 | } 213 | 214 | if hasMasterCreds { 215 | return profileName, nil 216 | } 217 | 218 | if profileName == config.SourceProfileName { 219 | return "", fmt.Errorf("No master credentials found") 220 | } 221 | 222 | return FindMasterCredentialsNameFor(config.SourceProfileName, keyring, config) 223 | } 224 | 225 | type TempCredentialsCreator struct { 226 | Keyring *CredentialKeyring 227 | // DisableSessions will disable the use of GetSessionToken 228 | DisableSessions bool 229 | // DisableCache will disable the use of the session cache 230 | DisableCache bool 231 | // DisableSessionsForProfile is a profile for which sessions should not be used 232 | DisableSessionsForProfile string 233 | 234 | chainedMfa string 235 | } 236 | 237 | func (t *TempCredentialsCreator) getSourceCreds(config *ProfileConfig, hasStoredCredentials bool) (sourcecredsProvider aws.CredentialsProvider, err error) { 238 | if hasStoredCredentials { 239 | log.Printf("profile %s: using stored credentials", config.ProfileName) 240 | return NewMasterCredentialsProvider(t.Keyring, config.ProfileName), nil 241 | } 242 | 243 | if config.HasSourceProfile() { 244 | log.Printf("profile %s: sourcing credentials from profile %s", config.ProfileName, config.SourceProfile.ProfileName) 245 | return t.GetProviderForProfile(config.SourceProfile) 246 | } 247 | 248 | return nil, fmt.Errorf("profile %s: credentials missing", config.ProfileName) 249 | } 250 | 251 | func (t *TempCredentialsCreator) getSourceCredWithSession(config *ProfileConfig, hasStoredCredentials bool) (sourcecredsProvider aws.CredentialsProvider, err error) { 252 | sourcecredsProvider, err = t.getSourceCreds(config, hasStoredCredentials) 253 | if err != nil { 254 | return nil, err 255 | } 256 | 257 | if config.HasRole() { 258 | isMfaChained := config.MfaSerial != "" && config.MfaSerial == t.chainedMfa 259 | if isMfaChained { 260 | config.MfaSerial = "" 261 | } 262 | log.Printf("profile %s: using AssumeRole %s", config.ProfileName, mfaDetails(isMfaChained, config)) 263 | return NewAssumeRoleProvider(sourcecredsProvider, t.Keyring.Keyring, config, !t.DisableCache) 264 | } 265 | 266 | if isMasterCredentialsProvider(sourcecredsProvider) { 267 | canUseGetSessionToken, reason := t.canUseGetSessionToken(config) 268 | if canUseGetSessionToken { 269 | t.chainedMfa = config.MfaSerial 270 | log.Printf("profile %s: using GetSessionToken %s", config.ProfileName, mfaDetails(false, config)) 271 | return NewSessionTokenProvider(sourcecredsProvider, t.Keyring.Keyring, config, !t.DisableCache) 272 | } 273 | log.Printf("profile %s: skipping GetSessionToken because %s", config.ProfileName, reason) 274 | } 275 | 276 | return sourcecredsProvider, nil 277 | } 278 | 279 | func (t *TempCredentialsCreator) GetProviderForProfile(config *ProfileConfig) (aws.CredentialsProvider, error) { 280 | hasStoredCredentials, err := t.Keyring.Has(config.ProfileName) 281 | if err != nil { 282 | return nil, err 283 | } 284 | 285 | if hasStoredCredentials || config.HasSourceProfile() { 286 | return t.getSourceCredWithSession(config, hasStoredCredentials) 287 | } 288 | 289 | if config.HasSSOStartURL() { 290 | log.Printf("profile %s: using SSO role credentials", config.ProfileName) 291 | return NewSSORoleCredentialsProvider(t.Keyring.Keyring, config, !t.DisableCache) 292 | } 293 | 294 | if config.HasWebIdentity() { 295 | log.Printf("profile %s: using web identity", config.ProfileName) 296 | return NewAssumeRoleWithWebIdentityProvider(t.Keyring.Keyring, config, !t.DisableCache) 297 | } 298 | 299 | if config.HasCredentialProcess() { 300 | log.Printf("profile %s: using credential process", config.ProfileName) 301 | return NewCredentialProcessProvider(t.Keyring.Keyring, config, !t.DisableCache) 302 | } 303 | 304 | return nil, fmt.Errorf("profile %s: credentials missing", config.ProfileName) 305 | } 306 | 307 | // canUseGetSessionToken determines if GetSessionToken should be used, and if not returns a reason 308 | func (t *TempCredentialsCreator) canUseGetSessionToken(c *ProfileConfig) (bool, string) { 309 | if t.DisableSessions { 310 | return false, "sessions are disabled" 311 | } 312 | if t.DisableSessionsForProfile == c.ProfileName { 313 | return false, "sessions are disabled for this profile" 314 | } 315 | 316 | if c.IsChained() { 317 | if !c.ChainedFromProfile.HasMfaSerial() { 318 | return false, fmt.Sprintf("profile '%s' has no MFA serial defined", c.ChainedFromProfile.ProfileName) 319 | } 320 | 321 | if !c.HasMfaSerial() && c.ChainedFromProfile.HasMfaSerial() { 322 | return false, fmt.Sprintf("profile '%s' has no MFA serial defined", c.ProfileName) 323 | } 324 | 325 | if c.ChainedFromProfile.MfaSerial != c.MfaSerial { 326 | return false, fmt.Sprintf("MFA serial doesn't match profile '%s'", c.ChainedFromProfile.ProfileName) 327 | } 328 | 329 | if c.ChainedFromProfile.AssumeRoleDuration > roleChainingMaximumDuration { 330 | 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) 331 | } 332 | } 333 | 334 | return true, "" 335 | } 336 | 337 | func mfaDetails(mfaChained bool, config *ProfileConfig) string { 338 | if mfaChained { 339 | return "(chained MFA)" 340 | } 341 | if config.HasMfaSerial() { 342 | return "(with MFA)" 343 | } 344 | return "" 345 | } 346 | 347 | // NewTempCredentialsProvider creates a credential provider for the given config 348 | func NewTempCredentialsProvider(config *ProfileConfig, keyring *CredentialKeyring, disableSessions bool, disableCache bool) (aws.CredentialsProvider, error) { 349 | t := TempCredentialsCreator{ 350 | Keyring: keyring, 351 | DisableSessions: disableSessions, 352 | DisableCache: disableCache, 353 | } 354 | return t.GetProviderForProfile(config) 355 | } 356 | -------------------------------------------------------------------------------- /vault/vault_test.go: -------------------------------------------------------------------------------- 1 | package vault_test 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/99designs/aws-vault/v7/vault" 8 | "github.com/99designs/keyring" 9 | ) 10 | 11 | func TestUsageWebIdentityExample(t *testing.T) { 12 | f := newConfigFile(t, []byte(` 13 | [profile role2] 14 | role_arn = arn:aws:iam::33333333333:role/role2 15 | web_identity_token_process = oidccli raw 16 | `)) 17 | defer os.Remove(f) 18 | configFile, err := vault.LoadConfig(f) 19 | if err != nil { 20 | t.Fatal(err) 21 | } 22 | configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "role2"} 23 | config, err := configLoader.GetProfileConfig("role2") 24 | if err != nil { 25 | t.Fatalf("Should have found a profile: %v", err) 26 | } 27 | 28 | ckr := &vault.CredentialKeyring{Keyring: keyring.NewArrayKeyring([]keyring.Item{})} 29 | p, err := vault.NewTempCredentialsProvider(config, ckr, true, true) 30 | if err != nil { 31 | t.Fatal(err) 32 | } 33 | 34 | _, ok := p.(*vault.AssumeRoleWithWebIdentityProvider) 35 | if !ok { 36 | t.Fatalf("Expected AssumeRoleWithWebIdentityProvider, got %T", p) 37 | } 38 | } 39 | 40 | func TestIssue1176(t *testing.T) { 41 | f := newConfigFile(t, []byte(` 42 | [profile my-shared-base-profile] 43 | credential_process=aws-vault exec my-shared-base-profile -j 44 | mfa_serial=arn:aws:iam::1234567890:mfa/danielholz 45 | region=eu-west-1 46 | 47 | [profile profile-with-role] 48 | source_profile=my-shared-base-profile 49 | include_profile=my-shared-base-profile 50 | region=eu-west-1 51 | role_arn=arn:aws:iam::12345678901:role/allow-view-only-access-from-other-accounts 52 | `)) 53 | defer os.Remove(f) 54 | configFile, err := vault.LoadConfig(f) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "my-shared-base-profile"} 59 | config, err := configLoader.GetProfileConfig("my-shared-base-profile") 60 | if err != nil { 61 | t.Fatalf("Should have found a profile: %v", err) 62 | } 63 | 64 | ckr := &vault.CredentialKeyring{Keyring: keyring.NewArrayKeyring([]keyring.Item{})} 65 | p, err := vault.NewTempCredentialsProvider(config, ckr, true, true) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | _, ok := p.(*vault.CredentialProcessProvider) 71 | if !ok { 72 | t.Fatalf("Expected CredentialProcessProvider, got %T", p) 73 | } 74 | } 75 | 76 | func TestIssue1195(t *testing.T) { 77 | f := newConfigFile(t, []byte(` 78 | [profile test] 79 | source_profile=dev 80 | region=ap-northeast-2 81 | 82 | [profile dev] 83 | sso_session=common 84 | sso_account_id=2160xxxx 85 | sso_role_name=AdministratorAccess 86 | region=ap-northeast-2 87 | output=json 88 | 89 | [default] 90 | sso_session=common 91 | sso_account_id=3701xxxx 92 | sso_role_name=AdministratorAccess 93 | region=ap-northeast-2 94 | output=json 95 | 96 | [sso-session common] 97 | sso_start_url=https://xxxx.awsapps.com/start 98 | sso_region=ap-northeast-2 99 | sso_registration_scopes=sso:account:access 100 | `)) 101 | defer os.Remove(f) 102 | configFile, err := vault.LoadConfig(f) 103 | if err != nil { 104 | t.Fatal(err) 105 | } 106 | configLoader := &vault.ConfigLoader{File: configFile, ActiveProfile: "test"} 107 | config, err := configLoader.GetProfileConfig("test") 108 | if err != nil { 109 | t.Fatalf("Should have found a profile: %v", err) 110 | } 111 | 112 | ckr := &vault.CredentialKeyring{Keyring: keyring.NewArrayKeyring([]keyring.Item{})} 113 | p, err := vault.NewTempCredentialsProvider(config, ckr, true, true) 114 | if err != nil { 115 | t.Fatal(err) 116 | } 117 | 118 | ssoProvider, ok := p.(*vault.SSORoleCredentialsProvider) 119 | if !ok { 120 | t.Fatalf("Expected SSORoleCredentialsProvider, got %T", p) 121 | } 122 | if ssoProvider.AccountID != "2160xxxx" { 123 | t.Fatalf("Expected AccountID to be 2160xxxx, got %s", ssoProvider.AccountID) 124 | } 125 | } 126 | --------------------------------------------------------------------------------