mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-11-14 18:30:21 +00:00
d7bcb42481
libopenstorage has added a new feature that makes it possible to destroy the contents of a key/value in the Hashicorp Vault kv-v2 secrets backend. See-also: https://github.com/libopenstorage/secrets/pull/55 Signed-off-by: Niels de Vos <ndevos@redhat.com>
415 lines
9.9 KiB
Go
415 lines
9.9 KiB
Go
package vault
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/hashicorp/vault/api"
|
|
"github.com/libopenstorage/secrets"
|
|
"github.com/libopenstorage/secrets/vault/utils"
|
|
)
|
|
|
|
const (
|
|
Name = secrets.TypeVault
|
|
DefaultBackendPath = "secret/"
|
|
VaultBackendPathKey = "VAULT_BACKEND_PATH"
|
|
VaultBackendKey = "VAULT_BACKEND"
|
|
kvVersionKey = "version"
|
|
kvDataKey = "data"
|
|
kvMetadataKey = "metadata"
|
|
kvVersion1 = "kv"
|
|
kvVersion2 = "kv-v2"
|
|
|
|
AuthMethodKubernetes = utils.AuthMethodKubernetes
|
|
AuthMethod = utils.AuthMethod
|
|
AuthMountPath = utils.AuthMountPath
|
|
AuthKubernetesRole = utils.AuthKubernetesRole
|
|
AuthKubernetesTokenPath = utils.AuthKubernetesTokenPath
|
|
AuthKubernetesMountPath = utils.AuthKubernetesMountPath
|
|
)
|
|
|
|
func init() {
|
|
if err := secrets.Register(Name, New); err != nil {
|
|
panic(err.Error())
|
|
}
|
|
}
|
|
|
|
type vaultSecrets struct {
|
|
mu sync.RWMutex
|
|
client *api.Client
|
|
|
|
currentNamespace string
|
|
lockClientToken sync.Mutex
|
|
|
|
endpoint string
|
|
backendPath string
|
|
namespace string
|
|
isKvBackendV2 bool
|
|
autoAuth bool
|
|
config map[string]interface{}
|
|
}
|
|
|
|
// These variables are helpful in testing to stub method call from packages
|
|
var (
|
|
newVaultClient = api.NewClient
|
|
isKvV2 = isKvBackendV2
|
|
)
|
|
|
|
func New(
|
|
secretConfig map[string]interface{},
|
|
) (secrets.Secrets, error) {
|
|
// DefaultConfig uses the environment variables if present.
|
|
config := api.DefaultConfig()
|
|
|
|
if len(secretConfig) == 0 && config.Error != nil {
|
|
return nil, config.Error
|
|
}
|
|
|
|
address := utils.GetVaultParam(secretConfig, api.EnvVaultAddress)
|
|
if address == "" {
|
|
return nil, utils.ErrVaultAddressNotSet
|
|
}
|
|
if err := utils.IsValidAddr(address); err != nil {
|
|
return nil, err
|
|
}
|
|
config.Address = address
|
|
|
|
if err := utils.ConfigureTLS(config, secretConfig); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
client, err := newVaultClient(config)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
namespace := utils.GetVaultParam(secretConfig, api.EnvVaultNamespace)
|
|
if len(namespace) > 0 {
|
|
// use a namespace as a header for setup purposes
|
|
// later use it as a key prefix
|
|
client.SetNamespace(namespace)
|
|
defer client.SetNamespace("")
|
|
}
|
|
|
|
token, autoAuth, err := utils.Authenticate(client, secretConfig)
|
|
if err != nil {
|
|
utils.CloseIdleConnections(config)
|
|
return nil, fmt.Errorf("failed to get the authentication token: %w", err)
|
|
}
|
|
client.SetToken(token)
|
|
|
|
backendPath := utils.GetVaultParam(secretConfig, VaultBackendPathKey)
|
|
if backendPath == "" {
|
|
backendPath = DefaultBackendPath
|
|
}
|
|
|
|
var isBackendV2 bool
|
|
backend := utils.GetVaultParam(secretConfig, VaultBackendKey)
|
|
if backend == kvVersion1 {
|
|
isBackendV2 = false
|
|
} else if backend == kvVersion2 {
|
|
isBackendV2 = true
|
|
} else {
|
|
// TODO: Handle backends other than kv
|
|
isBackendV2, err = isKvV2(client, backendPath)
|
|
if err != nil {
|
|
utils.CloseIdleConnections(config)
|
|
return nil, err
|
|
}
|
|
}
|
|
return &vaultSecrets{
|
|
endpoint: config.Address,
|
|
namespace: namespace,
|
|
currentNamespace: namespace,
|
|
client: client,
|
|
backendPath: backendPath,
|
|
isKvBackendV2: isBackendV2,
|
|
autoAuth: autoAuth,
|
|
config: secretConfig,
|
|
}, nil
|
|
}
|
|
|
|
func (v *vaultSecrets) String() string {
|
|
return Name
|
|
}
|
|
|
|
func (v *vaultSecrets) keyPath(secretID string, keyContext map[string]string) keyPath {
|
|
backendPath := v.backendPath
|
|
var namespace string
|
|
var ok bool
|
|
if namespace, ok = keyContext[secrets.KeyVaultNamespace]; !ok {
|
|
namespace = v.namespace
|
|
}
|
|
|
|
var isDestroyKey bool
|
|
if v.isKvBackendV2 {
|
|
if destroyAllSecrets, ok := keyContext[secrets.DestroySecret]; ok {
|
|
// checking for any value seems sufficient to assume 'destroy' is requested
|
|
if destroyAllSecrets != "" {
|
|
isDestroyKey = true
|
|
}
|
|
}
|
|
}
|
|
return keyPath{
|
|
backendPath: backendPath,
|
|
isBackendV2: v.isKvBackendV2,
|
|
namespace: namespace,
|
|
secretID: secretID,
|
|
isDestroyKey: isDestroyKey,
|
|
}
|
|
}
|
|
|
|
func (v *vaultSecrets) GetSecret(
|
|
secretID string,
|
|
keyContext map[string]string,
|
|
) (map[string]interface{}, error) {
|
|
key := v.keyPath(secretID, keyContext)
|
|
secretValue, err := v.read(key)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get secret: %s: %s", key, err)
|
|
}
|
|
if secretValue == nil {
|
|
return nil, secrets.ErrInvalidSecretId
|
|
}
|
|
|
|
if v.isKvBackendV2 {
|
|
if data, exists := secretValue.Data[kvDataKey]; exists && data != nil {
|
|
if data, ok := data.(map[string]interface{}); ok {
|
|
return data, nil
|
|
}
|
|
}
|
|
return nil, secrets.ErrInvalidSecretId
|
|
} else {
|
|
return secretValue.Data, nil
|
|
}
|
|
}
|
|
|
|
func (v *vaultSecrets) PutSecret(
|
|
secretID string,
|
|
secretData map[string]interface{},
|
|
keyContext map[string]string,
|
|
) error {
|
|
if v.isKvBackendV2 {
|
|
secretData = map[string]interface{}{
|
|
kvDataKey: secretData,
|
|
}
|
|
}
|
|
|
|
key := v.keyPath(secretID, keyContext)
|
|
if _, err := v.write(key, secretData); err != nil {
|
|
return fmt.Errorf("failed to put secret: %s: %s", key, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *vaultSecrets) DeleteSecret(
|
|
secretID string,
|
|
keyContext map[string]string,
|
|
) error {
|
|
key := v.keyPath(secretID, keyContext)
|
|
if _, err := v.delete(key); err != nil {
|
|
return fmt.Errorf("failed to delete secret: %s: %s", key, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (v *vaultSecrets) Encrypt(
|
|
secretID string,
|
|
plaintTextData string,
|
|
keyContext map[string]string,
|
|
) (string, error) {
|
|
return "", secrets.ErrNotSupported
|
|
}
|
|
|
|
func (v *vaultSecrets) Decrypt(
|
|
secretID string,
|
|
encryptedData string,
|
|
keyContext map[string]string,
|
|
) (string, error) {
|
|
return "", secrets.ErrNotSupported
|
|
}
|
|
|
|
func (v *vaultSecrets) Rencrypt(
|
|
originalSecretID string,
|
|
newSecretID string,
|
|
originalKeyContext map[string]string,
|
|
newKeyContext map[string]string,
|
|
encryptedData string,
|
|
) (string, error) {
|
|
return "", secrets.ErrNotSupported
|
|
}
|
|
|
|
func (v *vaultSecrets) ListSecrets() ([]string, error) {
|
|
return nil, secrets.ErrNotSupported
|
|
}
|
|
|
|
func (v *vaultSecrets) read(path keyPath) (*api.Secret, error) {
|
|
if v.autoAuth {
|
|
v.lockClientToken.Lock()
|
|
defer v.lockClientToken.Unlock()
|
|
|
|
if err := v.setNamespaceToken(path.Namespace()); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
secretValue, err := v.lockedRead(path.Path())
|
|
if v.isTokenExpired(err) {
|
|
if err = v.renewToken(path.Namespace()); err != nil {
|
|
return nil, fmt.Errorf("failed to renew token: %s", err)
|
|
}
|
|
return v.lockedRead(path.Path())
|
|
}
|
|
return secretValue, err
|
|
}
|
|
|
|
func (v *vaultSecrets) write(path keyPath, data map[string]interface{}) (*api.Secret, error) {
|
|
if v.autoAuth {
|
|
v.lockClientToken.Lock()
|
|
defer v.lockClientToken.Unlock()
|
|
|
|
if err := v.setNamespaceToken(path.Namespace()); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
secretValue, err := v.lockedWrite(path.Path(), data)
|
|
if v.isTokenExpired(err) {
|
|
if err = v.renewToken(path.Namespace()); err != nil {
|
|
return nil, fmt.Errorf("failed to renew token: %s", err)
|
|
}
|
|
return v.lockedWrite(path.Path(), data)
|
|
}
|
|
return secretValue, err
|
|
}
|
|
|
|
func (v *vaultSecrets) delete(path keyPath) (*api.Secret, error) {
|
|
if v.autoAuth {
|
|
v.lockClientToken.Lock()
|
|
defer v.lockClientToken.Unlock()
|
|
|
|
if err := v.setNamespaceToken(path.Namespace()); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
secretValue, err := v.lockedDelete(path.Path())
|
|
if v.isTokenExpired(err) {
|
|
if err = v.renewToken(path.Namespace()); err != nil {
|
|
return nil, fmt.Errorf("failed to renew token: %s", err)
|
|
}
|
|
return v.lockedDelete(path.Path())
|
|
}
|
|
return secretValue, err
|
|
}
|
|
|
|
func (v *vaultSecrets) lockedRead(path string) (*api.Secret, error) {
|
|
v.mu.RLock()
|
|
defer v.mu.RUnlock()
|
|
|
|
return v.client.Logical().Read(path)
|
|
}
|
|
|
|
func (v *vaultSecrets) lockedWrite(path string, data map[string]interface{}) (*api.Secret, error) {
|
|
v.mu.RLock()
|
|
defer v.mu.RUnlock()
|
|
|
|
return v.client.Logical().Write(path, data)
|
|
}
|
|
|
|
func (v *vaultSecrets) lockedDelete(path string) (*api.Secret, error) {
|
|
v.mu.RLock()
|
|
defer v.mu.RUnlock()
|
|
|
|
return v.client.Logical().Delete(path)
|
|
}
|
|
|
|
func (v *vaultSecrets) renewToken(namespace string) error {
|
|
v.mu.Lock()
|
|
defer v.mu.Unlock()
|
|
|
|
if len(namespace) > 0 {
|
|
v.client.SetNamespace(namespace)
|
|
defer v.client.SetNamespace("")
|
|
}
|
|
token, err := utils.GetAuthToken(v.client, v.config)
|
|
if err != nil {
|
|
return fmt.Errorf("get auth token for %s namespace: %s", namespace, err)
|
|
}
|
|
|
|
v.currentNamespace = namespace
|
|
v.client.SetToken(token)
|
|
return nil
|
|
}
|
|
|
|
func (v *vaultSecrets) isTokenExpired(err error) bool {
|
|
return err != nil && v.autoAuth && strings.Contains(err.Error(), "permission denied")
|
|
}
|
|
|
|
// setNamespaceToken is used for a multi-token support with a kubernetes auto auth setup.
|
|
//
|
|
// This allows to talk with a multiple vault namespaces (which are not sub-namespace). Create
|
|
// the same “Kubernetes Auth Role” in each of the configured namespace. For every request it
|
|
// fetches the token for that specific namespace.
|
|
func (v *vaultSecrets) setNamespaceToken(namespace string) error {
|
|
if v.currentNamespace == namespace {
|
|
return nil
|
|
}
|
|
|
|
return v.renewToken(namespace)
|
|
}
|
|
|
|
func isKvBackendV2(client *api.Client, backendPath string) (bool, error) {
|
|
mounts, err := client.Sys().ListMounts()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
for path, mount := range mounts {
|
|
// path is represented as 'path/'
|
|
if trimSlash(path) == trimSlash(backendPath) {
|
|
version := mount.Options[kvVersionKey]
|
|
if version == "2" {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return false, fmt.Errorf("Secrets engine with mount path '%s' not found",
|
|
backendPath)
|
|
}
|
|
|
|
func trimSlash(in string) string {
|
|
return strings.Trim(in, "/")
|
|
}
|
|
|
|
type keyPath struct {
|
|
backendPath string
|
|
isBackendV2 bool
|
|
namespace string
|
|
secretID string
|
|
isDestroyKey bool
|
|
}
|
|
|
|
func (k keyPath) Path() string {
|
|
if k.isBackendV2 {
|
|
keyType := kvDataKey
|
|
if k.isDestroyKey {
|
|
keyType = kvMetadataKey
|
|
}
|
|
return path.Join(k.namespace, k.backendPath, keyType, k.secretID)
|
|
}
|
|
return path.Join(k.namespace, k.backendPath, k.secretID)
|
|
}
|
|
|
|
func (k keyPath) Namespace() string {
|
|
return k.namespace
|
|
}
|
|
|
|
func (k keyPath) String() string {
|
|
return fmt.Sprintf("backendPath=%s, backendV2=%t, namespace=%s, secretID=%s", k.backendPath, k.isBackendV2, k.namespace, k.secretID)
|
|
}
|