allow root user to be disabled via config settings (#17089)

This commit is contained in:
Harshavardhana 2023-04-28 12:24:14 -07:00 committed by GitHub
parent 701b89f377
commit 7ae69accc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 303 additions and 178 deletions

34
.github/workflows/root-disable.yml vendored Normal file
View File

@ -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

View File

@ -49,6 +49,10 @@ test: verifiers build ## builds minio, runs linters, tests
@echo "Running unit tests" @echo "Running unit tests"
@MINIO_API_REQUESTS_MAX=10000 CGO_ENABLED=0 go test -tags kqueue ./... @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 test-decom: install
@echo "Running minio decom tests" @echo "Running minio decom tests"
@env bash $(PWD)/docs/distributed/decom.sh @env bash $(PWD)/docs/distributed/decom.sh

119
buildscripts/disable-root.sh Executable file
View File

@ -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

View File

@ -44,7 +44,7 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
defer logger.AuditLog(ctx, w, r, mustGetClaimsFromToken(r)) 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 { if objectAPI == nil {
return return
} }
@ -62,6 +62,13 @@ func (a adminAPIHandlers) RemoveUser(w http.ResponseWriter, r *http.Request) {
return 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 { if err := globalIAMSys.DeleteUser(ctx, accessKey, true); err != nil {
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, err), r.URL)
return return
@ -239,6 +246,26 @@ func (a adminAPIHandlers) UpdateGroupMembers(w http.ResponseWriter, r *http.Requ
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL)
return 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 var updatedAt time.Time
if updReq.IsRemove { if updReq.IsRemove {
updatedAt, err = globalIAMSys.RemoveUsersFromGroup(ctx, updReq.Group, updReq.Members) 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)) 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 { if objectAPI == nil {
return return
} }
@ -383,9 +410,9 @@ func (a adminAPIHandlers) SetUserStatus(w http.ResponseWriter, r *http.Request)
accessKey := vars["accessKey"] accessKey := vars["accessKey"]
status := vars["status"] status := vars["status"]
// This API is not allowed to lookup master access key user status // you cannot enable or disable yourself.
if accessKey == globalActiveCred.AccessKey { if accessKey == creds.AccessKey {
writeErrorResponseJSON(ctx, w, errorCodes.ToAPIErr(ErrInvalidRequest), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errInvalidArgument), r.URL)
return return
} }
@ -1627,6 +1654,12 @@ func (a adminAPIHandlers) SetPolicyForUserOrGroup(w http.ResponseWriter, r *http
writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL) writeErrorResponseJSON(ctx, w, toAdminAPIErr(ctx, errIAMActionNotAllowed), r.URL)
return 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. // Validate that user or group exists.
@ -1771,6 +1804,13 @@ func (a adminAPIHandlers) AttachPolicyBuiltin(w http.ResponseWriter, r *http.Req
return 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. // Validate that user exists.
if globalIAMSys.GetUsersSysType() == MinIOUsersSysType { if globalIAMSys.GetUsersSysType() == MinIOUsersSysType {
_, ok := globalIAMSys.GetUser(ctx, userOrGroup) _, 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) userAccounts := make(map[string]madmin.AddOrUpdateUserReq)
for u, uid := range userIdentities { for u, uid := range userIdentities {
status := madmin.AccountDisabled
if uid.Credentials.IsValid() {
status = madmin.AccountEnabled
}
userAccounts[u] = madmin.AddOrUpdateUserReq{ userAccounts[u] = madmin.AddOrUpdateUserReq{
SecretKey: uid.Credentials.SecretKey, 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) 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) svcAccts := make(map[string]madmin.SRSvcAccCreate)
for user, acc := range serviceAccounts { for user, acc := range serviceAccounts {
if user == siteReplicatorSvcAcc {
// skip site-replication service account.
continue
}
claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey) claims, err := globalIAMSys.GetClaimsForSvcAcc(ctx, acc.Credentials.AccessKey)
if err != nil { if err != nil {
writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL) writeErrorResponse(ctx, w, exportError(ctx, err, iamFile, ""), r.URL)
@ -2461,12 +2507,13 @@ func (a adminAPIHandlers) ImportIAM(w http.ResponseWriter, r *http.Request) {
continue continue
} }
opts := newServiceAccountOpts{ opts := newServiceAccountOpts{
accessKey: user, accessKey: user,
secretKey: svcAcctReq.SecretKey, secretKey: svcAcctReq.SecretKey,
sessionPolicy: sp, sessionPolicy: sp,
claims: svcAcctReq.Claims, claims: svcAcctReq.Claims,
comment: svcAcctReq.Comment, comment: svcAcctReq.Comment,
expiration: svcAcctReq.Expiration, expiration: svcAcctReq.Expiration,
allowSiteReplicatorAccount: false,
} }
// In case of LDAP we need to resolve the targetUser to a DN and // In case of LDAP we need to resolve the targetUser to a DN and

View File

@ -375,8 +375,6 @@ func TestIsReqAuthenticated(t *testing.T) {
initConfigSubsystem(ctx, objLayer) initConfigSubsystem(ctx, objLayer)
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
creds, err := auth.CreateCredentials("myuser", "mypassword") creds, err := auth.CreateCredentials("myuser", "mypassword")
if err != nil { if err != nil {
t.Fatalf("unable create credential, %s", err) t.Fatalf("unable create credential, %s", err)
@ -384,6 +382,8 @@ func TestIsReqAuthenticated(t *testing.T) {
globalActiveCred = creds globalActiveCred = creds
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
// List of test cases for validating http request authentication. // List of test cases for validating http request authentication.
testCases := []struct { testCases := []struct {
req *http.Request req *http.Request
@ -464,9 +464,8 @@ func TestValidateAdminSignature(t *testing.T) {
} }
initAllSubsystems(ctx) initAllSubsystems(ctx)
initConfigSubsystem(ctx, objLayer)
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second) initConfigSubsystem(ctx, objLayer)
creds, err := auth.CreateCredentials("admin", "mypassword") creds, err := auth.CreateCredentials("admin", "mypassword")
if err != nil { if err != nil {
@ -474,6 +473,8 @@ func TestValidateAdminSignature(t *testing.T) {
} }
globalActiveCred = creds globalActiveCred = creds
globalIAMSys.Init(ctx, objLayer, globalEtcdClient, 2*time.Second)
testCases := []struct { testCases := []struct {
AccessKey string AccessKey string
SecretKey string SecretKey string

View File

@ -197,7 +197,7 @@ var (
globalBucketTargetSys *BucketTargetSys globalBucketTargetSys *BucketTargetSys
// globalAPIConfig controls S3 API requests throttling, // globalAPIConfig controls S3 API requests throttling,
// healthcheck readiness deadlines and cors settings. // healthcheck readiness deadlines and cors settings.
globalAPIConfig = apiConfig{listQuorum: "strict"} globalAPIConfig = apiConfig{listQuorum: "strict", rootAccess: true}
globalStorageClass storageclass.Config globalStorageClass storageclass.Config

View File

@ -51,6 +51,7 @@ type apiConfig struct {
deleteCleanupInterval time.Duration deleteCleanupInterval time.Duration
disableODirect bool disableODirect bool
gzipObjects bool gzipObjects bool
rootAccess bool
} }
const cgroupLimitFile = "/sys/fs/cgroup/memory/memory.limit_in_bytes" 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.deleteCleanupInterval = cfg.DeleteCleanupInterval
t.disableODirect = cfg.DisableODirect t.disableODirect = cfg.DisableODirect
t.gzipObjects = cfg.GzipObjects t.gzipObjects = cfg.GzipObjects
t.rootAccess = cfg.RootAccess
} }
func (t *apiConfig) isDisableODirect() bool { func (t *apiConfig) isDisableODirect() bool {
@ -168,6 +170,13 @@ func (t *apiConfig) shouldGzipObjects() bool {
return t.gzipObjects return t.gzipObjects
} }
func (t *apiConfig) permitRootAccess() bool {
t.mu.RLock()
defer t.mu.RUnlock()
return t.rootAccess
}
func (t *apiConfig) getListQuorum() string { func (t *apiConfig) getListQuorum() string {
t.mu.RLock() t.mu.RLock()
defer t.mu.RUnlock() defer t.mu.RUnlock()

View File

@ -914,11 +914,12 @@ func (sys *IAMSys) notifyForServiceAccount(ctx context.Context, accessKey string
} }
type newServiceAccountOpts struct { type newServiceAccountOpts struct {
sessionPolicy *iampolicy.Policy sessionPolicy *iampolicy.Policy
accessKey string accessKey string
secretKey string secretKey string
comment string comment string
expiration *time.Time expiration *time.Time
allowSiteReplicatorAccount bool // allow creating internal service account for site-replication.
claims map[string]interface{} claims map[string]interface{}
} }
@ -953,7 +954,9 @@ func (sys *IAMSys) NewServiceAccount(ctx context.Context, parentUser string, gro
if parentUser == opts.accessKey { if parentUser == opts.accessKey {
return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
} }
if siteReplicatorSvcAcc == opts.accessKey && !opts.allowSiteReplicatorAccount {
return auth.Credentials{}, time.Time{}, errIAMActionNotAllowed
}
m := make(map[string]interface{}) m := make(map[string]interface{})
m[parentClaim] = parentUser m[parentClaim] = parentUser

View File

@ -18,7 +18,6 @@
package cmd package cmd
import ( import (
"context"
"errors" "errors"
"net/http" "net/http"
"time" "time"
@ -40,48 +39,15 @@ const (
// Inter-node JWT token expiry is 15 minutes. // Inter-node JWT token expiry is 15 minutes.
defaultInterNodeJWTExpiry = 15 * time.Minute defaultInterNodeJWTExpiry = 15 * time.Minute
// URL JWT token expiry is one minute (might be exposed).
defaultURLJWTExpiry = time.Minute
) )
var ( var (
errInvalidAccessKeyID = errors.New("The access key ID you provided does not exist in our records") 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") errAuthentication = errors.New("Authentication failed, check your access credentials")
errNoAuthToken = errors.New("JWT token missing") 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. // cachedAuthenticateNode will cache authenticateNode results for given values up to ttl.
func cachedAuthenticateNode(ttl time.Duration) func(accessKey, secretKey, audience string) (string, error) { func cachedAuthenticateNode(ttl time.Duration) func(accessKey, secretKey, audience string) (string, error) {
type key struct { type key struct {
@ -121,14 +87,6 @@ func authenticateNode(accessKey, secretKey, audience string) (string, error) {
return jwt.SignedString([]byte(secretKey)) 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. // Check if the request is authenticated.
// Returns nil if the request is authenticated. errNoAuthToken if token missing. // Returns nil if the request is authenticated. errNoAuthToken if token missing.
// Returns errAuthentication for all other errors. // Returns errAuthentication for all other errors.
@ -142,15 +100,24 @@ func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, b
} }
claims := xjwt.NewMapClaims() claims := xjwt.NewMapClaims()
if err := xjwt.ParseWithClaims(token, claims, func(claims *xjwt.MapClaims) ([]byte, error) { if err := xjwt.ParseWithClaims(token, claims, func(claims *xjwt.MapClaims) ([]byte, error) {
if claims.AccessKey == globalActiveCred.AccessKey { if claims.AccessKey != globalActiveCred.AccessKey {
return []byte(globalActiveCred.SecretKey), nil 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) return []byte(globalActiveCred.SecretKey), nil
if !ok {
return nil, errInvalidAccessKeyID
}
cred := u.Credentials
return []byte(cred.SecretKey), nil
}); err != nil { }); err != nil {
return claims, nil, false, errAuthentication return claims, nil, false, errAuthentication
} }
@ -173,6 +140,11 @@ func metricsRequestAuthenticate(req *http.Request) (*xjwt.MapClaims, []string, b
claims.MapClaims[k] = v 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. // Now check if we have a sessionPolicy.
if _, ok = eclaims[iampolicy.SessionPolicyName]; ok { if _, ok = eclaims[iampolicy.SessionPolicyName]; ok {
owner = false owner = false

View File

@ -25,78 +25,9 @@ import (
"time" "time"
jwtgo "github.com/golang-jwt/jwt/v4" jwtgo "github.com/golang-jwt/jwt/v4"
"github.com/minio/minio/internal/auth"
xjwt "github.com/minio/minio/internal/jwt" 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: <nil>", 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: <nil>, 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) { func getTokenString(accessKey, secretKey string) (string, error) {
claims := xjwt.NewMapClaims() claims := xjwt.NewMapClaims()
claims.SetExpiry(UTCNow().Add(defaultJWTExpiry)) 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)
}
}

View File

@ -30,6 +30,7 @@ import (
"github.com/minio/minio/internal/hash/sha256" "github.com/minio/minio/internal/hash/sha256"
xhttp "github.com/minio/minio/internal/http" xhttp "github.com/minio/minio/internal/http"
"github.com/minio/minio/internal/logger" "github.com/minio/minio/internal/logger"
iampolicy "github.com/minio/pkg/iam/policy"
) )
// http Header "x-amz-content-sha256" == "UNSIGNED-PAYLOAD" indicates that the // 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 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 return cred, owner, ErrNone
} }

View File

@ -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)) return madmin.ReplicateAddStatus{}, errSRServiceAccount(fmt.Errorf("unable to create local service account: %w", err))
} }
svcCred, _, err = globalIAMSys.NewServiceAccount(ctx, sites[selfIdx].AccessKey, nil, newServiceAccountOpts{ svcCred, _, err = globalIAMSys.NewServiceAccount(ctx, sites[selfIdx].AccessKey, nil, newServiceAccountOpts{
accessKey: siteReplicatorSvcAcc, accessKey: siteReplicatorSvcAcc,
secretKey: secretKey, secretKey: secretKey,
allowSiteReplicatorAccount: true,
}) })
if err != nil { if err != nil {
return madmin.ReplicateAddStatus{}, errSRServiceAccount(fmt.Errorf("unable to create local service account: %w", err)) 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 return result, nil
} }
// PeerJoinReq - internal API handler to respond to a peer cluster's request // PeerJoinReq - internal API handler to respond to a peer cluster's request to join.
// to join.
func (c *SiteReplicationSys) PeerJoinReq(ctx context.Context, arg madmin.SRPeerJoinReq) error { func (c *SiteReplicationSys) PeerJoinReq(ctx context.Context, arg madmin.SRPeerJoinReq) error {
var ourName string var ourName string
for d, p := range arg.Peers { 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) _, _, err := globalIAMSys.GetServiceAccount(ctx, arg.SvcAcctAccessKey)
if err == errNoSuchServiceAccount { if err == errNoSuchServiceAccount {
_, _, err = globalIAMSys.NewServiceAccount(ctx, arg.SvcAcctParent, nil, newServiceAccountOpts{ _, _, err = globalIAMSys.NewServiceAccount(ctx, arg.SvcAcctParent, nil, newServiceAccountOpts{
accessKey: arg.SvcAcctAccessKey, accessKey: arg.SvcAcctAccessKey,
secretKey: arg.SvcAcctSecretKey, secretKey: arg.SvcAcctSecretKey,
allowSiteReplicatorAccount: arg.SvcAcctAccessKey == siteReplicatorSvcAcc,
}) })
} }
if err != nil { if err != nil {

View File

@ -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 // This file is part of MinIO Object Storage stack
// //
@ -45,6 +45,7 @@ const (
apiDeleteCleanupInterval = "delete_cleanup_interval" apiDeleteCleanupInterval = "delete_cleanup_interval"
apiDisableODirect = "disable_odirect" apiDisableODirect = "disable_odirect"
apiGzipObjects = "gzip_objects" apiGzipObjects = "gzip_objects"
apiRootAccess = "root_access"
EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX" EnvAPIRequestsMax = "MINIO_API_REQUESTS_MAX"
EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE" EnvAPIRequestsDeadline = "MINIO_API_REQUESTS_DEADLINE"
@ -61,6 +62,7 @@ const (
EnvDeleteCleanupInterval = "MINIO_DELETE_CLEANUP_INTERVAL" EnvDeleteCleanupInterval = "MINIO_DELETE_CLEANUP_INTERVAL"
EnvAPIDisableODirect = "MINIO_API_DISABLE_ODIRECT" EnvAPIDisableODirect = "MINIO_API_DISABLE_ODIRECT"
EnvAPIGzipObjects = "MINIO_API_GZIP_OBJECTS" EnvAPIGzipObjects = "MINIO_API_GZIP_OBJECTS"
EnvAPIRootAccess = "MINIO_API_ROOT_ACCESS" // default "on"
) )
// Deprecated key and ENVs // Deprecated key and ENVs
@ -130,6 +132,10 @@ var (
Key: apiGzipObjects, Key: apiGzipObjects,
Value: "off", Value: "off",
}, },
config.KV{
Key: apiRootAccess,
Value: "on",
},
} }
) )
@ -148,6 +154,7 @@ type Config struct {
DeleteCleanupInterval time.Duration `json:"delete_cleanup_interval"` DeleteCleanupInterval time.Duration `json:"delete_cleanup_interval"`
DisableODirect bool `json:"disable_odirect"` DisableODirect bool `json:"disable_odirect"`
GzipObjects bool `json:"gzip_objects"` GzipObjects bool `json:"gzip_objects"`
RootAccess bool `json:"root_access"`
} }
// UnmarshalJSON - Validate SS and RRS parity when unmarshalling JSON. // 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 disableODirect := env.Get(EnvAPIDisableODirect, kvs.Get(apiDisableODirect)) == config.EnableOn
gzipObjects := env.Get(EnvAPIGzipObjects, kvs.Get(apiGzipObjects)) == config.EnableOn gzipObjects := env.Get(EnvAPIGzipObjects, kvs.Get(apiGzipObjects)) == config.EnableOn
rootAccess := env.Get(EnvAPIRootAccess, kvs.Get(apiRootAccess)) == config.EnableOn
return Config{ return Config{
RequestsMax: requestsMax, RequestsMax: requestsMax,
@ -262,5 +270,6 @@ func LookupConfig(kvs config.KVS) (cfg Config, err error) {
DeleteCleanupInterval: deleteCleanupInterval, DeleteCleanupInterval: deleteCleanupInterval,
DisableODirect: disableODirect, DisableODirect: disableODirect,
GzipObjects: gzipObjects, GzipObjects: gzipObjects,
RootAccess: rootAccess,
}, nil }, nil
} }

View File

@ -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 // This file is part of MinIO Object Storage stack
// //
@ -94,7 +94,13 @@ var (
}, },
config.HelpKV{ config.HelpKV{
Key: apiDisableODirect, 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, Optional: true,
Type: "boolean", Type: "boolean",
}, },