Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mfa_process config #1123

Merged
merged 4 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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