From d28bcb4f841d546c167c63f1732bf32ee5fe43f7 Mon Sep 17 00:00:00 2001 From: Harshavardhana Date: Fri, 1 Nov 2019 15:53:16 -0700 Subject: [PATCH] Migrate all backend at .minio.sys/config to encrypted backend (#8474) - Supports migrating only when the credential ENVs are set, so any FS mode deployments which do not have ENVs set will continue to remain as is. - Credential ENVs can be rotated using MINIO_ACCESS_KEY_OLD and MINIO_SECRET_KEY_OLD envs, in such scenarios it allowed to rotate the encrypted content to a new admin key. --- cmd/common-main.go | 13 ++ cmd/config-encrypted.go | 308 ++++++++++++++++++++++++++++++++++++++++ cmd/config-migrate.go | 29 ++-- cmd/config.go | 37 +++++ cmd/config/constants.go | 16 ++- cmd/config/errors.go | 18 +++ cmd/gateway-common.go | 14 +- cmd/gateway-main.go | 23 ++- cmd/globals.go | 3 + cmd/iam-etcd-store.go | 15 ++ cmd/iam-object-store.go | 14 ++ cmd/server-main.go | 28 ++-- cmd/test-utils_test.go | 4 +- docs/config/README.md | 18 ++- pkg/madmin/encrypt.go | 13 +- 15 files changed, 510 insertions(+), 43 deletions(-) create mode 100644 cmd/config-encrypted.go diff --git a/cmd/common-main.go b/cmd/common-main.go index 3fd2d6f2d..419dbdee8 100644 --- a/cmd/common-main.go +++ b/cmd/common-main.go @@ -29,6 +29,7 @@ import ( "github.com/minio/minio-go/v6/pkg/set" "github.com/minio/minio/cmd/config" "github.com/minio/minio/cmd/logger" + "github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/certs" "github.com/minio/minio/pkg/env" ) @@ -202,6 +203,18 @@ func handleCommonEnvVars() { // or is not set to 'off', if MINIO_UPDATE is set to 'off' then // in-place update is off. globalInplaceUpdateDisabled = strings.EqualFold(env.Get(config.EnvUpdate, config.StateOn), config.StateOff) + + accessKey := env.Get(config.EnvAccessKey, "") + secretKey := env.Get(config.EnvSecretKey, "") + if accessKey != "" && secretKey != "" { + cred, err := auth.CreateCredentials(accessKey, secretKey) + if err != nil { + logger.Fatal(config.ErrInvalidCredentials(err), + "Unable to validate credentials inherited from the shell environment") + } + globalActiveCred = cred + globalConfigEncrypted = true + } } func logStartupMessage(msg string) { diff --git a/cmd/config-encrypted.go b/cmd/config-encrypted.go new file mode 100644 index 000000000..98c4ec4df --- /dev/null +++ b/cmd/config-encrypted.go @@ -0,0 +1,308 @@ +/* + * MinIO Cloud Storage, (C) 2019 MinIO, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "bytes" + "context" + "strings" + + etcd "github.com/coreos/etcd/clientv3" + "github.com/minio/minio/cmd/config" + "github.com/minio/minio/cmd/logger" + "github.com/minio/minio/pkg/auth" + "github.com/minio/minio/pkg/env" + "github.com/minio/minio/pkg/madmin" +) + +func handleEncryptedConfigBackend(objAPI ObjectLayer, server bool) error { + if !server { + return nil + } + + // If its server mode or nas gateway, migrate the backend. + doneCh := make(chan struct{}) + defer close(doneCh) + + var encrypted bool + var err error + + // Migrating Config backend needs a retry mechanism for + // the following reasons: + // - Read quorum is lost just after the initialization + // of the object layer. + for range newRetryTimerSimple(doneCh) { + if encrypted, err = checkBackendEncrypted(objAPI); err != nil { + if err == errDiskNotFound || + strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) || + strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) { + logger.Info("Waiting for config backend to be encrypted..") + continue + } + return err + } + break + } + + if encrypted { + // backend is encrypted, but credentials are not specified + // we shall fail right here. if not proceed forward. + if !globalConfigEncrypted || !globalActiveCred.IsValid() { + return config.ErrMissingCredentialsBackendEncrypted(nil) + } + } else { + // backend is not yet encrypted, check if encryption of + // backend is requested if not return nil and proceed + // forward. + if !globalConfigEncrypted { + return nil + } + if !globalActiveCred.IsValid() { + return errInvalidArgument + } + } + + accessKeyOld := env.Get(config.EnvAccessKeyOld, "") + secretKeyOld := env.Get(config.EnvSecretKeyOld, "") + var activeCredOld auth.Credentials + if accessKeyOld != "" && secretKeyOld != "" { + activeCredOld, err = auth.CreateCredentials(accessKeyOld, secretKeyOld) + if err != nil { + return err + } + } + + // Migrating Config backend needs a retry mechanism for + // the following reasons: + // - Read quorum is lost just after the initialization + // of the object layer. + for range newRetryTimerSimple(doneCh) { + // Migrate IAM configuration + if err = migrateConfigPrefixToEncrypted(objAPI, activeCredOld, encrypted); err != nil { + if err == errDiskNotFound || + strings.Contains(err.Error(), InsufficientReadQuorum{}.Error()) || + strings.Contains(err.Error(), InsufficientWriteQuorum{}.Error()) { + logger.Info("Waiting for config backend to be encrypted..") + continue + } + return err + } + break + } + return nil +} + +const ( + backendEncryptedFile = "backend-encrypted" +) + +var ( + backendEncryptedFileValue = []byte("encrypted") +) + +func checkBackendEtcdEncrypted(ctx context.Context, client *etcd.Client) (bool, error) { + data, err := readKeyEtcd(ctx, client, backendEncryptedFile) + if err != nil && err != errConfigNotFound { + return false, err + } + return err == nil && bytes.Equal(data, backendEncryptedFileValue), nil +} + +func checkBackendEncrypted(objAPI ObjectLayer) (bool, error) { + data, err := readConfig(context.Background(), objAPI, backendEncryptedFile) + if err != nil && err != errConfigNotFound { + return false, err + } + return err == nil && bytes.Equal(data, backendEncryptedFileValue), nil +} + +func migrateIAMConfigsEtcdToEncrypted(client *etcd.Client) error { + ctx, cancel := context.WithTimeout(context.Background(), defaultContextTimeout) + defer cancel() + + encrypted, err := checkBackendEtcdEncrypted(ctx, client) + if err != nil { + return err + } + + if encrypted { + // backend is encrypted, but credentials are not specified + // we shall fail right here. if not proceed forward. + if !globalConfigEncrypted || !globalActiveCred.IsValid() { + return config.ErrMissingCredentialsBackendEncrypted(nil) + } + } else { + // backend is not yet encrypted, check if encryption of + // backend is requested if not return nil and proceed + // forward. + if !globalConfigEncrypted { + return nil + } + if !globalActiveCred.IsValid() { + return errInvalidArgument + } + } + + accessKeyOld := env.Get(config.EnvAccessKeyOld, "") + secretKeyOld := env.Get(config.EnvSecretKeyOld, "") + var activeCredOld auth.Credentials + if accessKeyOld != "" && secretKeyOld != "" { + activeCredOld, err = auth.CreateCredentials(accessKeyOld, secretKeyOld) + if err != nil { + return err + } + } + + if encrypted { + // No key rotation requested, and backend is + // already encrypted. We proceed without migration. + if !activeCredOld.IsValid() { + return nil + } + + // No real reason to rotate if old and new creds are same. + if activeCredOld.Equal(globalActiveCred) { + return nil + } + } + + if !activeCredOld.IsValid() { + logger.Info("Attempting a one time encrypt of all IAM users and policies on etcd") + } else { + logger.Info("Attempting a rotation of encrypted IAM users and policies on etcd with newly supplied credentials") + } + + r, err := client.Get(ctx, minioConfigPrefix, etcd.WithPrefix(), etcd.WithKeysOnly()) + if err != nil { + return err + } + for _, kv := range r.Kvs { + var ( + cdata []byte + cencdata []byte + ) + cdata, err = readKeyEtcd(ctx, client, string(kv.Key)) + if err != nil { + switch err { + case errConfigNotFound: + // Perhaps not present or someone deleted it. + continue + } + return err + } + // Is rotating of creds requested? + if activeCredOld.IsValid() { + cdata, err = madmin.DecryptData(activeCredOld.String(), bytes.NewReader(cdata)) + if err != nil { + if err == madmin.ErrMaliciousData { + return config.ErrInvalidRotatingCredentialsBackendEncrypted(nil) + } + return err + } + } + + cencdata, err = madmin.EncryptData(globalActiveCred.String(), cdata) + if err != nil { + return err + } + + if err = saveKeyEtcd(ctx, client, string(kv.Key), cencdata); err != nil { + return err + } + } + return saveKeyEtcd(ctx, client, backendEncryptedFile, backendEncryptedFileValue) +} + +func migrateConfigPrefixToEncrypted(objAPI ObjectLayer, activeCredOld auth.Credentials, encrypted bool) error { + if encrypted { + // No key rotation requested, and backend is + // already encrypted. We proceed without migration. + if !activeCredOld.IsValid() { + return nil + } + + // No real reason to rotate if old and new creds are same. + if activeCredOld.Equal(globalActiveCred) { + return nil + } + } + + if !activeCredOld.IsValid() { + logger.Info("Attempting a one time encrypt of all config, IAM users and policies on MinIO backend") + } else { + logger.Info("Attempting a rotation of encrypted config, IAM users and policies on MinIO with newly supplied credentials") + } + + // Construct path to config/transaction.lock for locking + transactionConfigPrefix := minioConfigPrefix + "/transaction.lock" + + // As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket + // and configFile, take a transaction lock to avoid data race between readConfig() + // and saveConfig(). + objLock := globalNSMutex.NewNSLock(context.Background(), minioMetaBucket, transactionConfigPrefix) + if err := objLock.GetLock(globalOperationTimeout); err != nil { + return err + } + defer objLock.Unlock() + + var marker string + for { + res, err := objAPI.ListObjects(context.Background(), minioMetaBucket, minioConfigPrefix, marker, "", maxObjectList) + if err != nil { + return err + } + for _, obj := range res.Objects { + var ( + cdata []byte + cencdata []byte + ) + + cdata, err = readConfig(context.Background(), objAPI, obj.Name) + if err != nil { + return err + } + + // Is rotating of creds requested? + if activeCredOld.IsValid() { + cdata, err = madmin.DecryptData(activeCredOld.String(), bytes.NewReader(cdata)) + if err != nil { + if err == madmin.ErrMaliciousData { + return config.ErrInvalidRotatingCredentialsBackendEncrypted(nil) + } + return err + } + } + + cencdata, err = madmin.EncryptData(globalActiveCred.String(), cdata) + if err != nil { + return err + } + + if err = saveConfig(context.Background(), objAPI, obj.Name, cencdata); err != nil { + return err + } + } + + if !res.IsTruncated { + break + } + + marker = res.NextMarker + } + + return saveConfig(context.Background(), objAPI, backendEncryptedFile, backendEncryptedFileValue) +} diff --git a/cmd/config-migrate.go b/cmd/config-migrate.go index b681a7b09..599be3657 100644 --- a/cmd/config-migrate.go +++ b/cmd/config-migrate.go @@ -17,6 +17,7 @@ package cmd import ( + "bytes" "context" "encoding/json" "fmt" @@ -38,6 +39,7 @@ import ( "github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/event" "github.com/minio/minio/pkg/event/target" + "github.com/minio/minio/pkg/madmin" xnet "github.com/minio/minio/pkg/net" "github.com/minio/minio/pkg/quick" ) @@ -2442,12 +2444,13 @@ func migrateConfigToMinioSys(objAPI ObjectLayer) (err error) { } }() - transactionConfigFile := configFile + ".transaction" + // Construct path to config/transaction.lock for locking + transactionConfigPrefix := minioConfigPrefix + "/transaction.lock" // As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket // and configFile, take a transaction lock to avoid data race between readConfig() // and saveConfig(). - objLock := globalNSMutex.NewNSLock(context.Background(), minioMetaBucket, transactionConfigFile) + objLock := globalNSMutex.NewNSLock(context.Background(), minioMetaBucket, transactionConfigPrefix) if err = objLock.GetLock(globalOperationTimeout); err != nil { return err } @@ -2496,13 +2499,13 @@ func migrateMinioSysConfig(objAPI ObjectLayer) error { return nil } - // Construct path to config.json for the given bucket. - transactionConfigFile := configFile + ".transaction" + // Construct path to config/transaction.lock for locking + transactionConfigPrefix := minioConfigPrefix + "/transaction.lock" // As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket // and configFile, take a transaction lock to avoid data race between readConfig() // and saveConfig(). - objLock := globalNSMutex.NewNSLock(context.Background(), minioMetaBucket, transactionConfigFile) + objLock := globalNSMutex.NewNSLock(context.Background(), minioMetaBucket, transactionConfigPrefix) if err := objLock.GetLock(globalOperationTimeout); err != nil { return err } @@ -2532,6 +2535,16 @@ func checkConfigVersion(objAPI ObjectLayer, configFile string, version string) ( return false, nil, err } + if globalConfigEncrypted { + data, err = madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data)) + if err != nil { + if err == madmin.ErrMaliciousData { + return false, nil, config.ErrInvalidCredentialsBackendEncrypted(nil) + } + return false, nil, err + } + } + var versionConfig struct { Version string `json:"version"` } @@ -2793,13 +2806,13 @@ func migrateMinioSysConfigToKV(objAPI ObjectLayer) error { notify.SetNotifyWebhook(newCfg, k, args) } - // Construct path to config.json for the given bucket. - transactionConfigFile := configFile + ".transaction" + // Construct path to config/transaction.lock for locking + transactionConfigPrefix := minioConfigPrefix + "/transaction.lock" // As object layer's GetObject() and PutObject() take respective lock on minioMetaBucket // and configFile, take a transaction lock to avoid data race between readConfig() // and saveConfig(). - objLock := globalNSMutex.NewNSLock(context.Background(), minioMetaBucket, transactionConfigFile) + objLock := globalNSMutex.NewNSLock(context.Background(), minioMetaBucket, transactionConfigPrefix) if err = objLock.GetLock(globalOperationTimeout); err != nil { return err } diff --git a/cmd/config.go b/cmd/config.go index 1f8aedc50..1223fb115 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -17,6 +17,7 @@ package cmd import ( + "bytes" "context" "encoding/json" "fmt" @@ -82,6 +83,11 @@ func readServerConfigHistory(ctx context.Context, objAPI ObjectLayer, uuidKV str if err != nil { return nil, err } + + if globalConfigEncrypted { + data, err = madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data)) + } + return data, err } @@ -89,6 +95,14 @@ func saveServerConfigHistory(ctx context.Context, objAPI ObjectLayer, kv []byte) uuidKV := mustGetUUID() + ".kv" historyFile := pathJoin(minioConfigHistoryPrefix, uuidKV) + var err error + if globalConfigEncrypted { + kv, err = madmin.EncryptData(globalActiveCred.String(), kv) + if err != nil { + return err + } + } + // Save the new config KV settings into the history path. return saveConfig(ctx, objAPI, historyFile, kv) } @@ -121,6 +135,12 @@ func saveServerConfig(ctx context.Context, objAPI ObjectLayer, config interface{ if err != nil { return err } + if globalConfigEncrypted { + oldData, err = madmin.EncryptData(globalActiveCred.String(), oldData) + if err != nil { + return err + } + } } // No need to take backups for fresh setups. @@ -130,6 +150,13 @@ func saveServerConfig(ctx context.Context, objAPI ObjectLayer, config interface{ } } + if globalConfigEncrypted { + data, err = madmin.EncryptData(globalActiveCred.String(), data) + if err != nil { + return err + } + } + // Save the new config in the std config path return saveConfig(ctx, objAPI, configFile, data) } @@ -141,6 +168,16 @@ func readServerConfig(ctx context.Context, objAPI ObjectLayer) (config.Config, e return nil, err } + if globalConfigEncrypted { + configData, err = madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(configData)) + if err != nil { + if err == madmin.ErrMaliciousData { + return nil, config.ErrInvalidCredentialsBackendEncrypted(nil) + } + return nil, err + } + } + var config = config.New() if err = json.Unmarshal(configData, &config); err != nil { return nil, err diff --git a/cmd/config/constants.go b/cmd/config/constants.go index 77b961fa0..a81349ef0 100644 --- a/cmd/config/constants.go +++ b/cmd/config/constants.go @@ -23,13 +23,15 @@ const ( // Top level common ENVs const ( - EnvAccessKey = "MINIO_ACCESS_KEY" - EnvSecretKey = "MINIO_SECRET_KEY" - EnvBrowser = "MINIO_BROWSER" - EnvDomain = "MINIO_DOMAIN" - EnvRegionName = "MINIO_REGION_NAME" - EnvPublicIPs = "MINIO_PUBLIC_IPS" - EnvEndpoints = "MINIO_ENDPOINTS" + EnvAccessKey = "MINIO_ACCESS_KEY" + EnvSecretKey = "MINIO_SECRET_KEY" + EnvAccessKeyOld = "MINIO_ACCESS_KEY_OLD" + EnvSecretKeyOld = "MINIO_SECRET_KEY_OLD" + EnvBrowser = "MINIO_BROWSER" + EnvDomain = "MINIO_DOMAIN" + EnvRegionName = "MINIO_REGION_NAME" + EnvPublicIPs = "MINIO_PUBLIC_IPS" + EnvEndpoints = "MINIO_ENDPOINTS" EnvUpdate = "MINIO_UPDATE" EnvWormState = "MINIO_WORM_STATE" diff --git a/cmd/config/errors.go b/cmd/config/errors.go index 1fb10d930..37376a1c5 100644 --- a/cmd/config/errors.go +++ b/cmd/config/errors.go @@ -72,6 +72,24 @@ var ( "MINIO_CACHE_ENCRYPTION_MASTER_KEY: For more information, please refer to https://docs.min.io/docs/minio-disk-cache-guide", ) + ErrInvalidRotatingCredentialsBackendEncrypted = newErrFn( + "Invalid rotating credentials", + "Please set correct rotating credentials in the environment for decryption", + `Detected encrypted config backend, correct old access and secret keys should be specified via environment variables MINIO_ACCESS_KEY_OLD and MINIO_SECRET_KEY_OLD to be able to re-encrypt the MinIO config, user IAM and policies with new credentials`, + ) + + ErrInvalidCredentialsBackendEncrypted = newErrFn( + "Invalid credentials", + "Please set correct credentials in the environment for decryption", + `Detected encrypted config backend, correct access and secret keys should be specified via environment variables MINIO_ACCESS_KEY and MINIO_SECRET_KEY to be able to decrypt the MinIO config, user IAM and policies`, + ) + + ErrMissingCredentialsBackendEncrypted = newErrFn( + "Credentials missing", + "Please set your credentials in the environment", + `Detected encrypted config backend, access and secret keys should be specified via environment variables MINIO_ACCESS_KEY and MINIO_SECRET_KEY to be able to decrypt the MinIO config, user IAM and policies`, + ) + ErrInvalidCredentials = newErrFn( "Invalid credentials", "Please provide correct credentials", diff --git a/cmd/gateway-common.go b/cmd/gateway-common.go index 90085c160..f69c2656e 100644 --- a/cmd/gateway-common.go +++ b/cmd/gateway-common.go @@ -23,7 +23,6 @@ import ( "github.com/minio/minio/cmd/config" xhttp "github.com/minio/minio/cmd/http" "github.com/minio/minio/cmd/logger" - "github.com/minio/minio/pkg/auth" "github.com/minio/minio/pkg/env" "github.com/minio/minio/pkg/hash" xnet "github.com/minio/minio/pkg/net" @@ -373,15 +372,14 @@ func parseGatewaySSE(s string) (gatewaySSE, error) { } // handle gateway env vars -func handleGatewayEnvVars() { - accessKey := env.Get(config.EnvAccessKey, "") - secretKey := env.Get(config.EnvSecretKey, "") - cred, err := auth.CreateCredentials(accessKey, secretKey) - if err != nil { - logger.Fatal(config.ErrInvalidCredentials(err), +func gatewayHandleEnvVars() { + // Handle common env vars. + handleCommonEnvVars() + + if !globalActiveCred.IsValid() { + logger.Fatal(config.ErrInvalidCredentials(nil), "Unable to validate credentials inherited from the shell environment") } - globalActiveCred = cred gwsseVal := env.Get("MINIO_GATEWAY_SSE", "") if len(gwsseVal) != 0 { diff --git a/cmd/gateway-main.go b/cmd/gateway-main.go index 040cda669..ff47be7dd 100644 --- a/cmd/gateway-main.go +++ b/cmd/gateway-main.go @@ -136,11 +136,8 @@ func StartGateway(ctx *cli.Context, gw Gateway) { globalRootCAs, err = config.GetRootCAs(globalCertsCADir.Get()) logger.FatalIf(err, "Failed to read root CAs (%v)", err) - // Handle common env vars. - handleCommonEnvVars() - // Handle gateway specific env - handleGatewayEnvVars() + gatewayHandleEnvVars() // Set system resources to maximum. logger.LogIf(context.Background(), setMaxResources()) @@ -230,6 +227,16 @@ func StartGateway(ctx *cli.Context, gw Gateway) { initFederatorBackend(newObject) } + // Migrate all backend configs to encrypted backend, also handles rotation as well. + // For "nas" gateway we need to specially handle the backend migration as well. + // Internally code handles migrating etcd if enabled automatically. + logger.FatalIf(handleEncryptedConfigBackend(newObject, enableConfigOps), + "Unable to handle encrypted backend for config, iam and policies") + + // **** WARNING **** + // Migrating to encrypted backend should happen before initialization of any + // sub-systems, make sure that we do not move the above codeblock elsewhere. + if enableConfigOps { // Create a new config system. globalConfigSys = NewConfigSys() @@ -246,6 +253,14 @@ func StartGateway(ctx *cli.Context, gw Gateway) { globalDeploymentID = env.Get("MINIO_GATEWAY_DEPLOYMENT_ID", mustGetUUID()) logger.SetDeploymentID(globalDeploymentID) + if globalEtcdClient != nil { + // **** WARNING **** + // Migrating to encrypted backend on etcd should happen before initialization of + // IAM sub-systems, make sure that we do not move the above codeblock elsewhere. + logger.FatalIf(migrateIAMConfigsEtcdToEncrypted(globalEtcdClient), + "Unable to handle encrypted backend for iam and policies") + } + if globalCacheConfig.Enabled { // initialize the new disk cache objects. globalCacheObjectAPI, err = newServerCacheObjects(context.Background(), globalCacheConfig) diff --git a/cmd/globals.go b/cmd/globals.go index 4ffbd161d..18bfa72b1 100644 --- a/cmd/globals.go +++ b/cmd/globals.go @@ -186,6 +186,9 @@ var ( globalActiveCred auth.Credentials + // Indicates if config is to be encrypted + globalConfigEncrypted bool + globalPublicCerts []*x509.Certificate globalDomainNames []string // Root domains for virtual host style requests diff --git a/cmd/iam-etcd-store.go b/cmd/iam-etcd-store.go index 74ecc9bd1..d3933c760 100644 --- a/cmd/iam-etcd-store.go +++ b/cmd/iam-etcd-store.go @@ -17,6 +17,7 @@ package cmd import ( + "bytes" "context" "encoding/json" "errors" @@ -31,6 +32,7 @@ import ( "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" iampolicy "github.com/minio/minio/pkg/iam/policy" + "github.com/minio/minio/pkg/madmin" ) var defaultContextTimeout = 30 * time.Second @@ -109,6 +111,12 @@ func (ies *IAMEtcdStore) saveIAMConfig(item interface{}, path string) error { if err != nil { return err } + if globalConfigEncrypted { + data, err = madmin.EncryptData(globalActiveCred.String(), data) + if err != nil { + return err + } + } return saveKeyEtcd(ies.getContext(), ies.client, path, data) } @@ -118,6 +126,13 @@ func (ies *IAMEtcdStore) loadIAMConfig(item interface{}, path string) error { return err } + if globalConfigEncrypted { + pdata, err = madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(pdata)) + if err != nil { + return err + } + } + return json.Unmarshal(pdata, item) } diff --git a/cmd/iam-object-store.go b/cmd/iam-object-store.go index 1a34b968b..2d1fa4a67 100644 --- a/cmd/iam-object-store.go +++ b/cmd/iam-object-store.go @@ -17,6 +17,7 @@ package cmd import ( + "bytes" "context" "encoding/json" "errors" @@ -27,6 +28,7 @@ import ( "github.com/minio/minio/cmd/logger" "github.com/minio/minio/pkg/auth" iampolicy "github.com/minio/minio/pkg/iam/policy" + "github.com/minio/minio/pkg/madmin" ) // IAMObjectStore implements IAMStorageAPI @@ -215,6 +217,12 @@ func (iamOS *IAMObjectStore) saveIAMConfig(item interface{}, path string) error if err != nil { return err } + if globalConfigEncrypted { + data, err = madmin.EncryptData(globalActiveCred.String(), data) + if err != nil { + return err + } + } return saveConfig(context.Background(), objectAPI, path, data) } @@ -224,6 +232,12 @@ func (iamOS *IAMObjectStore) loadIAMConfig(item interface{}, path string) error if err != nil { return err } + if globalConfigEncrypted { + data, err = madmin.DecryptData(globalActiveCred.String(), bytes.NewReader(data)) + if err != nil { + return err + } + } return json.Unmarshal(data, item) } diff --git a/cmd/server-main.go b/cmd/server-main.go index 1468cd003..7567470f2 100644 --- a/cmd/server-main.go +++ b/cmd/server-main.go @@ -178,18 +178,6 @@ func serverHandleCmdArgs(ctx *cli.Context) { func serverHandleEnvVars() { // Handle common environment variables. handleCommonEnvVars() - - accessKey := env.Get(config.EnvAccessKey, "") - secretKey := env.Get(config.EnvSecretKey, "") - if accessKey != "" && secretKey != "" { - cred, err := auth.CreateCredentials(accessKey, secretKey) - if err != nil { - logger.Fatal(config.ErrInvalidCredentials(err), - "Unable to validate credentials inherited from the shell environment") - } - globalActiveCred = cred - } - } func initAllSubsystems(newObject ObjectLayer) { @@ -350,9 +338,25 @@ func serverMain(ctx *cli.Context) { // Re-enable logging logger.Disable = false + // Migrate all backend configs to encrypted backend, also handles rotation as well. + logger.FatalIf(handleEncryptedConfigBackend(newObject, true), + "Unable to handle encrypted backend for config, iam and policies") + + // **** WARNING **** + // Migrating to encrypted backend should happen before initialization of any + // sub-systems, make sure that we do not move the above codeblock elsewhere. + // Validate and initialize all subsystems. initAllSubsystems(newObject) + if globalEtcdClient != nil { + // **** WARNING **** + // Migrating to encrypted backend on etcd should happen before initialization of + // IAM sub-systems, make sure that we do not move the above codeblock elsewhere. + logger.FatalIf(migrateIAMConfigsEtcdToEncrypted(globalEtcdClient), + "Unable to handle encrypted backend for iam and policies") + } + if globalCacheConfig.Enabled { logger.StartupMessage(color.Red(color.Bold("Disk caching is recommended only for gateway deployments"))) diff --git a/cmd/test-utils_test.go b/cmd/test-utils_test.go index f73d5bfa7..8ac50a77c 100644 --- a/cmd/test-utils_test.go +++ b/cmd/test-utils_test.go @@ -523,13 +523,13 @@ func newTestConfig(bucketLocation string, obj ObjectLayer) (err error) { return err } - logger.Disable = true - globalActiveCred = auth.Credentials{ AccessKey: auth.DefaultAccessKey, SecretKey: auth.DefaultSecretKey, } + globalConfigEncrypted = true + // Set a default region. config.SetRegion(globalServerConfig, bucketLocation) diff --git a/docs/config/README.md b/docs/config/README.md index 383cbb0ab..939ee3472 100644 --- a/docs/config/README.md +++ b/docs/config/README.md @@ -32,7 +32,7 @@ $ mc tree --files ~/.minio You can provide a custom certs directory using `--certs-dir` command line option. #### Credentials -On MinIO admin credentials or root credentials are only allowed to be changed using ENVs `MINIO_ACCESS_KEY` and `MINIO_SECRET_KEY`. +On MinIO admin credentials or root credentials are only allowed to be changed using ENVs namely `MINIO_ACCESS_KEY` and `MINIO_SECRET_KEY`. Using the combination of these two values MinIO encrypts the config stored at the backend. ``` export MINIO_ACCESS_KEY=minio @@ -40,6 +40,22 @@ export MINIO_SECRET_KEY=minio13 minio server /data ``` +##### Rotating encryption with new credentials + +Additionally if you wish to change the admin credentials, then MinIO will automatically detect this and re-encrypt with new credentials as shown below. For one time only special ENVs as shown below needs to be set for rotating the encryption config. + +> Old ENVs are never remembered in memory and are destroyed right after they are used to migrate your existing content with new credentials. You are safe to remove them after the server as successfully started, by restarting the services once again. + +``` +export MINIO_ACCESS_KEY=newminio +export MINIO_SECRET_KEY=newminio123 +export MINIO_ACCESS_KEY_OLD=minio +export MINIO_SECRET_KEY_OLD=minio123 +minio server /data +``` + +Once the migration is complete and server has started successfully remove `MINIO_ACCESS_KEY_OLD` and `MINIO_SECRET_KEY_OLD` environment variables, restart the server. + #### Region | Field | Type | Description | |:--------------------------|:---------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| diff --git a/pkg/madmin/encrypt.go b/pkg/madmin/encrypt.go index 620bbfe2d..278d04907 100644 --- a/pkg/madmin/encrypt.go +++ b/pkg/madmin/encrypt.go @@ -76,6 +76,10 @@ func EncryptData(password string, data []byte) ([]byte, error) { return ciphertext.Bytes(), nil } +// ErrMaliciousData indicates that the stream cannot be +// decrypted by provided credentials. +var ErrMaliciousData = sio.NotAuthentic + // DecryptData decrypts the data with the key derived // from the salt (part of data) and the password using // the PBKDF used in EncryptData. DecryptData returns @@ -116,7 +120,14 @@ func DecryptData(password string, data io.Reader) ([]byte, error) { if err != nil { return nil, err } - return ioutil.ReadAll(stream.DecryptReader(data, nonce[:], nil)) + + enBytes, err := ioutil.ReadAll(stream.DecryptReader(data, nonce[:], nil)) + if err != nil { + if err == sio.NotAuthentic { + return enBytes, ErrMaliciousData + } + } + return enBytes, err } const (