mirror of
				https://github.com/minio/minio.git
				synced 2025-10-29 15:55:00 -04:00 
			
		
		
		
	kes: automatically reload KES client certificate (#15450)
This commit adds support for automatically reloading the MinIO client certificate for authentication to KES. The client certificate will now be reloaded: - when the private key / certificate file changes - when a SIGHUP signal is received - every 15 minutes Fixes #14869 Signed-off-by: Andreas Auernhammer <hi@aead.dev>
This commit is contained in:
		
							parent
							
								
									b3edb25377
								
							
						
					
					
						commit
						d774a3309b
					
				| @ -830,43 +830,56 @@ func handleKMSConfig() { | ||||
| 				endpoints = append(endpoints, strings.Join(lbls, "")) | ||||
| 			} | ||||
| 		} | ||||
| 		// Manually load the certificate and private key into memory. | ||||
| 		// We need to check whether the private key is encrypted, and | ||||
| 		// if so, decrypt it using the user-provided password. | ||||
| 		certBytes, err := os.ReadFile(env.Get(config.EnvKESClientCert, "")) | ||||
| 		if err != nil { | ||||
| 			logger.Fatal(err, "Unable to load KES client certificate as specified by the shell environment") | ||||
| 		} | ||||
| 		keyBytes, err := os.ReadFile(env.Get(config.EnvKESClientKey, "")) | ||||
| 		if err != nil { | ||||
| 			logger.Fatal(err, "Unable to load KES client private key as specified by the shell environment") | ||||
| 		} | ||||
| 		privateKeyPEM, rest := pem.Decode(bytes.TrimSpace(keyBytes)) | ||||
| 		if len(rest) != 0 { | ||||
| 			logger.Fatal(errors.New("private key contains additional data"), "Unable to load KES client private key as specified by the shell environment") | ||||
| 		} | ||||
| 		if x509.IsEncryptedPEMBlock(privateKeyPEM) { | ||||
| 			keyBytes, err = x509.DecryptPEMBlock(privateKeyPEM, []byte(env.Get(config.EnvKESClientPassword, ""))) | ||||
| 			if err != nil { | ||||
| 				logger.Fatal(err, "Unable to decrypt KES client private key as specified by the shell environment") | ||||
| 			} | ||||
| 			keyBytes = pem.EncodeToMemory(&pem.Block{Type: privateKeyPEM.Type, Bytes: keyBytes}) | ||||
| 		} | ||||
| 		certificate, err := tls.X509KeyPair(certBytes, keyBytes) | ||||
| 		if err != nil { | ||||
| 			logger.Fatal(err, "Unable to load KES client certificate as specified by the shell environment") | ||||
| 		} | ||||
| 		rootCAs, err := certs.GetRootCAs(env.Get(config.EnvKESServerCA, globalCertsCADir.Get())) | ||||
| 		if err != nil { | ||||
| 			logger.Fatal(err, fmt.Sprintf("Unable to load X.509 root CAs for KES from %q", env.Get(config.EnvKESServerCA, globalCertsCADir.Get()))) | ||||
| 		} | ||||
| 
 | ||||
| 		loadX509KeyPair := func(certFile, keyFile string) (tls.Certificate, error) { | ||||
| 			// Manually load the certificate and private key into memory. | ||||
| 			// We need to check whether the private key is encrypted, and | ||||
| 			// if so, decrypt it using the user-provided password. | ||||
| 			certBytes, err := os.ReadFile(certFile) | ||||
| 			if err != nil { | ||||
| 				return tls.Certificate{}, fmt.Errorf("Unable to load KES client certificate as specified by the shell environment: %v", err) | ||||
| 			} | ||||
| 			keyBytes, err := os.ReadFile(keyFile) | ||||
| 			if err != nil { | ||||
| 				return tls.Certificate{}, fmt.Errorf("Unable to load KES client private key as specified by the shell environment: %v", err) | ||||
| 			} | ||||
| 			privateKeyPEM, rest := pem.Decode(bytes.TrimSpace(keyBytes)) | ||||
| 			if len(rest) != 0 { | ||||
| 				return tls.Certificate{}, errors.New("Unable to load KES client private key as specified by the shell environment: private key contains additional data") | ||||
| 			} | ||||
| 			if x509.IsEncryptedPEMBlock(privateKeyPEM) { | ||||
| 				keyBytes, err = x509.DecryptPEMBlock(privateKeyPEM, []byte(env.Get(config.EnvKESClientPassword, ""))) | ||||
| 				if err != nil { | ||||
| 					return tls.Certificate{}, fmt.Errorf("Unable to decrypt KES client private key as specified by the shell environment: %v", err) | ||||
| 				} | ||||
| 				keyBytes = pem.EncodeToMemory(&pem.Block{Type: privateKeyPEM.Type, Bytes: keyBytes}) | ||||
| 			} | ||||
| 			certificate, err := tls.X509KeyPair(certBytes, keyBytes) | ||||
| 			if err != nil { | ||||
| 				return tls.Certificate{}, fmt.Errorf("Unable to load KES client certificate as specified by the shell environment: %v", err) | ||||
| 			} | ||||
| 			return certificate, nil | ||||
| 		} | ||||
| 
 | ||||
| 		reloadCertEvents := make(chan tls.Certificate, 1) | ||||
| 		certificate, err := certs.NewCertificate(env.Get(config.EnvKESClientCert, ""), env.Get(config.EnvKESClientKey, ""), loadX509KeyPair) | ||||
| 		if err != nil { | ||||
| 			logger.Fatal(err, "Failed to load KES client certificate") | ||||
| 		} | ||||
| 		certificate.Watch(context.Background(), 15*time.Minute, syscall.SIGHUP) | ||||
| 		certificate.Notify(reloadCertEvents) | ||||
| 
 | ||||
| 		defaultKeyID := env.Get(config.EnvKESKeyName, "") | ||||
| 		KMS, err := kms.NewWithConfig(kms.Config{ | ||||
| 			Endpoints:    endpoints, | ||||
| 			DefaultKeyID: defaultKeyID, | ||||
| 			Certificate:  certificate, | ||||
| 			RootCAs:      rootCAs, | ||||
| 			Endpoints:        endpoints, | ||||
| 			DefaultKeyID:     defaultKeyID, | ||||
| 			Certificate:      certificate, | ||||
| 			ReloadCertEvents: reloadCertEvents, | ||||
| 			RootCAs:          rootCAs, | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			logger.Fatal(err, "Unable to initialize a connection to KES as specified by the shell environment") | ||||
|  | ||||
							
								
								
									
										2
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.mod
									
									
									
									
									
								
							| @ -50,7 +50,7 @@ require ( | ||||
| 	github.com/minio/kes v0.20.0 | ||||
| 	github.com/minio/madmin-go v1.4.9 | ||||
| 	github.com/minio/minio-go/v7 v7.0.33 | ||||
| 	github.com/minio/pkg v1.1.26 | ||||
| 	github.com/minio/pkg v1.3.0 | ||||
| 	github.com/minio/selfupdate v0.5.0 | ||||
| 	github.com/minio/sha256-simd v1.0.0 | ||||
| 	github.com/minio/simdjson-go v0.4.2 | ||||
|  | ||||
							
								
								
									
										2
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								go.sum
									
									
									
									
									
								
							| @ -635,6 +635,8 @@ github.com/minio/minio-go/v7 v7.0.33/go.mod h1:nCrRzjoSUQh8hgKKtu3Y708OLvRLtuASM | ||||
| github.com/minio/pkg v1.1.20/go.mod h1:Xo7LQshlxGa9shKwJ7NzQbgW4s8T/Wc1cOStR/eUiMY= | ||||
| github.com/minio/pkg v1.1.26 h1:a8x4sHNBxCiHEkxZ/0EBTLqvV3nMtM2G/A6lXNfXN3U= | ||||
| github.com/minio/pkg v1.1.26/go.mod h1:z9PfmEI804KFkF6eY4LoGe8IDVvTCsYGVuaf58Dr0WI= | ||||
| github.com/minio/pkg v1.3.0 h1:myG72OiSi1dMN5kTrdfffzC1VHQaoRwC6jefyH3VGrU= | ||||
| github.com/minio/pkg v1.3.0/go.mod h1:z9PfmEI804KFkF6eY4LoGe8IDVvTCsYGVuaf58Dr0WI= | ||||
| github.com/minio/selfupdate v0.5.0 h1:0UH1HlL49+2XByhovKl5FpYTjKfvrQ2sgL1zEXK6mfI= | ||||
| github.com/minio/selfupdate v0.5.0/go.mod h1:mcDkzMgq8PRcpCRJo/NlPY7U45O5dfYl2Y0Rg7IustY= | ||||
| github.com/minio/sha256-simd v0.1.1/go.mod h1:B5e1o+1/KgNmWrSQK08Y6Z1Vb5pwIktudl0J58iy0KM= | ||||
|  | ||||
| @ -23,8 +23,10 @@ import ( | ||||
| 	"crypto/x509" | ||||
| 	"errors" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| 
 | ||||
| 	"github.com/minio/kes" | ||||
| 	"github.com/minio/pkg/certs" | ||||
| ) | ||||
| 
 | ||||
| const ( | ||||
| @ -46,7 +48,11 @@ type Config struct { | ||||
| 
 | ||||
| 	// Certificate is the client TLS certificate | ||||
| 	// to authenticate to KMS via mTLS. | ||||
| 	Certificate tls.Certificate | ||||
| 	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. | ||||
| @ -64,7 +70,7 @@ func NewWithConfig(config Config) (KMS, error) { | ||||
| 
 | ||||
| 	client := kes.NewClientWithConfig("", &tls.Config{ | ||||
| 		MinVersion:         tls.VersionTLS12, | ||||
| 		Certificates:       []tls.Certificate{config.Certificate}, | ||||
| 		Certificates:       []tls.Certificate{config.Certificate.Get()}, | ||||
| 		RootCAs:            config.RootCAs, | ||||
| 		ClientSessionCache: tls.NewLRUClientSessionCache(tlsClientSessionCacheSize), | ||||
| 	}) | ||||
| @ -81,14 +87,35 @@ func NewWithConfig(config Config) (KMS, error) { | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	return &kesClient{ | ||||
| 
 | ||||
| 	c := &kesClient{ | ||||
| 		client:        client, | ||||
| 		defaultKeyID:  config.DefaultKeyID, | ||||
| 		bulkAvailable: bulkAvailable, | ||||
| 	}, nil | ||||
| 	} | ||||
| 	go func() { | ||||
| 		for { | ||||
| 			select { | ||||
| 			case certificate := <-config.ReloadCertEvents: | ||||
| 				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() | ||||
| 			} | ||||
| 		} | ||||
| 	}() | ||||
| 	return c, nil | ||||
| } | ||||
| 
 | ||||
| type kesClient struct { | ||||
| 	lock         sync.RWMutex | ||||
| 	defaultKeyID string | ||||
| 	client       *kes.Client | ||||
| 
 | ||||
| @ -113,6 +140,9 @@ func (c *kesClient) Stat(ctx context.Context) (Status, error) { | ||||
| } | ||||
| 
 | ||||
| func (c *kesClient) Metrics(ctx context.Context) (kes.Metric, error) { | ||||
| 	c.lock.RLock() | ||||
| 	defer c.lock.RUnlock() | ||||
| 
 | ||||
| 	return c.client.Metrics(ctx) | ||||
| } | ||||
| 
 | ||||
| @ -122,6 +152,9 @@ func (c *kesClient) Metrics(ctx context.Context) (kes.Metric, error) { | ||||
| // 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) | ||||
| } | ||||
| 
 | ||||
| @ -134,6 +167,9 @@ func (c *kesClient) CreateKey(ctx context.Context, keyID string) error { | ||||
| // 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 | ||||
| 	} | ||||
| @ -156,6 +192,9 @@ func (c *kesClient) GenerateKey(ctx context.Context, keyID string, cryptoCtx Con | ||||
| // 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 | ||||
| @ -164,6 +203,9 @@ func (c *kesClient) DecryptKey(keyID string, ciphertext []byte, ctx Context) ([] | ||||
| } | ||||
| 
 | ||||
| func (c *kesClient) DecryptAll(ctx context.Context, keyID string, ciphertexts [][]byte, contexts []Context) ([][]byte, error) { | ||||
| 	c.lock.RLock() | ||||
| 	defer c.lock.RUnlock() | ||||
| 
 | ||||
| 	if c.bulkAvailable { | ||||
| 		CCPs := make([]kes.CCP, 0, len(ciphertexts)) | ||||
| 		for i := range ciphertexts { | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user