Skip to content

Commit

Permalink
feat: allow vault secret to handle write operation (#5068)
Browse files Browse the repository at this point in the history
Signed-off-by: Loïs Postula <lois@postu.la>
  • Loading branch information
loispostula authored Oct 13, 2023
1 parent 7601743 commit 0462144
Show file tree
Hide file tree
Showing 12 changed files with 1,119 additions and 154 deletions.
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))
- **Pulsar Scaler**: support endpointParams in pulsar oauth ([#5069](https://github.com/kedacore/keda/issues/5069))
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) {
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

0 comments on commit 0462144

Please sign in to comment.