mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-11-22 22:30:23 +00:00
util: add support for Hashicorp Vault with Tokens per Tenant
Tenants (Kubernetes Namespaces) can use their own Vault Token to manage the encryption keys for PVCs. The working is documented in #1743. See-also: #1743 Closes: #1500 Signed-off-by: Niels de Vos <ndevos@redhat.com>
This commit is contained in:
parent
cb1899b8c0
commit
cc5684dbd8
@ -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)
|
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)
|
return InitVaultKMS(kmsID, kmsConfig, secrets)
|
||||||
|
case kmsTypeVaultTokens:
|
||||||
|
return InitVaultTokensKMS(tenant, kmsID, kmsConfig, secrets)
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("unknown encryption KMS type %s", kmsType)
|
return nil, fmt.Errorf("unknown encryption KMS type %s", kmsType)
|
||||||
}
|
}
|
||||||
|
227
internal/util/vault_tokens.go
Normal file
227
internal/util/vault_tokens.go
Normal file
@ -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
|
||||||
|
}
|
114
internal/util/vault_tokens_test.go
Normal file
114
internal/util/vault_tokens_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user