Skip to content

Commit

Permalink
Merge pull request #1123 from 99designs/add-mfa-process-config
Browse files Browse the repository at this point in the history
Add `mfa_process` config
  • Loading branch information
mtibben authored Feb 3, 2023
2 parents 0f1764c + 63625aa commit 13b57fc
Show file tree
Hide file tree
Showing 11 changed files with 116 additions and 80 deletions.
27 changes: 25 additions & 2 deletions USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- [`include_profile`](#include_profile)
- [`session_tags` and `transitive_session_tags`](#session_tags-and-transitive_session_tags)
- [`source_identity`](#source_identity)
- [`mfa_process`](#mfa_process)
- [Environment variables](#environment-variables)
- [Backends](#backends)
- [Keychain](#keychain)
Expand All @@ -26,9 +27,11 @@
- [Temporary credentials limitations with STS, IAM](#temporary-credentials-limitations-with-sts-iam)
- [MFA](#mfa)
- [Gotchas with MFA config](#gotchas-with-mfa-config)
- [Single sign on with AWS IAM Identity Center (formerly AWS SSO)](#aws-single-sign-on-aws-sso)
- [Single Sign On (SSO)](#single-sign-on-sso)
- [Assuming roles with web identities](#assuming-roles-with-web-identities)
- [Using `credential_process`](#using-credential_process)
- [Invoking `aws-vault` via `credential_process`](#invoking-aws-vault-via-credential_process)
- [Invoking `credential_process` via `aws-vault`](#invoking-credential_process-via-aws-vault)
- [Using a Yubikey](#using-a-yubikey)
- [Prerequisites](#prerequisites)
- [Setup](#setup)
Expand Down Expand Up @@ -135,6 +138,26 @@ role_arn=arn:aws:iam::123456789:role/developers
source_identity=your_user_name
```

#### `mfa_process`
If you have a method to generate an MFA token, you can use it with `aws-vault` by specifying the `mfa_process` option in a profile of your `~/.aws/config` file. The value of `mfa_process` should be a command that will output the MFA token to stdout.

For example, to use `pass` to retrieve an MFA token from a password store entry, you could use the following:

```ini
[profile foo]
mfa_serial=arn:aws:iam::123456789:mfa/johnsmith
mfa_process=pass otp my_aws_mfa
```

Or another example using 1Password

```ini
[profile foo]
mfa_serial=arn:aws:iam::123456789:mfa/johnsmith
mfa_process=op item get my_aws_mfa --otp
```

WARNING: Use of this option runs against security best practices. It is recommended that you use a dedicated MFA device.

### Environment variables

Expand Down Expand Up @@ -429,7 +452,7 @@ role_arn = arn:aws:iam::33333333333:role/role2
include_profile = jon
```

## AWS Single Sign-On (AWS SSO)
## Single Sign On (SSO)

_AWS IAM Identity Center provides single sign on, and was previously known as AWS SSO._

Expand Down
4 changes: 3 additions & 1 deletion prompt/kdialog.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ func KDialogMfaPrompt(mfaSerial string) (string, error) {
}

func init() {
Methods["kdialog"] = KDialogMfaPrompt
if _, err := exec.LookPath("kdialog"); err == nil {
Methods["kdialog"] = KDialogMfaPrompt
}
}
4 changes: 3 additions & 1 deletion prompt/osascript.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,7 @@ func OSAScriptMfaPrompt(mfaSerial string) (string, error) {
}

func init() {
Methods["osascript"] = OSAScriptMfaPrompt
if _, err := exec.LookPath("osascript"); err == nil {
Methods["osascript"] = OSAScriptMfaPrompt
}
}
34 changes: 0 additions & 34 deletions prompt/passotp.go

This file was deleted.

4 changes: 3 additions & 1 deletion prompt/ykman.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,5 +46,7 @@ func YkmanMfaProvider(mfaSerial string) (string, error) {
}

func init() {
Methods["ykman"] = YkmanMfaProvider
if _, err := exec.LookPath("ykman"); err == nil {
Methods["ykman"] = YkmanMfaProvider
}
}
4 changes: 3 additions & 1 deletion prompt/zenity.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,7 @@ func ZenityMfaPrompt(mfaSerial string) (string, error) {
}

func init() {
Methods["zenity"] = ZenityMfaPrompt
if _, err := exec.LookPath("zenity"); err == nil {
Methods["zenity"] = ZenityMfaPrompt
}
}
6 changes: 3 additions & 3 deletions vault/assumeroleprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type AssumeRoleProvider struct {
Tags map[string]string
TransitiveTagKeys []string
SourceIdentity string
Mfa
*Mfa
}

// Retrieve generates a new set of temporary credentials using STS AssumeRole
Expand Down Expand Up @@ -62,8 +62,8 @@ func (p *AssumeRoleProvider) assumeRole(ctx context.Context) (*ststypes.Credenti
input.ExternalId = aws.String(p.ExternalID)
}

if p.MfaSerial != "" {
input.SerialNumber = aws.String(p.MfaSerial)
if p.GetMfaSerial() != "" {
input.SerialNumber = aws.String(p.GetMfaSerial())
input.TokenCode, err = p.GetMfaToken()
if err != nil {
return nil, err
Expand Down
7 changes: 7 additions & 0 deletions vault/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ type ProfileSection struct {
TransitiveSessionTags string `ini:"transitive_session_tags,omitempty"`
SourceIdentity string `ini:"source_identity,omitempty"`
CredentialProcess string `ini:"credential_process,omitempty"`
MfaProcess string `ini:"mfa_process,omitempty"`
}

// SSOSessionSection is a [sso-session] section of the config file
Expand Down Expand Up @@ -379,6 +380,9 @@ func (cl *ConfigLoader) populateFromConfigFile(config *Config, profileName strin
if config.CredentialProcess == "" {
config.CredentialProcess = psection.CredentialProcess
}
if config.MfaProcess == "" {
config.MfaProcess = psection.MfaProcess
}
if sessionTags := psection.SessionTags; sessionTags != "" && config.SessionTags == nil {
err := config.SetSessionTags(sessionTags)
if err != nil {
Expand Down Expand Up @@ -559,6 +563,9 @@ type Config struct {
MfaToken string
MfaPromptMethod string

// MfaProcess specifies external command to run to get an MFA token
MfaProcess string

// AssumeRole config
RoleARN string
RoleSessionName string
Expand Down
64 changes: 64 additions & 0 deletions vault/mfa.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package vault

import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"strings"

"github.com/99designs/aws-vault/v7/prompt"
"github.com/aws/aws-sdk-go-v2/aws"
)

// Mfa contains options for an MFA device
type Mfa struct {
mfaSerial string
mfaPromptFunc prompt.Func
}

// GetMfaToken returns the MFA token
func (m *Mfa) GetMfaToken() (*string, error) {
if m.mfaPromptFunc != nil {
token, err := m.mfaPromptFunc(m.mfaSerial)
return aws.String(token), err
}

return nil, errors.New("No prompt found")
}

// GetMfaSerial returns the MFA serial
func (m *Mfa) GetMfaSerial() string {
return m.mfaSerial
}

func NewMfa(config *Config) *Mfa {
m := Mfa{
mfaSerial: config.MfaSerial,
}
if config.MfaToken != "" {
m.mfaPromptFunc = func(_ string) (string, error) { return config.MfaToken, nil }
} else if config.MfaProcess != "" {
m.mfaPromptFunc = func(_ string) (string, error) {
log.Println("Executing mfa_process")
return ProcessMfaProvider(config.MfaProcess)
}
} else {
m.mfaPromptFunc = prompt.Method(config.MfaPromptMethod)
}

return &m
}

func ProcessMfaProvider(processCmd string) (string, error) {
cmd := exec.Command("/bin/sh", "-c", processCmd)
cmd.Stderr = os.Stderr

out, err := cmd.Output()
if err != nil {
return "", fmt.Errorf("process provider: %w", err)
}

return strings.TrimSpace(string(out)), nil
}
6 changes: 3 additions & 3 deletions vault/sessiontokenprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (
type SessionTokenProvider struct {
StsClient *sts.Client
Duration time.Duration
Mfa
*Mfa
}

// Retrieve generates a new set of temporary credentials using STS GetSessionToken
Expand All @@ -41,8 +41,8 @@ func (p *SessionTokenProvider) GetSessionToken(ctx context.Context) (*ststypes.C
DurationSeconds: aws.Int32(int32(p.Duration.Seconds())),
}

if p.MfaSerial != "" {
input.SerialNumber = aws.String(p.MfaSerial)
if p.GetMfaSerial() != "" {
input.SerialNumber = aws.String(p.GetMfaSerial())
input.TokenCode, err = p.GetMfaToken()
if err != nil {
return nil, err
Expand Down
36 changes: 2 additions & 34 deletions vault/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ package vault

import (
"context"
"errors"
"fmt"
"log"
"os"
"time"

"github.com/99designs/aws-vault/v7/prompt"
"github.com/99designs/keyring"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/sso"
Expand Down Expand Up @@ -45,28 +43,6 @@ func FormatKeyForDisplay(k string) string {
return fmt.Sprintf("****************%s", k[len(k)-4:])
}

// Mfa contains options for an MFA device
type Mfa struct {
MfaToken string
MfaPromptMethod string
MfaSerial string
}

// GetMfaToken returns the MFA token
func (m *Mfa) GetMfaToken() (*string, error) {
if m.MfaToken != "" {
return aws.String(m.MfaToken), nil
}

if m.MfaPromptMethod != "" {
promptFunc := prompt.Method(m.MfaPromptMethod)
token, err := promptFunc(m.MfaSerial)
return aws.String(token), err
}

return nil, errors.New("No prompt found")
}

// NewMasterCredentialsProvider creates a provider for the master credentials
func NewMasterCredentialsProvider(k *CredentialKeyring, credentialsName string) *KeyringProvider {
return &KeyringProvider{k, credentialsName}
Expand All @@ -78,11 +54,7 @@ func NewSessionTokenProvider(credsProvider aws.CredentialsProvider, k keyring.Ke
sessionTokenProvider := &SessionTokenProvider{
StsClient: sts.NewFromConfig(cfg),
Duration: config.GetSessionTokenDuration(),
Mfa: Mfa{
MfaToken: config.MfaToken,
MfaPromptMethod: config.MfaPromptMethod,
MfaSerial: config.MfaSerial,
},
Mfa: NewMfa(config),
}

if UseSessionCache {
Expand Down Expand Up @@ -114,11 +86,7 @@ func NewAssumeRoleProvider(credsProvider aws.CredentialsProvider, k keyring.Keyr
Tags: config.SessionTags,
TransitiveTagKeys: config.TransitiveSessionTags,
SourceIdentity: config.SourceIdentity,
Mfa: Mfa{
MfaSerial: config.MfaSerial,
MfaToken: config.MfaToken,
MfaPromptMethod: config.MfaPromptMethod,
},
Mfa: NewMfa(config),
}

if UseSessionCache && config.MfaSerial != "" {
Expand Down

0 comments on commit 13b57fc

Please sign in to comment.