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)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
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