diff --git a/.github/workflows/root-disable.yml b/.github/workflows/root-disable.yml new file mode 100644 index 000000000..7594a8887 --- /dev/null +++ b/.github/workflows/root-disable.yml @@ -0,0 +1,34 @@ +name: Root lockdown tests + +on: + pull_request: + branches: + - master + +# This ensures that previous jobs for the PR are canceled when the PR is +# updated. +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + build: + name: Go ${{ matrix.go-version }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + go-version: [1.20.x] + os: [ubuntu-latest] + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-go@v3 + with: + go-version: ${{ matrix.go-version }} + check-latest: true + - name: Start root lockdown tests + run: | + make test-root-disable diff --git a/Makefile b/Makefile index c4709f713..cb5eea2df 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,10 @@ test: verifiers build ## builds minio, runs linters, tests @echo "Running unit tests" @MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue ./... +test-root-disable: install + @echo "Running minio root lockdown tests" + @env bash $(PWD)/buildscripts/disable-root.sh + test-decom: install @echo "Running minio decom tests" @env bash $(PWD)/docs/distributed/decom.sh diff --git a/buildscripts/disable-root.sh b/buildscripts/disable-root.sh new file mode 100755 index 000000000..7f9068eca --- /dev/null +++ b/buildscripts/disable-root.sh @@ -0,0 +1,119 @@ +#!/bin/bash + +set -x + +export MINIO_CI_CD=1 +killall -9 minio + +rm -rf ${HOME}/tmp/dist + +scheme="http" +nr_servers=4 + +addr="localhost" +args="" +for ((i=0;i<$[${nr_servers}];i++)); do + args="$args $scheme://$addr:$[9100+$i]/${HOME}/tmp/dist/path1/$i" +done + +echo $args + +for ((i=0;i<$[${nr_servers}];i++)); do + (minio server --address ":$[9100+$i]" $args 2>&1 > /tmp/log$i.txt) & +done + +sleep 10s + +if [ ! -f ./mc ]; then + wget --quiet -O ./mc https://dl.minio.io/client/mc/release/linux-amd64/./mc && \ + chmod +x mc +fi + +set +e + +export MC_HOST_minioadm=http://minioadmin:minioadmin@localhost:9100/ + +./mc ls minioadm/ + +./mc admin config set minioadm/ api root_access=off + +sleep 3s # let things settle a little + +./mc ls minioadm/ +if [ $? -eq 0 ]; then + echo "listing succeeded, 'minioadmin' was not disabled" + exit 1 +fi + +set -e + +killall -9 minio + +export MINIO_API_ROOT_ACCESS=on +for ((i=0;i<$[${nr_servers}];i++)); do + (minio server --address ":$[9100+$i]" $args 2>&1 > /tmp/log$i.txt) & +done + +set +e + +./mc ls minioadm/ +if [ $? -ne 0 ]; then + echo "listing failed, 'minioadmin' should be enabled" + exit 1 +fi + +killall -9 minio + +rm -rf /tmp/multisitea/ +rm -rf /tmp/multisiteb/ + +echo "Setup site-replication and then disable root credentials" + +minio server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \ + "http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 & +minio server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \ + "http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 & + +minio server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \ + "http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 & +minio server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \ + "http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 & + +sleep 20s + +export MC_HOST_sitea=http://minioadmin:minioadmin@127.0.0.1:9001 +export MC_HOST_siteb=http://minioadmin:minioadmin@127.0.0.1:9004 + +./mc admin replicate add sitea siteb + +./mc admin user add sitea foobar foo12345 + +./mc admin policy attach sitea/ consoleAdmin --user=foobar + +./mc admin user info siteb foobar + +killall -9 minio + +echo "turning off root access, however site replication must continue" +export MINIO_API_ROOT_ACCESS=off + +minio server --address 127.0.0.1:9001 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \ + "http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_1.log 2>&1 & +minio server --address 127.0.0.1:9002 "http://127.0.0.1:9001/tmp/multisitea/data/disterasure/xl{1...4}" \ + "http://127.0.0.1:9002/tmp/multisitea/data/disterasure/xl{5...8}" >/tmp/sitea_2.log 2>&1 & + +minio server --address 127.0.0.1:9003 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \ + "http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_1.log 2>&1 & +minio server --address 127.0.0.1:9004 "http://127.0.0.1:9003/tmp/multisiteb/data/disterasure/xl{1...4}" \ + "http://127.0.0.1:9004/tmp/multisiteb/data/disterasure/xl{5...8}" >/tmp/siteb_2.log 2>&1 & + +sleep 20s + +export MC_HOST_sitea=http://foobar:foo12345@127.0.0.1:9001 +export MC_HOST_siteb=http://foobar:foo12345@127.0.0.1:9004 + +./mc admin user add sitea foobar-admin foo12345 + +sleep 2s + +./mc admin user info siteb foobar-admin diff --git a/cmd/admin-handlers-users.go b/cmd/admin-handlers-users.go index 391c3887c..9f085946b 100644 --- a/cmd/admin-handlers-users.go +++ b/cmd/admin-handlers-users.go @@ -44,7 +44,7 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) { defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) - objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction) + objectAPI, cred := validateAdminReq(ctx, w, r, iampolicy.DeleteUserAdminAction) if objectAPI == nil { return } @@ -62,6 +62,13 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) { return } + // When the user is root credential you are not allowed to + // remove the root user. Also you cannot delete yourself. + if accessKey == globalActiveCred.AccessKey || accessKey == cred.AccessKey { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) + return + } + if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil { writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) return @@ -239,6 +246,26 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) return } + + // Reject if the group add and remove are temporary credentials, or root credential. + for _, member := range updReq.Members { + ok, _, err := globalIAMSys.IsTempUser(member) + if err != nil && err != errNoSuchUser { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) + return + } + if ok { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) + return + } + // When the user is root credential you are not allowed to + // add policies for root user. + if member == globalActiveCred.AccessKey { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) + return + } + } + var updatedAt time.Time if updReq.IsRemove { updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members) @@ -374,7 +401,7 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) - objectAPI, _ := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction) + objectAPI, creds := validateAdminReq(ctx, w, r, iampolicy.EnableUserAdminAction) if objectAPI == nil { return } @@ -383,9 +410,9 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request) accessKey := vars["accessKey"] status := vars["status"] - // This API is not allowed to lookup master access key user status - if accessKey == globalActiveCred.AccessKey { - writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) + // you cannot enable or disable yourself. + if accessKey == creds.AccessKey { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL) return } @@ -1627,6 +1654,12 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) return } + // When the user is root credential you are not allowed to + // add policies for root user. + if entityName == globalActiveCred.AccessKey { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) + return + } } // Validate that user or group exists. @@ -1771,6 +1804,13 @@ func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Req return } + // When the user is root credential you are not allowed to + // add policies for root user. + if userOrGroup == globalActiveCred.AccessKey { + writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) + return + } + // Validate that user exists. if globalIAMSys.GetUsersSysType() == MinIOUsersSysType { _, ok := globalIAMSys.GetUser(ctx, userOrGroup) @@ -2056,13 +2096,15 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) { } userAccounts := make(map[string]madmin.AddOrUpdateUserReq) for u, uid := range userIdentities { - status := madmin.AccountDisabled - if uid.Credentials.IsValid() { - status = madmin.AccountEnabled - } userAccounts[u] = madmin.AddOrUpdateUserReq{ SecretKey: uid.Credentials.SecretKey, - Status: status, + Status: func() madmin.AccountStatus { + // Export current credential status + if uid.Credentials.Status == auth.AccountOff { + return madmin.AccountDisabled + } + return madmin.AccountEnabled + }(), } } userData, err := json.Marshal(userAccounts) @@ -2101,6 +2143,10 @@ func (a adminAPIHandlers) ExportIAM(w http.ResponseWriter, r *http.Request) { } svcAccts := make(map[string]madmin.SRSvcAccCreate) for user, acc := range serviceAccounts { + if user == siteReplicatorSvcAcc { + // skip site-replication service account. + continue + } claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey) if err != nil { writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) @@ -2461,12 +2507,13 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) { continue } opts := newServiceAccountOpts{ - accessKey: user, - secretKey: svcAcctReq.SecretKey, - sessionPolicy: sp, - claims: svcAcctReq.Claims, - comment: svcAcctReq.Comment, - expiration: svcAcctReq.Expiration, + accessKey: user, + secretKey: svcAcctReq.SecretKey, + sessionPolicy: sp, + claims: svcAcctReq.Claims, + comment: svcAcctReq.Comment, + expiration: svcAcctReq.Expiration, + allowSiteReplicatorAccount: false, } // In case of LDAP we need to resolve the targetUser to a DN and diff --git a/cmd/auth-handler_test.go b/cmd/auth-handler_test.go index 254c4c35d..3c8e6a85a 100644 --- a/cmd/auth-handler_test.go +++ b/cmd/auth-handler_test.go @@ -375,8 +375,6 @@ func TestIsReqAuthenticated(t *testing.T) { initConfigSubsystem(ctx, objLayer) - globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second) - creds, err := auth.CreateCredentials("myuser", "mypassword") if err != nil { t.Fatalf("unable create credential, %s", err) @@ -384,6 +382,8 @@ func TestIsReqAuthenticated(t *testing.T) { globalActiveCred = creds + globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second) + // List of test cases for validating http request authentication. testCases := []struct { req *http.Request @@ -464,9 +464,8 @@ func TestValidateAdminSignature(t *testing.T) { } initAllSubsystems(ctx) - initConfigSubsystem(ctx, objLayer) - globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second) + initConfigSubsystem(ctx, objLayer) creds, err := auth.CreateCredentials("admin", "mypassword") if err != nil { @@ -474,6 +473,8 @@ func TestValidateAdminSignature(t *testing.T) { } globalActiveCred = creds + globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second) + testCases := []struct { AccessKey string SecretKey string diff --git a/cmd/globals.go b/cmd/globals.go index 9124c4b5d..3acf67791 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -197,7 +197,7 @@ var ( globalBucketTargetSys *BucketTargetSys // globalAPIConfig controls S3 API requests throttling, // healthcheck readiness deadlines and cors settings. - globalAPIConfig = apiConfig{listQuorum: "strict"} + globalAPIConfig = apiConfig{listQuorum: "strict", rootAccess: true} globalStorageClass storageclass.Config diff --git a/cmd/handler-api.go b/cmd/handler-api.go index 182e9892b..29fe8f17d 100644 --- a/cmd/handler-api.go +++ b/cmd/handler-api.go @@ -51,6 +51,7 @@ type apiConfig struct { deleteCleanupInterval time.Duration disableODirect bool gzipObjects bool + rootAccess bool } const cgroupLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes" @@ -152,6 +153,7 @@ func (t *apiConfig) init(cfg api.Config, setDriveCounts []int) { t.deleteCleanupInterval = cfg.DeleteCleanupInterval t.disableODirect = cfg.DisableODirect t.gzipObjects = cfg.GzipObjects + t.rootAccess = cfg.RootAccess } func (t *apiConfig) isDisableODirect() bool { @@ -168,6 +170,13 @@ func (t *apiConfig) shouldGzipObjects() bool { return t.gzipObjects } +func (t *apiConfig) permitRootAccess() bool { + t.mu.RLock() + defer t.mu.RUnlock() + + return t.rootAccess +} + func (t *apiConfig) getListQuorum() string { t.mu.RLock() defer t.mu.RUnlock() diff --git a/cmd/iam.go b/cmd/iam.go index 0f0450f08..4f5b2b0a2 100644 --- a/cmd/iam.go +++ b/cmd/iam.go @@ -914,11 +914,12 @@ func (sys *IAMSys) notifyForServiceAccount(ctx context.Context, accessKey string } type newServiceAccountOpts struct { - sessionPolicy *iampolicy.Policy - accessKey string - secretKey string - comment string - expiration *time.Time + sessionPolicy *iampolicy.Policy + accessKey string + secretKey string + comment string + expiration *time.Time + allowSiteReplicatorAccount bool // allow creating internal service account for site-replication. claims map[string]interface{} } @@ -953,7 +954,9 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro if parentUser == opts.accessKey { return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed } - + if siteReplicatorSvcAcc == opts.accessKey && !opts.allowSiteReplicatorAccount { + return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed + } m := make(map[string]interface{}) m[parentClaim] = parentUser diff --git a/cmd/jwt.go b/cmd/jwt.go index cbbd4e988..154c92eba 100644 --- a/cmd/jwt.go +++ b/cmd/jwt.go @@ -18,7 +18,6 @@ package cmd import ( - "context" "errors" "net/http" "time" @@ -40,48 +39,15 @@ const ( // Inter-node JWT token expiry is 15 minutes. defaultInterNodeJWTExpiry = 15 * time.Minute - - // URL JWT token expiry is one minute (might be exposed). - defaultURLJWTExpiry = time.Minute ) var ( errInvalidAccessKeyID = errors.New("The access key ID you provided does not exist in our records") + errAccessKeyDisabled = errors.New("The access key you provided is disabled") errAuthentication = errors.New("Authentication failed, check your access credentials") errNoAuthToken = errors.New("JWT token missing") ) -func authenticateJWTUsers(accessKey, secretKey string, expiry time.Duration) (string, error) { - passedCredential, err := auth.CreateCredentials(accessKey, secretKey) - if err != nil { - return "", err - } - expiresAt := UTCNow().Add(expiry) - return authenticateJWTUsersWithCredentials(passedCredential, expiresAt) -} - -func authenticateJWTUsersWithCredentials(credentials auth.Credentials, expiresAt time.Time) (string, error) { - serverCred := globalActiveCred - if serverCred.AccessKey != credentials.AccessKey { - u, ok := globalIAMSys.GetUser(context.TODO(), credentials.AccessKey) - if !ok { - return "", errInvalidAccessKeyID - } - serverCred = u.Credentials - } - - if !serverCred.Equal(credentials) { - return "", errAuthentication - } - - claims := xjwt.NewMapClaims() - claims.SetExpiry(expiresAt) - claims.SetAccessKey(credentials.AccessKey) - - jwt := jwtgo.NewWithClaims(jwtgo.SigningMethodHS512, claims) - return jwt.SignedString([]byte(serverCred.SecretKey)) -} - // cachedAuthenticateNode will cache authenticateNode results for given values up to ttl. func cachedAuthenticateNode(ttl time.Duration) func(accessKey, secretKey, audience string) (string, error) { type key struct { @@ -121,14 +87,6 @@ func authenticateNode(accessKey, secretKey, audience string) (string, error) { return jwt.SignedString([]byte(secretKey)) } -func authenticateWeb(accessKey, secretKey string) (string, error) { - return authenticateJWTUsers(accessKey, secretKey, defaultJWTExpiry) -} - -func authenticateURL(accessKey, secretKey string) (string, error) { - return authenticateJWTUsers(accessKey, secretKey, defaultURLJWTExpiry) -} - // Check if the request is authenticated. // Returns nil if the request is authenticated. errNoAuthToken if token missing. // Returns errAuthentication for all other errors. @@ -142,15 +100,24 @@ func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, b } claims := xjwt.NewMapClaims() if err := xjwt.ParseWithClaims(token, claims, func(claims *xjwt.MapClaims) ([]byte, error) { - if claims.AccessKey == globalActiveCred.AccessKey { - return []byte(globalActiveCred.SecretKey), nil + if claims.AccessKey != globalActiveCred.AccessKey { + u, ok := globalIAMSys.GetUser(req.Context(), claims.AccessKey) + if !ok { + // Credentials will be invalid but for disabled + // return a different error in such a scenario. + if u.Credentials.Status == auth.AccountOff { + return nil, errAccessKeyDisabled + } + return nil, errInvalidAccessKeyID + } + cred := u.Credentials + return []byte(cred.SecretKey), nil + } // this means claims.AccessKey == rootAccessKey + if !globalAPIConfig.permitRootAccess() { + // if root access is disabled, fail this request. + return nil, errAccessKeyDisabled } - u, ok := globalIAMSys.GetUser(req.Context(), claims.AccessKey) - if !ok { - return nil, errInvalidAccessKeyID - } - cred := u.Credentials - return []byte(cred.SecretKey), nil + return []byte(globalActiveCred.SecretKey), nil }); err != nil { return claims, nil, false, errAuthentication } @@ -173,6 +140,11 @@ func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, b claims.MapClaims[k] = v } + // if root access is disabled, disable all its service accounts and temporary credentials. + if ucred.ParentUser == globalActiveCred.AccessKey && !globalAPIConfig.permitRootAccess() { + return nil, nil, false, errAccessKeyDisabled + } + // Now check if we have a sessionPolicy. if _, ok = eclaims[iampolicy.SessionPolicyName]; ok { owner = false diff --git a/cmd/jwt_test.go b/cmd/jwt_test.go index a40e7d7de..2e74547f0 100644 --- a/cmd/jwt_test.go +++ b/cmd/jwt_test.go @@ -25,78 +25,9 @@ import ( "time" jwtgo "github.com/golang-jwt/jwt/v4" - "github.com/minio/minio/internal/auth" xjwt "github.com/minio/minio/internal/jwt" ) -func testAuthenticate(authType string, t *testing.T) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - obj, fsDir, err := prepareFS(ctx) - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(fsDir) - if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil { - t.Fatal(err) - } - - cred, err := auth.GetNewCredentials() - if err != nil { - t.Fatalf("Error getting new credentials: %s", err) - } - - globalActiveCred = cred - - // Define test cases. - testCases := []struct { - accessKey string - secretKey string - expectedErr error - }{ - // Access key (less than 3 chrs) too small. - {"u1", cred.SecretKey, auth.ErrInvalidAccessKeyLength}, - // Secret key (less than 8 chrs) too small. - {cred.AccessKey, "pass", auth.ErrInvalidSecretKeyLength}, - // Authentication error. - {"myuser", "mypassword", errInvalidAccessKeyID}, - // Authentication error. - {cred.AccessKey, "mypassword", errAuthentication}, - // Success. - {cred.AccessKey, cred.SecretKey, nil}, - } - - // Run tests. - for _, testCase := range testCases { - var err error - if authType == "web" { - _, err = authenticateWeb(testCase.accessKey, testCase.secretKey) - } else if authType == "url" { - _, err = authenticateURL(testCase.accessKey, testCase.secretKey) - } - - if testCase.expectedErr != nil { - if err == nil { - t.Fatalf("%+v: expected: %s, got: ", testCase, testCase.expectedErr) - } - if testCase.expectedErr.Error() != err.Error() { - t.Fatalf("%+v: expected: %s, got: %s", testCase, testCase.expectedErr, err) - } - } else if err != nil { - t.Fatalf("%+v: expected: , got: %s", testCase, err) - } - } -} - -func TestAuthenticateWeb(t *testing.T) { - testAuthenticate("web", t) -} - -func TestAuthenticateURL(t *testing.T) { - testAuthenticate("url", t) -} - func getTokenString(accessKey, secretKey string) (string, error) { claims := xjwt.NewMapClaims() claims.SetExpiry(UTCNow().Add(defaultJWTExpiry)) @@ -258,24 +189,3 @@ func BenchmarkAuthenticateNode(b *testing.B) { } }) } - -func BenchmarkAuthenticateWeb(b *testing.B) { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - obj, fsDir, err := prepareFS(ctx) - if err != nil { - b.Fatal(err) - } - defer os.RemoveAll(fsDir) - if err = newTestConfig(globalMinioDefaultRegion, obj); err != nil { - b.Fatal(err) - } - - creds := globalActiveCred - b.ResetTimer() - b.ReportAllocs() - for i := 0; i < b.N; i++ { - authenticateWeb(creds.AccessKey, creds.SecretKey) - } -} diff --git a/cmd/signature-v4-utils.go b/cmd/signature-v4-utils.go index d51d362da..646ff9f6c 100644 --- a/cmd/signature-v4-utils.go +++ b/cmd/signature-v4-utils.go @@ -30,6 +30,7 @@ import ( "github.com/minio/minio/internal/hash/sha256" 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 @@ -169,7 +170,16 @@ func checkKeyValid(r *http.Request, accessKey string) (auth.Credentials, bool, A } cred.Claims = claims - owner := cred.AccessKey == globalActiveCred.AccessKey + owner := cred.AccessKey == globalActiveCred.AccessKey || (cred.ParentUser == globalActiveCred.AccessKey && cred.AccessKey != siteReplicatorSvcAcc) + if owner && !globalAPIConfig.permitRootAccess() { + // We disable root access and its service accounts if asked for. + return cred, owner, ErrAccessKeyDisabled + } + + if _, ok := claims[iampolicy.SessionPolicyName]; ok { + owner = false + } + return cred, owner, ErrNone } diff --git a/cmd/site-replication.go b/cmd/site-replication.go index 73c9ead52..f5e965bfb 100644 --- a/cmd/site-replication.go +++ b/cmd/site-replication.go @@ -459,8 +459,9 @@ func (c *SiteReplicationSys) AddPeerClusters(ctx context.Context, psites []madmi return madmin.ReplicateAddStatus{}, errSRServiceAccount(fmt.Errorf("unable to create local service account: %w", err)) } svcCred, _, err = globalIAMSys.NewServiceAccount(ctx, sites[selfIdx].AccessKey, nil, newServiceAccountOpts{ - accessKey: siteReplicatorSvcAcc, - secretKey: secretKey, + accessKey: siteReplicatorSvcAcc, + secretKey: secretKey, + allowSiteReplicatorAccount: true, }) if err != nil { return madmin.ReplicateAddStatus{}, errSRServiceAccount(fmt.Errorf("unable to create local service account: %w", err)) @@ -558,8 +559,7 @@ func (c *SiteReplicationSys) AddPeerClusters(ctx context.Context, psites []madmi return result, nil } -// PeerJoinReq - internal API handler to respond to a peer cluster's request -// to join. +// PeerJoinReq - internal API handler to respond to a peer cluster's request to join. func (c *SiteReplicationSys) PeerJoinReq(ctx context.Context, arg madmin.SRPeerJoinReq) error { var ourName string for d, p := range arg.Peers { @@ -575,8 +575,9 @@ func (c *SiteReplicationSys) PeerJoinReq(ctx context.Context, arg madmin.SRPeerJ _, _, err := globalIAMSys.GetServiceAccount(ctx, arg.SvcAcctAccessKey) if err == errNoSuchServiceAccount { _, _, err = globalIAMSys.NewServiceAccount(ctx, arg.SvcAcctParent, nil, newServiceAccountOpts{ - accessKey: arg.SvcAcctAccessKey, - secretKey: arg.SvcAcctSecretKey, + accessKey: arg.SvcAcctAccessKey, + secretKey: arg.SvcAcctSecretKey, + allowSiteReplicatorAccount: arg.SvcAcctAccessKey == siteReplicatorSvcAcc, }) } if err != nil { diff --git a/internal/config/api/api.go b/internal/config/api/api.go index 2b92cf49c..3509379fc 100644 --- a/internal/config/api/api.go +++ b/internal/config/api/api.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2021 MinIO, Inc. +// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // @@ -45,6 +45,7 @@ const ( apiDeleteCleanupInterval = "delete_cleanup_interval" apiDisableODirect = "disable_odirect" apiGzipObjects = "gzip_objects" + apiRootAccess = "root_access" EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX" EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE" @@ -61,6 +62,7 @@ const ( EnvDeleteCleanupInterval = "MINIO_DELETE_CLEANUP_INTERVAL" EnvAPIDisableODirect = "MINIO_API_DISABLE_ODIRECT" EnvAPIGzipObjects = "MINIO_API_GZIP_OBJECTS" + EnvAPIRootAccess = "MINIO_API_ROOT_ACCESS" // default "on" ) // Deprecated key and ENVs @@ -130,6 +132,10 @@ var ( Key: apiGzipObjects, Value: "off", }, + config.KV{ + Key: apiRootAccess, + Value: "on", + }, } ) @@ -148,6 +154,7 @@ type Config struct { DeleteCleanupInterval time.Duration `json:"delete_cleanup_interval"` DisableODirect bool `json:"disable_odirect"` GzipObjects bool `json:"gzip_objects"` + RootAccess bool `json:"root_access"` } // UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON. @@ -247,6 +254,7 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) { disableODirect := env.Get(EnvAPIDisableODirect, kvs.Get(apiDisableODirect)) == config.EnableOn gzipObjects := env.Get(EnvAPIGzipObjects, kvs.Get(apiGzipObjects)) == config.EnableOn + rootAccess := env.Get(EnvAPIRootAccess, kvs.Get(apiRootAccess)) == config.EnableOn return Config{ RequestsMax: requestsMax, @@ -262,5 +270,6 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) { DeleteCleanupInterval: deleteCleanupInterval, DisableODirect: disableODirect, GzipObjects: gzipObjects, + RootAccess: rootAccess, }, nil } diff --git a/internal/config/api/help.go b/internal/config/api/help.go index 35cabc876..db48ed29c 100644 --- a/internal/config/api/help.go +++ b/internal/config/api/help.go @@ -1,4 +1,4 @@ -// Copyright (c) 2015-2021 MinIO, Inc. +// Copyright (c) 2015-2023 MinIO, Inc. // // This file is part of MinIO Object Storage stack // @@ -94,7 +94,13 @@ var ( }, config.HelpKV{ Key: apiDisableODirect, - Description: "set to disable O_DIRECT for reads under special conditions. NOTE: it is not recommended to disable O_DIRECT without prior testing." + defaultHelpPostfix(apiDisableODirect), + Description: "set to disable O_DIRECT for reads under special conditions. NOTE: it is not recommended to disable O_DIRECT without prior testing" + defaultHelpPostfix(apiDisableODirect), + Optional: true, + Type: "boolean", + }, + config.HelpKV{ + Key: apiRootAccess, + Description: "turn 'off' root credential access for all API calls including s3, admin operations" + defaultHelpPostfix(apiRootAccess), Optional: true, Type: "boolean", },