minio/internal/kms/kes.go
Andreas Auernhammer 09626d78ff
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>
2024-03-01 13:09:42 -08:00

453 lines
13 KiB
Go

// Copyright (c) 2015-2021 MinIO, Inc.
//
// This file is part of MinIO Object Storage stack
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package kms
import (
"bytes"
"context"
"crypto/subtle"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"strings"
"sync"
"github.com/minio/kms-go/kes"
"github.com/minio/pkg/v2/certs"
"github.com/minio/pkg/v2/env"
)
const (
tlsClientSessionCacheSize = 100
)
// Config contains various KMS-related configuration
// parameters - like KMS endpoints or authentication
// credentials.
type Config struct {
// Endpoints contains a list of KMS server
// HTTP endpoints.
Endpoints []string
// DefaultKeyID is the key ID used when
// no explicit key ID is specified for
// a cryptographic operation.
DefaultKeyID string
// APIKey is an credential provided by env. var.
// to authenticate to a KES server. Either an
// API key or a client certificate must be specified.
APIKey kes.APIKey
// Certificate is the client TLS certificate
// to authenticate to KMS via mTLS.
Certificate *certs.Certificate
// ReloadCertEvents is an event channel that receives
// the reloaded client certificate.
ReloadCertEvents <-chan tls.Certificate
// RootCAs is a set of root CA certificates
// to verify the KMS server TLS certificate.
RootCAs *x509.CertPool
}
// NewWithConfig returns a new KMS using the given
// configuration.
func NewWithConfig(config Config) (KMS, error) {
if len(config.Endpoints) == 0 {
return nil, errors.New("kms: no server endpoints")
}
endpoints := make([]string, len(config.Endpoints)) // Copy => avoid being affect by any changes to the original slice
copy(endpoints, config.Endpoints)
var client *kes.Client
if config.APIKey != nil {
cert, err := kes.GenerateCertificate(config.APIKey)
if err != nil {
return nil, err
}
client = kes.NewClientWithConfig("", &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{cert},
RootCAs: config.RootCAs,
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
})
} else {
client = kes.NewClientWithConfig("", &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{config.Certificate.Get()},
RootCAs: config.RootCAs,
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
})
}
client.Endpoints = endpoints
c := &kesClient{
client: client,
defaultKeyID: config.DefaultKeyID,
}
go func() {
if config.Certificate == nil || config.ReloadCertEvents == nil {
return
}
var prevCertificate tls.Certificate
for {
certificate, ok := <-config.ReloadCertEvents
if !ok {
return
}
sameCert := len(certificate.Certificate) == len(prevCertificate.Certificate)
for i, b := range certificate.Certificate {
if !sameCert {
break
}
sameCert = sameCert && bytes.Equal(b, prevCertificate.Certificate[i])
}
// Do not reload if its the same cert as before.
if !sameCert {
client := kes.NewClientWithConfig("", &tls.Config{
MinVersion: tls.VersionTLS12,
Certificates: []tls.Certificate{certificate},
RootCAs: config.RootCAs,
ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize),
})
client.Endpoints = endpoints
c.lock.Lock()
c.client = client
c.lock.Unlock()
prevCertificate = certificate
}
}
}()
return c, nil
}
type kesClient struct {
lock sync.RWMutex
defaultKeyID string
client *kes.Client
}
var ( // compiler checks
_ KMS = (*kesClient)(nil)
_ KeyManager = (*kesClient)(nil)
_ IdentityManager = (*kesClient)(nil)
_ PolicyManager = (*kesClient)(nil)
)
// Stat returns the current KES status containing a
// list of KES endpoints and the default key ID.
func (c *kesClient) Stat(ctx context.Context) (Status, error) {
c.lock.RLock()
defer c.lock.RUnlock()
st, err := c.client.Status(ctx)
if err != nil {
return Status{}, err
}
endpoints := make([]string, len(c.client.Endpoints))
copy(endpoints, c.client.Endpoints)
return Status{
Name: "KES",
Endpoints: endpoints,
DefaultKey: c.defaultKeyID,
Details: st,
}, nil
}
// IsLocal returns true if the KMS is a local implementation
func (c *kesClient) IsLocal() bool {
return env.IsSet(EnvKMSSecretKey)
}
// List returns an array of local KMS Names
func (c *kesClient) List() []kes.KeyInfo {
var kmsSecret []kes.KeyInfo
envKMSSecretKey := env.Get(EnvKMSSecretKey, "")
values := strings.SplitN(envKMSSecretKey, ":", 2)
if len(values) == 2 {
kmsSecret = []kes.KeyInfo{
{
Name: values[0],
},
}
}
return kmsSecret
}
// Metrics retrieves server metrics in the Prometheus exposition format.
func (c *kesClient) Metrics(ctx context.Context) (kes.Metric, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.Metrics(ctx)
}
// Version retrieves version information
func (c *kesClient) Version(ctx context.Context) (string, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.Version(ctx)
}
// APIs retrieves a list of supported API endpoints
func (c *kesClient) APIs(ctx context.Context) ([]kes.API, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.APIs(ctx)
}
// CreateKey tries to create a new key at the KMS with the
// given key ID.
//
// If the a key with the same keyID already exists then
// CreateKey returns kes.ErrKeyExists.
func (c *kesClient) CreateKey(ctx context.Context, keyID string) error {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.CreateKey(ctx, keyID)
}
// DeleteKey deletes a key at the KMS with the given key ID.
// Please note that is a dangerous operation.
// Once a key has been deleted all data that has been encrypted with it cannot be decrypted
// anymore, and therefore, is lost.
func (c *kesClient) DeleteKey(ctx context.Context, keyID string) error {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.DeleteKey(ctx, keyID)
}
// ListKeys returns an iterator over all key names.
func (c *kesClient) ListKeys(ctx context.Context) (*kes.ListIter[string], error) {
c.lock.RLock()
defer c.lock.RUnlock()
return &kes.ListIter[string]{
NextFunc: c.client.ListKeys,
}, nil
}
// GenerateKey generates a new data encryption key using
// the key at the KES server referenced by the key ID.
//
// The default key ID will be used if keyID is empty.
//
// The context is associated and tied to the generated DEK.
// The same context must be provided when the generated
// key should be decrypted.
func (c *kesClient) GenerateKey(ctx context.Context, keyID string, cryptoCtx Context) (DEK, error) {
c.lock.RLock()
defer c.lock.RUnlock()
if keyID == "" {
keyID = c.defaultKeyID
}
ctxBytes, err := cryptoCtx.MarshalText()
if err != nil {
return DEK{}, err
}
dek, err := c.client.GenerateKey(ctx, keyID, ctxBytes)
if err != nil {
return DEK{}, err
}
return DEK{
KeyID: keyID,
Plaintext: dek.Plaintext,
Ciphertext: dek.Ciphertext,
}, nil
}
// ImportKey imports a cryptographic key into the KMS.
func (c *kesClient) ImportKey(ctx context.Context, keyID string, bytes []byte) error {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.ImportKey(ctx, keyID, &kes.ImportKeyRequest{
Key: bytes,
})
}
// EncryptKey Encrypts and authenticates a (small) plaintext with the cryptographic key
// The plaintext must not exceed 1 MB
func (c *kesClient) EncryptKey(keyID string, plaintext []byte, ctx Context) ([]byte, error) {
c.lock.RLock()
defer c.lock.RUnlock()
ctxBytes, err := ctx.MarshalText()
if err != nil {
return nil, err
}
return c.client.Encrypt(context.Background(), keyID, plaintext, ctxBytes)
}
// DecryptKey decrypts the ciphertext with the key at the KES
// server referenced by the key ID. The context must match the
// context value used to generate the ciphertext.
func (c *kesClient) DecryptKey(keyID string, ciphertext []byte, ctx Context) ([]byte, error) {
c.lock.RLock()
defer c.lock.RUnlock()
ctxBytes, err := ctx.MarshalText()
if err != nil {
return nil, err
}
return c.client.Decrypt(context.Background(), keyID, ciphertext, ctxBytes)
}
func (c *kesClient) DecryptAll(ctx context.Context, keyID string, ciphertexts [][]byte, contexts []Context) ([][]byte, error) {
c.lock.RLock()
defer c.lock.RUnlock()
plaintexts := make([][]byte, 0, len(ciphertexts))
for i := range ciphertexts {
ctxBytes, err := contexts[i].MarshalText()
if err != nil {
return nil, err
}
plaintext, err := c.client.Decrypt(ctx, keyID, ciphertexts[i], ctxBytes)
if err != nil {
return nil, err
}
plaintexts = append(plaintexts, plaintext)
}
return plaintexts, nil
}
// HMAC generates the HMAC checksum of the given msg using the key
// with the given keyID at the KMS.
func (c *kesClient) HMAC(ctx context.Context, keyID string, msg []byte) ([]byte, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.HMAC(context.Background(), keyID, msg)
}
// DescribePolicy describes a policy by returning its metadata.
// e.g. who created the policy at which point in time.
func (c *kesClient) DescribePolicy(ctx context.Context, policy string) (*kes.PolicyInfo, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.DescribePolicy(ctx, policy)
}
// ListPolicies returns an iterator over all policy names.
func (c *kesClient) ListPolicies(ctx context.Context) (*kes.ListIter[string], error) {
c.lock.RLock()
defer c.lock.RUnlock()
return &kes.ListIter[string]{
NextFunc: c.client.ListPolicies,
}, nil
}
// GetPolicy gets a policy from KMS.
func (c *kesClient) GetPolicy(ctx context.Context, policy string) (*kes.Policy, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.GetPolicy(ctx, policy)
}
// DescribeIdentity describes an identity by returning its metadata.
// e.g. which policy is currently assigned and whether its an admin identity.
func (c *kesClient) DescribeIdentity(ctx context.Context, identity string) (*kes.IdentityInfo, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.DescribeIdentity(ctx, kes.Identity(identity))
}
// DescribeSelfIdentity describes the identity issuing the request.
// It infers the identity from the TLS client certificate used to authenticate.
// It returns the identity and policy information for the client identity.
func (c *kesClient) DescribeSelfIdentity(ctx context.Context) (*kes.IdentityInfo, *kes.Policy, error) {
c.lock.RLock()
defer c.lock.RUnlock()
return c.client.DescribeSelf(ctx)
}
// ListPolicies returns an iterator over all identities.
func (c *kesClient) ListIdentities(ctx context.Context) (*kes.ListIter[kes.Identity], error) {
c.lock.RLock()
defer c.lock.RUnlock()
return &kes.ListIter[kes.Identity]{
NextFunc: c.client.ListIdentities,
}, nil
}
// Verify verifies all KMS endpoints and returns details
func (c *kesClient) Verify(ctx context.Context) []VerifyResult {
c.lock.RLock()
defer c.lock.RUnlock()
results := []VerifyResult{}
kmsContext := Context{"MinIO admin API": "ServerInfoHandler"} // Context for a test key operation
for _, endpoint := range c.client.Endpoints {
client := kes.Client{
Endpoints: []string{endpoint},
HTTPClient: c.client.HTTPClient,
}
// 1. Get stats for the KES instance
state, err := client.Status(ctx)
if err != nil {
results = append(results, VerifyResult{Status: "offline", Endpoint: endpoint})
continue
}
// 2. Generate a new key using the KMS.
kmsCtx, err := kmsContext.MarshalText()
if err != nil {
results = append(results, VerifyResult{Status: "offline", Endpoint: endpoint})
continue
}
result := VerifyResult{Status: "online", Endpoint: endpoint, Version: state.Version}
key, err := client.GenerateKey(ctx, env.Get(EnvKESKeyName, ""), kmsCtx)
if err != nil {
result.Encrypt = fmt.Sprintf("Encryption failed: %v", err)
} else {
result.Encrypt = "success"
}
// 3. Verify that we can indeed decrypt the (encrypted) key
decryptedKey, err := client.Decrypt(ctx, env.Get(EnvKESKeyName, ""), key.Ciphertext, kmsCtx)
switch {
case err != nil:
result.Decrypt = fmt.Sprintf("Decryption failed: %v", err)
case subtle.ConstantTimeCompare(key.Plaintext, decryptedKey) != 1:
result.Decrypt = "Decryption failed: decrypted key does not match generated key"
default:
result.Decrypt = "success"
}
results = append(results, result)
}
return results
}