Skip to content

Commit

Permalink
Merge pull request #558 from 99designs/ykman-support
Browse files Browse the repository at this point in the history
Yubikey support
  • Loading branch information
mtibben authored Apr 21, 2020
2 parents e8e98ba + 5153ff5 commit 09889cc
Show file tree
Hide file tree
Showing 8 changed files with 88 additions and 50 deletions.
73 changes: 36 additions & 37 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions prompt/kdialog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
4 changes: 2 additions & 2 deletions prompt/osascript.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 5 additions & 3 deletions prompt/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand All @@ -23,3 +21,7 @@ func Method(s string) PromptFunc {
}
return m
}

func mfaPromptMessage(mfaSerial string) string {
return fmt.Sprintf("Enter token for %s: ", mfaSerial)
}
9 changes: 7 additions & 2 deletions prompt/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
33 changes: 33 additions & 0 deletions prompt/ykman.go
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 2 additions & 3 deletions prompt/zenity.go
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion vault/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down

0 comments on commit 09889cc

Please sign in to comment.