diff --git a/USAGE.md b/USAGE.md index 7f4cd4972..829529540 100644 --- a/USAGE.md +++ b/USAGE.md @@ -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) @@ -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) @@ -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 @@ -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._ diff --git a/prompt/kdialog.go b/prompt/kdialog.go index d6b953d89..576b43b39 100644 --- a/prompt/kdialog.go +++ b/prompt/kdialog.go @@ -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 + } } diff --git a/prompt/osascript.go b/prompt/osascript.go index 1979c2bef..559fa30e5 100644 --- a/prompt/osascript.go +++ b/prompt/osascript.go @@ -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 + } } diff --git a/prompt/passotp.go b/prompt/passotp.go deleted file mode 100644 index f99ba414c..000000000 --- a/prompt/passotp.go +++ /dev/null @@ -1,34 +0,0 @@ -package prompt - -import ( - "fmt" - "log" - "os" - "os/exec" - "strings" -) - -// PassOTPProvider uses the pass otp extension to generate a OATH-TOTP token -// To set up pass otp, first create a pass otp credential with a name of your -// mfaSerial, or set PASS_OATH_CREDENTIAL_NAME. -func PassMfaProvider(mfaSerial string) (string, error) { - passOathCredName := os.Getenv("PASS_OATH_CREDENTIAL_NAME") - if passOathCredName == "" { - passOathCredName = mfaSerial - } - - log.Printf("Fetching MFA code using `pass otp %s`", passOathCredName) - cmd := exec.Command("pass", "otp", passOathCredName) - cmd.Stderr = os.Stderr - - out, err := cmd.Output() - if err != nil { - return "", fmt.Errorf("pass: %w", err) - } - - return strings.TrimSpace(string(out)), nil -} - -func init() { - Methods["pass"] = PassMfaProvider -} diff --git a/prompt/ykman.go b/prompt/ykman.go index cbe911400..5c32ac50b 100644 --- a/prompt/ykman.go +++ b/prompt/ykman.go @@ -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 + } } diff --git a/prompt/zenity.go b/prompt/zenity.go index d63e3ccbd..8b3b234a9 100644 --- a/prompt/zenity.go +++ b/prompt/zenity.go @@ -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 + } } diff --git a/vault/assumeroleprovider.go b/vault/assumeroleprovider.go index d482fb33d..2d5042fe5 100644 --- a/vault/assumeroleprovider.go +++ b/vault/assumeroleprovider.go @@ -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 @@ -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 diff --git a/vault/config.go b/vault/config.go index 2b684d81f..890e5c1f3 100644 --- a/vault/config.go +++ b/vault/config.go @@ -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 @@ -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 { @@ -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 diff --git a/vault/mfa.go b/vault/mfa.go new file mode 100644 index 000000000..c5b897edf --- /dev/null +++ b/vault/mfa.go @@ -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 +} diff --git a/vault/sessiontokenprovider.go b/vault/sessiontokenprovider.go index e19634ccf..e17b79076 100644 --- a/vault/sessiontokenprovider.go +++ b/vault/sessiontokenprovider.go @@ -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 @@ -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 diff --git a/vault/vault.go b/vault/vault.go index 692a3e617..e7b2b58b5 100644 --- a/vault/vault.go +++ b/vault/vault.go @@ -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" @@ -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} @@ -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 { @@ -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 != "" {