Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow vault secret to handle write operation #5068

Merged
merged 3 commits into from
Oct 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Here is an overview of all new **experimental** features:

- **General**: Add parameter queryParameters to prometheus-scaler ([#4962](https://github.com/kedacore/keda/issues/4962))
- **General**: TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX))
- **Hashicorp Vault**: Add support to get secret that needs write operation (e.g. pki) ([#5067](https://github.com/kedacore/keda/issues/5067))
- **Kafka Scaler**: Ability to set upper bound to the number of partitions with lag ([#3997](https://github.com/kedacore/keda/issues/3997))
- **Kafka Scaler**: Add support for Kerberos authentication (SASL / GSSAPI) ([#4836](https://github.com/kedacore/keda/issues/4836))

Expand Down
29 changes: 26 additions & 3 deletions apis/keda/v1alpha1/triggerauthentication_types.go
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -197,11 +197,34 @@ const (
// VaultAuthenticationAWS = "aws"
)

// VaultSecretType defines the type of vault secret
type VaultSecretType string

const (
VaultSecretTypeGeneric VaultSecretType = ""
VaultSecretTypeSecretV2 VaultSecretType = "secretV2"
VaultSecretTypeSecret VaultSecretType = "secret"
VaultSecretTypePki VaultSecretType = "pki"
)

type VaultPkiData struct {
CommonName string `json:"commonName,omitempty"`
AltNames string `json:"altNames,omitempty"`
IPSans string `json:"ipSans,omitempty"`
URISans string `json:"uriSans,omitempty"`
OtherSans string `json:"otherSans,omitempty"`
TTL string `json:"ttl,omitempty"`
Format string `json:"format,omitempty"`
}

// VaultSecret defines the mapping between the path of the secret in Vault to the parameter
type VaultSecret struct {
Parameter string `json:"parameter"`
Path string `json:"path"`
Key string `json:"key"`
Parameter string `json:"parameter"`
Path string `json:"path"`
Key string `json:"key"`
Type VaultSecretType `json:"type,omitempty"`
PkiData VaultPkiData `json:"pkiData,omitempty"`
Value string `json:"-"`
}

// AzureKeyVault is used to authenticate using Azure Key Vault
Expand Down
16 changes: 16 additions & 0 deletions apis/keda/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 20 additions & 0 deletions config/crd/bases/keda.sh_clustertriggerauthentications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,26 @@ spec:
type: string
path:
type: string
pkiData:
properties:
altNames:
type: string
commonName:
type: string
format:
type: string
ipSans:
type: string
otherSans:
type: string
ttl:
type: string
uriSans:
type: string
type: object
type:
description: VaultSecretType defines the type of vault secret
type: string
required:
- key
- parameter
Expand Down
20 changes: 20 additions & 0 deletions config/crd/bases/keda.sh_triggerauthentications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,26 @@ spec:
type: string
path:
type: string
pkiData:
properties:
altNames:
type: string
commonName:
type: string
format:
type: string
ipSans:
type: string
otherSans:
type: string
ttl:
type: string
uriSans:
type: string
type: object
type:
description: VaultSecretType defines the type of vault secret
type: string
required:
- key
- parameter
Expand Down
170 changes: 169 additions & 1 deletion pkg/scaling/resolver/hashicorpvault_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@ limitations under the License.
package resolver

import (
"encoding/json"
"errors"
"fmt"
"os"
"strings"

"github.com/go-logr/logr"
vaultapi "github.com/hashicorp/vault/api"
Expand Down Expand Up @@ -84,6 +86,7 @@ func (vh *HashicorpVaultHandler) Initialize(logger logr.Logger) error {
return nil
}

// token Extract a vault token from the Authentication method
func (vh *HashicorpVaultHandler) token(client *vaultapi.Client) (string, error) {
var token string

Expand Down Expand Up @@ -131,6 +134,7 @@ func (vh *HashicorpVaultHandler) token(client *vaultapi.Client) (string, error)
return token, nil
}

// renewToken takes charge of renewing the vault token
func (vh *HashicorpVaultHandler) renewToken(logger logr.Logger) {
secret, err := vh.client.Auth().Token().RenewSelf(0)
if err != nil {
Expand Down Expand Up @@ -166,13 +170,177 @@ RenewWatcherLoop:
}
}

// Read is used to get a secret from vault Read api. (e.g. secret)
func (vh *HashicorpVaultHandler) Read(path string) (*vaultapi.Secret, error) {
return vh.client.Logical().Read(path)
}

// Stop is responsible for stoping the renew token process
// Write is used to get a secret from vault that needs to pass along data and uses the vault Write api. (e.g. pki)
func (vh *HashicorpVaultHandler) Write(path string, data map[string]interface{}) (*vaultapi.Secret, error) {
loispostula marked this conversation as resolved.
Show resolved Hide resolved
return vh.client.Logical().Write(path, data)
}

// Stop is responsible for stopping the renewal token process
func (vh *HashicorpVaultHandler) Stop() {
if vh.stopCh != nil {
vh.stopCh <- struct{}{}
}
}

// getPkiRequest format the pkiData in a format that the vault sdk understands.
func (vh *HashicorpVaultHandler) getPkiRequest(pkiData *kedav1alpha1.VaultPkiData) (map[string]interface{}, error) {
var data map[string]interface{}
a, err := json.Marshal(pkiData)
if err != nil {
return nil, err
}
err = json.Unmarshal(a, &data)
if err != nil {
return nil, err
}
return data, nil
}

// getSecretValue extract the secret value from the vault api response. As the vault api returns us a map[string]interface{},
// specific handling might be needed for some secret type.
func (vh *HashicorpVaultHandler) getSecretValue(secret *kedav1alpha1.VaultSecret, vaultSecret *vaultapi.Secret) (string, error) {
if secret.Type == kedav1alpha1.VaultSecretTypeGeneric {
if _, ok := vaultSecret.Data["data"]; ok {
// Probably a v2 secret
secret.Type = kedav1alpha1.VaultSecretTypeSecretV2
} else {
secret.Type = kedav1alpha1.VaultSecretTypeSecret
}
}
switch secret.Type {
case kedav1alpha1.VaultSecretTypePki:
if vData, ok := vaultSecret.Data[secret.Key]; ok {
if secret.Key == "ca_chain" {
// Cast the secret to []interface{}
if ai, ok := vData.([]interface{}); ok {
// Cast the secret to []string
stringSlice := make([]string, len(ai))
for i, v := range ai {
stringSlice[i] = v.(string)
}
return strings.Join(stringSlice, "\n"), nil
}
err := fmt.Errorf("key '%s' is not castable to []interface{}", secret.Key)
return "", err
}
if s, ok := vData.(string); ok {
return s, nil
}
// If this happens, bad data from vault
err := fmt.Errorf("key '%s' is not castable to string", secret.Key)
return "", err
}
err := fmt.Errorf("key '%s' not found", secret.Key)
return "", err
case kedav1alpha1.VaultSecretTypeSecret:
if vData, ok := vaultSecret.Data[secret.Key]; ok {
if s, ok := vData.(string); ok {
return s, nil
}
err := fmt.Errorf("key '%s' is not castable to string", secret.Key)
return "", err
}
err := fmt.Errorf("key '%s' not found", secret.Key)
return "", err
case kedav1alpha1.VaultSecretTypeSecretV2:
if v2Data, ok := vaultSecret.Data["data"].(map[string]interface{}); ok {
if value, ok := v2Data[secret.Key]; ok {
if s, ok := value.(string); ok {
return s, nil
}
err := fmt.Errorf("key '%s' is not castable to string", secret.Key)
return "", err
}
err := fmt.Errorf("key '%s' not found", secret.Key)
return "", err
}
// Unreachable
return "", nil
default:
err := fmt.Errorf("unsupported vault secret type %s", secret.Type)
return "", err
}
}

// SecretGroup is used to group secret together by path, secretType and vaultPkiData.
type SecretGroup struct {
path string
secretType kedav1alpha1.VaultSecretType
vaultPkiData *kedav1alpha1.VaultPkiData
}

// fetchSecret returns the vaultSecret at a given vault path. If the secret is a pki, then the secret will use the
// vault Write method and will send the pkiData along
func (vh *HashicorpVaultHandler) fetchSecret(secretType kedav1alpha1.VaultSecretType, path string, vaultPkiData *kedav1alpha1.VaultPkiData) (*vaultapi.Secret, error) {
var vaultSecret *vaultapi.Secret
var err error
switch secretType {
case kedav1alpha1.VaultSecretTypePki:
data, err := vh.getPkiRequest(vaultPkiData)
if err != nil {
return nil, err
}
vaultSecret, err = vh.Write(path, data)
if err != nil {
return nil, err
}
case kedav1alpha1.VaultSecretTypeSecret, kedav1alpha1.VaultSecretTypeSecretV2, kedav1alpha1.VaultSecretTypeGeneric:
vaultSecret, err = vh.Read(path)
if err != nil {
return nil, err
}
default:
err = fmt.Errorf("unsupported vault secret type %s", secretType)
return nil, err
}
return vaultSecret, nil
}

// ResolveSecrets allows to resolve a slice of secrets by vault. The function returns the list of secrets with the value updated.
// If multiple secret refers to the same SecretGroup, the secret will be fetched only once.
func (vh *HashicorpVaultHandler) ResolveSecrets(secrets []kedav1alpha1.VaultSecret) ([]kedav1alpha1.VaultSecret, error) {
// Group secret by path and type, this allows to fetch a path only one. This is useful for dynamic credentials
grouped := make(map[SecretGroup][]kedav1alpha1.VaultSecret)
vaultSecrets := make(map[SecretGroup]*vaultapi.Secret)
for _, e := range secrets {
group := SecretGroup{secretType: e.Type, path: e.Path, vaultPkiData: &e.PkiData}
if _, ok := grouped[group]; !ok {
grouped[group] = make([]kedav1alpha1.VaultSecret, 0)
}
grouped[group] = append(grouped[group], e)
}
// For each group fetch the secret from vault
for group := range grouped {
vaultSecret, err := vh.fetchSecret(group.secretType, group.path, group.vaultPkiData)
if err != nil {
// could not fetch secret, skipping group
continue
}
vaultSecrets[group] = vaultSecret
}
// For each secret in each group, fetch the value and add to out
out := make([]kedav1alpha1.VaultSecret, 0)
for group, unFetchedSecrets := range grouped {
vaultSecret := vaultSecrets[group]
for _, secret := range unFetchedSecrets {
if vaultSecret == nil {
// This happens if we were not able to fetch the secret from vault
secret.Value = ""
} else {
value, err := vh.getSecretValue(&secret, vaultSecret)
if err != nil {
secret.Value = ""
} else {
secret.Value = value
}
}
out = append(out, secret)
}
}
return out, nil
}
Loading