diff --git a/internal/util/crypto.go b/internal/util/crypto.go index 269ad3fbd..8e1f6af35 100644 --- a/internal/util/crypto.go +++ b/internal/util/crypto.go @@ -137,8 +137,11 @@ func GetKMS(tenant, kmsID string, secrets map[string]string) (EncryptionKMS, err return nil, fmt.Errorf("encryption KMS configuration for %s is missing KMS type", kmsID) } - if kmsType == "vault" { + switch kmsType { + case "vault": return InitVaultKMS(kmsID, kmsConfig, secrets) + case kmsTypeVaultTokens: + return InitVaultTokensKMS(tenant, kmsID, kmsConfig, secrets) } return nil, fmt.Errorf("unknown encryption KMS type %s", kmsType) } diff --git a/internal/util/vault_tokens.go b/internal/util/vault_tokens.go new file mode 100644 index 000000000..f445989f6 --- /dev/null +++ b/internal/util/vault_tokens.go @@ -0,0 +1,227 @@ +/* +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" + "errors" + "fmt" + + "github.com/hashicorp/vault/api" + loss "github.com/libopenstorage/secrets" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +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" +) + +/* +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", + "vaultBackendPath": "secret/", + "vaultTLSServerName": "vault.default.svc.cluster.local", + "vaultCAFromSecret": "vault-ca", + "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 VaultTokensKMS struct { + vaultConnection + + // 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 + // TokenName is the name of the Secret in the Tenants Kubernetes Namespace + TokenName string +} + +// InitVaultTokensKMS returns an interface to HashiCorp Vault KMS. +func InitVaultTokensKMS(tenant, kmsID string, config map[string]interface{}, secrets map[string]string) (EncryptionKMS, error) { + kms := &VaultTokensKMS{} + err := kms.initConnection(kmsID, config, secrets) + 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, secrets) + if err != nil { + return nil, err + } + + // fetch the configuration for the tenant + if tenant != "" { + tenantsMap, ok := config["tenants"] + if ok { + // tenants is a map per tenant, containing key/values + tenants, ok := tenantsMap.(map[string]map[string]interface{}) + if ok { + // get the map for the tenant of the current operation + tenantConfig, ok := tenants[tenant] + if ok { + // override connection details from the tenant + err = kms.parseConfig(tenantConfig, secrets) + if err != nil { + return nil, err + } + } + } + } + } + + // fetch the Vault Token from the Secret (TokenName) in the Kubernetes + // Namespace (tenant) + kms.vaultConfig[api.EnvVaultToken], err = getToken(tenant, kms.TokenName) + if err != nil { + return nil, fmt.Errorf("failed fetching token from %s/%s: %w", tenant, kms.TokenName, err) + } + + // connect to the Vault service + err = kms.connectVault() + if err != nil { + return nil, err + } + + return kms, nil +} + +// 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 (kms *VaultTokensKMS) parseConfig(config map[string]interface{}, secrets map[string]string) error { + err := kms.initConnection(kms.EncryptionKMSID, config, secrets) + if err != nil { + return err + } + + err = setConfigString(&kms.ConfigName, config, "tenantConfigName") + if errors.Is(err, errConfigOptionInvalid) { + return err + } + + err = setConfigString(&kms.TokenName, config, "tenantTokenName") + if errors.Is(err, errConfigOptionInvalid) { + return err + } + + return nil +} + +// GetPassphrase returns passphrase from Vault. The passphrase is stored in a +// data.data.passphrase structure. +func (kms *VaultTokensKMS) GetPassphrase(key string) (string, error) { + s, err := kms.secrets.GetSecret(key, kms.keyContext) + if errors.Is(err, loss.ErrInvalidSecretId) { + return "", MissingPassphrase{err} + } else 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 +} + +// SavePassphrase saves new passphrase in Vault. +func (kms *VaultTokensKMS) SavePassphrase(key, value string) error { + data := map[string]interface{}{ + "data": map[string]string{ + "passphrase": value, + }, + } + + err := kms.secrets.PutSecret(key, data, kms.keyContext) + if err != nil { + return fmt.Errorf("saving passphrase at %s request to vault failed: %w", key, err) + } + + return nil +} + +// DeletePassphrase deletes passphrase from Vault. +func (kms *VaultTokensKMS) DeletePassphrase(key string) error { + err := kms.secrets.DeleteSecret(key, kms.keyContext) + if err != nil { + return fmt.Errorf("delete passphrase at %s request to vault failed: %w", key, err) + } + + return nil +} + +func getToken(tenant, tokenName string) (string, error) { + c := NewK8sClient() + secret, err := c.CoreV1().Secrets(tenant).Get(context.TODO(), 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 +} diff --git a/internal/util/vault_tokens_test.go b/internal/util/vault_tokens_test.go new file mode 100644 index 000000000..9864f192d --- /dev/null +++ b/internal/util/vault_tokens_test.go @@ -0,0 +1,114 @@ +/* +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" + "strings" + "testing" +) + +func TestParseConfig(t *testing.T) { + kms := VaultTokensKMS{} + + config := make(map[string]interface{}) + secrets := make(map[string]string) + + // empty config map + err := kms.parseConfig(config, secrets) + 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 + config["tenantTokenName"] = vaultTokensDefaultTokenName + + // parsing with all required options + err = kms.parseConfig(config, secrets) + switch { + case err != nil: + t.Errorf("unexpected error: %s", err) + case kms.ConfigName != vaultTokensDefaultConfigName: + t.Errorf("ConfigName contains unexpected value: %s", kms.ConfigName) + case kms.TokenName != vaultTokensDefaultTokenName: + t.Errorf("TokenName contains unexpected value: %s", kms.TokenName) + } + + // tenant "bob" uses a different kms.ConfigName + bob := make(map[string]interface{}) + bob["tenantConfigName"] = "the-config-from-bob" + err = kms.parseConfig(bob, secrets) + switch { + case err != nil: + t.Errorf("unexpected error: %s", err) + case kms.ConfigName != "the-config-from-bob": + t.Errorf("ConfigName contains unexpected value: %s", kms.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) { + if true { + // FIXME: testing only works when KUBE_CONFIG is set to a + // cluster that has a working Vault deployment + return + } + + config := make(map[string]interface{}) + secrets := make(map[string]string) + + // empty config map + _, err := InitVaultTokensKMS("bob", "vault-tokens-config", config, secrets) + if !errors.Is(err, errConfigOptionMissing) { + t.Errorf("unexpected error (%T): %s", err, err) + } + + // fill required options + config["vaultAddress"] = "https://vault.default.cluster.svc" + + // parsing with all required options + _, err = InitVaultTokensKMS("bob", "vault-tokens-config", config, secrets) + if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") { + t.Errorf("unexpected error: %s", err) + } + + // fill tenants + tenants := make(map[string]interface{}) + config["tenants"] = tenants + + // empty tenants list + _, err = InitVaultTokensKMS("bob", "vault-tokens-config", config, secrets) + if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") { + t.Errorf("unexpected error: %s", err) + } + + // add tenant "bob" + bob := make(map[string]interface{}) + config["tenants"].(map[string]interface{})["bob"] = bob + bob["vaultAddress"] = "https://vault.bob.example.org" + + _, err = InitVaultTokensKMS("bob", "vault-tokens-config", config, secrets) + if err != nil && !strings.Contains(err.Error(), "VAULT_TOKEN") { + t.Errorf("unexpected error: %s", err) + } +}