mirror of
https://github.com/ceph/ceph-csi.git
synced 2025-06-13 10:33:35 +00:00
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:
committed by
mergify[bot]
parent
778b5e86de
commit
4a3b1181ce
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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), §ion)
|
||||
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)
|
||||
}
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
@ -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().
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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"])
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
@ -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)
|
||||
}
|
Reference in New Issue
Block a user