cleanup: move KMS functionality into its own package

A new "internal/kms" package is introduced, it holds the API that can be
consumed by the RBD components.

The KMS providers are currently in the same package as the API. With
later follow-up changes the providers will be placed in their own
sub-package.

Because of the name of the package "kms", the types, functions and
structs inside the package should not be prefixed with KMS anymore:

    internal/kms/kms.go:213:6: type name will be used as kms.KMSInitializerArgs by other packages, and that stutters; consider calling this InitializerArgs (golint)

Updates: #852
Signed-off-by: Niels de Vos <ndevos@redhat.com>
This commit is contained in:
Niels de Vos
2021-08-26 13:43:20 +02:00
committed by mergify[bot]
parent 778b5e86de
commit 4a3b1181ce
16 changed files with 215 additions and 190 deletions

View File

@ -1,224 +0,0 @@
/*
Copyright 2021 The Ceph-CSI Authors.
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 util
import (
"context"
"encoding/base64"
"errors"
"fmt"
"github.com/ceph/ceph-csi/internal/util/k8s"
"github.com/aws/aws-sdk-go/aws"
awsCreds "github.com/aws/aws-sdk-go/aws/credentials"
awsSession "github.com/aws/aws-sdk-go/aws/session"
awsKMS "github.com/aws/aws-sdk-go/service/kms"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
kmsTypeAWSMetadata = "aws-metadata"
// awsMetadataDefaultSecretsName is the default name of the Kubernetes Secret
// that contains the credentials to access the Amazon KMS. The name of
// the Secret can be configured by setting the `KMS_SECRET_NAME`
// option.
//
// #nosec:G101, value not credential, just references token.
awsMetadataDefaultSecretsName = "ceph-csi-aws-credentials"
// awsSecretNameKey contains the name of the Kubernetes Secret that has
// the credentials to access the Amazon KMS.
//
// #nosec:G101, no hardcoded secret, this is a configuration key.
awsSecretNameKey = "KMS_SECRET_NAME"
awsRegionKey = "AWS_REGION"
// The following options are part of the Kubernetes Secrets.
//
// #nosec:G101, no hardcoded secrets, only configuration keys.
awsAccessKey = "AWS_ACCESS_KEY_ID"
// #nosec:G101.
awsSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
// #nosec:G101.
awsSessionToken = "AWS_SESSION_TOKEN"
awsCMK = "AWS_CMK_ARN"
)
var _ = RegisterKMSProvider(KMSProvider{
UniqueID: kmsTypeAWSMetadata,
Initializer: initAWSMetadataKMS,
})
type AWSMetadataKMS struct {
// basic options to get the secret
namespace string
secretName string
// standard AWS configuration options
region string
secretAccessKey string
accessKey string
sessionToken string
cmk string
}
func initAWSMetadataKMS(args KMSInitializerArgs) (EncryptionKMS, error) {
kms := &AWSMetadataKMS{
namespace: args.Namespace,
}
// required options for further configuration (getting secrets)
err := setConfigString(&kms.secretName, args.Config, awsSecretNameKey)
if errors.Is(err, errConfigOptionInvalid) {
return nil, err
} else if errors.Is(err, errConfigOptionMissing) {
kms.secretName = awsMetadataDefaultSecretsName
}
err = setConfigString(&kms.region, args.Config, awsRegionKey)
if err != nil {
return nil, err
}
// read the Kubernetes Secret with credentials
secrets, err := kms.getSecrets()
if err != nil {
return nil, fmt.Errorf("failed to get secrets for %T: %w", kms,
err)
}
err = setConfigString(&kms.accessKey, secrets, awsAccessKey)
if err != nil {
return nil, err
}
err = setConfigString(&kms.secretAccessKey, secrets,
awsSecretAccessKey)
if err != nil {
return nil, err
}
// awsSessionToken is optional
err = setConfigString(&kms.sessionToken, secrets, awsSessionToken)
if errors.Is(err, errConfigOptionInvalid) {
return nil, err
}
err = setConfigString(&kms.cmk, secrets, awsCMK)
if err != nil {
return nil, err
}
return kms, nil
}
func (kms *AWSMetadataKMS) getSecrets() (map[string]interface{}, error) {
c := k8s.NewK8sClient()
secret, err := c.CoreV1().Secrets(kms.namespace).Get(context.TODO(),
kms.secretName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get Secret %s/%s: %w",
kms.namespace, kms.secretName, err)
}
config := make(map[string]interface{})
for k, v := range secret.Data {
switch k {
case awsSecretAccessKey, awsAccessKey, awsSessionToken, awsCMK:
config[k] = string(v)
default:
return nil, fmt.Errorf("unsupported option for KMS "+
"provider %q: %s", kmsTypeAWSMetadata, k)
}
}
return config, nil
}
func (kms *AWSMetadataKMS) Destroy() {
// Nothing to do.
}
// requiresDEKStore indicates that the DEKs should get stored in the metadata
// of the volumes. This Amazon KMS provider does not support storing DEKs in
// AWS as that adds additional costs.
func (kms *AWSMetadataKMS) requiresDEKStore() DEKStoreType {
return DEKStoreMetadata
}
func (kms *AWSMetadataKMS) getService() (*awsKMS.KMS, error) {
creds := awsCreds.NewStaticCredentials(kms.accessKey,
kms.secretAccessKey, kms.sessionToken)
sess, err := awsSession.NewSessionWithOptions(awsSession.Options{
SharedConfigState: awsSession.SharedConfigDisable,
Config: aws.Config{
Credentials: creds,
Region: aws.String(kms.region),
},
})
if err != nil {
return nil, fmt.Errorf("failed to create AWS session: %w", err)
}
return awsKMS.New(sess), nil
}
// EncryptDEK uses the Amazon KMS and the configured CMK to encrypt the DEK.
func (kms *AWSMetadataKMS) EncryptDEK(volumeID, plainDEK string) (string, error) {
svc, err := kms.getService()
if err != nil {
return "", fmt.Errorf("could not get KMS service: %w", err)
}
result, err := svc.Encrypt(&awsKMS.EncryptInput{
KeyId: aws.String(kms.cmk),
Plaintext: []byte(plainDEK),
})
if err != nil {
return "", fmt.Errorf("failed to encrypt DEK: %w", err)
}
// base64 encode the encrypted DEK, so that storing it should not have
// issues
encryptedDEK :=
base64.StdEncoding.EncodeToString(result.CiphertextBlob)
return encryptedDEK, nil
}
// DecryptDEK uses the Amazon KMS and the configured CMK to decrypt the DEK.
func (kms *AWSMetadataKMS) DecryptDEK(volumeID, encryptedDEK string) (string, error) {
svc, err := kms.getService()
if err != nil {
return "", fmt.Errorf("could not get KMS service: %w", err)
}
ciphertextBlob, err := base64.StdEncoding.DecodeString(encryptedDEK)
if err != nil {
return "", fmt.Errorf("failed to decode base64 cipher: %w",
err)
}
result, err := svc.Decrypt(&awsKMS.DecryptInput{
CiphertextBlob: ciphertextBlob,
})
if err != nil {
return "", fmt.Errorf("failed to decrypt DEK: %w", err)
}
return string(result.Plaintext), nil
}

View File

@ -1,29 +0,0 @@
/*
Copyright 2021 The Ceph-CSI Authors.
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 util
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAWSMetadataKMSRegistered(t *testing.T) {
t.Parallel()
_, ok := kmsManager.providers[kmsTypeAWSMetadata]
assert.True(t, ok)
}

View File

@ -26,6 +26,7 @@ import (
"strconv"
"strings"
"github.com/ceph/ceph-csi/internal/kms"
"github.com/ceph/ceph-csi/internal/util/log"
)
@ -33,9 +34,6 @@ const (
mapperFilePrefix = "luks-rbd-"
mapperFilePathPrefix = "/dev/mapper"
// kmsConfigPath is the location of the vault config file.
kmsConfigPath = "/etc/ceph-csi-encryption-kms-config/config.json"
// Passphrase size - 20 bytes is 160 bits to satisfy:
// https://tools.ietf.org/html/rfc6749#section-10.10
encryptionPassphraseSize = 20
@ -54,11 +52,11 @@ var (
)
type VolumeEncryption struct {
KMS EncryptionKMS
KMS kms.EncryptionKMS
// dekStore that will be used, this can be the EncryptionKMS or a
// different object implementing the DEKStore interface.
dekStore DEKStore
dekStore kms.DEKStore
id string
}
@ -76,7 +74,7 @@ func FetchEncryptionKMSID(encrypted, kmsID string) (string, error) {
}
if kmsID == "" {
kmsID = defaultKMSType
kmsID = kms.DefaultKMSType
}
return kmsID, nil
@ -88,24 +86,24 @@ func FetchEncryptionKMSID(encrypted, kmsID string) (string, error) {
// Callers that receive a ErrDEKStoreNeeded error, should use
// VolumeEncryption.SetDEKStore() to configure an alternative storage for the
// DEKs.
func NewVolumeEncryption(id string, kms EncryptionKMS) (*VolumeEncryption, error) {
func NewVolumeEncryption(id string, ekms kms.EncryptionKMS) (*VolumeEncryption, error) {
kmsID := id
if kmsID == "" {
// if kmsID is not set, encryption is enabled, and the type is
// SecretsKMS
kmsID = defaultKMSType
kmsID = kms.DefaultKMSType
}
ve := &VolumeEncryption{
id: kmsID,
KMS: kms,
KMS: ekms,
}
if kms.requiresDEKStore() == DEKStoreIntegrated {
dekStore, ok := kms.(DEKStore)
if ekms.RequiresDEKStore() == kms.DEKStoreIntegrated {
dekStore, ok := ekms.(kms.DEKStore)
if !ok {
return nil, fmt.Errorf("KMS %T does not implement the "+
"DEKStore interface", kms)
"DEKStore interface", ekms)
}
ve.dekStore = dekStore
@ -118,7 +116,7 @@ func NewVolumeEncryption(id string, kms EncryptionKMS) (*VolumeEncryption, error
// SetDEKStore sets the DEKStore for this VolumeEncryption instance. It will be
// used when StoreNewCryptoPassphrase() or RemoveDEK() is called.
func (ve *VolumeEncryption) SetDEKStore(dekStore DEKStore) {
func (ve *VolumeEncryption) SetDEKStore(dekStore kms.DEKStore) {
ve.dekStore = dekStore
}
@ -141,72 +139,6 @@ func (ve *VolumeEncryption) GetID() string {
return ve.id
}
// EncryptionKMS provides external Key Management System for encryption
// passphrases storage.
type EncryptionKMS interface {
Destroy()
// requiresDEKStore returns the DEKStoreType that is needed to be
// configure for the KMS. Nothing needs to be done when this function
// returns DEKStoreIntegrated, otherwise you will need to configure an
// alternative storage for the DEKs.
requiresDEKStore() DEKStoreType
// EncryptDEK provides a way for a KMS to encrypt a DEK. In case the
// encryption is done transparently inside the KMS service, the
// function can return an unencrypted value.
EncryptDEK(volumeID, plainDEK string) (string, error)
// DecryptDEK provides a way for a KMS to decrypt a DEK. In case the
// encryption is done transparently inside the KMS service, the
// function does not need to do anything except return the encyptedDEK
// as it was received.
DecryptDEK(volumeID, encyptedDEK string) (string, error)
}
// DEKStoreType describes what DEKStore needs to be configured when using a
// particular KMS. A KMS might support different DEKStores depending on its
// configuration.
type DEKStoreType string
const (
// DEKStoreIntegrated indicates that the KMS itself supports storing
// DEKs.
DEKStoreIntegrated = DEKStoreType("")
// DEKStoreMetadata indicates that the KMS should be configured to
// store the DEK in the metadata of the volume.
DEKStoreMetadata = DEKStoreType("metadata")
)
// DEKStore allows KMS instances to implement a modular backend for DEK
// storage. This can be used to store the DEK in a different location, in case
// the KMS can not store passphrases for volumes.
type DEKStore interface {
// StoreDEK saves the DEK in the configured store.
StoreDEK(volumeID string, dek string) error
// FetchDEK reads the DEK from the configured store and returns it.
FetchDEK(volumeID string) (string, error)
// RemoveDEK deletes the DEK from the configured store.
RemoveDEK(volumeID string) error
}
// integratedDEK is a DEKStore that can not be configured. Either the KMS does
// not use a DEK, or the DEK is stored in the KMS without additional
// configuration options.
type integratedDEK struct{}
func (i integratedDEK) requiresDEKStore() DEKStoreType {
return DEKStoreIntegrated
}
func (i integratedDEK) EncryptDEK(volumeID, plainDEK string) (string, error) {
return plainDEK, nil
}
func (i integratedDEK) DecryptDEK(volumeID, encyptedDEK string) (string, error) {
return encyptedDEK, nil
}
// StoreCryptoPassphrase takes an unencrypted passphrase, encrypts it and saves
// it in the DEKStore.
func (ve *VolumeEncryption) StoreCryptoPassphrase(volumeID, passphrase string) error {

View File

@ -20,26 +20,12 @@ import (
"encoding/base64"
"testing"
"github.com/ceph/ceph-csi/internal/kms"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestInitSecretsKMS(t *testing.T) {
t.Parallel()
secrets := map[string]string{}
// no passphrase in the secrets, should fail
kms, err := initSecretsKMS(secrets)
assert.Error(t, err)
assert.Nil(t, kms)
// set a passphrase and it should pass
secrets[encryptionPassphraseKey] = "plaintext encryption key"
kms, err = initSecretsKMS(secrets)
assert.NotNil(t, kms)
assert.NoError(t, err)
}
func TestGenerateNewEncryptionPassphrase(t *testing.T) {
t.Parallel()
b64Passphrase, err := generateNewEncryptionPassphrase()
@ -55,17 +41,18 @@ func TestGenerateNewEncryptionPassphrase(t *testing.T) {
func TestKMSWorkflow(t *testing.T) {
t.Parallel()
secrets := map[string]string{
encryptionPassphraseKey: "workflow test",
// FIXME: use encryptionPassphraseKey from SecretsKMS
"encryptionPassphrase": "workflow test",
}
kms, err := GetKMS("tenant", defaultKMSType, secrets)
kmsProvider, err := kms.GetDefaultKMS(secrets)
assert.NoError(t, err)
require.NotNil(t, kms)
require.NotNil(t, kmsProvider)
ve, err := NewVolumeEncryption("", kms)
ve, err := NewVolumeEncryption("", kmsProvider)
assert.NoError(t, err)
require.NotNil(t, ve)
assert.Equal(t, defaultKMSType, ve.GetID())
assert.Equal(t, kms.DefaultKMSType, ve.GetID())
volumeID := "volume-id"
@ -74,5 +61,5 @@ func TestKMSWorkflow(t *testing.T) {
passphrase, err := ve.GetCryptoPassphrase(volumeID)
assert.NoError(t, err)
assert.Equal(t, secrets[encryptionPassphraseKey], passphrase)
assert.Equal(t, secrets["encryptionPassphrase"], passphrase)
}

View File

@ -1,288 +0,0 @@
/*
Copyright 2021 The Ceph-CSI Authors.
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 util
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"github.com/ceph/ceph-csi/internal/util/k8s"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// kmsProviderKey is the name of the KMS provider that is registered at
// the kmsManager. This is used in the ConfigMap configuration options.
kmsProviderKey = "KMS_PROVIDER"
// kmsTypeKey is the name of the KMS provider that is registered at
// the kmsManager. This is used in the configfile configuration
// options.
kmsTypeKey = "encryptionKMSType"
// podNamespaceEnv ENV should be set in the cephcsi container.
podNamespaceEnv = "POD_NAMESPACE"
// kmsConfigMapEnv env to read a ConfigMap by name.
kmsConfigMapEnv = "KMS_CONFIGMAP_NAME"
// defaultKMSConfigMapName default ConfigMap name to fetch kms
// connection details.
defaultKMSConfigMapName = "csi-kms-connection-details"
)
// GetKMS returns an instance of Key Management System.
//
// - tenant is the owner of the Volume, used to fetch the Vault Token from the
// Kubernetes Namespace where the PVC lives
// - kmsID is the service name of the KMS configuration
// - secrets contain additional details, like TLS certificates to connect to
// the KMS
func GetKMS(tenant, kmsID string, secrets map[string]string) (EncryptionKMS, error) {
if kmsID == "" || kmsID == defaultKMSType {
return initSecretsKMS(secrets)
}
config, err := getKMSConfiguration()
if err != nil {
return nil, err
}
// config contains a list of KMS connections, indexed by kmsID
section, ok := config[kmsID]
if !ok {
return nil, fmt.Errorf("could not get KMS configuration "+
"for %q (have %v)", kmsID, getKeys(config))
}
// kmsConfig can have additional sub-sections
kmsConfig, ok := section.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("failed to convert KMS configuration "+
"section: %s", kmsID)
}
return kmsManager.buildKMS(tenant, kmsConfig, secrets)
}
// getKMSConfiguration reads the configuration file from the filesystem, or if
// that fails the ConfigMap directly. The returned map contains all the KMS
// configuration sections, each keyed by its own kmsID.
func getKMSConfiguration() (map[string]interface{}, error) {
var config map[string]interface{}
// #nosec
content, err := ioutil.ReadFile(kmsConfigPath)
if err == nil {
// kmsConfigPath exists and was successfully read
err = json.Unmarshal(content, &config)
if err != nil {
return nil, fmt.Errorf("failed to parse KMS "+
"configuration: %w", err)
}
} else {
// an error occurred while reading kmsConfigPath
if !os.IsNotExist(err) {
return nil, fmt.Errorf("failed to read KMS "+
"configuration from %s: %w", kmsConfigPath,
err)
}
// If the configmap is not mounted to the CSI pods read the
// configmap the kubernetes.
config, err = getKMSConfigMap()
if err != nil {
return nil, err
}
}
return config, nil
}
// getPodNamespace reads the `podNamespaceEnv` from the environment and returns
// its value. In case the namespace can not be detected, an error is returned.
func getPodNamespace() (string, error) {
ns := os.Getenv(podNamespaceEnv)
if ns == "" {
return "", fmt.Errorf("%q is not set in the environment",
podNamespaceEnv)
}
return ns, nil
}
// getKMSConfigMapName reads the `kmsConfigMapEnv` from the environment, or
// returns the value of `defaultKMSConfigMapName` if it was not set.
func getKMSConfigMapName() string {
cmName := os.Getenv(kmsConfigMapEnv)
if cmName == "" {
cmName = defaultKMSConfigMapName
}
return cmName
}
// getKMSConfigMap returns the contents of the ConfigMap.
//
// FIXME: Ceph-CSI should not talk to Kubernetes directly.
func getKMSConfigMap() (map[string]interface{}, error) {
ns, err := getPodNamespace()
if err != nil {
return nil, err
}
cmName := getKMSConfigMapName()
c := k8s.NewK8sClient()
cm, err := c.CoreV1().ConfigMaps(ns).Get(context.Background(),
cmName, metav1.GetOptions{})
if err != nil {
return nil, err
}
// convert cm.Data from map[string]interface{}
kmsConfig := make(map[string]interface{})
for kmsID, data := range cm.Data {
section := make(map[string]interface{})
err = json.Unmarshal([]byte(data), &section)
if err != nil {
return nil, fmt.Errorf("could not convert contents "+
"of %q to s config section", kmsID)
}
kmsConfig[kmsID] = section
}
return kmsConfig, nil
}
// getKMSProvider inspects the configuration and tries to identify what
// KMSProvider is expected to be used with it. This returns the
// KMSProvider.UniqueID.
func getKMSProvider(config map[string]interface{}) (string, error) {
var name string
providerName, ok := config[kmsTypeKey]
if ok {
name, ok = providerName.(string)
if !ok {
return "", fmt.Errorf("could not convert KMS provider"+
"type (%v) to string", providerName)
}
return name, nil
}
providerName, ok = config[kmsProviderKey]
if ok {
name, ok = providerName.(string)
if !ok {
return "", fmt.Errorf("could not convert KMS provider"+
"type (%v) to string", providerName)
}
return name, nil
}
return "", fmt.Errorf("failed to get KMS provider, missing"+
"configuration option %q or %q", kmsTypeKey, kmsProviderKey)
}
// KMSInitializerArgs get passed to KMSInitializerFunc when a new instance of a
// KMSProvider is initialized.
type KMSInitializerArgs struct {
Tenant string
Config map[string]interface{}
Secrets map[string]string
// Namespace contains the Kubernetes Namespace where the Ceph-CSI Pods
// are running. This is an optional option, and might be unset when the
// KMSProvider.Initializer is called.
Namespace string
}
// KMSInitializerFunc gets called when the KMSProvider needs to be
// instantiated.
type KMSInitializerFunc func(args KMSInitializerArgs) (EncryptionKMS, error)
type KMSProvider struct {
UniqueID string
Initializer KMSInitializerFunc
}
type kmsProviderList struct {
providers map[string]KMSProvider
}
// kmsManager is used to create instances for a KMS provider.
var kmsManager = kmsProviderList{providers: map[string]KMSProvider{}}
// RegisterKMSProvider uses kmsManager to register the given KMSProvider. The
// KMSProvider.Initializer function will get called when a new instance of the
// KMS is required.
func RegisterKMSProvider(provider KMSProvider) bool {
// validate uniqueness of the UniqueID
if provider.UniqueID == "" {
panic("a provider MUST set a UniqueID")
}
_, ok := kmsManager.providers[provider.UniqueID]
if ok {
panic("duplicate registration of KMSProvider.UniqueID: " + provider.UniqueID)
}
// validate the Initializer
if provider.Initializer == nil {
panic("a provider MUST have an Initializer")
}
kmsManager.providers[provider.UniqueID] = provider
return true
}
// buildKMS creates a new KMSProvider instance, based on the configuration that
// was passed. This uses getKMSProvider() internally to identify the
// KMSProvider to instantiate.
func (kf *kmsProviderList) buildKMS(
tenant string,
config map[string]interface{},
secrets map[string]string) (EncryptionKMS, error) {
providerName, err := getKMSProvider(config)
if err != nil {
return nil, err
}
provider, ok := kf.providers[providerName]
if !ok {
return nil, fmt.Errorf("could not find KMS provider %q",
providerName)
}
kmsInitArgs := KMSInitializerArgs{
Tenant: tenant,
Config: config,
Secrets: secrets,
}
// Namespace is an optional parameter, it may not be set and is not
// required for all KMSProviders
ns, err := getPodNamespace()
if err == nil {
kmsInitArgs.Namespace = ns
}
return provider.Initializer(kmsInitArgs)
}

View File

@ -1,55 +0,0 @@
/*
Copyright 2021 The Ceph-CSI Authors.
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 util
import (
"testing"
"github.com/stretchr/testify/assert"
)
func noinitKMS(args KMSInitializerArgs) (EncryptionKMS, error) {
return nil, nil
}
func TestRegisterKMSProvider(t *testing.T) {
t.Parallel()
tests := []struct {
provider KMSProvider
panics bool
}{{
KMSProvider{
UniqueID: "incomplete-provider",
},
true,
}, {
KMSProvider{
UniqueID: "initializer-only",
Initializer: noinitKMS,
},
false,
}}
for _, test := range tests {
provider := test.provider
if test.panics {
assert.Panics(t, func() { RegisterKMSProvider(provider) })
} else {
assert.True(t, RegisterKMSProvider(provider))
}
}
}

View File

@ -1,285 +0,0 @@
/*
Copyright 2019 The Ceph-CSI Authors.
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 util
import (
"context"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/json"
"errors"
"fmt"
"io"
"github.com/ceph/ceph-csi/internal/util/k8s"
"golang.org/x/crypto/scrypt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
// Encryption passphrase location in K8s secrets.
encryptionPassphraseKey = "encryptionPassphrase"
// Default KMS type.
defaultKMSType = "default"
// kmsTypeSecretsMetadata is the SecretsKMS with per-volume encryption,
// where the DEK is stored in the metadata of the volume itself.
kmsTypeSecretsMetadata = "metadata"
// metadataSecretNameKey contains the key which corresponds to the
// kubernetes secret name from where encryptionPassphrase is feteched.
metadataSecretNameKey = "secretName"
// metadataSecretNamespaceKey contains the key which corresponds to the
// kubernetes secret namespace from where encryptionPassphrase is feteched.
metadataSecretNamespaceKey = "secretNamespace"
)
// SecretsKMS is default KMS implementation that means no KMS is in use.
type SecretsKMS struct {
integratedDEK
passphrase string
}
// initSecretsKMS initializes a SecretsKMS that uses the passphrase from the
// secret that is configured for the StorageClass. This KMS provider uses a
// single (LUKS) passhprase for all volumes.
func initSecretsKMS(secrets map[string]string) (EncryptionKMS, error) {
passphraseValue, ok := secrets[encryptionPassphraseKey]
if !ok {
return nil, errors.New("missing encryption passphrase in secrets")
}
return SecretsKMS{passphrase: passphraseValue}, nil
}
// Destroy frees all used resources.
func (kms SecretsKMS) Destroy() {
// nothing to do
}
// FetchDEK returns passphrase from Kubernetes secrets.
func (kms SecretsKMS) FetchDEK(key string) (string, error) {
return kms.passphrase, nil
}
// StoreDEK does nothing, as there is no passphrase per key (volume), so
// no need to store is anywhere.
func (kms SecretsKMS) StoreDEK(key, value string) error {
return nil
}
// RemoveDEK is doing nothing as no new passphrases are saved with
// SecretsKMS.
func (kms SecretsKMS) RemoveDEK(key string) error {
return nil
}
// SecretsMetadataKMS is a KMS based on the SecretsKMS, but stores the
// Data-Encryption-Key (DEK) in the metadata of the volume.
type SecretsMetadataKMS struct {
SecretsKMS
}
var _ = RegisterKMSProvider(KMSProvider{
UniqueID: kmsTypeSecretsMetadata,
Initializer: initSecretsMetadataKMS,
})
// initSecretsMetadataKMS initializes a SecretsMetadataKMS that wraps a SecretsKMS,
// so that the passphrase from the user provided or StorageClass secrets can be used
// for encrypting/decrypting DEKs that are stored in a detached DEKStore.
func initSecretsMetadataKMS(args KMSInitializerArgs) (EncryptionKMS, error) {
var (
smKMS SecretsMetadataKMS
encryptionPassphrase string
ok bool
err error
)
encryptionPassphrase, err = smKMS.fetchEncryptionPassphrase(
args.Config, args.Tenant)
if err != nil {
if !errors.Is(err, errConfigOptionMissing) {
return nil, err
}
// if 'userSecret' option is not specified, fetch encryptionPassphrase
// from storageclass secrets.
encryptionPassphrase, ok = args.Secrets[encryptionPassphraseKey]
if !ok {
return nil, fmt.Errorf(
"missing %q in storageclass secret", encryptionPassphraseKey)
}
}
smKMS.SecretsKMS = SecretsKMS{passphrase: encryptionPassphrase}
return smKMS, nil
}
// fetchEncryptionPassphrase fetches encryptionPassphrase from user provided secret.
func (kms SecretsMetadataKMS) fetchEncryptionPassphrase(
config map[string]interface{},
defaultNamespace string) (string, error) {
var (
secretName string
secretNamespace string
)
err := setConfigString(&secretName, config, metadataSecretNameKey)
if err != nil {
return "", err
}
err = setConfigString(&secretNamespace, config, metadataSecretNamespaceKey)
if err != nil {
if !errors.Is(err, errConfigOptionMissing) {
return "", err
}
// if 'secretNamespace' option is not specified, defaults to namespace in
// which PVC was created
secretNamespace = defaultNamespace
}
c := k8s.NewK8sClient()
secret, err := c.CoreV1().Secrets(secretNamespace).Get(context.TODO(),
secretName, metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("failed to get Secret %s/%s: %w",
secretNamespace, secretName, err)
}
passphraseValue, ok := secret.Data[encryptionPassphraseKey]
if !ok {
return "", fmt.Errorf("missing %q in Secret %s/%s",
encryptionPassphraseKey, secretNamespace, secretName)
}
return string(passphraseValue), nil
}
// Destroy frees all used resources.
func (kms SecretsMetadataKMS) Destroy() {
kms.SecretsKMS.Destroy()
}
func (kms SecretsMetadataKMS) requiresDEKStore() DEKStoreType {
return DEKStoreMetadata
}
// encryptedMetedataDEK contains the encrypted DEK and the Nonce that was used
// during encryption. This structure is stored (in JSON format) in the DEKStore
// that is linked to this KMS provider.
type encryptedMetedataDEK struct {
// DEK is the encrypted data-encryption-key for the volume.
DEK []byte `json:"dek"`
// Nonce is a random byte slice to guarantee the uniqueness of the
// encrypted DEK.
Nonce []byte `json:"nonce"`
}
// EncryptDEK encrypts the plainDEK with a key derived from the passphrase from
// the SecretsKMS and the volumeID.
// The resulting encryptedDEK contains a JSON with the encrypted DEK and the
// nonce that was used for encrypting.
func (kms SecretsMetadataKMS) EncryptDEK(volumeID, plainDEK string) (string, error) {
// use the passphrase from the SecretsKMS
passphrase, err := kms.SecretsKMS.FetchDEK(volumeID)
if err != nil {
return "", fmt.Errorf("failed to get passphrase: %w", err)
}
aead, err := generateCipher(passphrase, volumeID)
if err != nil {
return "", fmt.Errorf("failed to generate cipher: %w", err)
}
emd := encryptedMetedataDEK{}
emd.Nonce, err = generateNonce(aead.NonceSize())
if err != nil {
return "", fmt.Errorf("failed to generated nonce: %w", err)
}
emd.DEK = aead.Seal(nil, emd.Nonce, []byte(plainDEK), nil)
emdData, err := json.Marshal(&emd)
if err != nil {
return "", fmt.Errorf("failed to convert "+
"encryptedMetedataDEK to JSON: %w", err)
}
return string(emdData), nil
}
// DecryptDEK takes the JSON formatted `encryptedMetadataDEK` contents, and it
// fetches SecretsKMS passphrase to decrypt the DEK.
func (kms SecretsMetadataKMS) DecryptDEK(volumeID, encryptedDEK string) (string, error) {
// use the passphrase from the SecretsKMS
passphrase, err := kms.SecretsKMS.FetchDEK(volumeID)
if err != nil {
return "", fmt.Errorf("failed to get passphrase: %w", err)
}
aead, err := generateCipher(passphrase, volumeID)
if err != nil {
return "", fmt.Errorf("failed to generate cipher: %w", err)
}
emd := encryptedMetedataDEK{}
err = json.Unmarshal([]byte(encryptedDEK), &emd)
if err != nil {
return "", fmt.Errorf("failed to convert data to "+
"encryptedMetedataDEK: %w", err)
}
dek, err := aead.Open(nil, emd.Nonce, emd.DEK, nil)
if err != nil {
return "", fmt.Errorf("failed to decrypt DEK: %w", err)
}
return string(dek), nil
}
// generateCipher returns a AEAD cipher based on a passphrase and salt
// (volumeID). The cipher can then be used to encrypt/decrypt the DEK.
func generateCipher(passphrase, salt string) (cipher.AEAD, error) {
key, err := scrypt.Key([]byte(passphrase), []byte(salt), 32768, 8, 1, 32)
if err != nil {
return nil, err
}
blockCipher, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
aead, err := cipher.NewGCM(blockCipher)
if err != nil {
return nil, err
}
return aead, nil
}
// generateNonce returns a byte slice with random contents.
func generateNonce(size int) ([]byte, error) {
nonce := make([]byte, size)
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
return nonce, nil
}

View File

@ -1,110 +0,0 @@
/*
Copyright 2021 The Ceph-CSI Authors.
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 util
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGenerateNonce(t *testing.T) {
t.Parallel()
size := 64
nonce, err := generateNonce(size)
assert.Equal(t, size, len(nonce))
assert.NoError(t, err)
}
func TestGenerateCipher(t *testing.T) {
t.Parallel()
// nolint:gosec // this passphrase is intentionally hardcoded
passphrase := "my-cool-luks-passphrase"
salt := "unique-id-for-the-volume"
aead, err := generateCipher(passphrase, salt)
assert.NoError(t, err)
assert.NotNil(t, aead)
}
func TestInitSecretsMetadataKMS(t *testing.T) {
t.Parallel()
args := KMSInitializerArgs{
Tenant: "tenant",
Config: nil,
Secrets: map[string]string{},
}
// passphrase it not set, init should fail
kms, err := initSecretsMetadataKMS(args)
assert.Error(t, err)
assert.Nil(t, kms)
// set a passphrase to get a working KMS
args.Secrets[encryptionPassphraseKey] = "my-passphrase-from-kubernetes"
kms, err = initSecretsMetadataKMS(args)
assert.NoError(t, err)
require.NotNil(t, kms)
assert.Equal(t, DEKStoreMetadata, kms.requiresDEKStore())
}
func TestWorkflowSecretsMetadataKMS(t *testing.T) {
t.Parallel()
secrets := map[string]string{
encryptionPassphraseKey: "my-passphrase-from-kubernetes",
}
args := KMSInitializerArgs{
Tenant: "tenant",
Config: nil,
Secrets: secrets,
}
volumeID := "csi-vol-1b00f5f8-b1c1-11e9-8421-9243c1f659f0"
kms, err := initSecretsMetadataKMS(args)
assert.NoError(t, err)
require.NotNil(t, kms)
// plainDEK is the (LUKS) passphrase for the volume
plainDEK, err := generateNewEncryptionPassphrase()
assert.NoError(t, err)
assert.NotEqual(t, "", plainDEK)
encryptedDEK, err := kms.EncryptDEK(volumeID, plainDEK)
assert.NoError(t, err)
assert.NotEqual(t, "", encryptedDEK)
assert.NotEqual(t, plainDEK, encryptedDEK)
// with an incorrect volumeID, decrypting should fail
decryptedDEK, err := kms.DecryptDEK("incorrect-volumeID", encryptedDEK)
assert.Error(t, err)
assert.Equal(t, "", decryptedDEK)
assert.NotEqual(t, plainDEK, decryptedDEK)
// with the right volumeID, decrypting should return the plainDEK
decryptedDEK, err = kms.DecryptDEK(volumeID, encryptedDEK)
assert.NoError(t, err)
assert.NotEqual(t, "", decryptedDEK)
assert.Equal(t, plainDEK, decryptedDEK)
}
func TestSecretsMetadataKMSRegistered(t *testing.T) {
t.Parallel()
_, ok := kmsManager.providers[kmsTypeSecretsMetadata]
assert.True(t, ok)
}

View File

@ -344,20 +344,6 @@ func contains(s []string, key string) bool {
return false
}
// getKeys takes a map that uses strings for keys and returns a slice with the
// keys.
func getKeys(m map[string]interface{}) []string {
keys := make([]string, len(m))
i := 0
for k := range m {
keys[i] = k
i++
}
return keys
}
// CallStack returns the stack of the calls in the current goroutine. Useful
// for debugging or reporting errors. This is a friendly alternative to
// assert() or panic().

View File

@ -1,499 +0,0 @@
/*
Copyright 2019 The Ceph-CSI Authors.
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 util
import (
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/hashicorp/vault/api"
loss "github.com/libopenstorage/secrets"
"github.com/libopenstorage/secrets/vault"
)
const (
kmsTypeVault = "vault"
// path to service account token that will be used to authenticate with Vault
// #nosec
serviceAccountTokenPath = "/var/run/secrets/kubernetes.io/serviceaccount/token"
// vault configuration defaults.
vaultDefaultAuthPath = "/v1/auth/kubernetes/login"
vaultDefaultAuthMountPath = "kubernetes" // main component of vaultAuthPath
vaultDefaultRole = "csi-kubernetes"
vaultDefaultNamespace = ""
vaultDefaultPassphrasePath = ""
vaultDefaultCAVerify = "true"
vaultDefaultDestroyKeys = "true"
)
var (
errConfigOptionMissing = errors.New("configuration option not set")
errConfigOptionInvalid = errors.New("configuration option not valid")
)
/*
VaultKMS represents a Hashicorp Vault KMS configuration
Example JSON structure in the KMS config is,
{
"local_vault_unique_identifier": {
"encryptionKMSType": "vault",
"vaultAddress": "https://127.0.0.1:8500",
"vaultAuthPath": "/v1/auth/kubernetes/login",
"vaultRole": "csi-kubernetes",
"vaultNamespace": "",
"vaultPassphraseRoot": "/v1/secret",
"vaultPassphrasePath": "",
"vaultCAVerify": true,
"vaultCAFromSecret": "vault-ca"
},
...
}.
*/
type vaultConnection struct {
secrets loss.Secrets
vaultConfig map[string]interface{}
keyContext map[string]string
// vaultDestroyKeys will by default set to `true`, and causes secrets
// to be deleted from Hashicorp Vault to be completely removed. Usually
// secrets in a kv-v2 store will be soft-deleted, and recovering the
// contents is still possible.
//
// This option is only valid during deletion of keys, see
// getDeleteKeyContext() for more details.
vaultDestroyKeys bool
}
type VaultKMS struct {
vaultConnection
integratedDEK
// vaultPassphrasePath (VPP) used to be added before the "key" of the
// secret (like /v1/secret/data/<VPP>/key)
vaultPassphrasePath string
}
// setConfigString fetches a value from a configuration map and converts it to
// a string.
//
// If the value is not available, *option is not adjusted and
// errConfigOptionMissing is returned.
// In case the value is available, but can not be converted to a string,
// errConfigOptionInvalid is returned.
func setConfigString(option *string, config map[string]interface{}, key string) error {
value, ok := config[key]
if !ok {
return fmt.Errorf("%w: %s", errConfigOptionMissing, key)
}
s, ok := value.(string)
if !ok {
return fmt.Errorf("%w: expected string for %q, but got %T",
errConfigOptionInvalid, key, value)
}
*option = s
return nil
}
// initConnection sets VAULT_* environment variables in the vc.vaultConfig map,
// these settings will be used when connecting to the Vault service with
// vc.connectVault().
//
// nolint:gocyclo,cyclop // iterating through many config options, not complex at all.
func (vc *vaultConnection) initConnection(config map[string]interface{}) error {
vaultConfig := make(map[string]interface{})
keyContext := make(map[string]string)
firstInit := (vc.vaultConfig == nil)
vaultAddress := "" // required
err := setConfigString(&vaultAddress, config, "vaultAddress")
switch {
case errors.Is(err, errConfigOptionInvalid):
return err
case firstInit && errors.Is(err, errConfigOptionMissing):
return err
case !errors.Is(err, errConfigOptionMissing):
vaultConfig[api.EnvVaultAddress] = vaultAddress
}
// default: !firstInit
vaultBackend := "" // optional
err = setConfigString(&vaultBackend, config, "vaultBackend")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// set the option if the value was not invalid
if !errors.Is(err, errConfigOptionMissing) {
vaultConfig[vault.VaultBackendKey] = vaultBackend
}
vaultBackendPath := "" // optional
err = setConfigString(&vaultBackendPath, config, "vaultBackendPath")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// set the option if the value was not invalid
if !errors.Is(err, errConfigOptionMissing) {
vaultConfig[vault.VaultBackendPathKey] = vaultBackendPath
}
// always set the default to prevent recovering kv-v2 keys
vaultDestroyKeys := vaultDefaultDestroyKeys
err = setConfigString(&vaultDestroyKeys, config, "vaultDestroyKeys")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
if firstInit || !errors.Is(err, errConfigOptionMissing) {
if vaultDestroyKeys == vaultDefaultDestroyKeys {
vc.vaultDestroyKeys = true
} else {
vc.vaultDestroyKeys = false
}
}
vaultTLSServerName := "" // optional
err = setConfigString(&vaultTLSServerName, config, "vaultTLSServerName")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// set the option if the value was not invalid
if !errors.Is(err, errConfigOptionMissing) {
vaultConfig[api.EnvVaultTLSServerName] = vaultTLSServerName
}
vaultNamespace := vaultDefaultNamespace // optional
err = setConfigString(&vaultNamespace, config, "vaultNamespace")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
vaultAuthNamespace := vaultNamespace // optional, same as vaultNamespace
err = setConfigString(&vaultAuthNamespace, config, "vaultAuthNamespace")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// set the option if the value was not invalid
if firstInit || !errors.Is(err, errConfigOptionMissing) {
vaultConfig[api.EnvVaultNamespace] = vaultAuthNamespace
keyContext[loss.KeyVaultNamespace] = vaultNamespace
}
verifyCA := vaultDefaultCAVerify // optional
err = setConfigString(&verifyCA, config, "vaultCAVerify")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
if firstInit || !errors.Is(err, errConfigOptionMissing) {
var vaultCAVerify bool
vaultCAVerify, err = strconv.ParseBool(verifyCA)
if err != nil {
return fmt.Errorf("failed to parse 'vaultCAVerify': %w", err)
}
vaultConfig[api.EnvVaultInsecure] = strconv.FormatBool(!vaultCAVerify)
}
vaultCAFromSecret := "" // optional
err = setConfigString(&vaultCAFromSecret, config, "vaultCAFromSecret")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// update the existing config only if no config is available yet
if vc.keyContext != nil {
for key, value := range keyContext {
vc.keyContext[key] = value
}
} else {
vc.keyContext = keyContext
}
if vc.vaultConfig != nil {
for key, value := range vaultConfig {
vc.vaultConfig[key] = value
}
} else {
vc.vaultConfig = vaultConfig
}
return nil
}
// initCertificates sets VAULT_* environment variables in the vc.vaultConfig map,
// these settings will be used when connecting to the Vault service with
// vc.connectVault().
//
func (vc *vaultConnection) initCertificates(config map[string]interface{}, secrets map[string]string) error {
vaultConfig := make(map[string]interface{})
vaultCAFromSecret := "" // optional
err := setConfigString(&vaultCAFromSecret, config, "vaultCAFromSecret")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// ignore errConfigOptionMissing, no default was set
if vaultCAFromSecret != "" {
caPEM, ok := secrets[vaultCAFromSecret]
if !ok {
return fmt.Errorf("missing vault CA in secret %s", vaultCAFromSecret)
}
vaultConfig[api.EnvVaultCACert], err = createTempFile("vault-ca-cert", []byte(caPEM))
if err != nil {
return fmt.Errorf("failed to create temporary file for Vault CA: %w", err)
}
// update the existing config
for key, value := range vaultConfig {
vc.vaultConfig[key] = value
}
}
return nil
}
// connectVault creates a new connection to Vault. This should be called after
// filling vc.vaultConfig.
func (vc *vaultConnection) connectVault() error {
v, err := vault.New(vc.vaultConfig)
if err != nil {
return fmt.Errorf("failed connecting to Vault: %w", err)
}
vc.secrets = v
return nil
}
// Destroy frees allocated resources. For a vaultConnection that means removing
// the created temporary files.
func (vc *vaultConnection) Destroy() {
if vc.vaultConfig != nil {
tmpFile, ok := vc.vaultConfig[api.EnvVaultCACert]
if ok {
// ignore error on failure to remove tmpfile (gosec complains)
_ = os.Remove(tmpFile.(string))
}
}
}
// getDeleteKeyContext creates a new KeyContext that has an optional value set
// to destroy the contents of secrets. This is configurable with the
// `vaultDestroyKeys` configuration parameter.
//
// Setting the option `DestroySecret` option in the KeyContext while creating
// new keys, causes failures, so the option only needs to be set in the
// RemoveDEK() calls.
func (vc *vaultConnection) getDeleteKeyContext() map[string]string {
keyContext := map[string]string{}
for k, v := range vc.keyContext {
keyContext[k] = v
}
if vc.vaultDestroyKeys {
keyContext[loss.DestroySecret] = vaultDefaultDestroyKeys
}
return keyContext
}
var _ = RegisterKMSProvider(KMSProvider{
UniqueID: kmsTypeVault,
Initializer: initVaultKMS,
})
// InitVaultKMS returns an interface to HashiCorp Vault KMS.
func initVaultKMS(args KMSInitializerArgs) (EncryptionKMS, error) {
kms := &VaultKMS{}
err := kms.initConnection(args.Config)
if err != nil {
return nil, fmt.Errorf("failed to initialize Vault connection: %w", err)
}
err = kms.initCertificates(args.Config, args.Secrets)
if err != nil {
return nil, fmt.Errorf("failed to initialize Vault certificates: %w", err)
}
vaultAuthPath := vaultDefaultAuthPath
err = setConfigString(&vaultAuthPath, args.Config, "vaultAuthPath")
if err != nil {
return nil, err
}
kms.vaultConfig[vault.AuthMountPath], err = detectAuthMountPath(vaultAuthPath)
if err != nil {
return nil, fmt.Errorf("failed to set %s in Vault config: %w", vault.AuthMountPath, err)
}
vaultRole := vaultDefaultRole
err = setConfigString(&vaultRole, args.Config, "vaultRole")
if err != nil {
return nil, err
}
kms.vaultConfig[vault.AuthKubernetesRole] = vaultRole
// vault.VaultBackendPathKey is "secret/" by default, use vaultPassphraseRoot if configured
vaultPassphraseRoot := ""
err = setConfigString(&vaultPassphraseRoot, args.Config, "vaultPassphraseRoot")
if err == nil {
// the old example did have "/v1/secret/", convert that format
if strings.HasPrefix(vaultPassphraseRoot, "/v1/") {
kms.vaultConfig[vault.VaultBackendPathKey] = strings.TrimPrefix(vaultPassphraseRoot, "/v1/")
} else {
kms.vaultConfig[vault.VaultBackendPathKey] = vaultPassphraseRoot
}
} else if !errors.Is(err, errConfigOptionMissing) {
return nil, err
}
kms.vaultPassphrasePath = vaultDefaultPassphrasePath
err = setConfigString(&kms.vaultPassphrasePath, args.Config, "vaultPassphrasePath")
if err != nil {
return nil, err
}
// FIXME: vault.AuthKubernetesTokenPath is not enough? EnvVaultToken needs to be set?
kms.vaultConfig[vault.AuthMethod] = vault.AuthMethodKubernetes
kms.vaultConfig[vault.AuthKubernetesTokenPath] = serviceAccountTokenPath
err = kms.connectVault()
if err != nil {
return nil, err
}
return kms, nil
}
// FetchDEK returns passphrase from Vault. The passphrase is stored in a
// data.data.passphrase structure.
func (kms *VaultKMS) FetchDEK(key string) (string, error) {
s, err := kms.secrets.GetSecret(filepath.Join(kms.vaultPassphrasePath, key), kms.keyContext)
if err != nil {
return "", err
}
data, ok := s["data"].(map[string]interface{})
if !ok {
return "", fmt.Errorf("failed parsing data for get passphrase request for %q", key)
}
passphrase, ok := data["passphrase"].(string)
if !ok {
return "", fmt.Errorf("failed parsing passphrase for get passphrase request for %q", key)
}
return passphrase, nil
}
// StoreDEK saves new passphrase in Vault.
func (kms *VaultKMS) StoreDEK(key, value string) error {
data := map[string]interface{}{
"data": map[string]string{
"passphrase": value,
},
}
pathKey := filepath.Join(kms.vaultPassphrasePath, key)
err := kms.secrets.PutSecret(pathKey, data, kms.keyContext)
if err != nil {
return fmt.Errorf("saving passphrase at %s request to vault failed: %w", pathKey, err)
}
return nil
}
// RemoveDEK deletes passphrase from Vault.
func (kms *VaultKMS) RemoveDEK(key string) error {
pathKey := filepath.Join(kms.vaultPassphrasePath, key)
err := kms.secrets.DeleteSecret(pathKey, kms.getDeleteKeyContext())
if err != nil {
return fmt.Errorf("delete passphrase at %s request to vault failed: %w", pathKey, err)
}
return nil
}
// detectAuthMountPath takes the vaultAuthPath configuration option that
// defaults to "/v1/auth/kubernetes/login" and makes it a vault.AuthMountPath
// like "kubernetes".
func detectAuthMountPath(path string) (string, error) {
var authMountPath string
if path == "" {
return "", errors.New("path is empty")
}
// add all components between "login" and "auth" to authMountPath
match := false
parts := strings.Split(path, "/")
for _, part := range parts {
if part == "auth" {
match = true
continue
}
if part == "login" {
break
}
if match && authMountPath == "" {
authMountPath = part
} else if match {
authMountPath += "/" + part
}
}
// in case authMountPath is empty, return original path as it was
if authMountPath == "" {
authMountPath = path
}
return authMountPath, nil
}
// createTempFile writes data to a temporary file that contains the pattern in
// the filename (see ioutil.TempFile for details).
func createTempFile(pattern string, data []byte) (string, error) {
t, err := ioutil.TempFile("", pattern)
if err != nil {
return "", fmt.Errorf("failed to create temporary file: %w", err)
}
// delete the tmpfile on error
defer func() {
if err != nil {
// ignore error on failure to remove tmpfile (gosec complains)
_ = os.Remove(t.Name())
}
}()
s, err := t.Write(data)
if err != nil || s != len(data) {
return "", fmt.Errorf("failed to write temporary file: %w", err)
}
err = t.Close()
if err != nil {
return "", fmt.Errorf("failed to close temporary file: %w", err)
}
return t.Name(), nil
}

View File

@ -1,318 +0,0 @@
/*
Copyright 2021 The Ceph-CSI Authors.
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 util
import (
"context"
"errors"
"fmt"
"io/ioutil"
"os"
"github.com/libopenstorage/secrets/vault"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
kmsTypeVaultTenantSA = "vaulttenantsa"
// vaultTenantSAName is the default name of the ServiceAccount that
// should be available in the Tenants namespace. This ServiceAccount
// will be used to connect to Hashicorp Vault.
vaultTenantSAName = "ceph-csi-vault-sa"
)
/*
VaultTenantSA represents a Hashicorp Vault KMS configuration that uses a
ServiceAccount from the Tenant that owns the volume to store/retrieve the
encryption passphrase of volumes.
Example JSON structure in the KMS config is,
{
"vault-tenant-sa": {
"encryptionKMSType": "vaulttenantsa",
"vaultAddress": "http://vault.default.svc.cluster.local:8200",
"vaultBackendPath": "secret/",
"vaultTLSServerName": "vault.default.svc.cluster.local",
"vaultCAFromSecret": "vault-ca",
"vaultClientCertFromSecret": "vault-client-cert",
"vaultClientCertKeyFromSecret": "vault-client-cert-key",
"vaultCAVerify": "false",
"tenantConfigName": "ceph-csi-kms-config",
"tenantSAName": "ceph-csi-vault-sa",
"tenants": {
"my-app": {
"vaultAddress": "https://vault.example.com",
"vaultCAVerify": "true"
},
"an-other-app": {
"tenantSAName": "encryped-storage-sa"
}
},
...
}.
*/
type VaultTenantSA struct {
vaultTenantConnection
// tenantSAName is the name of the ServiceAccount in the Tenants Kubernetes Namespace
tenantSAName string
// saTokenDir contains the directory that holds the token to connect to Vault.
saTokenDir string
}
var _ = RegisterKMSProvider(KMSProvider{
UniqueID: kmsTypeVaultTenantSA,
Initializer: initVaultTenantSA,
})
// initVaultTenantSA returns an interface to HashiCorp Vault KMS where Tenants
// use their ServiceAccount to access the service.
func initVaultTenantSA(args KMSInitializerArgs) (EncryptionKMS, error) {
var err error
config := args.Config
if _, ok := config[kmsProviderKey]; ok {
// configuration comes from the ConfigMap, needs to be
// converted to vaultTokenConf type
config, err = transformConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to convert configuration: %w", err)
}
}
kms := &VaultTenantSA{}
kms.vaultTenantConnection.init()
kms.tenantConfigOptionFilter = isTenantSAConfigOption
err = kms.initConnection(config)
if err != nil {
return nil, fmt.Errorf("failed to initialize Vault connection: %w", err)
}
// set default values for optional config options
kms.ConfigName = vaultTokensDefaultConfigName
kms.tenantSAName = vaultTenantSAName
// "vaultAuthPath" is configurable per tenant
kms.vaultConfig[vault.AuthMountPath] = vaultDefaultAuthMountPath
// "vaultRole" is configurable per tenant
kms.vaultConfig[vault.AuthKubernetesRole] = vaultDefaultRole
err = kms.parseConfig(config)
if err != nil {
return nil, err
}
// fetch the configuration for the tenant
if args.Tenant != "" {
err = kms.configureTenant(config, args.Tenant)
if err != nil {
return nil, err
}
}
kms.vaultConfig[vault.AuthMethod] = vault.AuthMethodKubernetes
kms.vaultConfig[vault.AuthKubernetesTokenPath], err = kms.getTokenPath()
if err != nil {
return nil, fmt.Errorf("failed setting up token for %s/%s: %w", kms.Tenant, kms.tenantSAName, err)
}
err = kms.initCertificates(config)
if err != nil {
return nil, fmt.Errorf("failed to initialize Vault certificates: %w", err)
}
// connect to the Vault service
err = kms.connectVault()
if err != nil {
return nil, err
}
return kms, nil
}
// Destroy removes the temporary stored token from the ServiceAccount and
// destroys the vaultTenantConnection object.
func (kms *VaultTenantSA) Destroy() {
if kms.saTokenDir != "" {
_ = os.RemoveAll(kms.saTokenDir)
}
kms.vaultTenantConnection.Destroy()
}
func (kms *VaultTenantSA) configureTenant(config map[string]interface{}, tenant string) error {
kms.Tenant = tenant
tenantConfig, found := fetchTenantConfig(config, tenant)
if found {
// override connection details from the tenant
err := kms.parseConfig(tenantConfig)
if err != nil {
return err
}
}
// get the ConfigMap from the Tenant and apply the options
tenantConfig, err := kms.parseTenantConfig()
if err != nil {
return fmt.Errorf("failed to parse config for tenant: %w", err)
} else if tenantConfig != nil {
err = kms.parseConfig(tenantConfig)
if err != nil {
return err
}
}
return nil
}
// parseConfig calls vaultTenantConnection.parseConfig() and also set
// additional config options specific to VaultTenantSA. This function is called
// multiple times, for the different nested configuration layers.
// parseTenantConfig() calls this as well, with a reduced set of options,
// filtered by isTenantConfigOption().
func (kms *VaultTenantSA) parseConfig(config map[string]interface{}) error {
err := kms.vaultTenantConnection.parseConfig(config)
if err != nil {
return err
}
err = kms.setServiceAccountName(config)
if err != nil {
return fmt.Errorf("failed to set the ServiceAccount name from %s for tenant (%s): %w",
kms.ConfigName, kms.Tenant, err)
}
// default vaultAuthPath is set in initVaultTenantSA()
var vaultAuthPath string
err = setConfigString(&vaultAuthPath, config, "vaultAuthPath")
if errors.Is(err, errConfigOptionInvalid) {
return err
} else if err == nil {
kms.vaultConfig[vault.AuthMountPath], err = detectAuthMountPath(vaultAuthPath)
if err != nil {
return fmt.Errorf("failed to set %s in Vault config: %w", vault.AuthMountPath, err)
}
}
// default vaultRole is set in initVaultTenantSA()
var vaultRole string
err = setConfigString(&vaultRole, config, "vaultRole")
if errors.Is(err, errConfigOptionInvalid) {
return err
} else if err == nil {
kms.vaultConfig[vault.AuthKubernetesRole] = vaultRole
}
return nil
}
// isTenantSAConfigOption is used by vaultTenantConnection.parseTenantConfig()
// to filter options that should not be set by the configuration in the tenants
// ConfigMap. Options that are allowed to be set, will return true, options
// that are filtered return false.
func isTenantSAConfigOption(opt string) bool {
// standard vaultTenantConnection options are accepted
if isTenantConfigOption(opt) {
return true
}
// additional options for VaultTenantSA
switch opt {
case "tenantSAName":
case "vaultAuthPath":
case "vaultRole":
default:
return false
}
return true
}
// setServiceAccountName stores the name of the ServiceAccount in the
// configuration if it has been set in the options.
func (kms *VaultTenantSA) setServiceAccountName(config map[string]interface{}) error {
err := setConfigString(&kms.tenantSAName, config, "tenantSAName")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
return nil
}
// getServiceAccount returns the Tenants ServiceAccount with the name
// configured in the VaultTenantSA.
func (kms *VaultTenantSA) getServiceAccount() (*corev1.ServiceAccount, error) {
c := kms.getK8sClient()
sa, err := c.CoreV1().ServiceAccounts(kms.Tenant).Get(context.TODO(),
kms.tenantSAName, metav1.GetOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get ServiceAccount %s/%s: %w", kms.Tenant, kms.tenantSAName, err)
}
return sa, nil
}
// getToken looks up the ServiceAccount and the Secrets linked from it. When it
// finds the Secret that contains the `token` field, the contents is read and
// returned.
func (kms *VaultTenantSA) getToken() (string, error) {
sa, err := kms.getServiceAccount()
if err != nil {
return "", err
}
c := kms.getK8sClient()
for _, secretRef := range sa.Secrets {
secret, err := c.CoreV1().Secrets(kms.Tenant).Get(context.TODO(), secretRef.Name, metav1.GetOptions{})
if err != nil {
return "", fmt.Errorf("failed to get Secret %s/%s: %w", kms.Tenant, secretRef.Name, err)
}
token, ok := secret.Data["token"]
if ok {
return string(token), nil
}
}
return "", fmt.Errorf("failed to find token in ServiceAccount %s/%s", kms.Tenant, kms.tenantSAName)
}
// getTokenPath creates a temporary directory structure that contains the token
// linked from the ServiceAccount. This path can then be used in place of the
// standard `/var/run/secrets/kubernetes.io/serviceaccount/token` location.
func (kms *VaultTenantSA) getTokenPath() (string, error) {
dir, err := ioutil.TempDir("", kms.tenantSAName)
if err != nil {
return "", fmt.Errorf("failed to create directory for ServiceAccount %s/%s: %w", kms.tenantSAName, kms.Tenant, err)
}
token, err := kms.getToken()
if err != nil {
return "", err
}
err = ioutil.WriteFile(dir+"/token", []byte(token), 0600)
if err != nil {
return "", fmt.Errorf("failed to write token for ServiceAccount %s/%s: %w", kms.tenantSAName, kms.Tenant, err)
}
return dir + "/token", nil
}

View File

@ -1,77 +0,0 @@
/*
Copyright 2021 The Ceph-CSI Authors.
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 util
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestVaultTenantSAKMSRegistered(t *testing.T) {
t.Parallel()
_, ok := kmsManager.providers[kmsTypeVaultTenantSA]
assert.True(t, ok)
}
func TestTenantSAParseConfig(t *testing.T) {
t.Parallel()
vts := VaultTenantSA{}
config := make(map[string]interface{})
// empty config map
err := vts.parseConfig(config)
if !errors.Is(err, errConfigOptionMissing) {
t.Errorf("unexpected error (%T): %s", err, err)
}
// fill default options (normally done in initVaultTokensKMS)
config["vaultAddress"] = "https://vault.bob.cluster.svc"
config["vaultAuthPath"] = "/v1/auth/kube-auth/login"
// parsing with all required options
err = vts.parseConfig(config)
switch {
case err != nil:
t.Errorf("unexpected error: %s", err)
case vts.vaultConfig["VAULT_AUTH_MOUNT_PATH"] != "kube-auth":
t.Errorf("vaultAuthPath set to unexpected value: %s", vts.vaultConfig["VAULT_AUTH_MOUNT_PATH"])
}
// tenant "bob" uses a different auth mount path
bob := make(map[string]interface{})
bob["vaultAuthPath"] = "/v1/auth/bobs-cluster/login"
err = vts.parseConfig(bob)
switch {
case err != nil:
t.Errorf("unexpected error: %s", err)
case vts.vaultConfig["VAULT_AUTH_MOUNT_PATH"] != "bobs-cluster":
t.Errorf("vaultAuthPath set to unexpected value: %s", vts.vaultConfig["VAULT_AUTH_MOUNT_PATH"])
}
// auth mount path can be passed like VAULT_AUTH_MOUNT_PATH too
bob["vaultAuthPath"] = "bobs-2nd-cluster"
err = vts.parseConfig(bob)
switch {
case err != nil:
t.Errorf("unexpected error: %s", err)
case vts.vaultConfig["VAULT_AUTH_MOUNT_PATH"] != "bobs-2nd-cluster":
t.Errorf("vaultAuthPath set to unexpected value: %s", vts.vaultConfig["VAULT_AUTH_MOUNT_PATH"])
}
}

View File

@ -1,132 +0,0 @@
/*
Copyright 2020 The Ceph-CSI Authors.
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 util
import (
"errors"
"os"
"testing"
loss "github.com/libopenstorage/secrets"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDetectAuthMountPath(t *testing.T) {
t.Parallel()
authMountPath, err := detectAuthMountPath(vaultDefaultAuthPath)
if err != nil {
t.Errorf("detectAuthMountPath() failed: %s", err)
}
if authMountPath != "kubernetes" {
t.Errorf("authMountPath should be set to 'kubernetes', but is: %s", authMountPath)
}
authMountPath, err = detectAuthMountPath("kubernetes")
if err != nil {
t.Errorf("detectAuthMountPath() failed: %s", err)
}
if authMountPath != "kubernetes" {
t.Errorf("authMountPath should be set to 'kubernetes', but is: %s", authMountPath)
}
}
func TestCreateTempFile(t *testing.T) {
t.Parallel()
data := []byte("Hello World!")
tmpfile, err := createTempFile("my-file", data)
if err != nil {
t.Errorf("createTempFile() failed: %s", err)
}
if tmpfile == "" {
t.Errorf("createTempFile() returned an empty filename")
}
err = os.Remove(tmpfile)
if err != nil {
t.Errorf("failed to remove tmpfile (%s): %s", tmpfile, err)
}
}
func TestSetConfigString(t *testing.T) {
t.Parallel()
const defaultValue = "default-value"
options := make(map[string]interface{})
// noSuchOption: no default value, option unavailable
noSuchOption := ""
err := setConfigString(&noSuchOption, options, "nonexistent")
switch {
case err == nil:
t.Error("did not get an error when one was expected")
case !errors.Is(err, errConfigOptionMissing):
t.Errorf("expected errConfigOptionMissing, but got %T: %s", err, err)
case noSuchOption != "":
t.Error("value should not have been modified")
}
// noOptionDefault: default value, option unavailable
noOptionDefault := defaultValue
err = setConfigString(&noOptionDefault, options, "nonexistent")
switch {
case err == nil:
t.Error("did not get an error when one was expected")
case !errors.Is(err, errConfigOptionMissing):
t.Errorf("expected errConfigOptionMissing, but got %T: %s", err, err)
case noOptionDefault != defaultValue:
t.Error("value should not have been modified")
}
// optionDefaultOverload: default value, option available
optionDefaultOverload := defaultValue
options["set-me"] = "non-default"
err = setConfigString(&optionDefaultOverload, options, "set-me")
switch {
case err != nil:
t.Errorf("unexpected error returned: %s", err)
case optionDefaultOverload != "non-default":
t.Error("optionDefaultOverload should have been updated")
}
}
func TestDefaultVaultDestroyKeys(t *testing.T) {
t.Parallel()
vc := &vaultConnection{}
config := make(map[string]interface{})
config["vaultAddress"] = "https://vault.test.example.com"
err := vc.initConnection(config)
require.NoError(t, err)
keyContext := vc.getDeleteKeyContext()
destroySecret, ok := keyContext[loss.DestroySecret]
assert.NotEqual(t, destroySecret, "")
assert.True(t, ok)
// setting vaultDestroyKeys to !true should remove the loss.DestroySecret entry
config["vaultDestroyKeys"] = "false"
err = vc.initConnection(config)
require.NoError(t, err)
keyContext = vc.getDeleteKeyContext()
_, ok = keyContext[loss.DestroySecret]
assert.False(t, ok)
}
func TestVaultKMSRegistered(t *testing.T) {
t.Parallel()
_, ok := kmsManager.providers[kmsTypeVault]
assert.True(t, ok)
}

View File

@ -1,599 +0,0 @@
/*
Copyright 2020 The Ceph-CSI Authors.
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 util
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strconv"
"github.com/ceph/ceph-csi/internal/util/k8s"
"github.com/hashicorp/vault/api"
apierrs "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
const (
kmsTypeVaultTokens = "vaulttokens"
// vaultTokensDefaultConfigName is the name of the Kubernetes ConfigMap
// that contains the Vault connection configuration for the tenant.
// This ConfigMap is located in the Kubernetes Namespace where the
// tenant created the PVC.
//
// #nosec:G101, value not credential, just references token.
vaultTokensDefaultConfigName = "ceph-csi-kms-config"
// vaultTokensDefaultTokenName is the name of the Kubernetes Secret
// that contains the Vault Token for the tenant. This Secret is
// located in the Kubernetes Namespace where the tenant created the
// PVC.
//
// #nosec:G101, value not credential, just references token.
vaultTokensDefaultTokenName = "ceph-csi-kms-token"
// vaultTokenSecretKey refers to the key in the Kubernetes Secret that
// contains the VAULT_TOKEN.
vaultTokenSecretKey = "token"
)
type standardVault struct {
KmsPROVIDER string `json:"KMS_PROVIDER"`
VaultADDR string `json:"VAULT_ADDR"`
VaultBackend string `json:"VAULT_BACKEND"`
VaultBackendPath string `json:"VAULT_BACKEND_PATH"`
VaultDestroyKeys string `json:"VAULT_DESTROY_KEYS"`
VaultCACert string `json:"VAULT_CACERT"`
VaultTLSServerName string `json:"VAULT_TLS_SERVER_NAME"`
VaultClientCert string `json:"VAULT_CLIENT_CERT"`
VaultClientKey string `json:"VAULT_CLIENT_KEY"`
VaultAuthNamespace string `json:"VAULT_AUTH_NAMESPACE"`
VaultNamespace string `json:"VAULT_NAMESPACE"`
VaultSkipVerify string `json:"VAULT_SKIP_VERIFY"`
}
type vaultTokenConf struct {
EncryptionKMSType string `json:"encryptionKMSType"`
VaultAddress string `json:"vaultAddress"`
VaultBackend string `json:"vaultBackend"`
VaultBackendPath string `json:"vaultBackendPath"`
VaultDestroyKeys string `json:"vaultDestroyKeys"`
VaultCAFromSecret string `json:"vaultCAFromSecret"`
VaultTLSServerName string `json:"vaultTLSServerName"`
VaultClientCertFromSecret string `json:"vaultClientCertFromSecret"`
VaultClientCertKeyFromSecret string `json:"vaultClientCertKeyFromSecret"`
VaultAuthNamespace string `json:"vaultAuthNamespace"`
VaultNamespace string `json:"vaultNamespace"`
VaultCAVerify string `json:"vaultCAVerify"`
}
func (v *vaultTokenConf) convertStdVaultToCSIConfig(s *standardVault) {
v.EncryptionKMSType = s.KmsPROVIDER
v.VaultAddress = s.VaultADDR
v.VaultBackend = s.VaultBackend
v.VaultBackendPath = s.VaultBackendPath
v.VaultDestroyKeys = s.VaultDestroyKeys
v.VaultCAFromSecret = s.VaultCACert
v.VaultClientCertFromSecret = s.VaultClientCert
v.VaultClientCertKeyFromSecret = s.VaultClientKey
v.VaultAuthNamespace = s.VaultAuthNamespace
v.VaultNamespace = s.VaultNamespace
v.VaultTLSServerName = s.VaultTLSServerName
// by default the CA should get verified, only when VaultSkipVerify is
// set, verification should be disabled
v.VaultCAVerify = vaultDefaultCAVerify
verify, err := strconv.ParseBool(s.VaultSkipVerify)
if err == nil {
v.VaultCAVerify = strconv.FormatBool(!verify)
}
}
// convertConfig takes the keys/values in standard Vault environment variable
// format, and converts them to the format that is used in the configuration
// file.
// This uses JSON marshaling and unmarshalling to map the Vault environment
// configuration into bytes, then in the standardVault struct, which is passed
// through convertStdVaultToCSIConfig before converting back to a
// map[string]interface{} configuration.
//
// FIXME: this can surely be simplified?!
func transformConfig(svMap map[string]interface{}) (map[string]interface{}, error) {
// convert the map to JSON
data, err := json.Marshal(svMap)
if err != nil {
return nil, fmt.Errorf("failed to convert config %T to JSON: %w", svMap, err)
}
// convert the JSON back to a standardVault struct
sv := &standardVault{}
err = json.Unmarshal(data, sv)
if err != nil {
return nil, fmt.Errorf("failed to Unmarshal the vault configuration: %w", err)
}
// convert the standardVault struct to a vaultTokenConf struct
vc := vaultTokenConf{}
vc.convertStdVaultToCSIConfig(sv)
data, err = json.Marshal(vc)
if err != nil {
return nil, fmt.Errorf("failed to Marshal the CSI vault configuration: %w", err)
}
// convert the vaultTokenConf struct to a map[string]interface{}
jsonMap := make(map[string]interface{})
err = json.Unmarshal(data, &jsonMap)
if err != nil {
return nil, fmt.Errorf("failed to Unmarshal the CSI vault configuration: %w", err)
}
return jsonMap, nil
}
/*
VaultTokens represents a Hashicorp Vault KMS configuration that provides a
Token per tenant.
Example JSON structure in the KMS config is,
{
"vault-with-tokens": {
"encryptionKMSType": "vaulttokens",
"vaultAddress": "http://vault.default.svc.cluster.local:8200",
"vaultBackend": "kv-v2",
"vaultBackendPath": "secret/",
"vaultTLSServerName": "vault.default.svc.cluster.local",
"vaultCAFromSecret": "vault-ca",
"vaultClientCertFromSecret": "vault-client-cert",
"vaultClientCertKeyFromSecret": "vault-client-cert-key",
"vaultCAVerify": "false",
"tenantConfigName": "ceph-csi-kms-config",
"tenantTokenName": "ceph-csi-kms-token",
"tenants": {
"my-app": {
"vaultAddress": "https://vault.example.com",
"vaultCAVerify": "true"
},
"an-other-app": {
"tenantTokenName": "storage-encryption-token"
}
},
...
}.
*/
type vaultTenantConnection struct {
vaultConnection
integratedDEK
client *kubernetes.Clientset
// Tenant is the name of the owner of the volume
Tenant string
// ConfigName is the name of the ConfigMap in the Tenants Kubernetes Namespace
ConfigName string
// tenantConfigOptionFilter ise used to filter configuration options
// for the KMS that are provided by the ConfigMap in the Tenants
// Namespace. It defaults to isTenantConfigOption() as setup by the
// init() function.
tenantConfigOptionFilter func(string) bool
}
type VaultTokensKMS struct {
vaultTenantConnection
// TokenName is the name of the Secret in the Tenants Kubernetes Namespace
TokenName string
}
var _ = RegisterKMSProvider(KMSProvider{
UniqueID: kmsTypeVaultTokens,
Initializer: initVaultTokensKMS,
})
// InitVaultTokensKMS returns an interface to HashiCorp Vault KMS.
func initVaultTokensKMS(args KMSInitializerArgs) (EncryptionKMS, error) {
var err error
config := args.Config
if _, ok := config[kmsProviderKey]; ok {
// configuration comes from the ConfigMap, needs to be
// converted to vaultTokenConf type
config, err = transformConfig(config)
if err != nil {
return nil, fmt.Errorf("failed to convert configuration: %w", err)
}
}
kms := &VaultTokensKMS{}
kms.vaultTenantConnection.init()
err = kms.initConnection(config)
if err != nil {
return nil, fmt.Errorf("failed to initialize Vault connection: %w", err)
}
// set default values for optional config options
kms.ConfigName = vaultTokensDefaultConfigName
kms.TokenName = vaultTokensDefaultTokenName
err = kms.parseConfig(config)
if err != nil {
return nil, err
}
err = kms.setTokenName(config)
if err != nil && !errors.Is(err, errConfigOptionMissing) {
return nil, fmt.Errorf("failed to set the TokenName from global config %q: %w",
kms.ConfigName, err)
}
// fetch the configuration for the tenant
if args.Tenant != "" {
err = kms.configureTenant(config, args.Tenant)
if err != nil {
return nil, err
}
}
// fetch the Vault Token from the Secret (TokenName) in the Kubernetes
// Namespace (tenant)
kms.vaultConfig[api.EnvVaultToken], err = kms.getToken()
if err != nil {
return nil, fmt.Errorf("failed fetching token from %s/%s: %w", args.Tenant, kms.TokenName, err)
}
err = kms.initCertificates(config)
if err != nil {
return nil, fmt.Errorf("failed to initialize Vault certificates: %w", err)
}
// connect to the Vault service
err = kms.connectVault()
if err != nil {
return nil, err
}
return kms, nil
}
func (kms *VaultTokensKMS) configureTenant(config map[string]interface{}, tenant string) error {
kms.Tenant = tenant
tenantConfig, found := fetchTenantConfig(config, tenant)
if found {
// override connection details from the tenant
err := kms.parseConfig(tenantConfig)
if err != nil {
return err
}
err = kms.setTokenName(tenantConfig)
if err != nil {
return fmt.Errorf("failed to set the TokenName for tenant (%s): %w",
kms.Tenant, err)
}
}
// get the ConfigMap from the Tenant and apply the options
tenantConfig, err := kms.parseTenantConfig()
if err != nil {
return fmt.Errorf("failed to parse config for tenant: %w", err)
} else if tenantConfig != nil {
err = kms.parseConfig(tenantConfig)
if err != nil {
return fmt.Errorf("failed to parse config (%s) for tenant (%s): %w",
kms.ConfigName, kms.Tenant, err)
}
err = kms.setTokenName(tenantConfig)
if err != nil {
return fmt.Errorf("failed to set the TokenName from %s for tenant (%s): %w",
kms.ConfigName, kms.Tenant, err)
}
}
return nil
}
func (vtc *vaultTenantConnection) init() {
vtc.tenantConfigOptionFilter = isTenantConfigOption
}
// parseConfig updates the kms.vaultConfig with the options from config and
// secrets. This method can be called multiple times, i.e. to override
// configuration options from tenants.
func (vtc *vaultTenantConnection) parseConfig(config map[string]interface{}) error {
err := vtc.initConnection(config)
if err != nil {
return err
}
err = setConfigString(&vtc.ConfigName, config, "tenantConfigName")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
return nil
}
// setTokenName updates the kms.TokenName with the options from config. This
// method can be called multiple times, i.e. to override configuration options
// from tenants.
func (kms *VaultTokensKMS) setTokenName(config map[string]interface{}) error {
err := setConfigString(&kms.TokenName, config, "tenantTokenName")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
return nil
}
// initCertificates updates the kms.vaultConfig with the options from config
// it calls the kubernetes secrets and get the required data.
// nolint:gocyclo,cyclop // iterating through many config options, not complex at all.
func (vtc *vaultTenantConnection) initCertificates(config map[string]interface{}) error {
vaultConfig := make(map[string]interface{})
csiNamespace := os.Getenv("POD_NAMESPACE")
vaultCAFromSecret := "" // optional
err := setConfigString(&vaultCAFromSecret, config, "vaultCAFromSecret")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// ignore errConfigOptionMissing, no default was set
if vaultCAFromSecret != "" {
cert, cErr := vtc.getCertificate(vtc.Tenant, vaultCAFromSecret, "cert")
if cErr != nil && !apierrs.IsNotFound(cErr) {
return fmt.Errorf("failed to get CA certificate from secret %s: %w", vaultCAFromSecret, cErr)
}
// if the certificate is not present in tenant namespace get it from
// cephcsi pod namespace
if apierrs.IsNotFound(cErr) {
cert, cErr = vtc.getCertificate(csiNamespace, vaultCAFromSecret, "cert")
if cErr != nil {
return fmt.Errorf("failed to get CA certificate from secret %s: %w", vaultCAFromSecret, cErr)
}
}
vaultConfig[api.EnvVaultCACert], err = createTempFile("vault-ca-cert", []byte(cert))
if err != nil {
return fmt.Errorf("failed to create temporary file for Vault CA: %w", err)
}
}
vaultClientCertFromSecret := "" // optional
err = setConfigString(&vaultClientCertFromSecret, config, "vaultClientCertFromSecret")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// ignore errConfigOptionMissing, no default was set
if vaultClientCertFromSecret != "" {
cert, cErr := vtc.getCertificate(vtc.Tenant, vaultClientCertFromSecret, "cert")
if cErr != nil && !apierrs.IsNotFound(cErr) {
return fmt.Errorf("failed to get client certificate from secret %s: %w", vaultClientCertFromSecret, cErr)
}
// if the certificate is not present in tenant namespace get it from
// cephcsi pod namespace
if apierrs.IsNotFound(cErr) {
cert, cErr = vtc.getCertificate(csiNamespace, vaultClientCertFromSecret, "cert")
if cErr != nil {
return fmt.Errorf("failed to get client certificate from secret %s: %w", vaultCAFromSecret, cErr)
}
}
vaultConfig[api.EnvVaultClientCert], err = createTempFile("vault-ca-cert", []byte(cert))
if err != nil {
return fmt.Errorf("failed to create temporary file for Vault client certificate: %w", err)
}
}
vaultClientCertKeyFromSecret := "" // optional
err = setConfigString(&vaultClientCertKeyFromSecret, config, "vaultClientCertKeyFromSecret")
if errors.Is(err, errConfigOptionInvalid) {
return err
}
// ignore errConfigOptionMissing, no default was set
if vaultClientCertKeyFromSecret != "" {
certKey, err := vtc.getCertificate(vtc.Tenant, vaultClientCertKeyFromSecret, "key")
if err != nil && !apierrs.IsNotFound(err) {
return fmt.Errorf(
"failed to get client certificate key from secret %s: %w",
vaultClientCertKeyFromSecret,
err)
}
// if the certificate is not present in tenant namespace get it from
// cephcsi pod namespace
if apierrs.IsNotFound(err) {
certKey, err = vtc.getCertificate(csiNamespace, vaultClientCertKeyFromSecret, "key")
if err != nil {
return fmt.Errorf("failed to get client certificate key from secret %s: %w", vaultCAFromSecret, err)
}
}
vaultConfig[api.EnvVaultClientKey], err = createTempFile("vault-client-cert-key", []byte(certKey))
if err != nil {
return fmt.Errorf("failed to create temporary file for Vault client cert key: %w", err)
}
}
for key, value := range vaultConfig {
vtc.vaultConfig[key] = value
}
return nil
}
func (vtc *vaultTenantConnection) getK8sClient() *kubernetes.Clientset {
if vtc.client == nil {
vtc.client = k8s.NewK8sClient()
}
return vtc.client
}
// FetchDEK returns passphrase from Vault. The passphrase is stored in a
// data.data.passphrase structure.
func (vtc *vaultTenantConnection) FetchDEK(key string) (string, error) {
s, err := vtc.secrets.GetSecret(key, vtc.keyContext)
if err != nil {
return "", err
}
data, ok := s["data"].(map[string]interface{})
if !ok {
return "", fmt.Errorf("failed parsing data for get passphrase request for %s", key)
}
passphrase, ok := data["passphrase"].(string)
if !ok {
return "", fmt.Errorf("failed parsing passphrase for get passphrase request for %s", key)
}
return passphrase, nil
}
// StoreDEK saves new passphrase in Vault.
func (vtc *vaultTenantConnection) StoreDEK(key, value string) error {
data := map[string]interface{}{
"data": map[string]string{
"passphrase": value,
},
}
err := vtc.secrets.PutSecret(key, data, vtc.keyContext)
if err != nil {
return fmt.Errorf("saving passphrase at %s request to vault failed: %w", key, err)
}
return nil
}
// RemoveDEK deletes passphrase from Vault.
func (vtc *vaultTenantConnection) RemoveDEK(key string) error {
err := vtc.secrets.DeleteSecret(key, vtc.getDeleteKeyContext())
if err != nil {
return fmt.Errorf("delete passphrase at %s request to vault failed: %w", key, err)
}
return nil
}
func (kms *VaultTokensKMS) getToken() (string, error) {
c := kms.getK8sClient()
secret, err := c.CoreV1().Secrets(kms.Tenant).Get(context.TODO(), kms.TokenName, metav1.GetOptions{})
if err != nil {
return "", err
}
token, ok := secret.Data[vaultTokenSecretKey]
if !ok {
return "", errors.New("failed to parse token")
}
return string(token), nil
}
func (vtc *vaultTenantConnection) getCertificate(tenant, secretName, key string) (string, error) {
c := vtc.getK8sClient()
secret, err := c.CoreV1().Secrets(tenant).Get(context.TODO(), secretName, metav1.GetOptions{})
if err != nil {
return "", err
}
cert, ok := secret.Data[key]
if !ok {
return "", errors.New("failed to parse certificates")
}
return string(cert), nil
}
// isTenantConfigOption return true if a tenant may (re)configure the option in
// their own ConfigMap, false otherwise.
func isTenantConfigOption(opt string) bool {
switch opt {
case "vaultAddress":
case "vaultBackend":
case "vaultBackendPath":
case "vaultAuthNamespace":
case "vaultNamespace":
case "vaultDestroyKeys":
case "vaultTLSServerName":
case "vaultCAFromSecret":
case "vaultCAVerify":
default:
return false
}
return true
}
// parseTenantConfig gets the optional ConfigMap from the Tenants namespace,
// and applies the allowable options (see isTenantConfigOption) to the KMS
// configuration.
func (vtc *vaultTenantConnection) parseTenantConfig() (map[string]interface{}, error) {
if vtc.Tenant == "" || vtc.ConfigName == "" {
return nil, nil
}
// fetch the ConfigMap from the tenants namespace
c := vtc.getK8sClient()
cm, err := c.CoreV1().ConfigMaps(vtc.Tenant).Get(context.TODO(),
vtc.ConfigName, metav1.GetOptions{})
if apierrs.IsNotFound(err) {
// the tenant did not (re)configure any options
return nil, nil
} else if err != nil {
return nil, fmt.Errorf("failed to get config (%s) for tenant (%s): %w",
vtc.ConfigName, vtc.Tenant, err)
}
// create a new map with config options, but only include the options
// that a tenant may (re)configure
config := make(map[string]interface{})
for k, v := range cm.Data {
if vtc.tenantConfigOptionFilter(k) {
config[k] = v
} // else: silently ignore the option
}
if len(config) == 0 {
// no options configured by the tenant
return nil, nil
}
return config, nil
}
// fetchTenantConfig fetches the configuration for the tenant if it exists.
func fetchTenantConfig(config map[string]interface{}, tenant string) (map[string]interface{}, bool) {
tenantsMap, ok := config["tenants"]
if !ok {
return nil, false
}
// tenants is a map per tenant, containing key/values
tenants, ok := tenantsMap.(map[string]map[string]interface{})
if !ok {
return nil, false
}
// get the map for the tenant of the current operation
tenantConfig, ok := tenants[tenant]
if !ok {
return nil, false
}
return tenantConfig, true
}

View File

@ -1,215 +0,0 @@
/*
Copyright 2020 The Ceph-CSI Authors.
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 util
import (
"encoding/json"
"errors"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParseConfig(t *testing.T) {
t.Parallel()
vtc := vaultTenantConnection{}
config := make(map[string]interface{})
// empty config map
err := vtc.parseConfig(config)
if !errors.Is(err, errConfigOptionMissing) {
t.Errorf("unexpected error (%T): %s", err, err)
}
// fill default options (normally done in initVaultTokensKMS)
config["vaultAddress"] = "https://vault.default.cluster.svc"
config["tenantConfigName"] = vaultTokensDefaultConfigName
// parsing with all required options
err = vtc.parseConfig(config)
switch {
case err != nil:
t.Errorf("unexpected error: %s", err)
case vtc.ConfigName != vaultTokensDefaultConfigName:
t.Errorf("ConfigName contains unexpected value: %s", vtc.ConfigName)
}
// tenant "bob" uses a different kms.ConfigName
bob := make(map[string]interface{})
bob["tenantConfigName"] = "the-config-from-bob"
err = vtc.parseConfig(bob)
switch {
case err != nil:
t.Errorf("unexpected error: %s", err)
case vtc.ConfigName != "the-config-from-bob":
t.Errorf("ConfigName contains unexpected value: %s", vtc.ConfigName)
}
}
// TestInitVaultTokensKMS verifies that passing partial and complex
// configurations get applied correctly.
//
// When vault.New() is called at the end of initVaultTokensKMS(), errors will
// mention the missing VAULT_TOKEN, and that is expected.
func TestInitVaultTokensKMS(t *testing.T) {
t.Parallel()
if true {
// FIXME: testing only works when KUBE_CONFIG is set to a
// cluster that has a working Vault deployment
return
}
args := KMSInitializerArgs{
Tenant: "bob",
Config: make(map[string]interface{}),
Secrets: nil,
}
// empty config map
_, err := initVaultTokensKMS(args)
if !errors.Is(err, errConfigOptionMissing) {
t.Errorf("unexpected error (%T): %s", err, err)
}
// fill required options
args.Config["vaultAddress"] = "https://vault.default.cluster.svc"
// parsing with all required options
_, err = initVaultTokensKMS(args)
if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") {
t.Errorf("unexpected error: %s", err)
}
// fill tenants
tenants := make(map[string]interface{})
args.Config["tenants"] = tenants
// empty tenants list
_, err = initVaultTokensKMS(args)
if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") {
t.Errorf("unexpected error: %s", err)
}
// add tenant "bob"
bob := make(map[string]interface{})
bob["vaultAddress"] = "https://vault.bob.example.org"
args.Config["tenants"].(map[string]interface{})["bob"] = bob
_, err = initVaultTokensKMS(args)
if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") {
t.Errorf("unexpected error: %s", err)
}
}
// TestStdVaultToCSIConfig converts a JSON document with standard VAULT_*
// environment variables to a vaultTokenConf structure.
func TestStdVaultToCSIConfig(t *testing.T) {
t.Parallel()
vaultConfigMap := `{
"KMS_PROVIDER":"vaulttokens",
"VAULT_ADDR":"https://vault.example.com",
"VAULT_BACKEND":"kv-v2",
"VAULT_BACKEND_PATH":"/secret",
"VAULT_DESTROY_KEYS":"true",
"VAULT_CACERT":"",
"VAULT_TLS_SERVER_NAME":"vault.example.com",
"VAULT_CLIENT_CERT":"",
"VAULT_CLIENT_KEY":"",
"VAULT_AUTH_NAMESPACE":"devops",
"VAULT_NAMESPACE":"devops/homepage",
"VAULT_SKIP_VERIFY":"true"
}`
sv := &standardVault{}
err := json.Unmarshal([]byte(vaultConfigMap), sv)
if err != nil {
t.Errorf("unexpected error: %s", err)
return
}
v := vaultTokenConf{}
v.convertStdVaultToCSIConfig(sv)
switch {
case v.EncryptionKMSType != kmsTypeVaultTokens:
t.Errorf("unexpected value for EncryptionKMSType: %s", v.EncryptionKMSType)
case v.VaultAddress != "https://vault.example.com":
t.Errorf("unexpected value for VaultAddress: %s", v.VaultAddress)
case v.VaultBackend != "kv-v2":
t.Errorf("unexpected value for VaultBackend: %s", v.VaultBackend)
case v.VaultBackendPath != "/secret":
t.Errorf("unexpected value for VaultBackendPath: %s", v.VaultBackendPath)
case v.VaultDestroyKeys != vaultDefaultDestroyKeys:
t.Errorf("unexpected value for VaultDestroyKeys: %s", v.VaultDestroyKeys)
case v.VaultCAFromSecret != "":
t.Errorf("unexpected value for VaultCAFromSecret: %s", v.VaultCAFromSecret)
case v.VaultClientCertFromSecret != "":
t.Errorf("unexpected value for VaultClientCertFromSecret: %s", v.VaultClientCertFromSecret)
case v.VaultClientCertKeyFromSecret != "":
t.Errorf("unexpected value for VaultClientCertKeyFromSecret: %s", v.VaultClientCertKeyFromSecret)
case v.VaultAuthNamespace != "devops":
t.Errorf("unexpected value for VaultAuthNamespace: %s", v.VaultAuthNamespace)
case v.VaultNamespace != "devops/homepage":
t.Errorf("unexpected value for VaultNamespace: %s", v.VaultNamespace)
case v.VaultTLSServerName != "vault.example.com":
t.Errorf("unexpected value for VaultTLSServerName: %s", v.VaultTLSServerName)
case v.VaultCAVerify != "false":
t.Errorf("unexpected value for VaultCAVerify: %s", v.VaultCAVerify)
}
}
func TestTransformConfig(t *testing.T) {
t.Parallel()
cm := make(map[string]interface{})
cm["KMS_PROVIDER"] = "vaulttokens"
cm["VAULT_ADDR"] = "https://vault.example.com"
cm["VAULT_BACKEND"] = "kv-v2"
cm["VAULT_BACKEND_PATH"] = "/secret"
cm["VAULT_DESTROY_KEYS"] = "true"
cm["VAULT_CACERT"] = ""
cm["VAULT_TLS_SERVER_NAME"] = "vault.example.com"
cm["VAULT_CLIENT_CERT"] = ""
cm["VAULT_CLIENT_KEY"] = ""
cm["VAULT_AUTH_NAMESPACE"] = "devops"
cm["VAULT_NAMESPACE"] = "devops/homepage"
cm["VAULT_SKIP_VERIFY"] = "true" // inverse of "vaultCAVerify"
config, err := transformConfig(cm)
require.NoError(t, err)
assert.Equal(t, config["encryptionKMSType"], cm["KMS_PROVIDER"])
assert.Equal(t, config["vaultAddress"], cm["VAULT_ADDR"])
assert.Equal(t, config["vaultBackend"], cm["VAULT_BACKEND"])
assert.Equal(t, config["vaultBackendPath"], cm["VAULT_BACKEND_PATH"])
assert.Equal(t, config["vaultDestroyKeys"], cm["VAULT_DESTROY_KEYS"])
assert.Equal(t, config["vaultCAFromSecret"], cm["VAULT_CACERT"])
assert.Equal(t, config["vaultTLSServerName"], cm["VAULT_TLS_SERVER_NAME"])
assert.Equal(t, config["vaultClientCertFromSecret"], cm["VAULT_CLIENT_CERT"])
assert.Equal(t, config["vaultClientCertKeyFromSecret"], cm["VAULT_CLIENT_KEY"])
assert.Equal(t, config["vaultAuthNamespace"], cm["VAULT_AUTH_NAMESPACE"])
assert.Equal(t, config["vaultNamespace"], cm["VAULT_NAMESPACE"])
assert.Equal(t, config["vaultCAVerify"], "false")
}
func TestVaultTokensKMSRegistered(t *testing.T) {
t.Parallel()
_, ok := kmsManager.providers[kmsTypeVaultTokens]
assert.True(t, ok)
}