Cap AWS AssumeRole sessions to Teleport identity TTL (#67617)

This commit is contained in:
Alex Carrithers
2026-06-10 15:05:11 +00:00
committed by GitHub
parent ef833a355b
commit 31e4dc52cb
4 changed files with 629 additions and 38 deletions
+114 -34
View File
@@ -19,6 +19,7 @@
package aws
import (
"cmp"
"fmt"
"io"
"net/http"
@@ -36,69 +37,148 @@ import (
)
func updateAssumeRoleDuration(identity *tlsca.Identity, w http.ResponseWriter, req *http.Request, clock clockwork.Clock) error {
// Skip non-AssumeRole request
query, found, err := getAssumeRoleQuery(req)
if err != nil || !found {
assumeRoleReq, identityTTL, err := checkAssumeRoleDuration(identity, req, clock)
if err != nil {
return trace.Wrap(err)
}
// Deny access if identity duration is shorter than the minimum that can be
// requested.
identityTTL := identity.Expires.Sub(clock.Now())
if identityTTL < assumeRoleMinDuration {
// TODO write error message in XML so the client can understand.
return trace.AccessDenied("minimum AWS session duration is %v but Teleport identity expires in %v. Please re-login the app and try again.", assumeRoleMinDuration, identityTTL)
}
// Use shorter requested duration (no update required).
if getAssumeRoleQueryDuration(query) <= identityTTL {
// Skip non-AssumeRole requests and those already within the TTL.
if assumeRoleReq == nil || assumeRoleReq.getDuration() <= identityTTL {
return nil
}
// Rewrite the request.
if err := rewriteAssumeRoleQuery(req, withAssumeRoleQueryDuration(query, identityTTL)); err != nil {
if err := rewriteAssumeRoleRequest(req, assumeRoleReq, identityTTL); err != nil {
return trace.Wrap(err)
}
w.Header().Add(common.TeleportAPIInfoHeader, fmt.Sprintf("requested DurationSeconds of AssumeRole is lowered to \"%d\" as the Teleport identity will expire at %v", int(identityTTL.Seconds()), identity.Expires))
return nil
}
// getAssumeRoleQuery extracts AssumeRole query values from provided request.
func denyLongAssumeRoleDuration(identity *tlsca.Identity, req *http.Request, clock clockwork.Clock) error {
assumeRoleReq, identityTTL, err := checkAssumeRoleDuration(identity, req, clock)
if err != nil {
return trace.Wrap(err)
}
if assumeRoleReq == nil || assumeRoleReq.getDuration() <= identityTTL {
return nil
}
return trace.AccessDenied("requested DurationSeconds of AssumeRole is longer than the Teleport identity TTL of %v", identityTTL)
}
func checkAssumeRoleDuration(identity *tlsca.Identity, req *http.Request, clock clockwork.Clock) (*assumeRoleRequestParams, time.Duration, error) {
assumeRoleReq, err := getAssumeRoleRequest(req)
if err != nil {
return nil, 0, trace.Wrap(err)
}
if assumeRoleReq == nil {
return nil, 0, nil
}
identityTTL := identity.Expires.Sub(clock.Now())
if identityTTL < assumeRoleMinDuration {
// TODO write error message in XML so the client can understand.
return nil, 0, trace.AccessDenied("minimum AWS session duration is %v but Teleport identity expires in %v. Please re-login the app and try again.", assumeRoleMinDuration, identityTTL)
}
return assumeRoleReq, identityTTL, nil
}
type assumeRoleRequestParams struct {
query url.Values
postForm url.Values
actionInQuery bool
actionInPostForm bool
durationInQuery bool
durationInPostForm bool
}
// getAssumeRoleRequest extracts AssumeRole query and post form values from
// provided request.
//
// AWS SDK reference:
// https://github.com/aws/aws-sdk-go/blob/main/private/protocol/query/build.go
// https://github.com/aws/aws-sdk-go/blob/main/service/sts/api.go
func getAssumeRoleQuery(req *http.Request) (url.Values, bool, error) {
func getAssumeRoleRequest(req *http.Request) (*assumeRoleRequestParams, error) {
// http.Request.ParseForm may drain the body. Use a clone.
clone, err := cloneRequest(req)
if err != nil {
return nil, false, trace.Wrap(err)
return nil, trace.Wrap(err)
}
if err := clone.ParseForm(); err != nil || clone.PostForm == nil {
return nil, false, nil
if err := clone.ParseForm(); err != nil {
return nil, nil
}
if clone.PostForm.Get("Action") != "AssumeRole" {
return nil, false, nil
query := clone.URL.Query()
postForm := clone.PostForm
if postForm == nil {
postForm = url.Values{}
}
return clone.PostForm, true, nil
assumeRoleReq := &assumeRoleRequestParams{
query: query,
postForm: postForm,
actionInQuery: query.Get(assumeRoleQueryKeyAction) == assumeRoleQueryActionAssumeRole,
actionInPostForm: postForm.Get(assumeRoleQueryKeyAction) == assumeRoleQueryActionAssumeRole,
durationInQuery: query.Has(assumeRoleQueryKeyDurationSeconds),
durationInPostForm: postForm.Has(assumeRoleQueryKeyDurationSeconds),
}
if !assumeRoleReq.actionInQuery && !assumeRoleReq.actionInPostForm {
return nil, nil
}
return assumeRoleReq, nil
}
func getAssumeRoleQueryDuration(query url.Values) time.Duration {
if durationSeconds, err := strconv.ParseInt(query.Get(assumeRoleQueryKeyDurationSeconds), 10, 32); err == nil {
return time.Duration(durationSeconds) * time.Second
func parseAssumeRoleDuration(values url.Values) (time.Duration, bool) {
if durationSeconds, err := strconv.ParseInt(values.Get(assumeRoleQueryKeyDurationSeconds), 10, 32); err == nil {
return time.Duration(durationSeconds) * time.Second, true
}
return assumeRoleDefaultDuration
}
func withAssumeRoleQueryDuration(query url.Values, duration time.Duration) url.Values {
query.Set(assumeRoleQueryKeyDurationSeconds, strconv.Itoa(int(duration.Seconds())))
return query
return 0, false
}
func rewriteAssumeRoleQuery(req *http.Request, query url.Values) error {
return trace.Wrap(utils.ReplaceRequestBody(req, io.NopCloser(strings.NewReader(query.Encode()))))
func (r *assumeRoleRequestParams) getDuration() time.Duration {
var duration time.Duration
for _, values := range []url.Values{r.query, r.postForm} {
if candidate, ok := parseAssumeRoleDuration(values); ok && candidate > duration {
duration = candidate
}
}
return cmp.Or(duration, assumeRoleDefaultDuration)
}
func rewriteAssumeRoleRequest(req *http.Request, assumeRoleReq *assumeRoleRequestParams, duration time.Duration) error {
durationSeconds := strconv.Itoa(int(duration.Seconds()))
if assumeRoleReq.actionInQuery || assumeRoleReq.durationInQuery {
query := req.URL.Query()
query.Set(assumeRoleQueryKeyDurationSeconds, durationSeconds)
req.URL.RawQuery = query.Encode()
if req.RequestURI != "" {
req.RequestURI = req.URL.RequestURI()
}
}
if !assumeRoleReq.actionInPostForm && !assumeRoleReq.durationInPostForm {
return nil
}
assumeRoleReq.postForm.Set(assumeRoleQueryKeyDurationSeconds, durationSeconds)
body := assumeRoleReq.postForm.Encode()
if err := utils.ReplaceRequestBody(req, io.NopCloser(strings.NewReader(body))); err != nil {
return trace.Wrap(err)
}
req.ContentLength = int64(len(body))
return nil
}
const (
// assumeRoleQueryKeyAction is the query key for the STS API action.
assumeRoleQueryKeyAction = "Action"
// assumeRoleQueryActionAssumeRole is the STS AssumeRole API action name.
assumeRoleQueryActionAssumeRole = "AssumeRole"
// assumeRoleQueryKeyDurationSeconds is the query key for the duration
// seconds for the AssumeRole request.
assumeRoleQueryKeyDurationSeconds = "DurationSeconds"
@@ -108,8 +188,8 @@ const (
//
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
assumeRoleMinDuration = 15 * time.Minute
// assumeRoleMinDuration is the default duration if DurationSeconds is not
// explicitly set in the AssumeRole request.
// assumeRoleDefaultDuration is the default duration if DurationSeconds is
// not explicitly set in the AssumeRole request.
//
// https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html
assumeRoleDefaultDuration = 1 * time.Hour
+267
View File
@@ -0,0 +1,267 @@
/*
* Teleport
* Copyright (C) 2026 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package aws
import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/lib/srv/app/common"
"github.com/gravitational/teleport/lib/tlsca"
)
func TestGetAssumeRoleRequest(t *testing.T) {
t.Parallel()
tests := []struct {
name string
request *http.Request
found bool
wantDuration time.Duration
actionInQuery bool
actionInBody bool
}{
{
name: "post form",
request: newAssumeRolePostRequest(t, "3600"),
found: true,
wantDuration: time.Hour,
actionInBody: true,
},
{
name: "query",
request: newAssumeRoleQueryRequest(t, "1800"),
found: true,
wantDuration: 30 * time.Minute,
actionInQuery: true,
},
{
name: "default duration",
request: newAssumeRolePostRequest(t, ""),
found: true,
wantDuration: assumeRoleDefaultDuration,
actionInBody: true,
},
{
name: "uses longest duration from query and body",
request: func() *http.Request {
req := newAssumeRolePostRequest(t, "1800")
query := req.URL.Query()
query.Set(assumeRoleQueryKeyAction, assumeRoleQueryActionAssumeRole)
query.Set(assumeRoleQueryKeyDurationSeconds, "7200")
req.URL.RawQuery = query.Encode()
return req
}(),
found: true,
wantDuration: 2 * time.Hour,
actionInQuery: true,
actionInBody: true,
},
{
name: "not assume role",
request: httptest.NewRequest(http.MethodPost, "https://sts.amazonaws.com/", strings.NewReader(url.Values{"Action": {"GetCallerIdentity"}}.Encode())),
found: false,
},
{
name: "malformed post form",
request: func() *http.Request {
req := httptest.NewRequest(http.MethodPost, "https://sts.amazonaws.com/", strings.NewReader("Action=AssumeRole&DurationSeconds=%"))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req
}(),
found: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
assumeRoleReq, err := getAssumeRoleRequest(tt.request)
require.NoError(t, err)
require.Equal(t, tt.found, assumeRoleReq != nil)
if assumeRoleReq == nil {
return
}
require.Equal(t, tt.wantDuration, assumeRoleReq.getDuration())
require.Equal(t, tt.actionInQuery, assumeRoleReq.actionInQuery)
require.Equal(t, tt.actionInBody, assumeRoleReq.actionInPostForm)
})
}
}
func TestRewriteAssumeRoleRequest(t *testing.T) {
t.Parallel()
t.Run("post form", func(t *testing.T) {
t.Parallel()
req := newAssumeRolePostRequest(t, "43200")
assumeRoleReq, err := getAssumeRoleRequest(req)
require.NoError(t, err)
require.NotNil(t, assumeRoleReq)
require.NoError(t, rewriteAssumeRoleRequest(req, assumeRoleReq, 30*time.Minute))
body, err := io.ReadAll(req.Body)
require.NoError(t, err)
values, err := url.ParseQuery(string(body))
require.NoError(t, err)
require.Equal(t, "1800", values.Get(assumeRoleQueryKeyDurationSeconds))
require.Equal(t, int64(len(string(body))), req.ContentLength)
})
t.Run("query", func(t *testing.T) {
t.Parallel()
req := newAssumeRoleQueryRequest(t, "43200")
req.RequestURI = req.URL.RequestURI()
assumeRoleReq, err := getAssumeRoleRequest(req)
require.NoError(t, err)
require.NotNil(t, assumeRoleReq)
require.NoError(t, rewriteAssumeRoleRequest(req, assumeRoleReq, 30*time.Minute))
require.Equal(t, "1800", req.URL.Query().Get(assumeRoleQueryKeyDurationSeconds))
require.Equal(t, req.URL.RequestURI(), req.RequestURI)
})
t.Run("query and post form", func(t *testing.T) {
t.Parallel()
req := newAssumeRolePostRequest(t, "1800")
query := req.URL.Query()
query.Set(assumeRoleQueryKeyAction, assumeRoleQueryActionAssumeRole)
query.Set(assumeRoleQueryKeyDurationSeconds, "7200")
req.URL.RawQuery = query.Encode()
req.RequestURI = req.URL.RequestURI()
assumeRoleReq, err := getAssumeRoleRequest(req)
require.NoError(t, err)
require.NotNil(t, assumeRoleReq)
require.NoError(t, rewriteAssumeRoleRequest(req, assumeRoleReq, 45*time.Minute))
require.Equal(t, "2700", req.URL.Query().Get(assumeRoleQueryKeyDurationSeconds))
require.Equal(t, req.URL.RequestURI(), req.RequestURI)
body, err := io.ReadAll(req.Body)
require.NoError(t, err)
values, err := url.ParseQuery(string(body))
require.NoError(t, err)
require.Equal(t, "2700", values.Get(assumeRoleQueryKeyDurationSeconds))
require.Equal(t, int64(len(string(body))), req.ContentLength)
})
}
func TestUpdateAssumeRoleDuration(t *testing.T) {
t.Parallel()
t.Run("explicit duration", func(t *testing.T) {
t.Parallel()
clock := clockwork.NewFakeClockAt(time.Now())
identity := &tlsca.Identity{Expires: clock.Now().Add(time.Hour)}
req := newAssumeRolePostRequest(t, "43200")
w := httptest.NewRecorder()
require.NoError(t, updateAssumeRoleDuration(identity, w, req, clock))
require.Contains(t, w.Header().Get(common.TeleportAPIInfoHeader), "requested DurationSeconds of AssumeRole is lowered")
assumeRoleReq, err := getAssumeRoleRequest(req)
require.NoError(t, err)
require.NotNil(t, assumeRoleReq)
require.Equal(t, time.Hour, assumeRoleReq.getDuration())
})
t.Run("default duration", func(t *testing.T) {
t.Parallel()
clock := clockwork.NewFakeClockAt(time.Now())
identity := &tlsca.Identity{Expires: clock.Now().Add(30 * time.Minute)}
req := newAssumeRolePostRequest(t, "")
w := httptest.NewRecorder()
require.NoError(t, updateAssumeRoleDuration(identity, w, req, clock))
require.Contains(t, w.Header().Get(common.TeleportAPIInfoHeader), "requested DurationSeconds of AssumeRole is lowered")
assumeRoleReq, err := getAssumeRoleRequest(req)
require.NoError(t, err)
require.NotNil(t, assumeRoleReq)
require.Equal(t, 30*time.Minute, assumeRoleReq.getDuration())
})
}
func TestDenyLongAssumeRoleDuration(t *testing.T) {
t.Parallel()
clock := clockwork.NewFakeClockAt(time.Now())
identity := &tlsca.Identity{Expires: clock.Now().Add(time.Hour)}
require.NoError(t, denyLongAssumeRoleDuration(identity, newAssumeRolePostRequest(t, "1800"), clock))
err := denyLongAssumeRoleDuration(identity, newAssumeRolePostRequest(t, "7200"), clock)
require.True(t, trace.IsAccessDenied(err), "got %v", err)
req := newAssumeRolePostRequest(t, "1800")
query := req.URL.Query()
query.Set(assumeRoleQueryKeyDurationSeconds, "7200")
req.URL.RawQuery = query.Encode()
err = denyLongAssumeRoleDuration(identity, req, clock)
require.True(t, trace.IsAccessDenied(err), "got %v", err)
err = denyLongAssumeRoleDuration(&tlsca.Identity{Expires: clock.Now().Add(10 * time.Minute)}, newAssumeRolePostRequest(t, "900"), clock)
require.True(t, trace.IsAccessDenied(err), "got %v", err)
}
func newAssumeRolePostRequest(t *testing.T, durationSeconds string) *http.Request {
t.Helper()
form := url.Values{
assumeRoleQueryKeyAction: {assumeRoleQueryActionAssumeRole},
"RoleArn": {"arn:aws:iam::123456789012:role/test-role"},
"RoleSessionName": {"test-session"},
}
if durationSeconds != "" {
form.Set(assumeRoleQueryKeyDurationSeconds, durationSeconds)
}
req := httptest.NewRequest(http.MethodPost, "https://sts.amazonaws.com/", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
return req
}
func newAssumeRoleQueryRequest(t *testing.T, durationSeconds string) *http.Request {
t.Helper()
query := url.Values{
assumeRoleQueryKeyAction: {assumeRoleQueryActionAssumeRole},
"RoleArn": {"arn:aws:iam::123456789012:role/test-role"},
"RoleSessionName": {"test-session"},
}
if durationSeconds != "" {
query.Set(assumeRoleQueryKeyDurationSeconds, durationSeconds)
}
return httptest.NewRequest(http.MethodGet, "https://sts.amazonaws.com/?"+query.Encode(), http.NoBody)
}
+5
View File
@@ -210,6 +210,11 @@ func (s *signerHandler) serveRequestByAssumedRole(sessCtx *common.SessionContext
if err != nil {
return trace.Wrap(err)
}
if strings.EqualFold(re.SigningName, sts.ServiceID) {
if err := denyLongAssumeRoleDuration(sessCtx.Identity, req, s.Clock); err != nil {
return trace.Wrap(err)
}
}
reqCloneForAudit, err := cloneRequest(req)
if err != nil {
+243 -4
View File
@@ -27,6 +27,7 @@ import (
"net/http/httptest"
"net/url"
"os"
"strconv"
"strings"
"testing"
"time"
@@ -171,6 +172,164 @@ func assumeRoleRequest(requestDuration time.Duration) makeRequest {
}
}
func assumeRoleRequestByAssumedRole(requestDuration time.Duration) makeRequest {
return func(ctx context.Context, url string, region string, provider aws.CredentialsProvider, awsHost string) error {
stsClient := stsutils.NewFromConfig(aws.Config{
Credentials: provider,
BaseEndpoint: &url,
Region: region,
RetryMaxAttempts: 0,
HTTPClient: &http.Client{
Timeout: 5 * time.Second,
Transport: &requestByAssumedRoleTransport{xForwardedHost: awsHost},
},
})
_, err := stsClient.AssumeRole(ctx, &sts.AssumeRoleInput{
DurationSeconds: aws.Int32(int32(requestDuration.Seconds())),
RoleSessionName: aws.String("test-session"),
RoleArn: aws.String("arn:aws:iam::123456789012:role/test-role"),
})
return err
}
}
func assumeRoleQueryRequest(requestDuration time.Duration) makeRequest {
return func(ctx context.Context, urlString string, region string, provider aws.CredentialsProvider, awsHost string) error {
requestURL, err := url.Parse(urlString)
if err != nil {
return err
}
query := requestURL.Query()
query.Set("Action", "AssumeRole")
query.Set("Version", "2011-06-15")
query.Set("RoleArn", "arn:aws:iam::123456789012:role/test-role")
query.Set("RoleSessionName", "test-session")
query.Set("DurationSeconds", strconv.Itoa(int(requestDuration.Seconds())))
requestURL.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), http.NoBody)
if err != nil {
return err
}
creds, err := provider.Retrieve(ctx)
if err != nil {
return err
}
if err := awsutils.NewSigner("sts").SignHTTP(ctx, creds, req, awsutils.EmptyPayloadHash, "sts", region, time.Now()); err != nil {
return err
}
req.Host = awsHost
req.Header.Add("X-Forwarded-Host", awsHost)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body)
if resp.StatusCode >= http.StatusBadRequest {
return trace.BadParameter("unexpected status %v", resp.Status)
}
return nil
}
}
func assumeRoleQueryRequestByAssumedRole(requestDuration time.Duration) makeRequest {
return func(ctx context.Context, urlString string, region string, provider aws.CredentialsProvider, awsHost string) error {
requestURL, err := url.Parse(urlString)
if err != nil {
return err
}
query := requestURL.Query()
query.Set("Action", "AssumeRole")
query.Set("Version", "2011-06-15")
query.Set("RoleArn", "arn:aws:iam::123456789012:role/test-role")
query.Set("RoleSessionName", "test-session")
query.Set("DurationSeconds", strconv.Itoa(int(requestDuration.Seconds())))
requestURL.RawQuery = query.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL.String(), http.NoBody)
if err != nil {
return err
}
creds, err := provider.Retrieve(ctx)
if err != nil {
return err
}
if err := awsutils.NewSigner("sts").SignHTTP(ctx, creds, req, awsutils.EmptyPayloadHash, "sts", region, time.Now()); err != nil {
return err
}
req.Host = awsHost
req.Header.Add("X-Forwarded-Host", awsHost)
req.Header.Add(common.TeleportAWSAssumedRole, fakeAssumedRoleARN)
utils.RenameHeader(req.Header, awsutils.AuthorizationHeader, common.TeleportAWSAssumedRoleAuthorization)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body)
if resp.StatusCode >= http.StatusBadRequest {
return trace.BadParameter("unexpected status %v", resp.Status)
}
return nil
}
}
func assumeRoleMixedRequest(queryDuration, bodyDuration time.Duration) makeRequest {
return func(ctx context.Context, urlString string, region string, provider aws.CredentialsProvider, awsHost string) error {
requestURL, err := url.Parse(urlString)
if err != nil {
return err
}
query := requestURL.Query()
query.Set("Action", "AssumeRole")
query.Set("Version", "2011-06-15")
query.Set("RoleArn", "arn:aws:iam::123456789012:role/test-role")
query.Set("RoleSessionName", "test-session")
query.Set("DurationSeconds", strconv.Itoa(int(queryDuration.Seconds())))
requestURL.RawQuery = query.Encode()
form := url.Values{
"Action": {"AssumeRole"},
"Version": {"2011-06-15"},
"RoleArn": {"arn:aws:iam::123456789012:role/test-role"},
"RoleSessionName": {"test-session"},
"DurationSeconds": {strconv.Itoa(int(bodyDuration.Seconds()))},
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), strings.NewReader(form.Encode()))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
creds, err := provider.Retrieve(ctx)
if err != nil {
return err
}
payloadHash, err := awsutils.GetV4PayloadHash(req)
if err != nil {
return err
}
if err := awsutils.NewSigner("sts").SignHTTP(ctx, creds, req, payloadHash, "sts", region, time.Now()); err != nil {
return err
}
req.Host = awsHost
req.Header.Add("X-Forwarded-Host", awsHost)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
_, _ = io.Copy(io.Discard, resp.Body)
if resp.StatusCode >= http.StatusBadRequest {
return trace.BadParameter("unexpected status %v", resp.Status)
}
return nil
}
}
type requestByHTTPSProxy struct {
xForwardedHost string
}
@@ -203,6 +362,13 @@ func hasStatusCode(wantStatusCode int) require.ErrorAssertionFunc {
}
}
func hasUnexpectedStatus(wantStatusCode int) require.ErrorAssertionFunc {
return func(t require.TestingT, err error, msgAndArgs ...any) {
require.Error(t, err, msgAndArgs...)
require.Contains(t, err.Error(), strconv.Itoa(wantStatusCode)+" "+http.StatusText(wantStatusCode), msgAndArgs...)
}
}
// TestAWSSignerHandler test the AWS SigningService APP handler logic with mocked STS signing credentials.
func TestAWSSignerHandler(t *testing.T) {
consoleApp, err := types.NewAppV3(types.Metadata{
@@ -427,6 +593,40 @@ func TestAWSSignerHandler(t *testing.T) {
require.NoError,
},
},
{
name: "AssumeRole query success (shorter identity duration)",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-east-1",
request: assumeRoleQueryRequest(12 * time.Hour),
advanceClock: 30 * time.Minute,
wantHost: "sts.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "sts",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionRequest{},
verifySentRequest: verifyAssumeRoleDuration(30 * time.Minute), // 1h (suite default for identity) - 30m
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "AssumeRole query and body success (shorter identity duration)",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForClient,
awsRegion: "us-east-1",
request: assumeRoleMixedRequest(12*time.Hour, 45*time.Minute),
advanceClock: 30 * time.Minute,
wantHost: "sts.amazonaws.com",
wantAuthCredKeyID: "FAKEACCESSKEYID",
wantAuthCredService: "sts",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionRequest{},
verifySentRequest: verifyAssumeRoleDuration(30 * time.Minute), // 1h (suite default for identity) - 30m
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "AssumeRole success (shorter requested duration)",
app: consoleApp,
@@ -443,6 +643,46 @@ func TestAWSSignerHandler(t *testing.T) {
require.NoError,
},
},
{
name: "AssumeRole by assumed role success (shorter requested duration)",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForAssumedRole,
awsRegion: "us-east-1",
request: assumeRoleRequestByAssumedRole(32 * time.Minute),
wantHost: "sts.amazonaws.com",
wantAuthCredKeyID: assumedRoleKeyID, // not using service's access key ID
wantAuthCredService: "sts",
wantAuthCredRegion: "us-east-1",
wantEventType: &events.AppSessionRequest{},
wantAssumedRole: fakeAssumedRoleARN, // verifies assumed role is recorded in audit
skipVerifySignature: true, // not re-signing
verifySentRequest: verifyAssumeRoleDuration(32 * time.Minute),
errAssertionFns: []require.ErrorAssertionFunc{
require.NoError,
},
},
{
name: "AssumeRole query by assumed role denied",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForAssumedRole,
awsRegion: "us-east-1",
request: assumeRoleQueryRequestByAssumedRole(2 * time.Hour),
wantHost: "sts.amazonaws.com",
errAssertionFns: []require.ErrorAssertionFunc{
hasUnexpectedStatus(http.StatusForbidden),
},
},
{
name: "AssumeRole by assumed role denied",
app: consoleApp,
awsCredentialsProvider: staticAWSCredentialsForAssumedRole,
awsRegion: "us-east-1",
request: assumeRoleRequestByAssumedRole(2 * time.Hour),
wantHost: "sts.amazonaws.com",
errAssertionFns: []require.ErrorAssertionFunc{
hasStatusCode(http.StatusForbidden),
},
},
{
name: "AssumeRole denied",
app: consoleApp,
@@ -472,7 +712,6 @@ func TestAWSSignerHandler(t *testing.T) {
assert.Equal(t, tc.wantAuthCredRegion, awsAuthHeader.Region)
assert.Equal(t, tc.wantAuthCredKeyID, awsAuthHeader.KeyID)
assert.Equal(t, tc.wantAuthCredService, awsAuthHeader.Service)
// check that the signature is valid.
if !tc.skipVerifySignature {
err := awsutils.VerifyAWSSignature(r,
@@ -677,10 +916,10 @@ func createSuite(t *testing.T, mockAWSHandler http.HandlerFunc, app types.Applic
func verifyAssumeRoleDuration(wantDuration time.Duration) func(*testing.T, *http.Request) {
return func(t *testing.T, req *http.Request) {
clone, err := cloneRequest(req)
assumeRoleReq, err := getAssumeRoleRequest(req)
require.NoError(t, err)
require.NoError(t, clone.ParseForm())
require.Equal(t, wantDuration, getAssumeRoleQueryDuration(clone.PostForm))
require.NotNil(t, assumeRoleReq)
require.Equal(t, wantDuration, assumeRoleReq.getDuration())
}
}