automatically generate root credentials with KMS (#19025)

With this commit, MinIO generates root credentials automatically
and deterministically if:

 - No root credentials have been set.
 - A KMS (KES) is configured.
 - API access for the root credentials is disabled (lockdown mode).

Before, MinIO defaults to `minioadmin` for both the access and
secret keys. Now, MinIO generates unique root credentials
automatically on startup using the KMS.

Therefore, it uses the KMS HMAC function to generate pseudo-random
values. These values never change as long as the KMS key remains
the same, and the KMS key must continue to exist since all IAM data
is encrypted with it.

Backward compatibility:

This commit should not cause existing deployments to break. It only
changes the root credentials of deployments that have a KMS configured
(KES, not a static key) but have not set any admin credentials. Such
implementations should be rare or not exist at all.

Even if the worst case would be updating root credentials in mc
or other clients used to administer the cluster. Root credentials
are anyway not intended for regular S3 operations.

Signed-off-by: Andreas Auernhammer <github@aead.dev>
This commit is contained in:
Andreas Auernhammer
2024-03-01 22:09:42 +01:00
committed by GitHub
parent 8f03c6e0db
commit 09626d78ff
22 changed files with 167 additions and 165 deletions

View File

@@ -24,6 +24,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io"
"strconv"
"strings"
"time"
@@ -211,39 +212,70 @@ func ExpToInt64(expI interface{}) (expAt int64, err error) {
// GenerateCredentials - creates randomly generated credentials of maximum
// allowed length.
func GenerateCredentials() (accessKey, secretKey string, err error) {
readBytes := func(size int) (data []byte, err error) {
data = make([]byte, size)
var n int
if n, err = rand.Read(data); err != nil {
return nil, err
} else if n != size {
return nil, fmt.Errorf("Not enough data. Expected to read: %v bytes, got: %v bytes", size, n)
}
return data, nil
}
// Generate access key.
keyBytes, err := readBytes(accessKeyMaxLen)
accessKey, err = GenerateAccessKey(accessKeyMaxLen, rand.Reader)
if err != nil {
return "", "", err
}
for i := 0; i < accessKeyMaxLen; i++ {
keyBytes[i] = alphaNumericTable[keyBytes[i]%alphaNumericTableLen]
}
accessKey = string(keyBytes)
// Generate secret key.
keyBytes, err = readBytes(secretKeyMaxLen)
secretKey, err = GenerateSecretKey(secretKeyMaxLen, rand.Reader)
if err != nil {
return "", "", err
}
secretKey = strings.ReplaceAll(string([]byte(base64.StdEncoding.EncodeToString(keyBytes))[:secretKeyMaxLen]),
"/", "+")
return accessKey, secretKey, nil
}
// GenerateAccessKey returns a new access key generated randomly using
// the given io.Reader. If random is nil, crypto/rand.Reader is used.
// If length <= 0, the access key length is chosen automatically.
//
// GenerateAccessKey returns an error if length is too small for a valid
// access key.
func GenerateAccessKey(length int, random io.Reader) (string, error) {
if random == nil {
random = rand.Reader
}
if length <= 0 {
length = accessKeyMaxLen
}
if length < accessKeyMinLen {
return "", errors.New("auth: access key length is too short")
}
key := make([]byte, length)
if _, err := io.ReadFull(random, key); err != nil {
return "", err
}
for i := range key {
key[i] = alphaNumericTable[key[i]%alphaNumericTableLen]
}
return string(key), nil
}
// GenerateSecretKey returns a new secret key generated randomly using
// the given io.Reader. If random is nil, crypto/rand.Reader is used.
// If length <= 0, the secret key length is chosen automatically.
//
// GenerateSecretKey returns an error if length is too small for a valid
// secret key.
func GenerateSecretKey(length int, random io.Reader) (string, error) {
if random == nil {
random = rand.Reader
}
if length <= 0 {
length = secretKeyMaxLen
}
if length < secretKeyMinLen {
return "", errors.New("auth: secret key length is too short")
}
key := make([]byte, base64.RawStdEncoding.DecodedLen(length))
if _, err := io.ReadFull(random, key); err != nil {
return "", err
}
s := base64.RawStdEncoding.EncodeToString(key)
return strings.ReplaceAll(s, "/", "+"), nil
}
// GetNewCredentialsWithMetadata generates and returns new credential with expiry.
func GetNewCredentialsWithMetadata(m map[string]interface{}, tokenSecret string) (Credentials, error) {
accessKey, secretKey, err := GenerateCredentials()

View File

@@ -148,11 +148,11 @@ func TestCreateCredentials(t *testing.T) {
func TestCredentialsEqual(t *testing.T) {
cred, err := GetNewCredentials()
if err != nil {
t.Fatalf("Failed to get a new credential")
t.Fatalf("Failed to get a new credential: %v", err)
}
cred2, err := GetNewCredentials()
if err != nil {
t.Fatalf("Failed to get a new credential")
t.Fatalf("Failed to get a new credential: %v", err)
}
testCases := []struct {
cred Credentials