Skip to content

Commit

Permalink
add extra checks to avoid getSigninToken failure (#9792) (#9964)
Browse files Browse the repository at this point in the history
  • Loading branch information
greedy52 authored Jan 27, 2022
1 parent 7f62325 commit 8fecf57
Show file tree
Hide file tree
Showing 4 changed files with 498 additions and 37 deletions.
7 changes: 6 additions & 1 deletion docs/pages/application-access/guides/aws-console.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,11 @@ federated login and the name of your assumed IAM role:

Note that your federated login session is marked with your Teleport username.

<Admonition type="note" title="Session Duration">
If the Teleport agent is running with [temporary security credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html),
the management console session will be limited to a maximum of one hour.
</Admonition>

## Step 7. Use CloudTrail to see Teleport user activity

To view CloudTrail events for your federated sessions, navigate to the CloudTrail
Expand Down Expand Up @@ -272,7 +277,7 @@ $ tsh aws s3 ls
```

<Admonition type="note" title="Note">
The `aws` command-line tool should be available in PATH
The `aws` command-line tool should be available in PATH.
</Admonition>

To log out of the aws application and remove credentials:
Expand Down
195 changes: 159 additions & 36 deletions lib/srv/app/cloud.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ package app

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"time"

"github.com/gravitational/teleport/lib/tlsca"

"github.com/aws/aws-sdk-go/aws/arn"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/credentials/ssocreds"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
awssession "github.com/aws/aws-sdk-go/aws/session"

Expand Down Expand Up @@ -126,79 +128,186 @@ func (c *cloud) GetAWSSigninURL(req AWSSigninRequest) (*AWSSigninResponse, error
return nil, trace.Wrap(err)
}

stsCredentials, err := stscreds.NewCredentials(c.cfg.Session, req.Identity.RouteToApp.AWSRoleARN,
func(creds *stscreds.AssumeRoleProvider) {
// Setting role session name to Teleport username will allow to
// associate CloudTrail events with the Teleport user.
creds.RoleSessionName = req.Identity.Username
}).Get()
signinToken, err := c.getAWSSigninToken(&req, federationURL)
if err != nil {
return nil, trace.Wrap(err)
}

tokenURL, err := url.Parse(federationURL)
signinURL, err := url.Parse(federationURL)
if err != nil {
return nil, trace.Wrap(err)
}

signinURL.RawQuery = url.Values{
"Action": []string{"login"},
"SigninToken": []string{signinToken},
"Destination": []string{req.TargetURL},
"Issuer": []string{req.Issuer},
}.Encode()

return &AWSSigninResponse{
SigninURL: signinURL.String(),
}, nil
}

// getAWSSigninToken gets the signin token required for the AWS sign in URL.
//
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html
func (c *cloud) getAWSSigninToken(req *AWSSigninRequest, endpoint string, options ...func(*stscreds.AssumeRoleProvider)) (string, error) {
// It is stated in the user guide linked above:
// When you use DurationSeconds in an AssumeRole* operation, you must call
// it as an IAM user with long-term credentials. Otherwise, the call to the
// federation endpoint in step 3 fails.
//
// Experiments showed that the getSigninToken call will still succeed as
// long as the "SessionDuration" is not provided when calling the API, when
// the AWS session is using temporary credentials. However, when the
// "SessionDuration" is not provided, the web console session duration will
// be bound to the duration used in the next AssumeRole call.
temporarySession, err := isSessionUsingTemporaryCredentials(c.cfg.Session)
if err != nil {
return "", trace.Wrap(err)
}

duration, err := c.getFederationDuration(req, temporarySession)
if err != nil {
return "", trace.Wrap(err)
}

options = append(options, func(creds *stscreds.AssumeRoleProvider) {
// Setting role session name to Teleport username will allow to
// associate CloudTrail events with the Teleport user.
creds.RoleSessionName = req.Identity.Username

// Setting web console session duration through AssumeRole call for AWS
// sessions with temporary credentials.
// Technically the session duration can be set this way for
// non-temporary sessions. However, the AssumeRole call will fail if we
// are requesting duration longer than the maximum session duration of
// the role we are assuming. In addition, the session credentials may
// not have permission to perform a get-role on the role. Therefore,
// "SessionDuration" parameter will be defined when calling federation
// endpoint below instead of here, for non-temporary sessions.
//
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
if temporarySession {
creds.Duration = duration
}
})
stsCredentials, err := stscreds.NewCredentials(c.cfg.Session, req.Identity.RouteToApp.AWSRoleARN, options...).Get()
if err != nil {
return "", trace.Wrap(err)
}

tokenURL, err := url.Parse(endpoint)
if err != nil {
return "", trace.Wrap(err)
}

sessionBytes, err := json.Marshal(stsSession{
SessionID: stsCredentials.AccessKeyID,
SessionKey: stsCredentials.SecretAccessKey,
SessionToken: stsCredentials.SessionToken,
})
if err != nil {
return nil, trace.Wrap(err)
return "", trace.Wrap(err)
}

// Max AWS federation session duration is 12 hours. The federation endpoint
// will error out if we request more.
duration := req.Identity.Expires.Sub(c.cfg.Clock.Now())
if duration > maxSessionDuration {
duration = maxSessionDuration
values := url.Values{
"Action": []string{"getSigninToken"},
"Session": []string{string(sessionBytes)},
}
if !temporarySession {
values["SessionDuration"] = []string{strconv.Itoa(int(duration.Seconds()))}
}

tokenURL.RawQuery = url.Values{
"Action": []string{"getSigninToken"},
"SessionDuration": []string{fmt.Sprintf("%d", int(duration.Seconds()))},
"Session": []string{string(sessionBytes)},
}.Encode()

tokenURL.RawQuery = values.Encode()
resp, err := http.Get(tokenURL.String())
if err != nil {
return nil, trace.Wrap(err)
return "", trace.Wrap(err)
}

respBytes, err := io.ReadAll(resp.Body)
if err != nil {
return nil, trace.Wrap(err)
return "", trace.Wrap(err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, trace.BadParameter("non-200 response from AWS federation endpoint: %q %v %q",
return "", trace.BadParameter("non-200 response from AWS federation endpoint: %q %v %q",
resp.Status, resp.Header, string(respBytes))
}

var fedResp federationResponse
if err := json.Unmarshal(respBytes, &fedResp); err != nil {
return nil, trace.Wrap(err)
return "", trace.Wrap(err)
}

signinURL, err := url.Parse(federationURL)
return fedResp.SigninToken, nil
}

// isSessionUsingTemporaryCredentials checks if the current aws session is
// using temporary credentials.
//
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp.html
func isSessionUsingTemporaryCredentials(session *awssession.Session) (bool, error) {
if session.Config == nil || session.Config.Credentials == nil {
return false, trace.NotFound("session credentials not found")
}

credentials, err := session.Config.Credentials.Get()
if err != nil {
return nil, trace.Wrap(err)
return false, trace.Wrap(err)
}

signinURL.RawQuery = url.Values{
"Action": []string{"login"},
"SigninToken": []string{fedResp.SigninToken},
"Destination": []string{req.TargetURL},
"Issuer": []string{req.Issuer},
}.Encode()
switch credentials.ProviderName {
case ec2rolecreds.ProviderName:
return false, nil

case
// stscreds.AssumeRoleProvider retrieves temporary credentials from the
// STS service, and keeps track of their expiration time.
// https://docs.aws.amazon.com/sdk-for-go/api/aws/credentials/stscreds/#AssumeRoleProvider
stscreds.ProviderName,

// stscreds.WebIdentityRoleProvider is used to retrieve credentials
// using an OIDC token.
// https://docs.aws.amazon.com/sdk-for-go/api/aws/credentials/stscreds/#WebIdentityRoleProvider
//
// IAM roles for EKS service accounts are also granted through the OIDC tokens.
// https://aws.amazon.com/blogs/opensource/introducing-fine-grained-iam-roles-service-accounts/
stscreds.WebIdentityProviderName,

// ssocreds.Provider is an AWS credential provider that retrieves
// temporary AWS credentials by exchanging an SSO login token.
// https://docs.aws.amazon.com/sdk-for-go/api/aws/credentials/ssocreds/#Provider
ssocreds.ProviderName:
return true, nil
}

return &AWSSigninResponse{
SigninURL: signinURL.String(),
}, nil
// For other providers, make an assumption that a session token is only
// required for temporary security credentials retrieved via STS, otherwise
// it is an empty string.
// https://docs.aws.amazon.com/sdk-for-go/api/aws/credentials/#NewStaticCredentials
return credentials.SessionToken != "", nil
}

// getFederationDuration calculates the duration of the federated session.
func (c *cloud) getFederationDuration(req *AWSSigninRequest, temporarySession bool) (time.Duration, error) {
maxDuration := maxSessionDuration
if temporarySession {
maxDuration = maxTemporarySessionDuration
}

duration := req.Identity.Expires.Sub(c.cfg.Clock.Now())
if duration > maxDuration {
duration = maxDuration
}

if duration < minimumSessionDuration {
return 0, trace.AccessDenied("minimum AWS session duration is %v but Teleport identity expires in %v", minimumSessionDuration, duration)
}
return duration, nil
}

// stsSession combines parameters describing session built from temporary credentials.
Expand All @@ -222,6 +331,20 @@ const (
federationURL = "https://signin.aws.amazon.com/federation"
// consoleURL is the default AWS console destination.
consoleURL = "https://console.aws.amazon.com/ec2/v2/home"
// maxSessionDuration is the max federation session duration.
// maxSessionDuration is the max federation session duration, which is 12
// hours. The federation endpoint will error out if we request more.
//
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_enable-console-custom-url.html
maxSessionDuration = 12 * time.Hour
// maxTemporarySessionDuration is the max federation session duration when
// the AWS session is using temporary credentials. The maximum is one hour,
// which is the maximum duration you can set when role chaining.
//
// https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html
maxTemporarySessionDuration = time.Hour
// minimumSessionDuration is the minimum federation session duration. The
// AssumeRole call will error out if we request less than 15 minutes.
//
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
minimumSessionDuration = 15 * time.Minute
)
Loading

0 comments on commit 8fecf57

Please sign in to comment.