mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-29 11:15:34 +00:00
s3: tighten STS session token handling (#11383)
* s3api: test that a session token must not reveal its credential * sts: derive secret access key with HMAC keyed on the signing key * s3api: stop accepting STS session tokens as bearer credentials * security: reject STS session tokens on filer and admin gRPC auth * test: sign s3/iam framework requests with the session credential * s3api: exercise the real auth pipeline in the end-to-end harness
This commit is contained in:
@@ -2,9 +2,12 @@ package iam
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/hmac"
|
||||
cryptorand "crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -354,12 +357,12 @@ func (t *BearerTokenTransport) extractPrincipalFromJWT(tokenString string) strin
|
||||
}
|
||||
|
||||
// generateSTSSessionToken creates a session token using the actual STS service for proper validation
|
||||
func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string, validDuration time.Duration, account string, customClaims map[string]interface{}) (string, error) {
|
||||
func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string, validDuration time.Duration, account string, customClaims map[string]interface{}) (string, *credentials.Credentials, error) {
|
||||
now := time.Now()
|
||||
signingKeyB64 := "dGVzdC1zaWduaW5nLWtleS0zMi1jaGFyYWN0ZXJzLWxvbmc="
|
||||
signingKey, err := base64.StdEncoding.DecodeString(signingKeyB64)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode signing key: %v", err)
|
||||
return "", nil, fmt.Errorf("failed to decode signing key: %v", err)
|
||||
}
|
||||
|
||||
// Generate a session ID that would be created by the STS service
|
||||
@@ -404,10 +407,19 @@ func (f *S3IAMTestFramework) generateSTSSessionToken(username, roleName string,
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, sessionClaims)
|
||||
tokenString, err := token.SignedString(signingKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
accessKeyHash := sha256.Sum256([]byte("access-key:" + sessionId))
|
||||
mac := hmac.New(sha256.New, signingKey)
|
||||
mac.Write([]byte("secret-key:" + sessionId))
|
||||
creds := credentials.NewStaticCredentials(
|
||||
"ASIA"+hex.EncodeToString(accessKeyHash[:8]),
|
||||
base64.StdEncoding.EncodeToString(mac.Sum(nil)),
|
||||
tokenString,
|
||||
)
|
||||
|
||||
return tokenString, creds, nil
|
||||
}
|
||||
|
||||
// CreateS3ClientWithJWT creates an S3 client authenticated with a JWT token for the specified role
|
||||
@@ -417,36 +429,34 @@ func (f *S3IAMTestFramework) CreateS3ClientWithJWT(username, roleName string) (*
|
||||
|
||||
// CreateS3ClientWithCustomClaims creates an S3 client with specific account ID and custom claims
|
||||
func (f *S3IAMTestFramework) CreateS3ClientWithCustomClaims(username, roleName, account string, claims map[string]interface{}) (*s3.S3, error) {
|
||||
var token string
|
||||
var err error
|
||||
var httpClient *http.Client
|
||||
creds := credentials.AnonymousCredentials
|
||||
|
||||
if f.useKeycloak && claims == nil && account == "" {
|
||||
// Use real Keycloak authentication if no custom requirements
|
||||
token, err = f.getKeycloakToken(username)
|
||||
token, err := f.getKeycloakToken(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Keycloak token: %v", err)
|
||||
}
|
||||
httpClient = &http.Client{
|
||||
Transport: &BearerTokenTransport{
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Generate STS session token (mock mode or custom requirements)
|
||||
token, err = f.generateSTSSessionToken(username, roleName, time.Hour, account, claims)
|
||||
var err error
|
||||
_, creds, err = f.generateSTSSessionToken(username, roleName, time.Hour, account, claims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate STS session token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create custom HTTP client with Bearer token transport
|
||||
httpClient := &http.Client{
|
||||
Transport: &BearerTokenTransport{
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(TestRegion),
|
||||
Endpoint: aws.String(TestS3Endpoint),
|
||||
HTTPClient: httpClient,
|
||||
// Use anonymous credentials to avoid AWS signature generation
|
||||
Credentials: credentials.AnonymousCredentials,
|
||||
Region: aws.String(TestRegion),
|
||||
Endpoint: aws.String(TestS3Endpoint),
|
||||
HTTPClient: httpClient,
|
||||
Credentials: creds,
|
||||
DisableSSL: aws.Bool(true),
|
||||
S3ForcePathStyle: aws.Bool(true),
|
||||
})
|
||||
@@ -487,7 +497,7 @@ func (f *S3IAMTestFramework) CreateS3ClientWithInvalidJWT() (*s3.S3, error) {
|
||||
// CreateS3ClientWithExpiredJWT creates an S3 client with an expired JWT token
|
||||
func (f *S3IAMTestFramework) CreateS3ClientWithExpiredJWT(username, roleName string) (*s3.S3, error) {
|
||||
// Generate expired STS session token (expired 1 hour ago)
|
||||
token, err := f.generateSTSSessionToken(username, roleName, -time.Hour, "", nil)
|
||||
token, _, err := f.generateSTSSessionToken(username, roleName, -time.Hour, "", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate expired STS session token: %v", err)
|
||||
}
|
||||
@@ -942,36 +952,34 @@ func (f *S3IAMTestFramework) CreateIAMClientWithJWT(username, roleName string) (
|
||||
|
||||
// CreateIAMClientWithCustomClaims creates an IAM client with specific account ID and custom claims
|
||||
func (f *S3IAMTestFramework) CreateIAMClientWithCustomClaims(username, roleName, account string, claims map[string]interface{}) (*iam.IAM, error) {
|
||||
var token string
|
||||
var err error
|
||||
var httpClient *http.Client
|
||||
creds := credentials.AnonymousCredentials
|
||||
|
||||
if f.useKeycloak && claims == nil && account == "" {
|
||||
// Use real Keycloak authentication if no custom requirements
|
||||
token, err = f.getKeycloakToken(username)
|
||||
token, err := f.getKeycloakToken(username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Keycloak token: %v", err)
|
||||
}
|
||||
httpClient = &http.Client{
|
||||
Transport: &BearerTokenTransport{
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
// Generate STS session token (mock mode or custom requirements)
|
||||
token, err = f.generateSTSSessionToken(username, roleName, time.Hour, account, claims)
|
||||
var err error
|
||||
_, creds, err = f.generateSTSSessionToken(username, roleName, time.Hour, account, claims)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate STS session token: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create custom HTTP client with Bearer token transport
|
||||
httpClient := &http.Client{
|
||||
Transport: &BearerTokenTransport{
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(&aws.Config{
|
||||
Region: aws.String(TestRegion),
|
||||
Endpoint: aws.String(TestS3Endpoint),
|
||||
HTTPClient: httpClient,
|
||||
// Use anonymous credentials to avoid AWS signature generation
|
||||
Credentials: credentials.AnonymousCredentials,
|
||||
Region: aws.String(TestRegion),
|
||||
Endpoint: aws.String(TestS3Endpoint),
|
||||
HTTPClient: httpClient,
|
||||
Credentials: creds,
|
||||
DisableSSL: aws.Bool(true),
|
||||
})
|
||||
if err != nil {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
|
||||
"github.com/aws/aws-sdk-go/service/iam"
|
||||
"github.com/aws/aws-sdk-go/service/s3"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -693,23 +694,25 @@ type ListGroupsResponse struct {
|
||||
func callIAMAPIAuthenticated(_ *testing.T, framework *S3IAMTestFramework, action string, params url.Values) (*http.Response, error) {
|
||||
params.Set("Action", action)
|
||||
|
||||
body := params.Encode()
|
||||
req, err := http.NewRequest(http.MethodPost, TestIAMEndpoint+"/",
|
||||
strings.NewReader(params.Encode()))
|
||||
strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
token, err := framework.generateSTSSessionToken("admin-user", "TestAdminRole", time.Hour, "", nil)
|
||||
_, creds, err := framework.generateSTSSessionToken("admin-user", "TestAdminRole", time.Hour, "", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
Transport: &BearerTokenTransport{Token: token},
|
||||
if _, err := v4.NewSigner(creds).Sign(req, strings.NewReader(body), "iam", TestRegion, time.Now()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
return client.Do(req)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user