mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-03 22:57:23 +00:00
s3: enforce role trust policy on direct OIDC bearer authentication (#10302)
A raw OIDC token sent as Authorization: Bearer was validated and mapped to a role through the provider's roleMapping, then authorized against the role's attached policies without ever consulting the role's trust policy. A federated user could act as a role that AssumeRoleWithWebIdentity would refuse to issue a session for with the same token. Run the same trust-policy validation before returning the principal on the bearer path.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package s3api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/integration"
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/oidc"
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/policy"
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/providers"
|
||||
"github.com/seaweedfs/seaweedfs/weed/iam/sts"
|
||||
"github.com/seaweedfs/seaweedfs/weed/s3api/s3err"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// A raw OIDC token sent as Authorization: Bearer must clear the same role
|
||||
// trust-policy gate as AssumeRoleWithWebIdentity. The provider's role mapping
|
||||
// alone must not mint a principal for a role whose trust policy rejects the
|
||||
// token's federated provider.
|
||||
func TestOIDCBearerEnforcesRoleTrustPolicy(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
jwks := map[string]interface{}{
|
||||
"keys": []map[string]interface{}{{
|
||||
"kty": "RSA",
|
||||
"kid": "bearer-test-key",
|
||||
"use": "sig",
|
||||
"alg": "RS256",
|
||||
"n": base64.RawURLEncoding.EncodeToString(key.PublicKey.N.Bytes()),
|
||||
"e": "AQAB",
|
||||
}},
|
||||
}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/jwks" {
|
||||
json.NewEncoder(w).Encode(jwks)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
provider := oidc.NewOIDCProvider("test-oidc")
|
||||
require.NoError(t, provider.Initialize(&oidc.OIDCConfig{
|
||||
Issuer: server.URL,
|
||||
ClientID: "test-client",
|
||||
JWKSUri: server.URL + "/jwks",
|
||||
RoleMapping: &providers.RoleMapping{
|
||||
Rules: []providers.MappingRule{
|
||||
{Claim: "groups", Value: "denied-group", Role: "arn:aws:iam::role/BearerDeniedRole"},
|
||||
{Claim: "groups", Value: "trusted-group", Role: "arn:aws:iam::role/BearerTrustedRole"},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
iamManager := integration.NewIAMManager()
|
||||
require.NoError(t, iamManager.Initialize(&integration.IAMConfig{
|
||||
STS: &sts.STSConfig{
|
||||
TokenDuration: sts.FlexibleDuration{Duration: time.Hour},
|
||||
MaxSessionLength: sts.FlexibleDuration{Duration: 12 * time.Hour},
|
||||
Issuer: "test-sts",
|
||||
SigningKey: []byte("test-signing-key-32-characters-long"),
|
||||
},
|
||||
Policy: &policy.PolicyEngineConfig{DefaultEffect: "Deny", StoreType: "memory"},
|
||||
Roles: &integration.RoleStoreConfig{StoreType: "memory"},
|
||||
}, func() string { return "localhost:8888" }))
|
||||
require.NoError(t, iamManager.RegisterIdentityProvider(provider))
|
||||
|
||||
require.NoError(t, iamManager.CreatePolicy(ctx, "", "BearerTestPolicy", &policy.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
Statement: []policy.Statement{{
|
||||
Effect: "Allow",
|
||||
Action: []string{"s3:*"},
|
||||
Resource: []string{"arn:aws:s3:::*", "arn:aws:s3:::*/*"},
|
||||
}},
|
||||
}))
|
||||
|
||||
trustPolicy := func(federatedProvider string) *policy.PolicyDocument {
|
||||
return &policy.PolicyDocument{
|
||||
Version: "2012-10-17",
|
||||
Statement: []policy.Statement{{
|
||||
Effect: "Allow",
|
||||
Principal: map[string]interface{}{"Federated": federatedProvider},
|
||||
Action: []string{"sts:AssumeRoleWithWebIdentity"},
|
||||
}},
|
||||
}
|
||||
}
|
||||
// trusts a different federated provider than the one issuing the token
|
||||
require.NoError(t, iamManager.CreateRole(ctx, "", "BearerDeniedRole", &integration.RoleDefinition{
|
||||
RoleName: "BearerDeniedRole",
|
||||
TrustPolicy: trustPolicy("other-provider"),
|
||||
AttachedPolicies: []string{"BearerTestPolicy"},
|
||||
}))
|
||||
require.NoError(t, iamManager.CreateRole(ctx, "", "BearerTrustedRole", &integration.RoleDefinition{
|
||||
RoleName: "BearerTrustedRole",
|
||||
TrustPolicy: trustPolicy("test-oidc"),
|
||||
AttachedPolicies: []string{"BearerTestPolicy"},
|
||||
}))
|
||||
|
||||
signJWT := func(groups ...string) string {
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
|
||||
"iss": server.URL,
|
||||
"sub": "bearer-user",
|
||||
"aud": "test-client",
|
||||
"exp": time.Now().Add(time.Hour).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"groups": groups,
|
||||
})
|
||||
token.Header["kid"] = "bearer-test-key"
|
||||
signed, err := token.SignedString(key)
|
||||
require.NoError(t, err)
|
||||
return signed
|
||||
}
|
||||
|
||||
s3iam := NewS3IAMIntegration(iamManager, "localhost:8888")
|
||||
authenticate := func(token string) (*IAMIdentity, s3err.ErrorCode) {
|
||||
req := httptest.NewRequest("GET", "/test-bucket/test-object", http.NoBody)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
return s3iam.AuthenticateJWT(ctx, req)
|
||||
}
|
||||
|
||||
deniedToken := signJWT("denied-group")
|
||||
|
||||
// negative control: STS refuses to issue a session for this token and role
|
||||
_, err = iamManager.AssumeRoleWithWebIdentity(ctx, &sts.AssumeRoleWithWebIdentityRequest{
|
||||
RoleArn: "arn:aws:iam::role/BearerDeniedRole",
|
||||
WebIdentityToken: deniedToken,
|
||||
RoleSessionName: "trust-policy-check",
|
||||
})
|
||||
require.ErrorContains(t, err, "trust policy")
|
||||
|
||||
// the direct bearer path must reject the same token
|
||||
identity, errCode := authenticate(deniedToken)
|
||||
assert.Equal(t, s3err.ErrAccessDenied, errCode)
|
||||
assert.Nil(t, identity)
|
||||
|
||||
// a role whose trust policy trusts the issuing provider still works
|
||||
identity, errCode = authenticate(signJWT("trusted-group"))
|
||||
require.Equal(t, s3err.ErrNone, errCode)
|
||||
assert.Equal(t, "arn:aws:iam::role/BearerTrustedRole", identity.Principal)
|
||||
}
|
||||
@@ -143,6 +143,13 @@ func (s3iam *S3IAMIntegration) AuthenticateJWT(ctx context.Context, r *http.Requ
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
// Same trust-policy gate as AssumeRoleWithWebIdentity: the role mapping
|
||||
// alone must not grant a role that STS would refuse for this token.
|
||||
if err := s3iam.iamManager.ValidateTrustPolicyForWebIdentity(ctx, identity.RoleArn, sessionToken, nil); err != nil {
|
||||
glog.V(3).Infof("OIDC bearer token rejected by trust policy of role %s: %v", identity.RoleArn, err)
|
||||
return nil, s3err.ErrAccessDenied
|
||||
}
|
||||
|
||||
// Create claims map and populate with standard claims and attributes
|
||||
claims := make(map[string]interface{}, len(identity.Attributes)+5)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user