Skip to content

Commit

Permalink
Avoid asking MFA twice (#166)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jocgir authored Dec 4, 2020
1 parent e7b0667 commit d80574d
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 36 deletions.
113 changes: 82 additions & 31 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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]
}
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
6 changes: 5 additions & 1 deletion docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()))
}
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down

0 comments on commit d80574d

Please sign in to comment.