disallow sub-credentials based on root credentials to gain priviledges (#12947)

This happens because of a change added where any sub-credential
with parentUser == rootCredential i.e (MINIO_ROOT_USER) will
always be an owner, you cannot generate credentials with lower
session policy to restrict their access.

This doesn't affect user service accounts created with regular
users, LDAP or OpenID
This commit is contained in:
Harshavardhana 2021-08-12 18:07:08 -07:00 committed by GitHub
parent 89febdb3d6
commit 8f2a3efa85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 70 additions and 135 deletions

View File

@ -155,12 +155,7 @@ func validateAdminSignature(ctx context.Context, r *http.Request, region string)
return cred, nil, owner, s3Err
}
claims, s3Err := checkClaimsFromToken(r, cred)
if s3Err != ErrNone {
return cred, nil, owner, s3Err
}
return cred, claims, owner, ErrNone
return cred, cred.Claims, owner, ErrNone
}
// checkAdminRequestAuth checks for authentication and authorization for the incoming
@ -318,12 +313,6 @@ func checkRequestAuthTypeCredential(ctx context.Context, r *http.Request, action
return cred, owner, s3Err
}
var claims map[string]interface{}
claims, s3Err = checkClaimsFromToken(r, cred)
if s3Err != ErrNone {
return cred, owner, s3Err
}
// LocationConstraint is valid only for CreateBucketAction.
var locationConstraint string
if action == policy.CreateBucketAction {
@ -388,10 +377,10 @@ func checkRequestAuthTypeCredential(ctx context.Context, r *http.Request, action
Groups: cred.Groups,
Action: iampolicy.Action(action),
BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
ObjectName: objectName,
IsOwner: owner,
Claims: claims,
Claims: cred.Claims,
}) {
// Request is allowed return the appropriate access key.
return cred, owner, ErrNone
@ -405,10 +394,10 @@ func checkRequestAuthTypeCredential(ctx context.Context, r *http.Request, action
Groups: cred.Groups,
Action: iampolicy.ListBucketAction,
BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
ObjectName: objectName,
IsOwner: owner,
Claims: claims,
Claims: cred.Claims,
}) {
// Request is allowed return the appropriate access key.
return cred, owner, ErrNone
@ -520,75 +509,39 @@ func setAuthHandler(h http.Handler) http.Handler {
})
}
func validateSignature(atype authType, r *http.Request) (auth.Credentials, bool, map[string]interface{}, APIErrorCode) {
func validateSignature(atype authType, r *http.Request) (auth.Credentials, bool, APIErrorCode) {
var cred auth.Credentials
var owner bool
var s3Err APIErrorCode
switch atype {
case authTypeUnknown, authTypeStreamingSigned:
return cred, owner, nil, ErrSignatureVersionNotSupported
return cred, owner, ErrSignatureVersionNotSupported
case authTypeSignedV2, authTypePresignedV2:
if s3Err = isReqAuthenticatedV2(r); s3Err != ErrNone {
return cred, owner, nil, s3Err
return cred, owner, s3Err
}
cred, owner, s3Err = getReqAccessKeyV2(r)
case authTypePresigned, authTypeSigned:
region := globalServerRegion
if s3Err = isReqAuthenticated(GlobalContext, r, region, serviceS3); s3Err != ErrNone {
return cred, owner, nil, s3Err
return cred, owner, s3Err
}
cred, owner, s3Err = getReqAccessKeyV4(r, region, serviceS3)
}
if s3Err != ErrNone {
return cred, owner, nil, s3Err
return cred, owner, s3Err
}
claims, s3Err := checkClaimsFromToken(r, cred)
if s3Err != ErrNone {
return cred, owner, nil, s3Err
}
return cred, owner, claims, ErrNone
return cred, owner, ErrNone
}
func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate time.Time, retMode objectlock.RetMode, byPassSet bool, r *http.Request, cred auth.Credentials, owner bool, claims map[string]interface{}) (s3Err APIErrorCode) {
func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate time.Time, retMode objectlock.RetMode, byPassSet bool, r *http.Request, cred auth.Credentials, owner bool) (s3Err APIErrorCode) {
var retSet bool
if cred.AccessKey == "" {
conditions := getConditionValues(r, "", "", nil)
conditions["object-lock-mode"] = []string{string(retMode)}
conditions["object-lock-retain-until-date"] = []string{retDate.Format(time.RFC3339)}
if retDays > 0 {
conditions["object-lock-remaining-retention-days"] = []string{strconv.Itoa(retDays)}
}
if retMode == objectlock.RetGovernance && byPassSet {
byPassSet = globalPolicySys.IsAllowed(policy.Args{
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: policy.BypassGovernanceRetentionAction,
BucketName: bucketName,
ConditionValues: conditions,
IsOwner: false,
ObjectName: objectName,
})
}
if globalPolicySys.IsAllowed(policy.Args{
AccountName: cred.AccessKey,
Groups: cred.Groups,
Action: policy.PutObjectRetentionAction,
BucketName: bucketName,
ConditionValues: conditions,
IsOwner: false,
ObjectName: objectName,
}) {
retSet = true
}
if byPassSet || retSet {
return ErrNone
}
return ErrAccessDenied
}
conditions := getConditionValues(r, "", cred.AccessKey, claims)
conditions := getConditionValues(r, "", cred.AccessKey, cred.Claims)
conditions["object-lock-mode"] = []string{string(retMode)}
conditions["object-lock-retain-until-date"] = []string{retDate.Format(time.RFC3339)}
if retDays > 0 {
@ -603,7 +556,7 @@ func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate t
ObjectName: objectName,
ConditionValues: conditions,
IsOwner: owner,
Claims: claims,
Claims: cred.Claims,
})
}
if globalIAMSys.IsAllowed(iampolicy.Args{
@ -614,7 +567,7 @@ func isPutRetentionAllowed(bucketName, objectName string, retDays int, retDate t
ConditionValues: conditions,
ObjectName: objectName,
IsOwner: owner,
Claims: claims,
Claims: cred.Claims,
}) {
retSet = true
}
@ -643,11 +596,6 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
return s3Err
}
claims, s3Err := checkClaimsFromToken(r, cred)
if s3Err != ErrNone {
return s3Err
}
if cred.AccessKey != "" {
logger.GetReqInfo(ctx).AccessKey = cred.AccessKey
}
@ -681,10 +629,10 @@ func isPutActionAllowed(ctx context.Context, atype authType, bucketName, objectN
Groups: cred.Groups,
Action: action,
BucketName: bucketName,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
ObjectName: objectName,
IsOwner: owner,
Claims: claims,
Claims: cred.Claims,
}) {
return ErrNone
}

View File

@ -19,7 +19,6 @@ package cmd
import (
"bytes"
"crypto/subtle"
"encoding/base64"
"encoding/json"
"encoding/xml"
@ -345,9 +344,6 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
// Set delimiter value for "s3:delimiter" policy conditionals.
r.Header.Set("delimiter", SlashSeparator)
// err will be nil here as we already called this function
// earlier in this request.
claims, _ := getClaimsFromToken(getSessionToken(r))
n := 0
// Use the following trick to filter in place
// https://github.com/golang/go/wiki/SliceTricks#filter-in-place
@ -357,10 +353,10 @@ func (api objectAPIHandlers) ListBucketsHandler(w http.ResponseWriter, r *http.R
Groups: cred.Groups,
Action: iampolicy.ListBucketAction,
BucketName: bucketInfo.Name,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
IsOwner: owner,
ObjectName: "",
Claims: claims,
Claims: cred.Claims,
}) {
bucketsInfo[n] = bucketInfo
n++
@ -899,41 +895,17 @@ func (api objectAPIHandlers) PostPolicyBucketHandler(w http.ResponseWriter, r *h
// Once signature is validated, check if the user has
// explicit permissions for the user.
{
token := formValues.Get(xhttp.AmzSecurityToken)
if token != "" && cred.AccessKey == "" {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrNoAccessKey), r.URL)
return
}
if cred.IsServiceAccount() && token == "" {
token = cred.SessionToken
}
if subtle.ConstantTimeCompare([]byte(token), []byte(cred.SessionToken)) != 1 {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrInvalidToken), r.URL)
return
}
// Extract claims if any.
claims, err := getClaimsFromToken(token)
if err != nil {
writeErrorResponse(ctx, w, toAPIError(ctx, err), r.URL)
return
}
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.PutObjectAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, claims),
BucketName: bucket,
ObjectName: object,
IsOwner: globalActiveCred.AccessKey == cred.AccessKey,
Claims: claims,
}) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
if !globalIAMSys.IsAllowed(iampolicy.Args{
AccountName: cred.AccessKey,
Action: iampolicy.PutObjectAction,
ConditionValues: getConditionValues(r, "", cred.AccessKey, cred.Claims),
BucketName: bucket,
ObjectName: object,
IsOwner: globalActiveCred.AccessKey == cred.AccessKey,
Claims: cred.Claims,
}) {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(ErrAccessDenied), r.URL)
return
}
policyBytes, err := base64.StdEncoding.DecodeString(formValues.Get("Policy"))

View File

@ -175,7 +175,7 @@ func enforceRetentionBypassForDelete(ctx context.Context, r *http.Request, bucke
// For objects in "Governance" mode, overwrite is allowed if a) object retention date is past OR
// governance bypass headers are set and user has governance bypass permissions.
// Objects in compliance mode can be overwritten only if retention date is being extended. No mode change is permitted.
func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool, claims map[string]interface{}) (ObjectInfo, APIErrorCode) {
func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket, object string, getObjectInfoFn GetObjectInfoFn, objRetention *objectlock.ObjectRetention, cred auth.Credentials, owner bool) (ObjectInfo, APIErrorCode) {
byPassSet := objectlock.IsObjectLockGovernanceBypassSet(r.Header)
opts, err := getOpts(ctx, r, bucket, object)
if err != nil {
@ -203,7 +203,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
perm := isPutRetentionAllowed(bucket, object,
days, objRetention.RetainUntilDate.Time,
objRetention.Mode, byPassSet, r, cred,
owner, claims)
owner)
return oi, perm
}
@ -211,7 +211,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
case objectlock.RetGovernance:
govPerm := isPutRetentionAllowed(bucket, object, days,
objRetention.RetainUntilDate.Time, objRetention.Mode,
byPassSet, r, cred, owner, claims)
byPassSet, r, cred, owner)
// Governance mode retention period cannot be shortened, if x-amz-bypass-governance is not set.
if !byPassSet {
if objRetention.Mode != objectlock.RetGovernance || objRetention.RetainUntilDate.Before((ret.RetainUntilDate.Time)) {
@ -227,7 +227,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
}
compliancePerm := isPutRetentionAllowed(bucket, object,
days, objRetention.RetainUntilDate.Time, objRetention.Mode,
false, r, cred, owner, claims)
false, r, cred, owner)
return oi, compliancePerm
}
return oi, ErrNone
@ -235,7 +235,7 @@ func enforceRetentionBypassForPut(ctx context.Context, r *http.Request, bucket,
perm := isPutRetentionAllowed(bucket, object,
days, objRetention.RetainUntilDate.Time,
objRetention.Mode, byPassSet, r, cred, owner, claims)
objRetention.Mode, byPassSet, r, cred, owner)
return oi, perm
}

View File

@ -3502,7 +3502,7 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
return
}
cred, owner, claims, s3Err := validateSignature(getRequestAuthType(r), r)
cred, owner, s3Err := validateSignature(getRequestAuthType(r), r)
if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return
@ -3542,7 +3542,7 @@ func (api objectAPIHandlers) PutObjectRetentionHandler(w http.ResponseWriter, r
getObjectInfo = api.CacheAPI().GetObjectInfo
}
objInfo, s3Err := enforceRetentionBypassForPut(ctx, r, bucket, object, getObjectInfo, objRetention, cred, owner, claims)
objInfo, s3Err := enforceRetentionBypassForPut(ctx, r, bucket, object, getObjectInfo, objRetention, cred, owner)
if s3Err != ErrNone {
writeErrorResponse(ctx, w, errorCodes.ToAPIErr(s3Err), r.URL)
return

View File

@ -78,7 +78,9 @@ const (
// http://docs.aws.amazon.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationStringToSign
func doesPolicySignatureV2Match(formValues http.Header) (auth.Credentials, APIErrorCode) {
accessKey := formValues.Get(xhttp.AmzAccessKeyID)
cred, _, s3Err := checkKeyValid(accessKey)
r := &http.Request{Header: formValues}
cred, _, s3Err := checkKeyValid(r, accessKey)
if s3Err != ErrNone {
return cred, s3Err
}
@ -153,7 +155,7 @@ func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
return ErrInvalidQueryParams
}
cred, _, s3Err := checkKeyValid(accessKey)
cred, _, s3Err := checkKeyValid(r, accessKey)
if s3Err != ErrNone {
return s3Err
}
@ -184,7 +186,7 @@ func doesPresignV2SignatureMatch(r *http.Request) APIErrorCode {
func getReqAccessKeyV2(r *http.Request) (auth.Credentials, bool, APIErrorCode) {
if accessKey := r.Form.Get(xhttp.AmzAccessKeyID); accessKey != "" {
return checkKeyValid(accessKey)
return checkKeyValid(r, accessKey)
}
// below is V2 Signed Auth header format, splitting on `space` (after the `AWS` string).
@ -200,7 +202,7 @@ func getReqAccessKeyV2(r *http.Request) (auth.Credentials, bool, APIErrorCode) {
return auth.Credentials{}, false, ErrMissingFields
}
return checkKeyValid(keySignFields[0])
return checkKeyValid(r, keySignFields[0])
}
// Authorization = "AWS" + " " + AWSAccessKeyId + ":" + Signature;

View File

@ -63,7 +63,7 @@ func getReqAccessKeyV4(r *http.Request, region string, stype serviceType) (auth.
return auth.Credentials{}, false, s3Err
}
}
return checkKeyValid(ch.accessKey)
return checkKeyValid(r, ch.accessKey)
}
// parse credentialHeader string into its structured form.

View File

@ -31,6 +31,7 @@ import (
"github.com/minio/minio/internal/auth"
xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/iam/policy"
)
// http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" indicates that the
@ -120,7 +121,7 @@ func isValidRegion(reqRegion string, confRegion string) bool {
// check if the access key is valid and recognized, additionally
// also returns if the access key is owner/admin.
func checkKeyValid(accessKey string) (auth.Credentials, bool, APIErrorCode) {
func checkKeyValid(r *http.Request, accessKey string) (auth.Credentials, bool, APIErrorCode) {
if !globalIAMSys.Initialized() && !globalIsGateway {
// Check if server has initialized, then only proceed
// to check for IAM users otherwise its okay for clients
@ -135,7 +136,17 @@ func checkKeyValid(accessKey string) (auth.Credentials, bool, APIErrorCode) {
if !ok {
return cred, false, ErrInvalidAccessKeyID
}
owner = cred.AccessKey == ucred.ParentUser
claims, s3Err := checkClaimsFromToken(r, cred)
if s3Err != ErrNone {
return cred, false, s3Err
}
ucred.Claims = claims
// Now check if we have a sessionPolicy.
if _, ok = claims[iampolicy.SessionPolicyName]; ok {
owner = false
} else {
owner = cred.AccessKey == ucred.ParentUser
}
cred = ucred
}
return cred, owner, ErrNone

View File

@ -181,7 +181,8 @@ func doesPolicySignatureV4Match(formValues http.Header) (auth.Credentials, APIEr
return auth.Credentials{}, s3Err
}
cred, _, s3Err := checkKeyValid(credHeader.accessKey)
r := &http.Request{Header: formValues}
cred, _, s3Err := checkKeyValid(r, credHeader.accessKey)
if s3Err != ErrNone {
return cred, s3Err
}
@ -214,7 +215,7 @@ func doesPresignedSignatureMatch(hashedPayload string, r *http.Request, region s
return err
}
cred, _, s3Err := checkKeyValid(pSignValues.Credential.accessKey)
cred, _, s3Err := checkKeyValid(r, pSignValues.Credential.accessKey)
if s3Err != ErrNone {
return s3Err
}
@ -349,7 +350,7 @@ func doesSignatureMatch(hashedPayload string, r *http.Request, region string, st
return errCode
}
cred, _, s3Err := checkKeyValid(signV4Values.Credential.accessKey)
cred, _, s3Err := checkKeyValid(r, signV4Values.Credential.accessKey)
if s3Err != ErrNone {
return s3Err
}

View File

@ -93,7 +93,7 @@ func calculateSeedSignature(r *http.Request) (cred auth.Credentials, signature s
return cred, "", "", time.Time{}, errCode
}
cred, _, errCode = checkKeyValid(signV4Values.Credential.accessKey)
cred, _, errCode = checkKeyValid(r, signV4Values.Credential.accessKey)
if errCode != ErrNone {
return cred, "", "", time.Time{}, errCode
}

View File

@ -94,13 +94,14 @@ const (
// Credentials holds access and secret keys.
type Credentials struct {
AccessKey string `xml:"AccessKeyId" json:"accessKey,omitempty"`
SecretKey string `xml:"SecretAccessKey" json:"secretKey,omitempty"`
Expiration time.Time `xml:"Expiration" json:"expiration,omitempty"`
SessionToken string `xml:"SessionToken" json:"sessionToken,omitempty"`
Status string `xml:"-" json:"status,omitempty"`
ParentUser string `xml:"-" json:"parentUser,omitempty"`
Groups []string `xml:"-" json:"groups,omitempty"`
AccessKey string `xml:"AccessKeyId" json:"accessKey,omitempty"`
SecretKey string `xml:"SecretAccessKey" json:"secretKey,omitempty"`
Expiration time.Time `xml:"Expiration" json:"expiration,omitempty"`
SessionToken string `xml:"SessionToken" json:"sessionToken,omitempty"`
Status string `xml:"-" json:"status,omitempty"`
ParentUser string `xml:"-" json:"parentUser,omitempty"`
Groups []string `xml:"-" json:"groups,omitempty"`
Claims map[string]interface{} `xml:"-" json:"claims,omitempty"`
}
func (cred Credentials) String() string {