mirror of
https://github.com/ceph/ceph-csi.git
synced 2024-12-27 07:20:24 +00:00
552 lines
14 KiB
Go
552 lines
14 KiB
Go
|
package vault
|
||
|
|
||
|
import (
|
||
|
"context"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"net/http"
|
||
|
"os"
|
||
|
"path"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
|
||
|
"github.com/hashicorp/go-hclog"
|
||
|
"github.com/hashicorp/vault/api"
|
||
|
"github.com/hashicorp/vault/command/agent/auth"
|
||
|
"github.com/hashicorp/vault/command/agent/auth/kubernetes"
|
||
|
"github.com/libopenstorage/secrets"
|
||
|
)
|
||
|
|
||
|
const (
|
||
|
Name = secrets.TypeVault
|
||
|
DefaultBackendPath = "secret/"
|
||
|
VaultBackendPathKey = "VAULT_BACKEND_PATH"
|
||
|
VaultBackendKey = "VAULT_BACKEND"
|
||
|
vaultAddressPrefix = "http"
|
||
|
kvVersionKey = "version"
|
||
|
kvDataKey = "data"
|
||
|
kvVersion1 = "kv"
|
||
|
kvVersion2 = "kv-v2"
|
||
|
|
||
|
AuthMethodKubernetes = "kubernetes"
|
||
|
|
||
|
// AuthMethod is a vault authentication method used.
|
||
|
// https://www.vaultproject.io/docs/auth#auth-methods
|
||
|
AuthMethod = "VAULT_AUTH_METHOD"
|
||
|
// AuthMountPath defines a custom auth mount path.
|
||
|
AuthMountPath = "VAULT_AUTH_MOUNT_PATH"
|
||
|
// AuthKubernetesRole is the role to authenticate against on Vault
|
||
|
AuthKubernetesRole = "VAULT_AUTH_KUBERNETES_ROLE"
|
||
|
// AuthKubernetesTokenPath is the file path to a custom JWT token to use for authentication.
|
||
|
// If omitted, the default service account token path is used.
|
||
|
AuthKubernetesTokenPath = "VAULT_AUTH_KUBERNETES_TOKEN_PATH"
|
||
|
|
||
|
// AuthKubernetesMountPath
|
||
|
AuthKubernetesMountPath = "kubernetes"
|
||
|
)
|
||
|
|
||
|
var (
|
||
|
ErrVaultTokenNotSet = errors.New("VAULT_TOKEN not set.")
|
||
|
ErrVaultAddressNotSet = errors.New("VAULT_ADDR not set.")
|
||
|
ErrInvalidVaultToken = errors.New("VAULT_TOKEN is invalid")
|
||
|
ErrInvalidSkipVerify = errors.New("VAULT_SKIP_VERIFY is invalid")
|
||
|
ErrInvalidVaultAddress = errors.New("VAULT_ADDRESS is invalid. " +
|
||
|
"Should be of the form http(s)://<ip>:<port>")
|
||
|
|
||
|
ErrAuthMethodUnknown = fmt.Errorf("unknown auth method")
|
||
|
ErrKubernetesRole = fmt.Errorf("%s not set", AuthKubernetesRole)
|
||
|
)
|
||
|
|
||
|
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 := getVaultParam(secretConfig, api.EnvVaultAddress)
|
||
|
if address == "" {
|
||
|
return nil, ErrVaultAddressNotSet
|
||
|
}
|
||
|
// Vault fails if address is not in correct format
|
||
|
if !strings.HasPrefix(address, vaultAddressPrefix) {
|
||
|
return nil, ErrInvalidVaultAddress
|
||
|
}
|
||
|
config.Address = address
|
||
|
|
||
|
if err := configureTLS(config, secretConfig); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
client, err := newVaultClient(config)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
namespace := 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("")
|
||
|
}
|
||
|
|
||
|
var autoAuth bool
|
||
|
var token string
|
||
|
if getVaultParam(secretConfig, AuthMethod) != "" {
|
||
|
token, err = getAuthToken(client, secretConfig)
|
||
|
if err != nil {
|
||
|
closeIdleConnections(config)
|
||
|
return nil, err
|
||
|
}
|
||
|
|
||
|
autoAuth = true
|
||
|
} else {
|
||
|
token = getVaultParam(secretConfig, api.EnvVaultToken)
|
||
|
}
|
||
|
if token == "" {
|
||
|
closeIdleConnections(config)
|
||
|
return nil, ErrVaultTokenNotSet
|
||
|
}
|
||
|
client.SetToken(token)
|
||
|
|
||
|
backendPath := getVaultParam(secretConfig, VaultBackendPathKey)
|
||
|
if backendPath == "" {
|
||
|
backendPath = DefaultBackendPath
|
||
|
}
|
||
|
|
||
|
var isBackendV2 bool
|
||
|
backend := 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 {
|
||
|
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, namespace string) keyPath {
|
||
|
if namespace == "" {
|
||
|
namespace = v.namespace
|
||
|
}
|
||
|
return keyPath{
|
||
|
backendPath: v.backendPath,
|
||
|
isBackendV2: v.isKvBackendV2,
|
||
|
namespace: namespace,
|
||
|
secretID: secretID,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (v *vaultSecrets) GetSecret(
|
||
|
secretID string,
|
||
|
keyContext map[string]string,
|
||
|
) (map[string]interface{}, error) {
|
||
|
key := v.keyPath(secretID, keyContext[secrets.KeyVaultNamespace])
|
||
|
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[secrets.KeyVaultNamespace])
|
||
|
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[secrets.KeyVaultNamespace])
|
||
|
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 := 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 getVaultParam(secretConfig map[string]interface{}, name string) string {
|
||
|
if tokenIntf, exists := secretConfig[name]; exists {
|
||
|
tokenStr, ok := tokenIntf.(string)
|
||
|
if !ok {
|
||
|
return ""
|
||
|
}
|
||
|
return strings.TrimSpace(tokenStr)
|
||
|
} else {
|
||
|
return strings.TrimSpace(os.Getenv(name))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func configureTLS(config *api.Config, secretConfig map[string]interface{}) error {
|
||
|
tlsConfig := api.TLSConfig{}
|
||
|
skipVerify := getVaultParam(secretConfig, api.EnvVaultInsecure)
|
||
|
if skipVerify != "" {
|
||
|
insecure, err := strconv.ParseBool(skipVerify)
|
||
|
if err != nil {
|
||
|
return ErrInvalidSkipVerify
|
||
|
}
|
||
|
tlsConfig.Insecure = insecure
|
||
|
}
|
||
|
|
||
|
cacert := getVaultParam(secretConfig, api.EnvVaultCACert)
|
||
|
tlsConfig.CACert = cacert
|
||
|
|
||
|
capath := getVaultParam(secretConfig, api.EnvVaultCAPath)
|
||
|
tlsConfig.CAPath = capath
|
||
|
|
||
|
clientcert := getVaultParam(secretConfig, api.EnvVaultClientCert)
|
||
|
tlsConfig.ClientCert = clientcert
|
||
|
|
||
|
clientkey := getVaultParam(secretConfig, api.EnvVaultClientKey)
|
||
|
tlsConfig.ClientKey = clientkey
|
||
|
|
||
|
tlsserverName := getVaultParam(secretConfig, api.EnvVaultTLSServerName)
|
||
|
tlsConfig.TLSServerName = tlsserverName
|
||
|
|
||
|
return config.ConfigureTLS(&tlsConfig)
|
||
|
}
|
||
|
|
||
|
func getAuthToken(client *api.Client, config map[string]interface{}) (string, error) {
|
||
|
path, _, data, err := authenticate(client, config)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
|
||
|
secret, err := client.Logical().Write(path, data)
|
||
|
if err != nil {
|
||
|
return "", err
|
||
|
}
|
||
|
if secret == nil || secret.Auth == nil {
|
||
|
return "", errors.New("authentication returned nil auth info")
|
||
|
}
|
||
|
if secret.Auth.ClientToken == "" {
|
||
|
return "", errors.New("authentication returned empty client token")
|
||
|
}
|
||
|
|
||
|
return secret.Auth.ClientToken, err
|
||
|
}
|
||
|
|
||
|
func authenticate(client *api.Client, config map[string]interface{}) (string, http.Header, map[string]interface{}, error) {
|
||
|
method := getVaultParam(config, AuthMethod)
|
||
|
switch method {
|
||
|
case AuthMethodKubernetes:
|
||
|
return authKubernetes(client, config)
|
||
|
}
|
||
|
return "", nil, nil, fmt.Errorf("%s method: %s", method, ErrAuthMethodUnknown)
|
||
|
}
|
||
|
|
||
|
func authKubernetes(client *api.Client, config map[string]interface{}) (string, http.Header, map[string]interface{}, error) {
|
||
|
authConfig, err := buildAuthConfig(config)
|
||
|
if err != nil {
|
||
|
return "", nil, nil, err
|
||
|
}
|
||
|
method, err := kubernetes.NewKubernetesAuthMethod(authConfig)
|
||
|
if err != nil {
|
||
|
return "", nil, nil, err
|
||
|
}
|
||
|
|
||
|
return method.Authenticate(context.TODO(), client)
|
||
|
}
|
||
|
|
||
|
func buildAuthConfig(config map[string]interface{}) (*auth.AuthConfig, error) {
|
||
|
role := getVaultParam(config, AuthKubernetesRole)
|
||
|
if role == "" {
|
||
|
return nil, ErrKubernetesRole
|
||
|
}
|
||
|
mountPath := getVaultParam(config, AuthMountPath)
|
||
|
if mountPath == "" {
|
||
|
mountPath = AuthKubernetesMountPath
|
||
|
}
|
||
|
tokenPath := getVaultParam(config, AuthKubernetesTokenPath)
|
||
|
|
||
|
authMountPath := path.Join("auth", mountPath)
|
||
|
return &auth.AuthConfig{
|
||
|
Logger: hclog.NewNullLogger(),
|
||
|
MountPath: authMountPath,
|
||
|
Config: map[string]interface{}{
|
||
|
"role": role,
|
||
|
"token_path": tokenPath,
|
||
|
},
|
||
|
}, nil
|
||
|
}
|
||
|
|
||
|
func trimSlash(in string) string {
|
||
|
return strings.Trim(in, "/")
|
||
|
}
|
||
|
|
||
|
func init() {
|
||
|
if err := secrets.Register(Name, New); err != nil {
|
||
|
panic(err.Error())
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type keyPath struct {
|
||
|
backendPath string
|
||
|
isBackendV2 bool
|
||
|
namespace string
|
||
|
secretID string
|
||
|
}
|
||
|
|
||
|
func (k keyPath) Path() string {
|
||
|
if k.isBackendV2 {
|
||
|
return path.Join(k.namespace, k.backendPath, kvDataKey, 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)
|
||
|
}
|
||
|
|
||
|
func closeIdleConnections(cfg *api.Config) {
|
||
|
if cfg == nil || cfg.HttpClient == nil {
|
||
|
return
|
||
|
}
|
||
|
// close connection in case of error (a fix for go version < 1.12)
|
||
|
if tp, ok := cfg.HttpClient.Transport.(*http.Transport); ok {
|
||
|
tp.CloseIdleConnections()
|
||
|
}
|
||
|
}
|