diff --git a/USAGE.md b/USAGE.md index 150406b69..6f65a75cd 100644 --- a/USAGE.md +++ b/USAGE.md @@ -19,9 +19,12 @@ * [Assuming a role for more than 1h](#assuming-a-role-for-more-than-1h) * [Being able to perform certain STS operations](#being-able-to-perform-certain-sts-operations) * [Rotating Credentials](#rotating-credentials) +* [Using a Yubikey](#using-a-yubikey) + * [Prerequisites](#prerequisites) + * [Setup](#setup) + * [Usage](#usage) * [Recipes](#recipes) * [Overriding the aws CLI to use aws-vault](#overriding-the-aws-cli-to-use-aws-vault) - * [Using a yubikey as a virtual MFA](#using-a-yubikey-as-a-virtual-mfa) * [An example config to switch profiles via environment variables](#an-example-config-to-switch-profiles-via-environment-variables) @@ -402,56 +405,52 @@ The minimal IAM policy required to rotate your own credentials is: } ``` -## Recipes - -### Overriding the aws CLI to use aws-vault - -If you want the `aws` command to use aws-vault automatically, you can create an overriding script -(make it higher precedence in your PATH) that looks like the below: - -```bash -#!/bin/bash -exec aws-vault exec "${AWS_DEFAULT_PROFILE:-work}" -- /usr/local/bin/aws "$@" -``` - -The exec helps reduce the number of processes that are hanging around. The `$@` passes on the -arguments from the wrapper to the original command. +## Using a Yubikey -### Using a yubikey as a virtual MFA +Yubikeys can be used with AWS Vault via Yubikey's OATH-TOTP support. TOTP is necessary because FIDO-U2F is unsupported on the AWS API. -There's been attempts in the past to support yubikeys natively (#392 , #230) there's another way to go -at this problem. [Newer](https://support.yubico.com/support/solutions/articles/15000006419-using-your-yubikey-with-authenticator-codes) -yubikeys support generating TOTP tokens. +### Prerequisites + 1. [A Yubikey that supports OATH-TOTP](https://support.yubico.com/support/solutions/articles/15000006419-using-your-yubikey-with-authenticator-codes) + 2. `ykman`, the [YubiKey Manager CLI](https://github.com/Yubico/yubikey-manager) tool -In this [blog](https://hackernoon.com/use-a-yubikey-as-a-mfa-device-to-replace-google-authenticator-b4f4c0215f2) you can -find information about this process but it boils down to this. +You can verify these prerequisites by running `ykman info` and checking `OATH` is enabled. -1. Go to AWS and click on add a MFA -2. Choose a virtual device -3. Instead of scanning the code you can get it as text (keep it safe). -4. Install [ykman](https://support.yubico.com/support/solutions/articles/15000012643-yubikey-manager-cli-ykman-user-manual#Introductionmrzmm1) -5. Run this: +### Setup + 1. Log into the AWS web console with your IAM user credentials, and navigate to _My Security Credentials_ + 2. Under _Multi-factor authentivation (MFA)_, click `Manage MFA device` and add a Virtual MFA device + 3. Instead of showing the QR code, click on `Show secret key` and copy the key. + 4. On a command line, run: + ```bash + ykman oath add -t arn:aws:iam::${ACCOUNT_ID}:mfa/${IAM_USERNAME} + ``` + replacing `${ACCOUNT_ID}` with your AWS account ID and `${IAM_USERNAME}` with your IAM username. It will prompt you for a base32 text and you can input the key from step 3. Notice the above command uses `-t` which requires you to touch your YubiKey to generate authentication codes. + 5. Now you have to enter two consecutive MFA codes into the AWS website to assign your key to your AWS login. Just run `ykman oath code arn:aws:iam::${ACCOUNT_ID}:mfa/${IAM_USERNAME}` to get an authentication code. The codes are re-generated every 30 seconds, so you have to run this command twice with about 30 seconds in between to get two distinct codes. Enter the two codes in the AWS form and click `Assign MFA` +### Usage +Using the `ykman` prompt driver, aws-vault will execute `ykman` to generate tokens for any profile in your `.aws/config` using an `mfa_device`. ```bash -ykman oath add YOUR_YUBIKEY_PROFILE -t +aws-vault exec --prompt ykman ${AWS_VAULT_PROFILE_USING_MFA} -- aws s3 ls ``` -It will ask you for a base32 text. Here you can input the text you got in 3. +Further config: + - `AWS_VAULT_PROMPT=ykman`: to avoid specifying `--prompt` each time + - `YKMAN_OATH_CREDENTIAL_NAME`: to use an alternative ykman credential -6. Run this command twice (wait 30 secs in between): -```bash -ykman oath code --single YOUR_YUBIKEY_PROFILE -``` -Input both values as tokens and your device should register as a virtual MFA. +## Recipes +### Overriding the aws CLI to use aws-vault -7. Now if you want to run any aws-vault command you should run this: -```bash -aws-vault exec --mfa-token $(ykman oath code --single ${YOUR_YUBIKEY_PROFILE}) ${YOUR_AWS_VAULT_PROFILE} -- aws s3 ls +If you want the `aws` command to use aws-vault automatically, you can create an overriding script +(make it higher precedence in your PATH) that looks like the below: + +```bash +#!/bin/bash +exec aws-vault exec "${AWS_DEFAULT_PROFILE:-work}" -- /usr/local/bin/aws "$@" ``` -[Here](https://gist.github.com/chtorr/0ecc8fca27a4c5e186c636c262cc4757) There're some helper scripts for this. +The exec helps reduce the number of processes that are hanging around. The `$@` passes on the +arguments from the wrapper to the original command. ### An example config to switch profiles via environment variables diff --git a/prompt/kdialog.go b/prompt/kdialog.go index ec4e9e14a..98d53580c 100644 --- a/prompt/kdialog.go +++ b/prompt/kdialog.go @@ -5,8 +5,8 @@ import ( "strings" ) -func KDialogPrompt(prompt string) (string, error) { - cmd := exec.Command("kdialog", "--inputbox", prompt, "--title", "aws-vault") +func KDialogPrompt(mfaSerial string) (string, error) { + cmd := exec.Command("kdialog", "--inputbox", mfaPromptMessage(mfaSerial), "--title", "aws-vault") out, err := cmd.Output() if err != nil { diff --git a/prompt/osascript.go b/prompt/osascript.go index ebfbc8ba7..660bd8352 100644 --- a/prompt/osascript.go +++ b/prompt/osascript.go @@ -6,12 +6,12 @@ import ( "strings" ) -func OSAScriptPrompt(prompt string) (string, error) { +func OSAScriptPrompt(mfaSerial string) (string, error) { cmd := exec.Command("osascript", "-e", fmt.Sprintf(` display dialog "%s" default answer "" buttons {"OK", "Cancel"} default button 1 text returned of the result return result`, - prompt)) + mfaPromptMessage(mfaSerial))) out, err := cmd.Output() if err != nil { diff --git a/prompt/prompt.go b/prompt/prompt.go index 39100d2f6..a18a0ebaf 100644 --- a/prompt/prompt.go +++ b/prompt/prompt.go @@ -4,9 +4,7 @@ import "fmt" type PromptFunc func(string) (string, error) -var Methods = map[string]PromptFunc{ - "terminal": TerminalPrompt, -} +var Methods = map[string]PromptFunc{} func Available() []string { methods := []string{} @@ -23,3 +21,7 @@ func Method(s string) PromptFunc { } return m } + +func mfaPromptMessage(mfaSerial string) string { + return fmt.Sprintf("Enter token for %s: ", mfaSerial) +} diff --git a/prompt/terminal.go b/prompt/terminal.go index 67d62fd2a..1e2b71862 100644 --- a/prompt/terminal.go +++ b/prompt/terminal.go @@ -7,13 +7,18 @@ import ( "strings" ) -func TerminalPrompt(prompt string) (string, error) { - fmt.Fprint(os.Stderr, prompt) +func TerminalPrompt(mfaSerial string) (string, error) { + fmt.Fprint(os.Stderr, mfaPromptMessage(mfaSerial)) reader := bufio.NewReader(os.Stdin) text, err := reader.ReadString('\n') if err != nil { return "", err } + return strings.TrimSpace(text), nil } + +func init() { + Methods["terminal"] = TerminalPrompt +} diff --git a/prompt/ykman.go b/prompt/ykman.go new file mode 100644 index 000000000..b4bf80507 --- /dev/null +++ b/prompt/ykman.go @@ -0,0 +1,33 @@ +package prompt + +import ( + "fmt" + "log" + "os" + "os/exec" + "strings" +) + +// YkmanProvider runs ykman to generate a OATH-TOTP token from the Yubikey device +// To set up ykman, first run `ykman oath add` +func YkmanProvider(mfaSerial string) (string, error) { + yubikeyOathCredName := os.Getenv("YKMAN_OATH_CREDENTIAL_NAME") + if yubikeyOathCredName == "" { + yubikeyOathCredName = mfaSerial + } + + log.Printf("Fetching MFA code using `ykman oath code --single %s`", yubikeyOathCredName) + cmd := exec.Command("ykman", "oath", "code", "--single", yubikeyOathCredName) + cmd.Stderr = os.Stderr + + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("ykman: %w", err) + } + + return strings.TrimSpace(string(out)), nil +} + +func init() { + Methods["ykman"] = YkmanProvider +} diff --git a/prompt/zenity.go b/prompt/zenity.go index 882353dfb..bcc43c184 100644 --- a/prompt/zenity.go +++ b/prompt/zenity.go @@ -1,13 +1,12 @@ package prompt import ( - "fmt" "os/exec" "strings" ) -func ZenityPrompt(prompt string) (string, error) { - cmd := exec.Command("zenity", "--entry", "--title=aws-vault", fmt.Sprintf(`--text=%s`, prompt)) +func ZenityPrompt(mfaSerial string) (string, error) { + cmd := exec.Command("zenity", "--entry", "--title", "aws-vault", "--text", mfaPromptMessage(mfaSerial)) out, err := cmd.Output() if err != nil { diff --git a/vault/vault.go b/vault/vault.go index 145f40780..9144cd9b2 100644 --- a/vault/vault.go +++ b/vault/vault.go @@ -46,7 +46,7 @@ func (m *Mfa) GetMfaToken() (*string, error) { if m.MfaPromptMethod != "" { promptFunc := prompt.Method(m.MfaPromptMethod) - token, err := promptFunc(fmt.Sprintf("Enter token for %s: ", m.MfaSerial)) + token, err := promptFunc(m.MfaSerial) return aws.String(token), err }