Skip to content
Open
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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))
Expand Down Expand Up @@ -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

Expand Down
3 changes: 3 additions & 0 deletions apis/keda/v1alpha1/triggerauthentication_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down
34 changes: 34 additions & 0 deletions apis/keda/v1alpha1/triggerauthentication_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
23 changes: 17 additions & 6 deletions apis/keda/v1alpha1/triggerauthentication_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,32 +177,43 @@ 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 == "" {
return nil, fmt.Errorf("externalID of PodIdentity requires roleArn to be set")
}
}
default:
return nil, nil
return warnings, nil
}
}
return nil, nil
return warnings, nil
}
89 changes: 89 additions & 0 deletions apis/keda/v1alpha1/triggerauthentication_webhook_unit_test.go
Original file line number Diff line number Diff line change
@@ -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))
})
}
}
7 changes: 6 additions & 1 deletion apis/keda/v1alpha1/zz_generated.deepcopy.go

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

15 changes: 15 additions & 0 deletions config/crd/bases/keda.sh_clustertriggerauthentications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions config/crd/bases/keda.sh_triggerauthentications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions pkg/scaling/resolver/hashicorpvault_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
23 changes: 23 additions & 0 deletions pkg/scaling/resolver/hashicorpvault_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
27 changes: 27 additions & 0 deletions pkg/scaling/resolver/scale_resolvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
Loading
Loading