├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── debian ├── .gitignore ├── README.md ├── changelog ├── control ├── copyright ├── rules └── source │ ├── format │ └── options ├── go.mod ├── go.sum ├── main.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | Thumbs.db 2 | *.DS_Store 3 | *.swp 4 | aws-rotate-key 5 | aws-rotate-key.exe 6 | release/ 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Fullscreen 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION = 1.2.0 2 | LDFLAGS = -ldflags='-s -w' -trimpath 3 | 4 | linux_amd64: export GOOS=linux 5 | linux_amd64: export GOARCH=amd64 6 | linux_arm: export GOOS=linux 7 | linux_arm: export GOARCH=arm 8 | linux_arm: export GOARM=6 9 | linux_arm64: export GOOS=linux 10 | linux_arm64: export GOARCH=arm64 11 | darwin_amd64: export GOOS=darwin 12 | darwin_amd64: export GOARCH=amd64 13 | darwin_arm64: export GOOS=darwin 14 | darwin_arm64: export GOARCH=arm64 15 | windows_amd64: export GOOS=windows 16 | windows_amd64: export GOARCH=amd64 17 | windows_arm: export GOOS=windows 18 | windows_arm: export GOARCH=arm 19 | windows_arm64: export GOOS=windows 20 | windows_arm64: export GOARCH=arm64 21 | 22 | .PHONY: all linux_amd64 linux_arm linux_arm64 darwin_amd64 darwin_arm64 windows_amd64 windows_arm windows_arm64 clean 23 | 24 | all: linux_amd64 linux_arm linux_arm64 darwin_amd64 darwin_arm64 windows_amd64 windows_arm windows_arm64 25 | 26 | linux_amd64: 27 | go build $(LDFLAGS) 28 | mkdir -p release 29 | rm -f release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip 30 | zip release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip aws-rotate-key 31 | 32 | linux_arm: 33 | go build $(LDFLAGS) 34 | mkdir -p release 35 | rm -f release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip 36 | zip release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip aws-rotate-key 37 | 38 | linux_arm64: 39 | go build $(LDFLAGS) 40 | mkdir -p release 41 | rm -f release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip 42 | zip release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip aws-rotate-key 43 | 44 | darwin_amd64: 45 | go build $(LDFLAGS) 46 | mkdir -p release 47 | rm -f release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip 48 | zip release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip aws-rotate-key 49 | 50 | darwin_arm64: 51 | go build $(LDFLAGS) 52 | mkdir -p release 53 | rm -f release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip 54 | zip release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip aws-rotate-key 55 | 56 | windows_amd64: 57 | go build $(LDFLAGS) 58 | mkdir -p release 59 | rm -f release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip 60 | zip release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip aws-rotate-key.exe 61 | 62 | windows_arm: 63 | go build $(LDFLAGS) 64 | mkdir -p release 65 | rm -f release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip 66 | zip release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip aws-rotate-key.exe 67 | 68 | windows_arm64: 69 | go build $(LDFLAGS) 70 | mkdir -p release 71 | rm -f release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip 72 | zip release/aws-rotate-key-${VERSION}-${GOOS}_${GOARCH}.zip aws-rotate-key.exe 73 | 74 | clean: 75 | rm -rf release 76 | rm -f aws-rotate-key aws-rotate-key.exe 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # aws-rotate-key 2 | 3 | As a security best practice, AWS recommends that users periodically 4 | [regenerate their API access keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html#Using_RotateAccessKey). 5 | This tool simplifies the rotation of access keys defined in your 6 | [credentials file](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html). 7 | 8 | When run, the program will list the current access keys associated with your 9 | IAM user, and print the steps it has to perform to rotate them. 10 | It will then wait for your confirmation before continuing. 11 | 12 | ## Usage 13 | 14 | ``` 15 | $ aws-rotate-key --help 16 | Usage of aws-rotate-key: 17 | -auth-profile string 18 | Use a different profile when calling AWS. 19 | -d Delete the old key instead of deactivating it. 20 | -mfa 21 | Use MFA. 22 | -mfa-serial-number string 23 | Specify the MFA device to use. (optional) 24 | -profile string 25 | The profile to use. (default "default") 26 | -version 27 | Print version number 28 | -y Automatic "yes" to prompts. 29 | ``` 30 | 31 | ## Example 32 | 33 | ``` 34 | $ aws-rotate-key --profile work 35 | Using access key AKIAJMIGD6UPCXCFWVOA from profile "work". 36 | Your user ARN is: arn:aws:iam::123456789012:user/your_username 37 | 38 | Your user has 2 access keys: 39 | - AKIAI3KI7UC6BPI4O57A (Inactive, created 2018-11-22 21:47:46 +0000 UTC, last used 2018-11-30 20:35:41 +0000 UTC for service s3 in us-west-2) 40 | - AKIAJMIGD6UPCXCFWVOA (Active, created 2018-11-30 21:55:57 +0000 UTC, last used 2018-12-20 12:14:10 +0000 UTC for service s3 in us-west-2) 41 | 42 | You have two access keys, which is the maximum number of access keys allowed. 43 | Do you want to delete AKIAI3KI7UC6BPI4O57A and create a new key? [yN] y 44 | Deleted access key: AKIAI3KI7UC6BPI4O57A 45 | Created access key: AKIAIX46CKYT7E5I3KVQ 46 | Wrote new key pair to /Users/your_username/.aws/credentials 47 | Deactivated old access key: AKIAJMIGD6UPCXCFWVOA 48 | Please make sure this key is not used elsewhere. 49 | Please note that it may take a minute for your new access key to propagate in the AWS control plane. 50 | ``` 51 | 52 | ## Install 53 | 54 | You can download binaries from [the releases section](https://github.com/stefansundin/aws-rotate-key/releases/latest). 55 | 56 | You can use Homebrew to install on macOS: 57 | 58 | ```shell 59 | brew install aws-rotate-key 60 | ``` 61 | 62 | You can install [using a PPA](https://launchpad.net/~stefansundin/+archive/ubuntu/aws-rotate-key) on Ubuntu Linux: 63 | 64 | ```shell 65 | sudo add-apt-repository ppa:stefansundin/aws-rotate-key 66 | sudo apt install aws-rotate-key 67 | ``` 68 | 69 | If you have Go installed then you can download and build the program using: 70 | 71 | ```shell 72 | go install github.com/stefansundin/aws-rotate-key@latest 73 | ``` 74 | 75 | ## Setup 76 | 77 | Make sure your users have permissions to update their own access keys. 78 | The following AWS documentation page explains the required permissions: 79 | https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_delegate-permissions_examples.html. 80 | 81 | The following IAM policy is enough for aws-rotate-key: 82 | 83 | ```json 84 | { 85 | "Version": "2012-10-17", 86 | "Statement": [ 87 | { 88 | "Effect": "Allow", 89 | "Action": [ 90 | "iam:ListAccessKeys", 91 | "iam:GetAccessKeyLastUsed", 92 | "iam:DeleteAccessKey", 93 | "iam:CreateAccessKey", 94 | "iam:UpdateAccessKey" 95 | ], 96 | "Resource": [ 97 | "arn:aws:iam::AWS_ACCOUNT_ID:user/${aws:username}" 98 | ] 99 | } 100 | ] 101 | } 102 | ``` 103 | 104 | > [!WARNING] 105 | > Replace `AWS_ACCOUNT_ID` with your AWS account id. 106 | 107 | ### Require MFA 108 | 109 | You can require MFA by adding a `Condition` clause. Please note that you 110 | have to use the `-mfa` option when running the program. 111 | 112 | ```json 113 | { 114 | "Version": "2012-10-17", 115 | "Statement": [ 116 | { 117 | "Effect": "Allow", 118 | "Action": [ 119 | "iam:ListMFADevices" 120 | ], 121 | "Resource": [ 122 | "arn:aws:iam::AWS_ACCOUNT_ID:user/${aws:username}" 123 | ] 124 | }, 125 | { 126 | "Effect": "Allow", 127 | "Action": [ 128 | "iam:ListAccessKeys", 129 | "iam:GetAccessKeyLastUsed", 130 | "iam:DeleteAccessKey", 131 | "iam:CreateAccessKey", 132 | "iam:UpdateAccessKey" 133 | ], 134 | "Resource": [ 135 | "arn:aws:iam::AWS_ACCOUNT_ID:user/${aws:username}" 136 | ], 137 | "Condition": { 138 | "Bool": { 139 | "aws:MultiFactorAuthPresent": true 140 | } 141 | } 142 | } 143 | ] 144 | } 145 | ``` 146 | 147 | Note that this makes it harder to rotate access keys using aws-cli commands, 148 | as it only supports MFA when assuming roles. You will still be able to use 149 | the AWS management console. 150 | 151 | ## Contribute 152 | 153 | To download and hack on the source code, run: 154 | 155 | ```shell 156 | git clone https://github.com/stefansundin/aws-rotate-key.git 157 | cd aws-rotate-key 158 | go build 159 | ``` 160 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /.debhelper 3 | debhelper-build-stamp 4 | files 5 | *.debhelper.log 6 | *.substvars 7 | -------------------------------------------------------------------------------- /debian/README.md: -------------------------------------------------------------------------------- 1 | ```shell 2 | sudo apt install --no-install-recommends devscripts debhelper dh-golang golang golang-any golang-github-aws-aws-sdk-go-v2-dev 3 | 4 | # optional: 5 | sudo apt install --no-install-recommends lintian 6 | 7 | # in the repository root: 8 | git clean -fdX 9 | tar cvJf ../aws-rotate-key_1.2.0.orig.tar.xz --exclude=debian * 10 | 11 | # build deb file: 12 | debuild -i -us -uc -b 13 | 14 | # publish to ppa: 15 | debuild -S 16 | cd .. 17 | dput ppa:stefansundin/aws-rotate-key aws-rotate-key_1.2.0-1_source.changes 18 | ``` 19 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | aws-rotate-key (1.2.0-1) noble; urgency=medium 2 | 3 | * Upgrade AWS SDK to aws-sdk-go-v2. 4 | * Default `-profile` option to the AWS_PROFILE environment variable. 5 | 6 | -- Stefan Sundin Sun, 06 Oct 2024 22:44:01 -0700 7 | 8 | aws-rotate-key (1.1.0-1) jammy; urgency=medium 9 | 10 | * Add support for multiple MFA devices. 11 | 12 | -- Stefan Sundin Sun, 26 Feb 2023 19:29:59 -0800 13 | 14 | aws-rotate-key (1.0.8-4) bionic; urgency=medium 15 | 16 | * Build on bionic. 17 | 18 | -- Stefan Sundin Tue, 05 Apr 2022 23:50:13 -0700 19 | 20 | aws-rotate-key (1.0.8-3) focal; urgency=medium 21 | 22 | * Another attempt at fixing the i386 build. 23 | 24 | -- Stefan Sundin Tue, 05 Apr 2022 23:02:47 -0700 25 | 26 | aws-rotate-key (1.0.8-2) focal; urgency=medium 27 | 28 | * Fix Launchpad i386 build. 29 | 30 | -- Stefan Sundin Tue, 05 Apr 2022 21:42:53 -0700 31 | 32 | aws-rotate-key (1.0.8-1) focal; urgency=medium 33 | 34 | * Add `-auth-profile` option (use a different profile when calling AWS). 35 | Thanks Matt Tucker. 36 | 37 | -- Stefan Sundin Mon, 04 Apr 2022 22:27:30 -0700 38 | 39 | aws-rotate-key (1.0.7-1) eoan; urgency=medium 40 | 41 | * Debian package created. 42 | 43 | -- Stefan Sundin Wed, 08 Jan 2020 15:50:11 -0800 44 | 45 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: aws-rotate-key 2 | Maintainer: Stefan Sundin 3 | Section: devel 4 | Priority: optional 5 | Build-Depends: 6 | debhelper (>= 13), 7 | debhelper-compat (= 13), 8 | dh-golang, 9 | Build-Depends-Arch: 10 | golang-any (>= 1.19), 11 | golang-github-aws-aws-sdk-go-v2-dev, 12 | Standards-Version: 4.6.2 13 | Rules-Requires-Root: no 14 | Homepage: https://github.com/stefansundin/aws-rotate-key 15 | XS-Go-Import-Path: github.com/stefansundin/aws-rotate-key 16 | 17 | Package: aws-rotate-key 18 | Architecture: any 19 | Depends: 20 | ${misc:Depends}, 21 | ${shlibs:Depends}, 22 | Static-Built-Using: ${misc:Static-Built-Using} 23 | Description: Easily rotate your AWS access key. 24 | When run, the program will list the current access keys associated with your 25 | IAM user, and print the steps it has to perform to rotate them. 26 | It will then wait for your confirmation before continuing. 27 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | License: Expat 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | 3 | override_dh_auto_install: 4 | dh_auto_install -- --no-source 5 | 6 | %: 7 | dh $@ --builddirectory=debian/_build --buildsystem=golang --with=golang 8 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /debian/source/options: -------------------------------------------------------------------------------- 1 | extend-diff-ignore = "\.github" 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/stefansundin/aws-rotate-key 2 | 3 | go 1.21 4 | 5 | toolchain go1.23.2 6 | 7 | require ( 8 | github.com/aws/aws-sdk-go-v2 v1.32.0 9 | github.com/aws/aws-sdk-go-v2/config v1.27.41 10 | github.com/aws/aws-sdk-go-v2/credentials v1.17.39 11 | github.com/aws/aws-sdk-go-v2/service/iam v1.37.0 12 | github.com/aws/aws-sdk-go-v2/service/sts v1.32.0 13 | ) 14 | 15 | require ( 16 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.15 // indirect 17 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.19 // indirect 18 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.19 // indirect 19 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect 20 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 // indirect 21 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.0 // indirect 22 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.0 // indirect 23 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.0 // indirect 24 | github.com/aws/smithy-go v1.22.0 // indirect 25 | ) 26 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/aws/aws-sdk-go-v2 v1.32.0 h1:GuHp7GvMN74PXD5C97KT5D87UhIy4bQPkflQKbfkndg= 2 | github.com/aws/aws-sdk-go-v2 v1.32.0/go.mod h1:2SK5n0a2karNTv5tbP1SjsX0uhttou00v/HpXKM1ZUo= 3 | github.com/aws/aws-sdk-go-v2/config v1.27.41 h1:esG3WpmEuNJ6F4kVFLumN8nCfA5VBav1KKb3JPx83O4= 4 | github.com/aws/aws-sdk-go-v2/config v1.27.41/go.mod h1:haUg09ebP+ClvPjU3EB/xe0HF9PguO19PD2fdjM2X14= 5 | github.com/aws/aws-sdk-go-v2/credentials v1.17.39 h1:tmVexAhoGqJxNE2oc4/SJqL+Jz1x1iCPt5ts9XcqZCU= 6 | github.com/aws/aws-sdk-go-v2/credentials v1.17.39/go.mod h1:zgOdbDI9epE608PdboJ87CYvPIejAgFevazeJW6iauQ= 7 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.15 h1:kGjlNc2IXXcxPDcfMyCshNCjVgxUhC/vTJv7NvC9wKk= 8 | github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.15/go.mod h1:rk/HmqPo+dX0Uv0Q1+4w3QKFdICEGSsTYz1hRWvH8UI= 9 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.19 h1:Q/k5wCeJkSWs+62kDfOillkNIJ5NqmE3iOfm48g/W8c= 10 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.19/go.mod h1:Wns1C66VvtA2Bv/cUBuKZKQKdjo7EVMhp90aAa+8oTI= 11 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.19 h1:AYLE0lUfKvN6icFTR/p+NmD1amYKTbqHQ1Nm+jwE6BM= 12 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.19/go.mod h1:1giLakj64GjuH1NBzF/DXqly5DWHtMTaOzRZ53nFX0I= 13 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= 14 | github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= 15 | github.com/aws/aws-sdk-go-v2/service/iam v1.37.0 h1:FLdmwEJUDWdAflqxRNkIKNZki8dFmi5SUeTjAjxrdJU= 16 | github.com/aws/aws-sdk-go-v2/service/iam v1.37.0/go.mod h1:Xctz/06SeHDUc3ZheMxXekSZ2rx0RX9SVhV5JeQgoqY= 17 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0 h1:TToQNkvGguu209puTojY/ozlqy2d/SFNcoLIqTFi42g= 18 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.0/go.mod h1:0jp+ltwkf+SwG2fm/PKo8t4y8pJSgOCO4D8Lz3k0aHQ= 19 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.0 h1:AdbiDUgQZmM28rDIZbiSwFxz8+3B94aOXxzs6oH+EA0= 20 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.0/go.mod h1:uV476Bd80tiDTX4X2redMtagQUg65aU/gzPojSJ4kSI= 21 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.0 h1:71FvP6XFj53NK+YiAEGVzeiccLVeFnHOCvMig0zOHsE= 22 | github.com/aws/aws-sdk-go-v2/service/sso v1.24.0/go.mod h1:UVJqtKXSd9YppRKgdBIkyv7qgbSGv5DchM3yX0BN2mU= 23 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.0 h1:Uco4o19bi3AmBapImNzuMk+rfzlui52BDyVK1UfJeRA= 24 | github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.0/go.mod h1:+HLFhCpnG08hBee8bUdfd1mBK+rFKPt4O5igR9lXDfk= 25 | github.com/aws/aws-sdk-go-v2/service/sts v1.32.0 h1:GiQUjZM2KUZX68o/LpZ1xqxYMuvoxpRrOwYARYog3vc= 26 | github.com/aws/aws-sdk-go-v2/service/sts v1.32.0/go.mod h1:dKnu7M4MAS2SDlng1ytxd03H+y0LoUfEQ5E2VaaSw/4= 27 | github.com/aws/smithy-go v1.22.0 h1:uunKnWlcoL3zO7q+gG2Pk53joueEOsnNB28QdMsmiMM= 28 | github.com/aws/smithy-go v1.22.0/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "context" 6 | "flag" 7 | "fmt" 8 | "os" 9 | "os/user" 10 | "path/filepath" 11 | "regexp" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/aws/aws-sdk-go-v2/aws" 16 | "github.com/aws/aws-sdk-go-v2/config" 17 | "github.com/aws/aws-sdk-go-v2/credentials" 18 | "github.com/aws/aws-sdk-go-v2/service/iam" 19 | iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types" 20 | "github.com/aws/aws-sdk-go-v2/service/sts" 21 | ) 22 | 23 | const version = "1.2.0" 24 | 25 | var defaultProfile = "default" 26 | var credentialsPath string 27 | 28 | func init() { 29 | // Do not fail if a region is not specified anywhere 30 | if _, present := os.LookupEnv("AWS_DEFAULT_REGION"); !present { 31 | os.Setenv("AWS_DEFAULT_REGION", "us-east-1") 32 | } 33 | // Respect AWS_PROFILE if it is set 34 | if v, ok := os.LookupEnv("AWS_PROFILE"); ok { 35 | defaultProfile = v 36 | } 37 | // Locate the credentials file 38 | credentialsPath = os.Getenv("AWS_SHARED_CREDENTIALS_FILE") 39 | if credentialsPath == "" { 40 | usr, err := user.Current() 41 | if err != nil { 42 | fmt.Println("Error: Could not locate your home directory. Please set the AWS_SHARED_CREDENTIALS_FILE environment variable.") 43 | os.Exit(1) 44 | } 45 | credentialsPath = filepath.Join(usr.HomeDir, ".aws", "credentials") 46 | } 47 | if _, err := os.Stat(credentialsPath); os.IsNotExist(err) { 48 | fmt.Printf("Error locating the credentials file, expected it at: %s\n", credentialsPath) 49 | fmt.Println("Please set the AWS_SHARED_CREDENTIALS_FILE environment variable if it is located elsewhere.") 50 | os.Exit(1) 51 | } 52 | } 53 | 54 | func main() { 55 | var yesFlag bool 56 | var mfaFlag bool 57 | var profileFlag string 58 | var authProfileFlag string 59 | var mfaSerialNumber string 60 | var versionFlag bool 61 | var deleteFlag bool 62 | flag.BoolVar(&yesFlag, "y", false, `Automatic "yes" to prompts.`) 63 | flag.BoolVar(&mfaFlag, "mfa", false, "Use MFA.") 64 | flag.BoolVar(&deleteFlag, "d", false, "Delete the old key instead of deactivating it.") 65 | flag.StringVar(&profileFlag, "profile", defaultProfile, "The profile to use.") 66 | flag.StringVar(&authProfileFlag, "auth-profile", "", "Use a different profile when calling AWS.") 67 | flag.StringVar(&mfaSerialNumber, "mfa-serial-number", "", "Specify the MFA device to use. (optional)") 68 | flag.BoolVar(&versionFlag, "version", false, "Print version number") 69 | flag.Parse() 70 | 71 | if versionFlag { 72 | fmt.Println(version) 73 | os.Exit(0) 74 | } 75 | 76 | // Get credentials 77 | sharedConfig, err := config.LoadSharedConfigProfile(context.TODO(), 78 | profileFlag, 79 | func(o *config.LoadSharedConfigOptions) { 80 | o.CredentialsFiles = []string{credentialsPath} 81 | }, 82 | ) 83 | if sharedConfig.Credentials.AccessKeyID == "" { 84 | fmt.Printf("Error loading credentials using profile \"%s\".\n", profileFlag) 85 | if err != nil { 86 | fmt.Fprintln(os.Stderr, err.Error()) 87 | } 88 | os.Exit(1) 89 | } 90 | check(err) 91 | creds := sharedConfig.Credentials 92 | fmt.Printf("Using access key %s from profile \"%s\".\n", creds.AccessKeyID, profileFlag) 93 | 94 | if authProfileFlag != "" { 95 | profileFlag = authProfileFlag 96 | } 97 | 98 | // Read credentials file 99 | bytes, err := os.ReadFile(credentialsPath) 100 | check(err) 101 | credentialsText := string(bytes) 102 | // Check if we can find the credentials in the file 103 | // It's better to detect a malformed file now than after we have created the new key 104 | re_aws_access_key_id := regexp.MustCompile(fmt.Sprintf(`(?m)^aws_access_key_id *= *%s`, regexp.QuoteMeta(creds.AccessKeyID))) 105 | re_aws_secret_access_key := regexp.MustCompile(fmt.Sprintf(`(?m)^aws_secret_access_key *= *%s`, regexp.QuoteMeta(creds.SecretAccessKey))) 106 | if !re_aws_access_key_id.MatchString(credentialsText) || !re_aws_secret_access_key.MatchString(credentialsText) { 107 | fmt.Println() 108 | fmt.Printf("Unable to find your credentials in %s\n", credentialsPath) 109 | fmt.Println("Please make sure your file is formatted like the following:") 110 | fmt.Println() 111 | fmt.Printf("aws_access_key_id=%s\n", creds.AccessKeyID) 112 | fmt.Println("aws_secret_access_key=...") 113 | fmt.Println() 114 | os.Exit(1) 115 | } 116 | 117 | // Load config 118 | cfg, err := config.LoadDefaultConfig(context.TODO(), 119 | config.WithSharedConfigProfile(profileFlag), 120 | ) 121 | check(err) 122 | 123 | stsClient := sts.NewFromConfig(cfg) 124 | respGetCallerIdentity, err := stsClient.GetCallerIdentity(context.TODO(), 125 | &sts.GetCallerIdentityInput{}, 126 | ) 127 | if err != nil { 128 | fmt.Println("Error getting caller identity. Is the key disabled?") 129 | fmt.Println() 130 | check(err) 131 | } 132 | fmt.Printf("Your user ARN is: %s\n", aws.ToString(respGetCallerIdentity.Arn)) 133 | 134 | // mfa 135 | if mfaFlag || mfaSerialNumber != "" { 136 | if mfaSerialNumber == "" { 137 | // If the UserName field is not specified, the UserName is determined implicitly based on the AWS access key ID used to sign the request. 138 | iamClient := iam.NewFromConfig(cfg) 139 | respMFADevices, err := iamClient.ListMFADevices(context.TODO(), 140 | &iam.ListMFADevicesInput{}, 141 | ) 142 | check(err) 143 | if len(respMFADevices.MFADevices) == 0 { 144 | fmt.Println("You do not have any MFA devices assigned to your user.") 145 | os.Exit(1) 146 | } 147 | 148 | supportedSerialNumbers := make([]string, 0, len(respMFADevices.MFADevices)) 149 | for _, device := range respMFADevices.MFADevices { 150 | if !isU2F(aws.ToString(device.SerialNumber)) { 151 | supportedSerialNumbers = append(supportedSerialNumbers, aws.ToString(device.SerialNumber)) 152 | } 153 | } 154 | 155 | if len(supportedSerialNumbers) == 0 { 156 | fmt.Println() 157 | fmt.Println("You have an U2F MFA device assigned to your user. These are not supported.") 158 | fmt.Println("Please add another MFA to your user.") 159 | os.Exit(1) 160 | } else if len(supportedSerialNumbers) == 1 { 161 | mfaSerialNumber = supportedSerialNumbers[0] 162 | fmt.Printf("Your MFA serial number is: %s\n\n", mfaSerialNumber) 163 | } else { 164 | fmt.Println() 165 | fmt.Println("You have multiple MFA devices assigned to your user.") 166 | if len(supportedSerialNumbers) != len(respMFADevices.MFADevices) { 167 | fmt.Println("Note: You have U2F MFA devices assigned to your user. These are not supported and are not in this list.") 168 | } 169 | fmt.Println() 170 | for i, serialNumber := range supportedSerialNumbers { 171 | fmt.Printf("%d: %s\n", i+1, serialNumber) 172 | } 173 | fmt.Println() 174 | if yesFlag { 175 | mfaSerialNumber = supportedSerialNumbers[0] 176 | fmt.Println("Because you used -y, the first MFA device was automatically chosen. You can use -mfa-serial-number to pick a different device.") 177 | } else { 178 | var input string 179 | fmt.Println("Which MFA device do you want to use?") 180 | fmt.Print("Enter a number from the list above or the full serial number: ") 181 | _, err = fmt.Scanln(&input) 182 | check(err) 183 | if isNumeric(input) { 184 | i, err := strconv.Atoi(input) 185 | check(err) 186 | if i < 1 || i > len(supportedSerialNumbers) { 187 | fmt.Println("Invalid selection!") 188 | os.Exit(1) 189 | } 190 | mfaSerialNumber = supportedSerialNumbers[i-1] 191 | } else { 192 | mfaSerialNumber = input 193 | } 194 | } 195 | } 196 | } 197 | 198 | // I have no idea how much work it would be to support U2F 199 | if isU2F(mfaSerialNumber) { 200 | fmt.Println("Sorry, U2F MFA devices are not supported. Please use another MFA.") 201 | os.Exit(1) 202 | } 203 | 204 | // Prompt for the code 205 | var code string 206 | fmt.Print("MFA token code: ") 207 | _, err = fmt.Scanln(&code) 208 | check(err) 209 | 210 | // Get the new credentials 211 | respSessionToken, err := stsClient.GetSessionToken(context.TODO(), 212 | &sts.GetSessionTokenInput{ 213 | DurationSeconds: aws.Int32(900), // valid for 15 minutes (the minimum) 214 | SerialNumber: aws.String(mfaSerialNumber), 215 | TokenCode: aws.String(code), 216 | }, 217 | ) 218 | check(err) 219 | 220 | // Create a new config that use the new credentials 221 | c := respSessionToken.Credentials 222 | mfaCreds := aws.NewCredentialsCache(credentials.NewStaticCredentialsProvider( 223 | aws.ToString(c.AccessKeyId), 224 | aws.ToString(c.SecretAccessKey), 225 | aws.ToString(c.SessionToken), 226 | )) 227 | cfg, err = config.LoadDefaultConfig(context.TODO(), 228 | config.WithCredentialsProvider(mfaCreds), 229 | ) 230 | check(err) 231 | } 232 | fmt.Println() 233 | 234 | // iam list-access-keys 235 | // If the UserName field is not specified, the UserName is determined implicitly based on the AWS access key ID used to sign the request. 236 | iamClient := iam.NewFromConfig(cfg) 237 | respListAccessKeys, err := iamClient.ListAccessKeys(context.TODO(), 238 | &iam.ListAccessKeysInput{}, 239 | ) 240 | check(err) 241 | 242 | // Print key information 243 | fmt.Printf("Your user has %d access key%s:\n", 244 | len(respListAccessKeys.AccessKeyMetadata), 245 | pluralize(len(respListAccessKeys.AccessKeyMetadata)), 246 | ) 247 | for _, key := range respListAccessKeys.AccessKeyMetadata { 248 | respAccessKeyLastUsed, err2 := iamClient.GetAccessKeyLastUsed(context.TODO(), 249 | &iam.GetAccessKeyLastUsedInput{ 250 | AccessKeyId: key.AccessKeyId, 251 | }, 252 | ) 253 | if err2 != nil { 254 | fmt.Printf("- %s (%s, created %s)\n", 255 | aws.ToString(key.AccessKeyId), 256 | key.Status, 257 | key.CreateDate, 258 | ) 259 | } else if respAccessKeyLastUsed.AccessKeyLastUsed.LastUsedDate == nil { 260 | fmt.Printf("- %s (%s, created %s, never used)\n", 261 | aws.ToString(key.AccessKeyId), 262 | key.Status, 263 | key.CreateDate, 264 | ) 265 | } else { 266 | fmt.Printf("- %s (%s, created %s, last used %s for service %s in %s)\n", 267 | aws.ToString(key.AccessKeyId), 268 | key.Status, 269 | key.CreateDate, 270 | respAccessKeyLastUsed.AccessKeyLastUsed.LastUsedDate, 271 | aws.ToString(respAccessKeyLastUsed.AccessKeyLastUsed.ServiceName), 272 | aws.ToString(respAccessKeyLastUsed.AccessKeyLastUsed.Region), 273 | ) 274 | } 275 | } 276 | fmt.Println() 277 | 278 | if len(respListAccessKeys.AccessKeyMetadata) == 2 { 279 | keyIndex := 0 280 | if aws.ToString(respListAccessKeys.AccessKeyMetadata[0].AccessKeyId) == creds.AccessKeyID { 281 | keyIndex = 1 282 | } 283 | 284 | if !yesFlag { 285 | fmt.Println("You have two access keys, which is the maximum number of access keys allowed.") 286 | fmt.Printf("Do you want to delete %s and create a new key? [yN] ", 287 | aws.ToString(respListAccessKeys.AccessKeyMetadata[keyIndex].AccessKeyId), 288 | ) 289 | if respListAccessKeys.AccessKeyMetadata[keyIndex].Status == iamTypes.StatusTypeActive { 290 | fmt.Printf("\nWARNING: This key is currently Active! ") 291 | } 292 | reader := bufio.NewReader(os.Stdin) 293 | yn, err2 := reader.ReadString('\n') 294 | check(err2) 295 | if yn[0] != 'y' && yn[0] != 'Y' { 296 | fmt.Println("Aborted with no changes performed.") 297 | os.Exit(1) 298 | } 299 | } 300 | 301 | _, err2 := iamClient.DeleteAccessKey(context.TODO(), 302 | &iam.DeleteAccessKeyInput{ 303 | AccessKeyId: respListAccessKeys.AccessKeyMetadata[keyIndex].AccessKeyId, 304 | }, 305 | ) 306 | check(err2) 307 | fmt.Printf("Deleted access key: %s\n", 308 | aws.ToString(respListAccessKeys.AccessKeyMetadata[keyIndex].AccessKeyId), 309 | ) 310 | } else if !yesFlag { 311 | cleanupAction := "deactivate" 312 | if deleteFlag { 313 | cleanupAction = "delete" 314 | } 315 | fmt.Printf("Do you want to create a new key and %s %s? [yN] ", 316 | cleanupAction, 317 | aws.ToString(respListAccessKeys.AccessKeyMetadata[0].AccessKeyId), 318 | ) 319 | reader := bufio.NewReader(os.Stdin) 320 | yn, err2 := reader.ReadString('\n') 321 | check(err2) 322 | if yn[0] != 'y' && yn[0] != 'Y' { 323 | fmt.Println("Aborted with no changes performed.") 324 | os.Exit(1) 325 | } 326 | } 327 | 328 | // Create the new access key 329 | // If you do not specify a user name, IAM determines the user name implicitly based on the AWS access key ID signing the request. 330 | respCreateAccessKey, err := iamClient.CreateAccessKey(context.TODO(), 331 | &iam.CreateAccessKeyInput{}, 332 | ) 333 | check(err) 334 | newAccessKeyId := aws.ToString(respCreateAccessKey.AccessKey.AccessKeyId) 335 | newSecretAccessKey := aws.ToString(respCreateAccessKey.AccessKey.SecretAccessKey) 336 | fmt.Printf("Created access key: %s\n", newAccessKeyId) 337 | 338 | // Replace key pair in credentials file 339 | // This search & replace does not limit itself to the specified profile, which is useful if the user is using the same key in multiple profiles 340 | credentialsText = re_aws_access_key_id.ReplaceAllString(credentialsText, `aws_access_key_id=`+newAccessKeyId) 341 | credentialsText = re_aws_secret_access_key.ReplaceAllString(credentialsText, `aws_secret_access_key=`+newSecretAccessKey) 342 | 343 | // Verify that the regexp actually replaced something 344 | if !strings.Contains(credentialsText, newAccessKeyId) || !strings.Contains(credentialsText, newSecretAccessKey) { 345 | fmt.Println("Error: Failed to replace the old access key.") 346 | fmt.Printf("Please verify that the file %s is formatted correctly.\n", credentialsPath) 347 | // Delete the key we created 348 | _, err2 := iamClient.DeleteAccessKey(context.TODO(), 349 | &iam.DeleteAccessKeyInput{ 350 | AccessKeyId: aws.String(newAccessKeyId), 351 | }, 352 | ) 353 | check(err2) 354 | fmt.Printf("Deleted access key: %s\n", newAccessKeyId) 355 | os.Exit(1) 356 | } 357 | 358 | // Write new file 359 | err = os.WriteFile(credentialsPath, []byte(credentialsText), 0600) 360 | check(err) 361 | fmt.Printf("Wrote new key pair to %s\n", credentialsPath) 362 | 363 | // Delete the old key if flag is set, otherwise deactivate it 364 | if deleteFlag { 365 | _, err := iamClient.DeleteAccessKey(context.TODO(), 366 | &iam.DeleteAccessKeyInput{ 367 | AccessKeyId: aws.String(creds.AccessKeyID), 368 | }, 369 | ) 370 | check(err) 371 | fmt.Printf("Deleted old access key: %s\n", creds.AccessKeyID) 372 | } else { 373 | _, err = iamClient.UpdateAccessKey(context.TODO(), 374 | &iam.UpdateAccessKeyInput{ 375 | AccessKeyId: aws.String(creds.AccessKeyID), 376 | Status: iamTypes.StatusTypeInactive, 377 | }, 378 | ) 379 | check(err) 380 | fmt.Printf("Deactivated old access key: %s\n", creds.AccessKeyID) 381 | fmt.Println("Please make sure this key is not used elsewhere.") 382 | } 383 | fmt.Println("Please note that it may take a minute for your new access key to propagate in the AWS control plane.") 384 | } 385 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | ) 8 | 9 | func pluralize(n int) string { 10 | if n == 1 { 11 | return "" 12 | } 13 | return "s" 14 | } 15 | 16 | func isNumeric(s string) bool { 17 | for _, c := range s { 18 | if c < '0' || c > '9' { 19 | return false 20 | } 21 | } 22 | return true 23 | } 24 | 25 | func isU2F(serialNumber string) bool { 26 | // Hardware token serial numbers are not ARNs and look like this: GAHT12345678 27 | if strings.HasPrefix(serialNumber, "arn:") { 28 | split := strings.Split(serialNumber, ":") 29 | if len(split) > 5 && strings.HasPrefix(split[5], "u2f/") { 30 | return true 31 | } 32 | } 33 | return false 34 | } 35 | 36 | func check(err error) { 37 | if err != nil { 38 | fmt.Fprintln(os.Stderr, err.Error()) 39 | os.Exit(1) 40 | } 41 | } 42 | --------------------------------------------------------------------------------