diff --git a/cli/exec.go b/cli/exec.go index 10e7d4c75..93b64388c 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -15,7 +15,6 @@ import ( "github.com/99designs/aws-vault/server" "github.com/99designs/aws-vault/vault" "github.com/99designs/keyring" - "github.com/aws/aws-sdk-go/aws/credentials" "gopkg.in/alecthomas/kingpin.v2" ) @@ -50,7 +49,7 @@ func ConfigureExecCommand(app *kingpin.Application) { Short('d'). DurationVar(&input.SessionDuration) - cmd.Flag("no-session", "Use master credentials, no session created"). + cmd.Flag("no-session", "Don't create a session with GetSessionToken"). Short('n'). BoolVar(&input.NoSession) @@ -93,28 +92,20 @@ func ExecCommand(input ExecCommandInput) error { return fmt.Errorf("aws-vault sessions should be nested with care, unset $AWS_VAULT to force") } - var setEnv = true - - if input.NoSession && input.StartServer { - return fmt.Errorf("Can't start a credential server without a session") - } + vault.UseSession = !input.NoSession + setEnv := true configLoader.BaseConfig = input.Config - configLoader.ProfileNameForEnv = input.ProfileName + configLoader.ActiveProfile = input.ProfileName config, err := configLoader.LoadFromProfile(input.ProfileName) if err != nil { return err } credKeyring := &vault.CredentialKeyring{Keyring: input.Keyring} - var creds *credentials.Credentials - if input.NoSession { - creds = vault.NewMasterCredentials(credKeyring, config.ProfileName) - } else { - creds, err = vault.NewTempCredentials(input.ProfileName, credKeyring, configLoader) - if err != nil { - return fmt.Errorf("Error getting temporary credentials: %w", err) - } + creds, err := vault.NewTempCredentials(config, credKeyring) + if err != nil { + return fmt.Errorf("Error getting temporary credentials: %w", err) } val, err := creds.Get() @@ -125,9 +116,8 @@ func ExecCommand(input ExecCommandInput) error { if input.StartServer { if err := server.StartCredentialsServer(creds); err != nil { return fmt.Errorf("Failed to start credential server: %w", err) - } else { - setEnv = false } + setEnv = false } if input.CredentialHelper { diff --git a/cli/login.go b/cli/login.go index 596ece20b..afae92592 100644 --- a/cli/login.go +++ b/cli/login.go @@ -64,7 +64,7 @@ func ConfigureLoginCommand(app *kingpin.Application) { func LoginCommand(input LoginCommandInput) error { configLoader.BaseConfig = input.Config - configLoader.ProfileNameForEnv = input.ProfileName + configLoader.ActiveProfile = input.ProfileName config, err := configLoader.LoadFromProfile(input.ProfileName) if err != nil { return err @@ -74,9 +74,9 @@ func LoginCommand(input LoginCommandInput) error { // if AssumeRole isn't used, GetFederationToken has to be used for IAM credentials if config.RoleARN == "" { - creds, err = vault.NewFederationTokenCredentials(input.ProfileName, input.Keyring, configLoader) + creds, err = vault.NewFederationTokenCredentials(input.ProfileName, input.Keyring, config) } else { - creds, err = vault.NewTempCredentials(input.ProfileName, input.Keyring, configLoader) + creds, err = vault.NewTempCredentials(config, input.Keyring) } if err != nil { return err diff --git a/cli/rotate.go b/cli/rotate.go index db799da2d..e59ab5e2d 100644 --- a/cli/rotate.go +++ b/cli/rotate.go @@ -24,7 +24,7 @@ func ConfigureRotateCommand(app *kingpin.Application) { cmd := app.Command("rotate", "Rotates credentials") - cmd.Flag("no-session", "Use master credentials, no session created"). + cmd.Flag("no-session", "Use master credentials, no session or role used"). Short('n'). BoolVar(&input.NoSession) @@ -42,16 +42,17 @@ func ConfigureRotateCommand(app *kingpin.Application) { } func RotateCommand(input RotateCommandInput) error { + vault.UseSession = !input.NoSession vault.UseSessionCache = false configLoader.BaseConfig = input.Config - configLoader.ProfileNameForEnv = input.ProfileName + configLoader.ActiveProfile = input.ProfileName config, err := configLoader.LoadFromProfile(input.ProfileName) if err != nil { return err } - masterCredentialsName, err := vault.MasterCredentialsFor(input.ProfileName, input.Keyring, configLoader) + masterCredentialsName, err := vault.MasterCredentialsFor(input.ProfileName, input.Keyring, config) if err != nil { return err } @@ -77,7 +78,7 @@ func RotateCommand(input RotateCommandInput) error { if input.NoSession { sessCreds = vault.NewMasterCredentials(input.Keyring, config.ProfileName) } else { - sessCreds, err = vault.NewTempCredentials(input.ProfileName, input.Keyring, configLoader) + sessCreds, err = vault.NewTempCredentials(config, input.Keyring) if err != nil { return fmt.Errorf("Error getting temporary credentials: %w", err) } @@ -165,7 +166,7 @@ func retry(maxTime time.Duration, sleep time.Duration, f func() error) (err erro } } -func getUsernameIfAssumingRole(sess *session.Session, config vault.Config) (*string, error) { +func getUsernameIfAssumingRole(sess *session.Session, config *vault.Config) (*string, error) { if config.RoleARN != "" { n, err := vault.GetUsernameFromSession(sess) if err != nil { @@ -185,8 +186,8 @@ func getProfilesInChain(profileName string, configLoader *vault.ConfigLoader) (p return profileNames, err } - if config.SourceProfile != "" { - newProfileNames, err := getProfilesInChain(config.SourceProfile, configLoader) + if config.SourceProfile != nil { + newProfileNames, err := getProfilesInChain(config.SourceProfileName, configLoader) if err != nil { return profileNames, err } diff --git a/vault/config.go b/vault/config.go index 8945c31d9..03634c5df 100644 --- a/vault/config.go +++ b/vault/config.go @@ -14,21 +14,6 @@ import ( ) const ( - // MinGetSessionTokenDuration is the AWS minumum duration for GetSessionToken - MinGetSessionTokenDuration = time.Minute * 15 - // MaxGetSessionTokenDuration is the AWS maximum duration for GetSessionToken - MaxGetSessionTokenDuration = time.Hour * 36 - - // MinAssumeRoleDuration is the AWS minumum duration for AssumeRole - MinAssumeRoleDuration = time.Minute * 15 - // MaxAssumeRoleDuration is the AWS maximum duration for AssumeRole - MaxAssumeRoleDuration = time.Hour * 12 - - // MinGetFederationTokenDuration is the AWS minumum duration for GetFederationToke - MinGetFederationTokenDuration = time.Minute * 15 - // MaxGetFederationTokenDuration is the AWS maximum duration for GetFederationToke - MaxGetFederationTokenDuration = time.Hour * 36 - // DefaultSessionDuration is the default duration for GetSessionToken or AssumeRole sessions DefaultSessionDuration = time.Hour * 1 @@ -240,12 +225,9 @@ func (c *ConfigFile) ProfileNames() []string { // ConfigLoader loads config from configfile and environment variables type ConfigLoader struct { - BaseConfig Config - File *ConfigFile - - // profile env should be applied to - ProfileNameForEnv string - + BaseConfig Config + File *ConfigFile + ActiveProfile string visitedProfiles []string } @@ -307,8 +289,8 @@ func (cl *ConfigLoader) populateFromConfigFile(config *Config, profileName strin if config.AssumeRoleDuration == 0 { config.AssumeRoleDuration = time.Duration(psection.DurationSeconds) * time.Second } - if config.SourceProfile == "" { - config.SourceProfile = psection.SourceProfile + if config.SourceProfileName == "" { + config.SourceProfileName = psection.SourceProfile } if psection.ParentProfile != "" { @@ -372,7 +354,7 @@ func (cl *ConfigLoader) populateFromEnv(profile *Config) { } // AWS_ROLE_ARN and AWS_ROLE_SESSION_NAME only apply to the target profile - if profile.ProfileName == cl.ProfileNameForEnv { + if profile.ProfileName == cl.ActiveProfile { if roleARN := os.Getenv("AWS_ROLE_ARN"); roleARN != "" && profile.RoleARN == "" { log.Printf("Using role_arn %q from AWS_ROLE_ARN", roleARN) profile.RoleARN = roleARN @@ -385,8 +367,20 @@ func (cl *ConfigLoader) populateFromEnv(profile *Config) { } } +func (cl *ConfigLoader) hydrateSourceConfig(config *Config) error { + if config.SourceProfileName != "" { + sc, err := cl.LoadFromProfile(config.SourceProfileName) + if err != nil { + return err + } + sc.ChainedFromProfile = config + config.SourceProfile = sc + } + return nil +} + // LoadFromProfile loads the profile from the config file and environment variables into config -func (cl *ConfigLoader) LoadFromProfile(profileName string) (Config, error) { +func (cl *ConfigLoader) LoadFromProfile(profileName string) (*Config, error) { config := cl.BaseConfig config.ProfileName = profileName cl.populateFromEnv(&config) @@ -394,17 +388,17 @@ func (cl *ConfigLoader) LoadFromProfile(profileName string) (Config, error) { cl.resetLoopDetection() err := cl.populateFromConfigFile(&config, profileName) if err != nil { - return Config{}, err + return nil, err } cl.populateFromDefaults(&config) - err = config.Validate() + err = cl.hydrateSourceConfig(&config) if err != nil { - return Config{}, err + return nil, err } - return config, nil + return &config, nil } // Config is a collection of configuration options for creating temporary credentials @@ -413,7 +407,13 @@ type Config struct { ProfileName string // SourceProfile is the profile where credentials come from - SourceProfile string + SourceProfileName string + + // SourceProfile is the profile where credentials come from + SourceProfile *Config + + // ChainedFromProfile is the profile that used this profile as it's source profile + ChainedFromProfile *Config // Region is the AWS region Region string @@ -441,26 +441,20 @@ type Config struct { GetFederationTokenDuration time.Duration } -// Validate checks that the Config is valid -func (cl *Config) Validate() error { - if cl.GetSessionTokenDuration < MinGetSessionTokenDuration { - return fmt.Errorf("Minimum GetSessionToken duration is %s", MinGetSessionTokenDuration) - } - if cl.GetSessionTokenDuration > MaxGetSessionTokenDuration { - return fmt.Errorf("Maximum GetSessionToken duration is %s", MaxGetSessionTokenDuration) - } - if cl.AssumeRoleDuration < MinAssumeRoleDuration { - return fmt.Errorf("Minimum AssumeRole duration is %s", MinAssumeRoleDuration) - } - if cl.AssumeRoleDuration > MaxAssumeRoleDuration { - return fmt.Errorf("Maximum AssumeRole duration is %s", MaxAssumeRoleDuration) - } - if cl.GetFederationTokenDuration < MinGetFederationTokenDuration { - return fmt.Errorf("Minimum GetFederationToken duration is %s", MinAssumeRoleDuration) - } - if cl.GetFederationTokenDuration > MaxGetFederationTokenDuration { - return fmt.Errorf("Maximum GetFederationToken duration is %s", MaxAssumeRoleDuration) - } +func (c *Config) IsChained() bool { + return c.ChainedFromProfile != nil +} - return nil +func (c *Config) HasSourceProfile() bool { + return c.SourceProfile != nil +} + +func (c *Config) HasMfaSerial() bool { + return c.MfaSerial != "" +} + +func (c *Config) MfaAlreadyUsedInSourceProfile() bool { + return c.HasSourceProfile() && + c.MfaSerial != "" && + c.SourceProfile.MfaSerial == c.MfaSerial } diff --git a/vault/vault.go b/vault/vault.go index 4f0fe2023..4465305fb 100644 --- a/vault/vault.go +++ b/vault/vault.go @@ -15,6 +15,7 @@ import ( const defaultExpirationWindow = 5 * time.Minute +var UseSession = true var UseSessionCache = true func NewSession(creds *credentials.Credentials, region string) (*session.Session, error) { @@ -56,7 +57,7 @@ func NewMasterCredentials(k *CredentialKeyring, credentialsName string) *credent return credentials.NewCredentials(NewMasterCredentialsProvider(k, credentialsName)) } -func NewSessionTokenProvider(creds *credentials.Credentials, k *CredentialKeyring, config Config) (credentials.Provider, error) { +func NewSessionTokenProvider(creds *credentials.Credentials, k *CredentialKeyring, config *Config) (credentials.Provider, error) { sess, err := NewSession(creds, config.Region) if err != nil { return nil, err @@ -86,12 +87,17 @@ func NewSessionTokenProvider(creds *credentials.Credentials, k *CredentialKeyrin } // NewAssumeRoleProvider returns a provider that generates credentials using AssumeRole -func NewAssumeRoleProvider(creds *credentials.Credentials, config Config) (*AssumeRoleProvider, error) { +func NewAssumeRoleProvider(creds *credentials.Credentials, config *Config, noMfa bool) (*AssumeRoleProvider, error) { sess, err := NewSession(creds, config.Region) if err != nil { return nil, err } + mfa := config.MfaSerial + if noMfa { + mfa = "" + } + return &AssumeRoleProvider{ StsClient: sts.New(sess), RoleARN: config.RoleARN, @@ -100,106 +106,87 @@ func NewAssumeRoleProvider(creds *credentials.Credentials, config Config) (*Assu Duration: config.AssumeRoleDuration, ExpiryWindow: defaultExpirationWindow, Mfa: Mfa{ - MfaSerial: config.MfaSerial, + MfaSerial: mfa, MfaToken: config.MfaToken, MfaPromptMethod: config.MfaPromptMethod, }, }, nil } -type CredentialLoader struct { - Keyring *CredentialKeyring - ConfigLoader *ConfigLoader -} - -// Provider creates a credential provider for the given config -func (c *CredentialLoader) Provider(profileName string) (credentials.Provider, error) { - return c.ProviderWithChainedMfa(profileName, false, "") -} - -var errChainedMfaNotMatched = errors.New("Chained MFA serial didn't match") - // Provider creates a credential provider for the given config. To chain the MFA serial with a source credential, pass the MFA serial in chainMfaSerial -func (c *CredentialLoader) ProviderWithChainedMfa(profileName string, isChained bool, chainedMfaSerial string) (credentials.Provider, error) { - config, err := c.ConfigLoader.LoadFromProfile(profileName) - if err != nil { - return nil, err - } - - if chainedMfaSerial != "" && config.MfaSerial != "" && chainedMfaSerial != config.MfaSerial { - return nil, errChainedMfaNotMatched - } - - var skipMfaBecauseSourceProfileHasItCovered = false +func NewTempCredentialsProvider(config *Config, keyring *CredentialKeyring) (credentials.Provider, error) { var sourceCredProvider credentials.Provider - hasMasterCredentials, err := c.Keyring.Has(config.ProfileName) + hasStoredCredentials, err := keyring.Has(config.ProfileName) if err != nil { return nil, err } - if hasMasterCredentials { - if config.SourceProfile != "" { - log.Printf("profile %s: using stored credentials (ignoring source_profile)", profileName) - } else { - log.Printf("profile %s: using stored credentials", profileName) - } - sourceCredProvider = NewMasterCredentialsProvider(c.Keyring, config.ProfileName) - } else if config.SourceProfile != "" { - sourceCredProvider, err = c.ProviderWithChainedMfa(config.SourceProfile, true, config.MfaSerial) - if err == nil && config.MfaSerial != "" { - skipMfaBecauseSourceProfileHasItCovered = true - config.MfaSerial = "" - } - if err == errChainedMfaNotMatched { - sourceCredProvider, err = c.ProviderWithChainedMfa(config.SourceProfile, true, "") - } + if hasStoredCredentials { + log.Printf("profile %s: using stored credentials %s", config.ProfileName, logSourceDetails(config)) + sourceCredProvider = NewMasterCredentialsProvider(keyring, config.ProfileName) + } else if config.HasSourceProfile() { + sourceCredProvider, err = NewTempCredentialsProvider(config.SourceProfile, keyring) if err != nil { return nil, err } } else { - return nil, fmt.Errorf("profile %s: credentials missing", profileName) + return nil, fmt.Errorf("profile %s: credentials missing", config.ProfileName) } - if config.RoleARN == "" && isChained && chainedMfaSerial == "" { - return sourceCredProvider, nil - } + mfaChained := config.MfaAlreadyUsedInSourceProfile() + sourceCreds := credentials.NewCredentials(sourceCredProvider) if config.RoleARN == "" { - if isChained { + if !UseSession { + // log.Printf("profile %s: GetSessionToken disabled", config.ProfileName) + config.MfaSerial = "" + return sourceCredProvider, nil + } + + if config.IsChained() { + if !config.ChainedFromProfile.HasMfaSerial() { + log.Printf("profile %s: not using GetSessionToken because profile '%s' has no MFA serial defined", config.ProfileName, config.ChainedFromProfile.ProfileName) + return sourceCredProvider, nil + } + + if config.ChainedFromProfile.MfaSerial != config.MfaSerial { + log.Printf("profile %s: not using GetSessionToken because MFA serial doesn't match with profile '%s'", config.ProfileName, config.ChainedFromProfile.ProfileName) + return sourceCredProvider, nil + } + config.GetSessionTokenDuration = config.ChainedGetSessionTokenDuration } - log.Printf("profile %s: using GetSessionToken %s", profileName, mfaDetails(skipMfaBecauseSourceProfileHasItCovered, config)) - return NewSessionTokenProvider(credentials.NewCredentials(sourceCredProvider), c.Keyring, config) + log.Printf("profile %s: using GetSessionToken %s", config.ProfileName, mfaDetails(false, config)) + return NewSessionTokenProvider(sourceCreds, keyring, config) + + } else { + log.Printf("profile %s: using AssumeRole %s", config.ProfileName, mfaDetails(mfaChained, config)) + return NewAssumeRoleProvider(sourceCreds, config, mfaChained) } +} - log.Printf("profile %s: using AssumeRole %s", profileName, mfaDetails(skipMfaBecauseSourceProfileHasItCovered, config)) - return NewAssumeRoleProvider(credentials.NewCredentials(sourceCredProvider), config) +func logSourceDetails(config *Config) string { + if config.SourceProfile != nil { + return "(ignoring source_profile)" + } + return "" } -func mfaDetails(skipMfaBecauseSourceProfileHasItCovered bool, config Config) string { - if skipMfaBecauseSourceProfileHasItCovered { +func mfaDetails(mfaChained bool, config *Config) string { + if mfaChained { return "(chained MFA)" } - if config.MfaSerial != "" { + if config.HasMfaSerial() { return "(using MFA)" } return "" } -func NewTempCredentialsProvider(profileName string, k *CredentialKeyring, configLoader *ConfigLoader) (credentials.Provider, error) { - cl := CredentialLoader{ - Keyring: k, - ConfigLoader: configLoader, - } - - return cl.Provider(profileName) -} - // NewTempCredentials returns credentials for the given config -func NewTempCredentials(profileName string, k *CredentialKeyring, cl *ConfigLoader) (*credentials.Credentials, error) { - provider, err := NewTempCredentialsProvider(profileName, k, cl) +func NewTempCredentials(config *Config, k *CredentialKeyring) (*credentials.Credentials, error) { + provider, err := NewTempCredentialsProvider(config, k) if err != nil { return nil, err } @@ -207,13 +194,8 @@ func NewTempCredentials(profileName string, k *CredentialKeyring, cl *ConfigLoad return credentials.NewCredentials(provider), nil } -func NewFederationTokenCredentials(profileName string, k *CredentialKeyring, configLoader *ConfigLoader) (*credentials.Credentials, error) { - config, err := configLoader.LoadFromProfile(profileName) - if err != nil { - return nil, err - } - - credentialsName, err := MasterCredentialsFor(profileName, k, configLoader) +func NewFederationTokenCredentials(profileName string, k *CredentialKeyring, config *Config) (*credentials.Credentials, error) { + credentialsName, err := MasterCredentialsFor(profileName, k, config) if err != nil { return nil, err } @@ -236,7 +218,7 @@ func NewFederationTokenCredentials(profileName string, k *CredentialKeyring, con }), nil } -func MasterCredentialsFor(profileName string, keyring *CredentialKeyring, configLoader *ConfigLoader) (string, error) { +func MasterCredentialsFor(profileName string, keyring *CredentialKeyring, config *Config) (string, error) { hasMasterCreds, err := keyring.Has(profileName) if err != nil { return "", err @@ -246,10 +228,5 @@ func MasterCredentialsFor(profileName string, keyring *CredentialKeyring, config return profileName, nil } - config, err := configLoader.LoadFromProfile(profileName) - if err != nil { - return "", err - } - - return MasterCredentialsFor(config.SourceProfile, keyring, configLoader) + return MasterCredentialsFor(config.SourceProfileName, keyring, config) }