From d80574dc7501af2c78f4b8f1f8666ab8b4c3c4d1 Mon Sep 17 00:00:00 2001 From: Jo Giroux Date: Fri, 4 Dec 2020 07:55:47 -0500 Subject: [PATCH] Avoid asking MFA twice (#166) * Avoid asking MFA twice - Prevent debug trace to display the secrets - Find the maximum session duration associated with a role - Automatically extend session time if badly configured - Hide the MFA (while is not so secret, act as AWS cli does) - Exit if there is an error when initializing AWS credentials * Update dependencies * Change following review comments --- config.go | 113 +++++++++++++++++++++++++++++++++++-------------- config_test.go | 2 +- docker.go | 6 ++- go.mod | 3 +- go.sum | 4 +- 5 files changed, 92 insertions(+), 36 deletions(-) diff --git a/config.go b/config.go index 381e558f..9a946bb9 100644 --- a/config.go +++ b/config.go @@ -21,14 +21,17 @@ import ( "time" "github.com/aws/aws-sdk-go/aws" - "github.com/aws/aws-sdk-go/aws/credentials/stscreds" "github.com/aws/aws-sdk-go/aws/session" + awsSession "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" "github.com/aws/aws-sdk-go/service/ssm" + "github.com/aws/aws-sdk-go/service/sts" "github.com/blang/semver" "github.com/coveooss/gotemplate/v3/collections" "github.com/fatih/color" "github.com/hashicorp/go-getter" "github.com/inconshreveable/go-update" + "golang.org/x/crypto/ssh/terminal" yaml "gopkg.in/yaml.v2" ) @@ -84,6 +87,16 @@ type TGFConfigBuild struct { source string } +var ( + cachedAWSConfigExistCheck *bool + cachedSession *session.Session +) + +func resetCache() { + cachedAWSConfigExistCheck = nil + cachedSession = nil +} + func (cb TGFConfigBuild) hash() string { h := md5.New() io.WriteString(h, filepath.Base(filepath.Dir(cb.source))) @@ -148,38 +161,79 @@ func (config TGFConfig) String() string { return string(bytes) } -func (config *TGFConfig) getAwsSession() (*session.Session, error) { - return session.NewSessionWithOptions(session.Options{ - Profile: config.tgf.AwsProfile, - SharedConfigState: session.SharedConfigEnable, - AssumeRoleTokenProvider: stscreds.StdinTokenProvider, - }) -} - -// InitAWS tries to open an AWS session and init AWS environment variable on success -func (config *TGFConfig) InitAWS() error { - if config.tgf.AwsProfile == "" && os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_PROFILE") != "" { - log.Warning("You set both AWS_ACCESS_KEY_ID and AWS_PROFILE, AWS_PROFILE will be ignored") +func (config *TGFConfig) getAwsSession(duration int64) (*session.Session, error) { + if cachedSession != nil { + return cachedSession, nil } - session, err := config.getAwsSession() - if err != nil { - return err + options := awsSession.Options{ + Profile: config.tgf.AwsProfile, + SharedConfigState: awsSession.SharedConfigEnable, + AssumeRoleTokenProvider: func() (string, error) { + fmt.Fprintf(os.Stderr, "Assume Role MFA token code: ") + v, err := terminal.ReadPassword(int(os.Stdin.Fd())) + fmt.Fprintln(os.Stderr) + return string(v), err + }, + } + if duration > 0 { + options.AssumeRoleDuration = time.Duration(duration) * time.Second + } + + session, err := awsSession.NewSessionWithOptions(options) + + if err == nil { + // We must get the current credentials before verifying the expiration + _, err = session.Config.Credentials.Get() } - creds, err := session.Config.Credentials.Get() if err != nil { - return err + return session, err } + expiration, _ := session.Config.Credentials.ExpiresAt() if duration := time.Until(expiration).Round(time.Minute); duration > 0 && duration < 55*time.Minute { + // The duration is less that 1 hour, we try to extend the session + + // We try to find the maximum role session duration allowed (but not complain if not successful) + maxDuration := int64(3600) + roleRegex := regexp.MustCompile(".*:assumed-role/(.*)/.*") + if identity, err := sts.New(session).GetCallerIdentity(&sts.GetCallerIdentityInput{}); err == nil { + if matches := roleRegex.FindStringSubmatch(*identity.Arn); len(matches) > 0 { + if role, err := iam.New(session).GetRole(&iam.GetRoleInput{RoleName: &matches[1]}); err == nil { + maxDuration = *role.Role.MaxSessionDuration + } + } + } var profile string if profile = config.tgf.AwsProfile; profile == "" { if profile = os.Getenv("AWS_PROFILE"); profile == "" { profile = "default" } } - log.Warningf("Your AWS configuration is set to expire your session in %v", duration) + log.Warningf("Your AWS configuration is set to expire your session in %v (automatically extended to %v)", + duration, + time.Duration(maxDuration)*time.Second) log.Warningf(color.WhiteString("You should consider defining %s in your AWS config profile %s"), - color.HiBlueString("duration_seconds = 14400"), color.HiBlueString(profile)) + color.HiBlueString("duration_seconds = %d", maxDuration), color.HiBlueString(profile)) + session, err = config.getAwsSession(maxDuration) + } + if err == nil { + cachedSession = session + } + return session, err +} + +// InitAWS tries to open an AWS session and init AWS environment variable on success +func (config *TGFConfig) InitAWS() error { + if config.tgf.AwsProfile == "" && os.Getenv("AWS_ACCESS_KEY_ID") != "" && os.Getenv("AWS_PROFILE") != "" { + log.Warning("You set both AWS_ACCESS_KEY_ID and AWS_PROFILE, AWS_PROFILE will be ignored") + } + session, err := config.getAwsSession(0) + if err != nil { + return err + } + creds, err := session.Config.Credentials.Get() + if err != nil { + return err } os.Unsetenv("AWS_PROFILE") os.Unsetenv("AWS_DEFAULT_PROFILE") @@ -215,14 +269,13 @@ func (config *TGFConfig) setDefaultValues() { // Fetch SSM configs if config.awsConfigExist() { if err := config.InitAWS(); err != nil { - log.Errorf("Unable to authentify to AWS: %v\nPararameter store is ignored\n", err) - } else { - if app.ConfigLocation == "" { - values := config.readSSMParameterStore(app.PsPath) - app.ConfigLocation = values[remoteConfigLocationParameter] - if app.ConfigFiles == "" { - app.ConfigFiles = values[remoteConfigPathsParameter] - } + log.Fatal(err) + } + if app.ConfigLocation == "" { + values := config.readSSMParameterStore(app.PsPath) + app.ConfigLocation = values[remoteConfigLocationParameter] + if app.ConfigFiles == "" { + app.ConfigFiles = values[remoteConfigPathsParameter] } } } @@ -412,7 +465,7 @@ func (config *TGFConfig) ParseAliases() { func (config *TGFConfig) readSSMParameterStore(ssmParameterFolder string) map[string]string { values := make(map[string]string) - session, err := config.getAwsSession() + session, err := config.getAwsSession(0) log.Debugf("Reading configuration from SSM %s in %s", ssmParameterFolder, *session.Config.Region) if err != nil { log.Warningf("Caught an error while creating an AWS session: %v", err) @@ -543,8 +596,6 @@ func (config TGFConfig) awsConfigExist() (result bool) { return awsFolderExists } -var cachedAWSConfigExistCheck *bool - // Return the list of configuration files found from the current working directory up to the root folder func (config TGFConfig) findConfigFiles(folder string) (result []string) { app := config.tgf diff --git a/config_test.go b/config_test.go index e7356ddd..03a64d87 100644 --- a/config_test.go +++ b/config_test.go @@ -63,7 +63,7 @@ func TestCheckVersionRange(t *testing.T) { func TestSetConfigDefaultValues(t *testing.T) { // We must reset the cached AWS config check since it could have been modified by another test - cachedAWSConfigExistCheck = nil + resetCache() tempDir, _ := filepath.EvalSymlinks(must(ioutil.TempDir("", "TestGetConfig")).(string)) currentDir, _ := os.Getwd() assert.NoError(t, os.Chdir(tempDir)) diff --git a/docker.go b/docker.go index 9463bb07..855a3cf7 100644 --- a/docker.go +++ b/docker.go @@ -163,7 +163,11 @@ func (docker *dockerConfig) call() int { if log.GetLevel() >= logrus.DebugLevel { exportedVariables := make(collections.StringArray, len(config.Environment)) for i, key := range collections.AsDictionary(config.Environment).KeysAsString() { - exportedVariables[i] = String(fmt.Sprintf("%s = %s", key, config.Environment[key.String()])) + if key == "AWS_SECRET_ACCESS_KEY" || key == "AWS_SESSION_TOKEN" { + exportedVariables[i] = String(fmt.Sprintf("%s = ******", key)) + } else { + exportedVariables[i] = String(fmt.Sprintf("%s = %s", key, config.Environment[key.String()])) + } } log.Debugf("Environment variables\n%s", color.HiBlackString(exportedVariables.Join("\n").IndentN(4).Str())) } diff --git a/go.mod b/go.mod index b2b7dd69..fe3346c2 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/Microsoft/go-winio v0.4.15 // indirect - github.com/aws/aws-sdk-go v1.36.0 + github.com/aws/aws-sdk-go v1.36.1 github.com/blang/semver v3.5.1+incompatible github.com/coveooss/gotemplate/v3 v3.6.0 github.com/coveooss/multilogger v0.5.2 @@ -19,5 +19,6 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/sirupsen/logrus v1.7.0 github.com/stretchr/testify v1.6.1 + golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 3ad7d05c..51395179 100644 --- a/go.sum +++ b/go.sum @@ -38,8 +38,8 @@ github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYU github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= -github.com/aws/aws-sdk-go v1.36.0 h1:CscTrS+szX5iu34zk2bZrChnGO/GMtUYgMK1Xzs2hYo= -github.com/aws/aws-sdk-go v1.36.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go v1.36.1 h1:rDgSL20giXXu48Ycx6Qa4vWaNTVTltUl6vA73ObCSVk= +github.com/aws/aws-sdk-go v1.36.1/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ=