diff --git a/CHANGELOG.md b/CHANGELOG.md index 408e747563e..ac33837e423 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,6 +78,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio - **General**: Add CRD-level validation markers (Minimum, MinLength, MinItems, Enum) for ScaledObject, ScaledJob, ScaleTriggers, and TriggerAuthentication API types ([#7533](https://github.com/kedacore/keda/pull/7533)) - **General**: Add `--leader-election-id` flag to allow configuring the leader election Lease name ([#7564](https://github.com/kedacore/keda/issues/7564)) +- **General**: Allow Hashicorp Vault token authentication to read `credential.tokenFrom.secretKeyRef` from Kubernetes Secrets ([#6026](https://github.com/kedacore/keda/issues/6026)) - **General**: Allow more control of TLS versions & ciphers via `KEDA_HTTP_TLS_CIPHER_LIST`, `KEDA_SERVICE_TLS_CIPHER_LIST` and `KEDA_SERVICE_MIN_TLS_VERSION` env vars ([#7617](https://github.com/kedacore/keda/pull/7617)) - **General**: Make APIService cert injections optional ([#7559](https://github.com/kedacore/keda/pull/7559)) - **AWS Scalers**: Add support for AWS External ID in TriggerAuthentication podIdentity for all AWS scalers (SQS, Kinesis, DynamoDB, CloudWatch, etc.) to enable cross-account access scenarios ([#6921](https://github.com/kedacore/keda/issues/6921)) @@ -91,6 +92,7 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio - **General**: Check updated status for Fallback condition instead of ScaledObject ([#7488](https://github.com/kedacore/keda/issues/7488)) - **General**: Fix int64 overflow in milli-quantity conversion for very large metric values ([#7441](https://github.com/kedacore/keda/issues/7441)) +- **General**: Fix nil reference panic in Hashicorp Vault token authentication when no credentials are configured ([#6026](https://github.com/kedacore/keda/issues/6026)) - **General**: Fix ScaledObject admission webhook to return validation error from `verifyReplicaCount`, preventing invalid ScaledObjects from being created ([#5954](https://github.com/kedacore/keda/issues/5954)) - **General**: Handle paused scaling directly in reconciler ([#7663](https://github.com/kedacore/keda/issues/7663)) - **Azure Data Explorer Scaler**: Remove clientSecretFromEnv support ([#7554](https://github.com/kedacore/keda/pull/7554)) @@ -121,7 +123,7 @@ You can find all deprecations in [this overview](https://github.com/kedacore/ked New deprecation(s): -- TODO ([#XXX](https://github.com/kedacore/keda/issues/XXX)) +- **General**: Deprecate plain-text `spec.hashiCorpVault.credential.token` in favor of `spec.hashiCorpVault.credential.tokenFrom.secretKeyRef` ([#6026](https://github.com/kedacore/keda/issues/6026)) ### Breaking Changes diff --git a/apis/keda/v1alpha1/triggerauthentication_types.go b/apis/keda/v1alpha1/triggerauthentication_types.go index a82636b261e..dee06931e5b 100644 --- a/apis/keda/v1alpha1/triggerauthentication_types.go +++ b/apis/keda/v1alpha1/triggerauthentication_types.go @@ -248,6 +248,9 @@ type Credential struct { // +optional Token string `json:"token,omitempty"` + // +optional + TokenFrom *ValueFromSecret `json:"tokenFrom,omitempty"` + // +optional ServiceAccount string `json:"serviceAccount,omitempty"` diff --git a/apis/keda/v1alpha1/triggerauthentication_types_test.go b/apis/keda/v1alpha1/triggerauthentication_types_test.go index 5b63187cfa0..2fcd3e43435 100644 --- a/apis/keda/v1alpha1/triggerauthentication_types_test.go +++ b/apis/keda/v1alpha1/triggerauthentication_types_test.go @@ -37,3 +37,37 @@ func TestTriggerAuthenticationSpec_WithFilePath(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "/mnt/auth/creds.json", unmarshaled.FilePath) } + +func TestTriggerAuthenticationSpec_WithHashiCorpVaultTokenFrom(t *testing.T) { + spec := TriggerAuthenticationSpec{ + HashiCorpVault: &HashiCorpVault{ + Address: "http://vault.example.com", + Authentication: VaultAuthenticationToken, + Credential: &Credential{ + TokenFrom: &ValueFromSecret{ + SecretKeyRef: SecretKeyRef{ + Name: "vault-token", + Key: "token", + }, + }, + }, + Secrets: []VaultSecret{{ + Parameter: "connection", + Path: "secret/data/app", + Key: "connectionString", + }}, + }, + } + + data, err := json.Marshal(spec) + assert.NoError(t, err) + assert.Contains(t, string(data), `"tokenFrom":{"secretKeyRef":{"name":"vault-token","key":"token"}}`) + + var unmarshaled TriggerAuthenticationSpec + err = json.Unmarshal(data, &unmarshaled) + assert.NoError(t, err) + if assert.NotNil(t, unmarshaled.HashiCorpVault) && assert.NotNil(t, unmarshaled.HashiCorpVault.Credential) && assert.NotNil(t, unmarshaled.HashiCorpVault.Credential.TokenFrom) { + assert.Equal(t, "vault-token", unmarshaled.HashiCorpVault.Credential.TokenFrom.SecretKeyRef.Name) + assert.Equal(t, "token", unmarshaled.HashiCorpVault.Credential.TokenFrom.SecretKeyRef.Key) + } +} diff --git a/apis/keda/v1alpha1/triggerauthentication_webhook.go b/apis/keda/v1alpha1/triggerauthentication_webhook.go index 55ac5ff2e50..50c7d2c57ca 100644 --- a/apis/keda/v1alpha1/triggerauthentication_webhook.go +++ b/apis/keda/v1alpha1/triggerauthentication_webhook.go @@ -177,23 +177,34 @@ func isTriggerAuthenticationRemovingFinalizer(om metav1.ObjectMeta, oldOm metav1 } func validateSpec(spec *TriggerAuthenticationSpec) (admission.Warnings, error) { + var warnings admission.Warnings + + if spec.HashiCorpVault != nil && spec.HashiCorpVault.Credential != nil { + if spec.HashiCorpVault.Credential.Token != "" { + warnings = append(warnings, "spec.hashiCorpVault.credential.token is deprecated; use spec.hashiCorpVault.credential.tokenFrom.secretKeyRef instead") + } + if spec.HashiCorpVault.Credential.Token != "" && spec.HashiCorpVault.Credential.TokenFrom != nil { + warnings = append(warnings, "spec.hashiCorpVault.credential.tokenFrom.secretKeyRef takes precedence over spec.hashiCorpVault.credential.token") + } + } + if spec.PodIdentity != nil { switch spec.PodIdentity.Provider { case PodIdentityProviderAzureWorkload: if spec.PodIdentity.IdentityID != nil && *spec.PodIdentity.IdentityID == "" { - return nil, fmt.Errorf("identityId of PodIdentity should not be empty. If it's set, identityId has to be different than \"\"") + return warnings, fmt.Errorf("identityId of PodIdentity should not be empty. If it's set, identityId has to be different than \"\"") } if spec.PodIdentity.IdentityAuthorityHost != nil && *spec.PodIdentity.IdentityAuthorityHost != "" { if spec.PodIdentity.IdentityTenantID == nil || *spec.PodIdentity.IdentityTenantID == "" { - return nil, fmt.Errorf("identityTenantID of PodIdentity should not be nil or empty when identityAuthorityHost of PodIdentity is set") + return warnings, fmt.Errorf("identityTenantID of PodIdentity should not be nil or empty when identityAuthorityHost of PodIdentity is set") } } else if spec.PodIdentity.IdentityTenantID != nil && *spec.PodIdentity.IdentityTenantID == "" { - return nil, fmt.Errorf("identityTenantId of PodIdentity should not be empty. If it's set, identityTenantId has to be different than \"\"") + return warnings, fmt.Errorf("identityTenantId of PodIdentity should not be empty. If it's set, identityTenantId has to be different than \"\"") } case PodIdentityProviderAws: if spec.PodIdentity.RoleArn != nil && *spec.PodIdentity.RoleArn != "" && spec.PodIdentity.IsWorkloadIdentityOwner() { - return nil, fmt.Errorf("roleArn of PodIdentity can't be set if KEDA isn't identityOwner") + return warnings, fmt.Errorf("roleArn of PodIdentity can't be set if KEDA isn't identityOwner") } if spec.PodIdentity.ExternalID != nil && *spec.PodIdentity.ExternalID != "" { if spec.PodIdentity.RoleArn == nil || *spec.PodIdentity.RoleArn == "" { @@ -201,8 +212,8 @@ func validateSpec(spec *TriggerAuthenticationSpec) (admission.Warnings, error) { } } default: - return nil, nil + return warnings, nil } } - return nil, nil + return warnings, nil } diff --git a/apis/keda/v1alpha1/triggerauthentication_webhook_unit_test.go b/apis/keda/v1alpha1/triggerauthentication_webhook_unit_test.go new file mode 100644 index 00000000000..cb3f5454e1a --- /dev/null +++ b/apis/keda/v1alpha1/triggerauthentication_webhook_unit_test.go @@ -0,0 +1,89 @@ +/* +Copyright 2026 The KEDA 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 v1alpha1 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestValidateSpecHashiCorpVaultTokenWarnings(t *testing.T) { + tests := []struct { + name string + spec TriggerAuthenticationSpec + expected []string + }{ + { + name: "plain text token warning", + spec: TriggerAuthenticationSpec{ + HashiCorpVault: &HashiCorpVault{ + Credential: &Credential{ + Token: "legacy-token", + }, + }, + }, + expected: []string{ + "spec.hashiCorpVault.credential.token is deprecated; use spec.hashiCorpVault.credential.tokenFrom.secretKeyRef instead", + }, + }, + { + name: "tokenFrom precedence warning", + spec: TriggerAuthenticationSpec{ + HashiCorpVault: &HashiCorpVault{ + Credential: &Credential{ + Token: "legacy-token", + TokenFrom: &ValueFromSecret{ + SecretKeyRef: SecretKeyRef{ + Name: "vault-token", + Key: "token", + }, + }, + }, + }, + }, + expected: []string{ + "spec.hashiCorpVault.credential.token is deprecated; use spec.hashiCorpVault.credential.tokenFrom.secretKeyRef instead", + "spec.hashiCorpVault.credential.tokenFrom.secretKeyRef takes precedence over spec.hashiCorpVault.credential.token", + }, + }, + { + name: "tokenFrom only has no warnings", + spec: TriggerAuthenticationSpec{ + HashiCorpVault: &HashiCorpVault{ + Credential: &Credential{ + TokenFrom: &ValueFromSecret{ + SecretKeyRef: SecretKeyRef{ + Name: "vault-token", + Key: "token", + }, + }, + }, + }, + }, + expected: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + warnings, err := validateSpec(&test.spec) + assert.NoError(t, err) + assert.Equal(t, test.expected, []string(warnings)) + }) + } +} diff --git a/apis/keda/v1alpha1/zz_generated.deepcopy.go b/apis/keda/v1alpha1/zz_generated.deepcopy.go index f2835f7affd..ff8e3f0e9a4 100755 --- a/apis/keda/v1alpha1/zz_generated.deepcopy.go +++ b/apis/keda/v1alpha1/zz_generated.deepcopy.go @@ -480,6 +480,11 @@ func (in Conditions) DeepCopy() Conditions { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Credential) DeepCopyInto(out *Credential) { *out = *in + if in.TokenFrom != nil { + in, out := &in.TokenFrom, &out.TokenFrom + *out = new(ValueFromSecret) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Credential. @@ -610,7 +615,7 @@ func (in *HashiCorpVault) DeepCopyInto(out *HashiCorpVault) { if in.Credential != nil { in, out := &in.Credential, &out.Credential *out = new(Credential) - **out = **in + (*in).DeepCopyInto(*out) } } diff --git a/config/crd/bases/keda.sh_clustertriggerauthentications.yaml b/config/crd/bases/keda.sh_clustertriggerauthentications.yaml index 13f7c1f901b..192dc1222e9 100644 --- a/config/crd/bases/keda.sh_clustertriggerauthentications.yaml +++ b/config/crd/bases/keda.sh_clustertriggerauthentications.yaml @@ -484,6 +484,21 @@ spec: type: string token: type: string + tokenFrom: + properties: + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + required: + - secretKeyRef + type: object type: object mount: type: string diff --git a/config/crd/bases/keda.sh_triggerauthentications.yaml b/config/crd/bases/keda.sh_triggerauthentications.yaml index 1cbb2d56651..ade4f708312 100644 --- a/config/crd/bases/keda.sh_triggerauthentications.yaml +++ b/config/crd/bases/keda.sh_triggerauthentications.yaml @@ -480,6 +480,21 @@ spec: type: string token: type: string + tokenFrom: + properties: + secretKeyRef: + properties: + key: + type: string + name: + type: string + required: + - key + - name + type: object + required: + - secretKeyRef + type: object type: object mount: type: string diff --git a/pkg/scaling/resolver/hashicorpvault_handler.go b/pkg/scaling/resolver/hashicorpvault_handler.go index d4b8b3d6daf..b44e60444f5 100644 --- a/pkg/scaling/resolver/hashicorpvault_handler.go +++ b/pkg/scaling/resolver/hashicorpvault_handler.go @@ -105,10 +105,10 @@ func (vh *HashicorpVaultHandler) token(client *vaultapi.Client) (string, error) switch { case len(client.Token()) > 0: break - case len(vh.vault.Credential.Token) > 0: + case vh.vault.Credential != nil && len(vh.vault.Credential.Token) > 0: token = vh.vault.Credential.Token default: - return token, errors.New("could not get Vault token") + return token, errors.New("could not get Vault token from VAULT_TOKEN env variable, credential.tokenFrom.secretKeyRef, or credential.token") } case kedav1alpha1.VaultAuthenticationKubernetes: if len(vh.vault.Mount) == 0 { diff --git a/pkg/scaling/resolver/hashicorpvault_handler_test.go b/pkg/scaling/resolver/hashicorpvault_handler_test.go index 9bd5b6bec7d..6f7de549023 100644 --- a/pkg/scaling/resolver/hashicorpvault_handler_test.go +++ b/pkg/scaling/resolver/hashicorpvault_handler_test.go @@ -767,3 +767,26 @@ func TestHashicorpVaultHandler_Token_ServiceAccountAuth(t *testing.T) { assert.NoError(t, err) assert.NotEmpty(t, token) } + +func TestHashicorpVaultHandler_Token_VaultTokenAuth_NoCredential(t *testing.T) { + ctrl := gomock.NewController(t) + mockCoreV1Interface := mock_serviceaccounts.NewMockCoreV1Interface(ctrl) + mockSecretLister := mock_secretlister.NewMockSecretLister(ctrl) + authClientSet := &authentication.AuthClientSet{ + CoreV1Interface: mockCoreV1Interface, + SecretLister: mockSecretLister, + } + + vault := kedav1alpha1.HashiCorpVault{ + Authentication: kedav1alpha1.VaultAuthenticationToken, + } + + vaultHandler := NewHashicorpVaultHandler(&vault, authClientSet, "default") + config := vaultapi.DefaultConfig() + client, err := vaultapi.NewClient(config) + assert.NoError(t, err) + + token, err := vaultHandler.token(client) + assert.Empty(t, token) + assert.EqualError(t, err, "could not get Vault token from VAULT_TOKEN env variable, credential.tokenFrom.secretKeyRef, or credential.token") +} diff --git a/pkg/scaling/resolver/scale_resolvers.go b/pkg/scaling/resolver/scale_resolvers.go index ab74b7735d9..7e1c3167310 100644 --- a/pkg/scaling/resolver/scale_resolvers.go +++ b/pkg/scaling/resolver/scale_resolvers.go @@ -248,6 +248,23 @@ func ResolveAuthRefAndPodIdentity(ctx context.Context, client client.Client, log return resolveAuthRef(ctx, client, logger, triggerAuthRef, nil, namespace, authClientSet) } +func resolveHashicorpVaultCredential(ctx context.Context, client client.Client, logger logr.Logger, + vault *kedav1alpha1.HashiCorpVault, namespace string, secretsLister corev1listers.SecretLister, +) error { + if vault == nil || vault.Authentication != kedav1alpha1.VaultAuthenticationToken || vault.Credential == nil || vault.Credential.TokenFrom == nil { + return nil + } + + secretKeyRef := vault.Credential.TokenFrom.SecretKeyRef + token := resolveAuthSecret(ctx, client, logger, secretKeyRef.Name, namespace, secretKeyRef.Key, secretsLister) + if token == "" { + return fmt.Errorf("error reading hashiCorpVault token from secret %s/%s key %s", namespace, secretKeyRef.Name, secretKeyRef.Key) + } + + vault.Credential.Token = token + return nil +} + // resolveAuthRef provides authentication parameters needed authenticate scaler with the environment. // based on authentication method defined in TriggerAuthentication, authParams and podIdentity is returned func resolveAuthRef(ctx context.Context, client client.Client, logger logr.Logger, @@ -304,6 +321,16 @@ func resolveAuthRef(ctx context.Context, client client.Client, logger logr.Logge } } if triggerAuthSpec.HashiCorpVault != nil && len(triggerAuthSpec.HashiCorpVault.Secrets) > 0 { + if err := resolveHashicorpVaultCredential(ctx, client, logger, triggerAuthSpec.HashiCorpVault, triggerNamespace, authClientSet.SecretLister); err != nil { + return result, podIdentity, err + } + + if triggerAuthSpec.HashiCorpVault.Credential != nil && + triggerAuthSpec.HashiCorpVault.Credential.Token != "" && + triggerAuthSpec.HashiCorpVault.Credential.TokenFrom == nil { + logger.Info("WARNING: spec.hashiCorpVault.credential.token is deprecated and will be removed in KEDA v3. Use spec.hashiCorpVault.credential.tokenFrom.secretKeyRef instead") + } + vault := NewHashicorpVaultHandler(triggerAuthSpec.HashiCorpVault, authClientSet, namespace) err := vault.Initialize(logger) defer vault.Stop() diff --git a/pkg/scaling/resolver/scale_resolvers_test.go b/pkg/scaling/resolver/scale_resolvers_test.go index 5176a49fc12..77e9f14d9e9 100644 --- a/pkg/scaling/resolver/scale_resolvers_test.go +++ b/pkg/scaling/resolver/scale_resolvers_test.go @@ -48,6 +48,9 @@ var ( secretName = "supersecret" secretKey = "mysecretkey" secretData = "secretDataHere" + vaultTokenSecretName = "vault-token" + vaultTokenSecretKey = "token" + vaultTokenValue = "vault-token-value" cmName = "supercm" cmKey = "mycmkey" cmData = "cmDataHere" @@ -263,6 +266,7 @@ func TestResolveAuthRef(t *testing.T) { expected map[string]string expectedPodIdentity kedav1alpha1.AuthPodIdentity isError bool + errorContains string comment string }{ { @@ -377,6 +381,133 @@ func TestResolveAuthRef(t *testing.T) { expected: map[string]string{}, expectedPodIdentity: kedav1alpha1.AuthPodIdentity{Provider: kedav1alpha1.PodIdentityProviderNone}, }, + { + name: "triggerauth resolves hashicorp vault token from secret", + existing: []runtime.Object{ + &kedav1alpha1.TriggerAuthentication{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: triggerAuthenticationName, + }, + Spec: kedav1alpha1.TriggerAuthenticationSpec{ + HashiCorpVault: &kedav1alpha1.HashiCorpVault{ + Address: "invalid-vault-address", + Authentication: kedav1alpha1.VaultAuthenticationToken, + Credential: &kedav1alpha1.Credential{ + TokenFrom: &kedav1alpha1.ValueFromSecret{ + SecretKeyRef: kedav1alpha1.SecretKeyRef{ + Name: vaultTokenSecretName, + Key: vaultTokenSecretKey, + }, + }, + }, + Secrets: []kedav1alpha1.VaultSecret{ + { + Key: "password", + Parameter: "password", + Path: "secret_v2/data/my-password-path", + }, + }, + }, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: vaultTokenSecretName, + }, + Data: map[string][]byte{vaultTokenSecretKey: []byte(vaultTokenValue)}, + }, + }, + soar: &kedav1alpha1.AuthenticationRef{Name: triggerAuthenticationName}, + expected: map[string]string{}, + expectedPodIdentity: kedav1alpha1.AuthPodIdentity{Provider: kedav1alpha1.PodIdentityProviderNone}, + isError: true, + errorContains: "\"invalid-vault-address/v1/auth/token/lookup-self\": unsupported protocol scheme \"\"", + comment: "hashicorp vault tokenFrom secret should resolve before initialization", + }, + { + name: "clustertriggerauth resolves hashicorp vault token from cluster secret namespace", + existing: []runtime.Object{ + &kedav1alpha1.ClusterTriggerAuthentication{ + ObjectMeta: metav1.ObjectMeta{ + Name: triggerAuthenticationName, + }, + Spec: kedav1alpha1.TriggerAuthenticationSpec{ + HashiCorpVault: &kedav1alpha1.HashiCorpVault{ + Address: "invalid-vault-address", + Authentication: kedav1alpha1.VaultAuthenticationToken, + Credential: &kedav1alpha1.Credential{ + TokenFrom: &kedav1alpha1.ValueFromSecret{ + SecretKeyRef: kedav1alpha1.SecretKeyRef{ + Name: vaultTokenSecretName, + Key: vaultTokenSecretKey, + }, + }, + }, + Secrets: []kedav1alpha1.VaultSecret{ + { + Key: "password", + Parameter: "password", + Path: "secret_v2/data/my-password-path", + }, + }, + }, + }, + }, + &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: clusterNamespace, + Name: vaultTokenSecretName, + }, + Data: map[string][]byte{vaultTokenSecretKey: []byte(vaultTokenValue)}, + }, + }, + soar: &kedav1alpha1.AuthenticationRef{Name: triggerAuthenticationName, Kind: "ClusterTriggerAuthentication"}, + expected: map[string]string{}, + expectedPodIdentity: kedav1alpha1.AuthPodIdentity{Provider: kedav1alpha1.PodIdentityProviderNone}, + isError: true, + errorContains: "\"invalid-vault-address/v1/auth/token/lookup-self\": unsupported protocol scheme \"\"", + comment: "cluster triggerauthentication should resolve hashiCorpVault tokenFrom from the cluster object namespace", + }, + { + name: "triggerauth fails when hashicorp vault token secret is missing", + existing: []runtime.Object{ + &kedav1alpha1.TriggerAuthentication{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: triggerAuthenticationName, + }, + Spec: kedav1alpha1.TriggerAuthenticationSpec{ + HashiCorpVault: &kedav1alpha1.HashiCorpVault{ + Address: "invalid-vault-address", + Authentication: kedav1alpha1.VaultAuthenticationToken, + Credential: &kedav1alpha1.Credential{ + TokenFrom: &kedav1alpha1.ValueFromSecret{ + SecretKeyRef: kedav1alpha1.SecretKeyRef{ + Name: vaultTokenSecretName, + Key: vaultTokenSecretKey, + }, + }, + }, + Secrets: []kedav1alpha1.VaultSecret{ + { + Key: "password", + Parameter: "password", + Path: "secret_v2/data/my-password-path", + }, + }, + }, + }, + }, + }, + soar: &kedav1alpha1.AuthenticationRef{Name: triggerAuthenticationName}, + expected: map[string]string{}, + expectedPodIdentity: kedav1alpha1.AuthPodIdentity{Provider: kedav1alpha1.PodIdentityProviderNone}, + isError: true, + errorContains: "error reading hashiCorpVault token from secret test-namespace/vault-token key token", + comment: "missing hashiCorpVault token secret should fail fast", + }, { name: "triggerauth exists and config map", existing: []runtime.Object{ @@ -746,6 +877,13 @@ func TestResolveAuthRef(t *testing.T) { if test.isError && err == nil { t.Errorf("Expected error because %s but got success, %#v", test.comment, test) } + if test.errorContains != "" { + if err == nil { + t.Errorf("Expected error containing %q because %s but got success", test.errorContains, test.comment) + } else if !assert.Contains(t, err.Error(), test.errorContains) { + t.Errorf("Unexpected error because %s, expected to contain %q but got %q", test.comment, test.errorContains, err.Error()) + } + } if diff := cmp.Diff(gotMap, test.expected); diff != "" { t.Errorf("Returned authParams are different: %s", diff) diff --git a/tests/secret-providers/hashicorp_vault/hashicorp_vault_test.go b/tests/secret-providers/hashicorp_vault/hashicorp_vault_test.go index fe672b2fb68..a57b5b7981d 100644 --- a/tests/secret-providers/hashicorp_vault/hashicorp_vault_test.go +++ b/tests/secret-providers/hashicorp_vault/hashicorp_vault_test.go @@ -43,6 +43,7 @@ var ( postgreSQLDatabase = "test_db" postgreSQLConnectionString = fmt.Sprintf("postgresql://%s:%s@postgresql.%s.svc.cluster.local:5432/%s?sslmode=disable", postgreSQLUsername, postgreSQLPassword, testNamespace, postgreSQLDatabase) + vaultTokenSecretName = fmt.Sprintf("%s-vault-token", testName) prometheusServerName = fmt.Sprintf("%s-prom-server", testName) minReplicaCount = 0 maxReplicaCount = 1 @@ -57,6 +58,7 @@ type templateData struct { ScaledObjectName string TriggerAuthenticationName string VaultSecretPath string + VaultTokenSecretName string VaultPromDomain string SecretName string HashiCorpAuthentication string @@ -125,6 +127,17 @@ data: postgresql_conn_str: {{.PostgreSQLConnectionStringBase64}} ` + vaultTokenSecretTemplate = ` +apiVersion: v1 +kind: Secret +metadata: + name: {{.VaultTokenSecretName}} + namespace: {{.TestNamespace}} +type: Opaque +stringData: + token: "{{.HashiCorpToken}}" +` + triggerAuthenticationTemplate = ` apiVersion: keda.sh/v1alpha1 kind: TriggerAuthentication @@ -137,9 +150,18 @@ spec: authentication: {{.HashiCorpAuthentication}} role: {{.VaultRole}} mount: kubernetes +{{- if or (eq .HashiCorpAuthentication "token") .VaultServiceAccountName }} credential: - token: {{.HashiCorpToken}} +{{- if eq .HashiCorpAuthentication "token" }} + tokenFrom: + secretKeyRef: + name: {{.VaultTokenSecretName}} + key: token +{{- end }} +{{- if .VaultServiceAccountName }} serviceAccountName: {{.VaultServiceAccountName}} +{{- end }} +{{- end }} secrets: - parameter: connection key: connectionString @@ -350,8 +372,14 @@ spec: role: keda mount: kubernetes credential: - token: {{.HashiCorpToken}} +{{- if eq .HashiCorpAuthentication "token" }} + tokenFrom: + secretKeyRef: + name: {{.VaultTokenSecretName}} + key: token +{{- else }} serviceAccount: /var/run/secrets/kubernetes.io/serviceaccount/token +{{- end }} secrets: - key: "ca_chain" parameter: "ca" @@ -723,6 +751,7 @@ var data = templateData{ MaxReplicaCount: maxReplicaCount, TriggerAuthenticationName: triggerAuthenticationName, SecretName: secretName, + VaultTokenSecretName: vaultTokenSecretName, PostgreSQLUsername: postgreSQLUsername, PostgreSQLPassword: postgreSQLPassword, PostgreSQLDatabase: postgreSQLDatabase, @@ -746,6 +775,7 @@ func getPostgreSQLTemplateData() (templateData, []Template) { func getPrometheusTemplateData() (templateData, []Template) { return data, []Template{ + {Name: "vaultTokenSecretTemplate", Config: vaultTokenSecretTemplate}, {Name: "triggerAuthenticationTemplate", Config: prometheusTriggerAuthenticationTemplate}, {Name: "deploymentTemplate", Config: prometheusDeploymentTemplate}, {Name: "monitoredAppDeploymentTemplate", Config: monitoredAppDeploymentTemplate}, @@ -757,6 +787,7 @@ func getPrometheusTemplateData() (templateData, []Template) { func getTemplateData() (templateData, []Template) { return data, []Template{ {Name: "secretTemplate", Config: secretTemplate}, + {Name: "vaultTokenSecretTemplate", Config: vaultTokenSecretTemplate}, {Name: "deploymentTemplate", Config: deploymentTemplate}, {Name: "triggerAuthenticationTemplate", Config: triggerAuthenticationTemplate}, {Name: "scaledObjectTemplate", Config: scaledObjectTemplate},