544 lines
15 KiB
Go
544 lines
15 KiB
Go
/*
|
|
Copyright 2021 The Dapr 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 vault
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
|
|
jsoniter "github.com/json-iterator/go"
|
|
"golang.org/x/net/http2"
|
|
|
|
"github.com/dapr/components-contrib/metadata"
|
|
"github.com/dapr/components-contrib/secretstores"
|
|
"github.com/dapr/kit/logger"
|
|
kitmd "github.com/dapr/kit/metadata"
|
|
)
|
|
|
|
const (
|
|
defaultVaultAddress string = "https://127.0.0.1:8200"
|
|
defaultVaultEnginePath string = "secret"
|
|
componentVaultAddress string = "vaultAddr"
|
|
componentCaCert string = "caCert"
|
|
componentCaPath string = "caPath"
|
|
componentCaPem string = "caPem"
|
|
componentSkipVerify string = "skipVerify"
|
|
componentTLSServerName string = "tlsServerName"
|
|
componentVaultToken string = "vaultToken"
|
|
componentVaultTokenMountPath string = "vaultTokenMountPath"
|
|
componentVaultKVPrefix string = "vaultKVPrefix"
|
|
componentVaultKVUsePrefix string = "vaultKVUsePrefix"
|
|
defaultVaultKVPrefix string = "dapr"
|
|
vaultHTTPHeader string = "X-Vault-Token"
|
|
vaultHTTPRequestHeader string = "X-Vault-Request"
|
|
vaultEnginePath string = "enginePath"
|
|
vaultValueType string = "vaultValueType"
|
|
versionID string = "version_id"
|
|
|
|
DataStr string = "data"
|
|
)
|
|
|
|
type valueType string
|
|
|
|
const (
|
|
valueTypeMap valueType = "map"
|
|
valueTypeText valueType = "text"
|
|
)
|
|
|
|
var _ secretstores.SecretStore = (*vaultSecretStore)(nil)
|
|
|
|
func (v valueType) isMapType() bool {
|
|
return v == valueTypeMap
|
|
}
|
|
|
|
var ErrNotFound = errors.New("secret key or version not exist")
|
|
|
|
// vaultSecretStore is a secret store implementation for HashiCorp Vault.
|
|
type vaultSecretStore struct {
|
|
client *http.Client
|
|
vaultAddress string
|
|
vaultToken string
|
|
vaultTokenMountPath string
|
|
vaultKVPrefix string
|
|
vaultEnginePath string
|
|
vaultValueType valueType
|
|
|
|
json jsoniter.API
|
|
|
|
logger logger.Logger
|
|
}
|
|
|
|
type VaultMetadata struct {
|
|
CaCert string
|
|
CaPath string
|
|
CaPem string
|
|
SkipVerify string
|
|
TLSServerName string
|
|
VaultAddr string
|
|
VaultKVPrefix string
|
|
VaultKVUsePrefix bool
|
|
VaultToken string
|
|
VaultTokenMountPath string
|
|
EnginePath string
|
|
VaultValueType string
|
|
}
|
|
|
|
// tlsConfig is TLS configuration to interact with HashiCorp Vault.
|
|
type tlsConfig struct {
|
|
vaultCAPem string
|
|
vaultCACert string
|
|
vaultCAPath string
|
|
vaultSkipVerify bool
|
|
vaultServerName string
|
|
}
|
|
|
|
// vaultKVResponse is the response data from Vault KV.
|
|
type vaultKVResponse struct {
|
|
Data struct {
|
|
Data map[string]string `json:"data"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
// vaultListKVResponse is the response data from Vault KV.
|
|
type vaultListKVResponse struct {
|
|
Data struct {
|
|
Keys []string `json:"keys"`
|
|
} `json:"data"`
|
|
}
|
|
|
|
// NewHashiCorpVaultSecretStore returns a new HashiCorp Vault secret store.
|
|
func NewHashiCorpVaultSecretStore(logger logger.Logger) secretstores.SecretStore {
|
|
return &vaultSecretStore{
|
|
client: &http.Client{},
|
|
logger: logger,
|
|
json: jsoniter.ConfigFastest,
|
|
}
|
|
}
|
|
|
|
// Init creates a HashiCorp Vault client.
|
|
func (v *vaultSecretStore) Init(_ context.Context, meta secretstores.Metadata) error {
|
|
m := VaultMetadata{
|
|
VaultKVUsePrefix: true,
|
|
}
|
|
err := kitmd.DecodeMetadata(meta.Properties, &m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Get Vault address
|
|
address := m.VaultAddr
|
|
if address == "" {
|
|
address = defaultVaultAddress
|
|
}
|
|
|
|
v.vaultAddress = address
|
|
|
|
v.vaultEnginePath = defaultVaultEnginePath
|
|
if m.EnginePath != "" {
|
|
v.vaultEnginePath = m.EnginePath
|
|
}
|
|
|
|
v.vaultValueType = valueTypeMap
|
|
if m.VaultValueType != "" {
|
|
switch valueType(m.VaultValueType) {
|
|
case valueTypeMap:
|
|
case valueTypeText:
|
|
v.vaultValueType = valueTypeText
|
|
default:
|
|
return fmt.Errorf("vault init error, invalid value type %s, accepted values are map or text", m.VaultValueType)
|
|
}
|
|
}
|
|
|
|
v.vaultToken = m.VaultToken
|
|
v.vaultTokenMountPath = m.VaultTokenMountPath
|
|
initErr := v.initVaultToken()
|
|
if initErr != nil {
|
|
return initErr
|
|
}
|
|
|
|
vaultKVPrefix := m.VaultKVPrefix
|
|
if !m.VaultKVUsePrefix {
|
|
vaultKVPrefix = ""
|
|
} else if vaultKVPrefix == "" {
|
|
vaultKVPrefix = defaultVaultKVPrefix
|
|
}
|
|
v.vaultKVPrefix = vaultKVPrefix
|
|
|
|
// Generate TLS config
|
|
tlsConf := metadataToTLSConfig(&m)
|
|
|
|
client, err := v.createHTTPClient(tlsConf)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't create client using config: %w", err)
|
|
}
|
|
|
|
v.client = client
|
|
|
|
return nil
|
|
}
|
|
|
|
func metadataToTLSConfig(meta *VaultMetadata) *tlsConfig {
|
|
tlsConf := tlsConfig{}
|
|
|
|
// Configure TLS settings
|
|
skipVerify := meta.SkipVerify
|
|
tlsConf.vaultSkipVerify = false
|
|
if skipVerify == "true" {
|
|
tlsConf.vaultSkipVerify = true
|
|
}
|
|
|
|
tlsConf.vaultCACert = meta.CaCert
|
|
tlsConf.vaultCAPem = meta.CaPem
|
|
tlsConf.vaultCAPath = meta.CaPath
|
|
tlsConf.vaultServerName = meta.TLSServerName
|
|
|
|
return &tlsConf
|
|
}
|
|
|
|
// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values.
|
|
func (v *vaultSecretStore) getSecret(ctx context.Context, secret, version string) (*vaultKVResponse, error) {
|
|
// Create get secret url
|
|
var vaultSecretPathAddr string
|
|
if v.vaultKVPrefix == "" {
|
|
vaultSecretPathAddr = v.vaultAddress + "/v1/" + v.vaultEnginePath + "/data/" + secret + "?version=" + version
|
|
} else {
|
|
vaultSecretPathAddr = v.vaultAddress + "/v1/" + v.vaultEnginePath + "/data/" + v.vaultKVPrefix + "/" + secret + "?version=" + version
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodGet, vaultSecretPathAddr, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't generate request: %w", err)
|
|
}
|
|
// Set vault token.
|
|
httpReq.Header.Set(vaultHTTPHeader, v.vaultToken)
|
|
// Set X-Vault-Request header
|
|
httpReq.Header.Set(vaultHTTPRequestHeader, "true")
|
|
|
|
httpresp, err := v.client.Do(httpReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't get secret: %w", err)
|
|
}
|
|
|
|
defer httpresp.Body.Close()
|
|
|
|
if httpresp.StatusCode != http.StatusOK {
|
|
var b bytes.Buffer
|
|
io.Copy(&b, httpresp.Body)
|
|
v.logger.Debugf("getSecret %s couldn't get successful response: %#v, %s", secret, httpresp, b.String())
|
|
if httpresp.StatusCode == http.StatusNotFound {
|
|
// handle not found error
|
|
return nil, fmt.Errorf("getSecret %s failed %w", secret, ErrNotFound)
|
|
}
|
|
|
|
return nil, fmt.Errorf("couldn't get successful response, status code %d, body %s",
|
|
httpresp.StatusCode, b.String())
|
|
}
|
|
|
|
var d vaultKVResponse
|
|
|
|
if v.vaultValueType.isMapType() {
|
|
// parse the secret value to map[string]string
|
|
if err := json.NewDecoder(httpresp.Body).Decode(&d); err != nil {
|
|
return nil, fmt.Errorf("couldn't decode response body: %s", err)
|
|
}
|
|
} else {
|
|
// treat the secret as string
|
|
b, err := io.ReadAll(httpresp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't read response: %s", err)
|
|
}
|
|
res := v.json.Get(b, DataStr, DataStr).ToString()
|
|
d.Data.Data = map[string]string{
|
|
secret: res,
|
|
}
|
|
}
|
|
|
|
return &d, nil
|
|
}
|
|
|
|
// GetSecret retrieves a secret using a key and returns a map of decrypted string/string values.
|
|
func (v *vaultSecretStore) GetSecret(ctx context.Context, req secretstores.GetSecretRequest) (secretstores.GetSecretResponse, error) {
|
|
// version 0 represent for latest version
|
|
version := "0"
|
|
if value, ok := req.Metadata[versionID]; ok {
|
|
version = value
|
|
}
|
|
d, err := v.getSecret(ctx, req.Name, version)
|
|
if err != nil {
|
|
return secretstores.GetSecretResponse{Data: nil}, err
|
|
}
|
|
|
|
resp := secretstores.GetSecretResponse{
|
|
Data: d.Data.Data,
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// BulkGetSecret retrieves all secrets in the store and returns a map of decrypted string/string values.
|
|
func (v *vaultSecretStore) BulkGetSecret(ctx context.Context, req secretstores.BulkGetSecretRequest) (secretstores.BulkGetSecretResponse, error) {
|
|
version := "0"
|
|
if value, ok := req.Metadata[versionID]; ok {
|
|
version = value
|
|
}
|
|
|
|
resp := secretstores.BulkGetSecretResponse{
|
|
Data: map[string]map[string]string{},
|
|
}
|
|
|
|
keys, err := v.listKeysUnderPath(ctx, "")
|
|
if err != nil {
|
|
return secretstores.BulkGetSecretResponse{}, err
|
|
}
|
|
|
|
for _, key := range keys {
|
|
keyValues := map[string]string{}
|
|
secrets, err := v.getSecret(ctx, key, version)
|
|
if err != nil {
|
|
if errors.Is(err, ErrNotFound) {
|
|
// version not exist skip
|
|
continue
|
|
}
|
|
|
|
return secretstores.BulkGetSecretResponse{Data: nil}, err
|
|
}
|
|
|
|
for k, v := range secrets.Data.Data {
|
|
keyValues[k] = v
|
|
}
|
|
resp.Data[key] = keyValues
|
|
}
|
|
|
|
return resp, nil
|
|
}
|
|
|
|
// listKeysUnderPath get all the keys recursively under a given path.(returned keys including path as prefix)
|
|
// path should not has `/` prefix.
|
|
func (v *vaultSecretStore) listKeysUnderPath(ctx context.Context, path string) ([]string, error) {
|
|
var vaultSecretsPathAddr string
|
|
|
|
// Create list secrets url
|
|
if v.vaultKVPrefix == "" {
|
|
vaultSecretsPathAddr = fmt.Sprintf("%s/v1/%s/metadata/%s", v.vaultAddress, v.vaultEnginePath, path)
|
|
} else {
|
|
vaultSecretsPathAddr = fmt.Sprintf("%s/v1/%s/metadata/%s/%s", v.vaultAddress, v.vaultEnginePath, v.vaultKVPrefix, path)
|
|
}
|
|
|
|
httpReq, err := http.NewRequestWithContext(ctx, "LIST", vaultSecretsPathAddr, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't generate request: %s", err)
|
|
}
|
|
// Set vault token.
|
|
httpReq.Header.Set(vaultHTTPHeader, v.vaultToken)
|
|
// Set X-Vault-Request header
|
|
httpReq.Header.Set(vaultHTTPRequestHeader, "true")
|
|
httpresp, err := v.client.Do(httpReq)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't get secret: %s", err)
|
|
}
|
|
|
|
defer httpresp.Body.Close()
|
|
|
|
if httpresp.StatusCode != http.StatusOK {
|
|
var b bytes.Buffer
|
|
io.Copy(&b, httpresp.Body)
|
|
v.logger.Debugf("list keys couldn't get successful response: %#v, %s", httpresp, b.String())
|
|
|
|
return nil, fmt.Errorf("list keys couldn't get successful response, status code: %d, status: %s, response %s",
|
|
httpresp.StatusCode, httpresp.Status, b.String())
|
|
}
|
|
|
|
var d vaultListKVResponse
|
|
|
|
if err := json.NewDecoder(httpresp.Body).Decode(&d); err != nil {
|
|
return nil, fmt.Errorf("couldn't decode response body: %s", err)
|
|
}
|
|
res := make([]string, 0, len(d.Data.Keys))
|
|
for _, key := range d.Data.Keys {
|
|
if v.isSecretPath(key) {
|
|
res = append(res, path+key)
|
|
} else {
|
|
subKeys, err := v.listKeysUnderPath(ctx, path+key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
res = append(res, subKeys...)
|
|
}
|
|
}
|
|
|
|
return res, nil
|
|
}
|
|
|
|
// isSecretPath checks if the key is a valid secret path or it is part of the secret path.
|
|
func (v *vaultSecretStore) isSecretPath(key string) bool {
|
|
return !strings.HasSuffix(key, "/")
|
|
}
|
|
|
|
// initVaultToken reads the vault token from the file if token is defined by mount path.
|
|
func (v *vaultSecretStore) initVaultToken() error {
|
|
// Test that at least one of them are set if not return error
|
|
if v.vaultToken == "" && v.vaultTokenMountPath == "" {
|
|
return fmt.Errorf("token mount path and token not set")
|
|
}
|
|
|
|
// Test that both are not set. If so return error
|
|
if v.vaultToken != "" && v.vaultTokenMountPath != "" {
|
|
return fmt.Errorf("token mount path and token both set")
|
|
}
|
|
|
|
if v.vaultToken != "" {
|
|
return nil
|
|
}
|
|
|
|
data, err := os.ReadFile(v.vaultTokenMountPath)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't read vault token from mount path %s err: %s", v.vaultTokenMountPath, err)
|
|
}
|
|
v.vaultToken = string(bytes.TrimSpace(data))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (v *vaultSecretStore) createHTTPClient(config *tlsConfig) (*http.Client, error) {
|
|
tlsClientConfig := &tls.Config{MinVersion: tls.VersionTLS12}
|
|
|
|
if config != nil && config.vaultSkipVerify {
|
|
v.logger.Infof("hashicorp vault: you are using 'skipVerify' to skip server config verify which is unsafe!")
|
|
}
|
|
|
|
tlsClientConfig.InsecureSkipVerify = config.vaultSkipVerify
|
|
if !config.vaultSkipVerify {
|
|
rootCAPools, err := v.getRootCAsPools(config.vaultCAPem, config.vaultCAPath, config.vaultCACert)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
tlsClientConfig.RootCAs = rootCAPools
|
|
|
|
if config.vaultServerName != "" {
|
|
tlsClientConfig.ServerName = config.vaultServerName
|
|
}
|
|
}
|
|
|
|
// Setup http transport
|
|
transport := &http.Transport{
|
|
TLSClientConfig: tlsClientConfig,
|
|
}
|
|
|
|
// Configure http2 client
|
|
err := http2.ConfigureTransport(transport)
|
|
if err != nil {
|
|
return nil, errors.New("failed to configure http2")
|
|
}
|
|
|
|
return &http.Client{
|
|
Transport: transport,
|
|
}, nil
|
|
}
|
|
|
|
// getRootCAsPools returns root CAs when you give it CA Pem file, CA path, and CA Certificate. Default is system certificates.
|
|
func (v *vaultSecretStore) getRootCAsPools(vaultCAPem string, vaultCAPath string, vaultCACert string) (*x509.CertPool, error) {
|
|
if vaultCAPem != "" {
|
|
certPool := x509.NewCertPool()
|
|
cert := []byte(vaultCAPem)
|
|
if ok := certPool.AppendCertsFromPEM(cert); !ok {
|
|
return nil, fmt.Errorf("couldn't read PEM")
|
|
}
|
|
|
|
return certPool, nil
|
|
}
|
|
|
|
if vaultCAPath != "" {
|
|
certPool := x509.NewCertPool()
|
|
if err := readCertificateFolder(certPool, vaultCAPath); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return certPool, nil
|
|
}
|
|
|
|
if vaultCACert != "" {
|
|
certPool := x509.NewCertPool()
|
|
if err := readCertificateFile(certPool, vaultCACert); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return certPool, nil
|
|
}
|
|
|
|
certPool, err := x509.SystemCertPool()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("couldn't read system certs: %s", err)
|
|
}
|
|
|
|
return certPool, nil
|
|
}
|
|
|
|
// readCertificateFile reads the certificate at given path.
|
|
func readCertificateFile(certPool *x509.CertPool, path string) error {
|
|
// Read certificate file
|
|
pemFile, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't read CA file from disk: %s", err)
|
|
}
|
|
|
|
if ok := certPool.AppendCertsFromPEM(pemFile); !ok {
|
|
return fmt.Errorf("couldn't read PEM")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// readCertificateFolder scans a folder for certificates.
|
|
func readCertificateFolder(certPool *x509.CertPool, path string) error {
|
|
err := filepath.Walk(path, func(p string, info os.FileInfo, err error) error {
|
|
if info.IsDir() {
|
|
return nil
|
|
}
|
|
|
|
return readCertificateFile(certPool, p)
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("couldn't read certificates at %s: %s", path, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Features returns the features available in this secret store.
|
|
func (v *vaultSecretStore) Features() []secretstores.Feature {
|
|
if v.vaultValueType == valueTypeText {
|
|
return []secretstores.Feature{}
|
|
}
|
|
|
|
return []secretstores.Feature{secretstores.FeatureMultipleKeyValuesPerSecret}
|
|
}
|
|
|
|
func (v *vaultSecretStore) GetComponentMetadata() (metadataInfo metadata.MetadataMap) {
|
|
metadataStruct := VaultMetadata{}
|
|
metadata.GetMetadataInfoFromStructType(reflect.TypeOf(metadataStruct), &metadataInfo, metadata.SecretStoreType)
|
|
return
|
|
}
|