Skip to content
Closed
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 @@ -79,6 +79,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**: Make APIService cert injections optional ([#7559](https://github.com/kedacore/keda/pull/7559))
- **AWS Scalers**: Add support for `externalId` in AssumeRole via TriggerAuthentication spec field ([#7388](https://github.com/kedacore/keda/pull/7388))
- **Elasticsearch Scaler**: Add HTTP status check for Elasticsearch errors ([#7480](https://github.com/kedacore/keda/pull/7480))
- **Kubernetes Workload Scaler**: Add `groupByNode` parameter ([#7628](https://github.com/kedacore/keda/issues/7628))

Expand Down
4 changes: 4 additions & 0 deletions apis/keda/v1alpha1/triggerauthentication_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,10 @@ type AuthPodIdentity struct {
// RoleArn sets the AWS RoleArn to be used. Mutually exclusive with IdentityOwner
RoleArn *string `json:"roleArn,omitempty"`

// +kubebuilder:validation:Optional
// ExternalID sets the AWS ExternalID to use when assuming the role specified by RoleArn
ExternalID *string `json:"externalId,omitempty"`

// +kubebuilder:validation:Enum=keda;workload
// +optional
// IdentityOwner configures which identity has to be used during auto discovery, keda or the scaled workload. Mutually exclusive with roleArn
Expand Down
5 changes: 5 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.

16 changes: 16 additions & 0 deletions config/crd/bases/keda.sh_clustertriggerauthentications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ spec:
AuthPodIdentity allows users to select the platform native identity
mechanism
properties:
externalId:
description: ExternalID sets the AWS ExternalID to use when
assuming the role specified by RoleArn
type: string
identityAuthorityHost:
description: Set identityAuthorityHost to override the default
Azure authority host. If this is set, then the IdentityTenantID
Expand Down Expand Up @@ -252,6 +256,10 @@ spec:
AuthPodIdentity allows users to select the platform native identity
mechanism
properties:
externalId:
description: ExternalID sets the AWS ExternalID to use when
assuming the role specified by RoleArn
type: string
identityAuthorityHost:
description: Set identityAuthorityHost to override the default
Azure authority host. If this is set, then the IdentityTenantID
Expand Down Expand Up @@ -392,6 +400,10 @@ spec:
AuthPodIdentity allows users to select the platform native identity
mechanism
properties:
externalId:
description: ExternalID sets the AWS ExternalID to use when
assuming the role specified by RoleArn
type: string
identityAuthorityHost:
description: Set identityAuthorityHost to override the default
Azure authority host. If this is set, then the IdentityTenantID
Expand Down Expand Up @@ -529,6 +541,10 @@ spec:
AuthPodIdentity allows users to select the platform native identity
mechanism
properties:
externalId:
description: ExternalID sets the AWS ExternalID to use when assuming
the role specified by RoleArn
type: string
identityAuthorityHost:
description: Set identityAuthorityHost to override the default
Azure authority host. If this is set, then the IdentityTenantID
Expand Down
16 changes: 16 additions & 0 deletions config/crd/bases/keda.sh_triggerauthentications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ spec:
AuthPodIdentity allows users to select the platform native identity
mechanism
properties:
externalId:
description: ExternalID sets the AWS ExternalID to use when
assuming the role specified by RoleArn
type: string
identityAuthorityHost:
description: Set identityAuthorityHost to override the default
Azure authority host. If this is set, then the IdentityTenantID
Expand Down Expand Up @@ -248,6 +252,10 @@ spec:
AuthPodIdentity allows users to select the platform native identity
mechanism
properties:
externalId:
description: ExternalID sets the AWS ExternalID to use when
assuming the role specified by RoleArn
type: string
identityAuthorityHost:
description: Set identityAuthorityHost to override the default
Azure authority host. If this is set, then the IdentityTenantID
Expand Down Expand Up @@ -388,6 +396,10 @@ spec:
AuthPodIdentity allows users to select the platform native identity
mechanism
properties:
externalId:
description: ExternalID sets the AWS ExternalID to use when
assuming the role specified by RoleArn
type: string
identityAuthorityHost:
description: Set identityAuthorityHost to override the default
Azure authority host. If this is set, then the IdentityTenantID
Expand Down Expand Up @@ -525,6 +537,10 @@ spec:
AuthPodIdentity allows users to select the platform native identity
mechanism
properties:
externalId:
description: ExternalID sets the AWS ExternalID to use when assuming
the role specified by RoleArn
type: string
identityAuthorityHost:
description: Set identityAuthorityHost to override the default
Azure authority host. If this is set, then the IdentityTenantID
Expand Down
3 changes: 2 additions & 1 deletion pkg/scalers/aws/aws_authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ limitations under the License.
package aws

type AuthorizationMetadata struct {
AwsRoleArn string
AwsRoleArn string
AwsRoleExternalID string

AwsAccessKeyID string
AwsSecretAccessKey string
Expand Down
9 changes: 8 additions & 1 deletion pkg/scalers/aws/aws_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ func GetAwsConfig(ctx context.Context, awsAuthorization AuthorizationMetadata) (

if awsAuthorization.AwsRoleArn != "" {
stsSvc := sts.NewFromConfig(cfg)
stsCredentialProvider := stscreds.NewAssumeRoleProvider(stsSvc, awsAuthorization.AwsRoleArn, func(_ *stscreds.AssumeRoleOptions) {})
stsCredentialProvider := stscreds.NewAssumeRoleProvider(stsSvc, awsAuthorization.AwsRoleArn, func(options *stscreds.AssumeRoleOptions) {
if awsAuthorization.AwsRoleExternalID != "" {
options.ExternalID = &awsAuthorization.AwsRoleExternalID
}
})
cfg.Credentials = aws.NewCredentialsCache(stsCredentialProvider)
}
return &cfg, err
Expand All @@ -86,6 +90,9 @@ func GetAwsAuthorization(uniqueKey, awsRegion string, podIdentity kedav1alpha1.A
if val, ok := authParams["awsRoleArn"]; ok && val != "" {
meta.AwsRoleArn = val
}
if val, ok := authParams["awsRoleExternalId"]; ok && val != "" {
meta.AwsRoleExternalID = val
}
return meta, nil
}

Expand Down
71 changes: 71 additions & 0 deletions pkg/scalers/aws/aws_common_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
Copyright 2024 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 aws

import (
"testing"

"github.com/stretchr/testify/assert"

kedav1alpha1 "github.com/kedacore/keda/v2/apis/keda/v1alpha1"
)

func TestGetAwsAuthorizationParsesExternalID(t *testing.T) {
authParams := map[string]string{
"awsRoleArn": "arn:aws:iam::123456789012:role/TestRole",
"awsRoleExternalId": "my-external-id",
}
podIdentity := kedav1alpha1.AuthPodIdentity{
Provider: kedav1alpha1.PodIdentityProviderAws,
}

meta, err := GetAwsAuthorization("test-key", "us-east-1", podIdentity, map[string]string{}, authParams, map[string]string{})

assert.NoError(t, err)
assert.Equal(t, "arn:aws:iam::123456789012:role/TestRole", meta.AwsRoleArn)
assert.Equal(t, "my-external-id", meta.AwsRoleExternalID)
}

func TestGetAwsAuthorizationWithEmptyExternalID(t *testing.T) {
authParams := map[string]string{
"awsRoleArn": "arn:aws:iam::123456789012:role/TestRole",
}
podIdentity := kedav1alpha1.AuthPodIdentity{
Provider: kedav1alpha1.PodIdentityProviderAws,
}

meta, err := GetAwsAuthorization("test-key", "us-east-1", podIdentity, map[string]string{}, authParams, map[string]string{})

assert.NoError(t, err)
assert.Equal(t, "arn:aws:iam::123456789012:role/TestRole", meta.AwsRoleArn)
assert.Empty(t, meta.AwsRoleExternalID)
}

func TestGetAwsAuthorizationWithOnlyExternalID(t *testing.T) {
authParams := map[string]string{
"awsRoleExternalId": "my-external-id",
}
podIdentity := kedav1alpha1.AuthPodIdentity{
Provider: kedav1alpha1.PodIdentityProviderAws,
}

meta, err := GetAwsAuthorization("test-key", "us-east-1", podIdentity, map[string]string{}, authParams, map[string]string{})

assert.NoError(t, err)
assert.Empty(t, meta.AwsRoleArn)
assert.Equal(t, "my-external-id", meta.AwsRoleExternalID)
}
9 changes: 6 additions & 3 deletions pkg/scalers/aws/aws_config_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (a *sharedConfigCache) getCacheKey(awsAuthorization AuthorizationMetadata)
if awsAuthorization.AwsAccessKeyID != "" {
key = fmt.Sprintf("%s-%s-%s-%s", awsAuthorization.AwsAccessKeyID, awsAuthorization.AwsSecretAccessKey, awsAuthorization.AwsSessionToken, awsAuthorization.AwsRegion)
} else if awsAuthorization.AwsRoleArn != "" {
key = fmt.Sprintf("%s-%s", awsAuthorization.AwsRoleArn, awsAuthorization.AwsRegion)
key = fmt.Sprintf("%s-%s-%s", awsAuthorization.AwsRoleArn, awsAuthorization.AwsRoleExternalID, awsAuthorization.AwsRegion)
}
// to avoid sensitive data as key and to use a constant key size,
// we hash the key with sha3
Expand Down Expand Up @@ -105,7 +105,7 @@ func (a *sharedConfigCache) GetCredentials(ctx context.Context, awsAuthorization

if awsAuthorization.UsingPodIdentity {
if awsAuthorization.AwsRoleArn != "" {
cfg.Credentials = a.retrievePodIdentityCredentials(ctx, cfg, awsAuthorization.AwsRoleArn)
cfg.Credentials = a.retrievePodIdentityCredentials(ctx, cfg, awsAuthorization.AwsRoleArn, awsAuthorization.AwsRoleExternalID)
}
} else {
cfg.Credentials = a.retrieveStaticCredentials(awsAuthorization)
Expand Down Expand Up @@ -145,7 +145,7 @@ func (a *sharedConfigCache) RemoveCachedEntry(awsAuthorization AuthorizationMeta
// retrievePodIdentityCredentials returns an *aws.CredentialsCache to assume given roleArn.
// It tries first to assume the role using WebIdentity (OIDC federation) and if this method fails,
// it tries to assume the role using KEDA's role (AssumeRole)
func (a *sharedConfigCache) retrievePodIdentityCredentials(ctx context.Context, cfg aws.Config, roleArn string) *aws.CredentialsCache {
func (a *sharedConfigCache) retrievePodIdentityCredentials(ctx context.Context, cfg aws.Config, roleArn, roleExternalID string) *aws.CredentialsCache {
stsSvc := sts.NewFromConfig(cfg)

if webIdentityTokenFile != "" {
Expand All @@ -167,6 +167,9 @@ func (a *sharedConfigCache) retrievePodIdentityCredentials(ctx context.Context,
a.logger.V(1).Info(fmt.Sprintf("using assume role to retrieve token for arnRole %s", roleArn))
assumeRoleCredentialProvider := stscreds.NewAssumeRoleProvider(stsSvc, roleArn, func(options *stscreds.AssumeRoleOptions) {
options.RoleSessionName = "KEDA"
if roleExternalID != "" {
options.ExternalID = &roleExternalID
}
})
return aws.NewCredentialsCache(assumeRoleCredentialProvider)
}
Expand Down
43 changes: 43 additions & 0 deletions pkg/scalers/aws/aws_config_cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,46 @@ func TestCredentialsShouldBeCachedPerRegion(t *testing.T) {
assert.NoError(t, err2)
assert.NotEqual(t, cred1, cred2, "Credentials should be stored per region")
}

func TestCredentialsShouldBeCachedPerExternalID(t *testing.T) {
cache := newSharedConfigsCache()
cache.logger = logr.Discard()
awsAuthorization1 := AuthorizationMetadata{
TriggerUniqueKey: "test5-key",
AwsRegion: "test5-region",
AwsRoleArn: "arn:aws:iam::123456789012:role/TestRole",
AwsRoleExternalID: "external-id-1",
}
awsAuthorization2 := AuthorizationMetadata{
TriggerUniqueKey: "test5-key",
AwsRegion: "test5-region",
AwsRoleArn: "arn:aws:iam::123456789012:role/TestRole",
AwsRoleExternalID: "external-id-2",
}
cacheKey1 := cache.getCacheKey(awsAuthorization1)
cacheKey2 := cache.getCacheKey(awsAuthorization2)

assert.NotEqual(t, cacheKey1, cacheKey2, "Cache keys should be different for different external IDs")
}

func TestCacheKeyIncludesExternalID(t *testing.T) {
cache := newSharedConfigsCache()
cache.logger = logr.Discard()

authWithExternalID := AuthorizationMetadata{
TriggerUniqueKey: "test6-key",
AwsRegion: "us-east-1",
AwsRoleArn: "arn:aws:iam::123456789012:role/TestRole",
AwsRoleExternalID: "my-external-id",
}
authWithoutExternalID := AuthorizationMetadata{
TriggerUniqueKey: "test6-key",
AwsRegion: "us-east-1",
AwsRoleArn: "arn:aws:iam::123456789012:role/TestRole",
}

keyWithExternalID := cache.getCacheKey(authWithExternalID)
keyWithoutExternalID := cache.getCacheKey(authWithoutExternalID)

assert.NotEqual(t, keyWithExternalID, keyWithoutExternalID, "Cache key should include external ID")
}
3 changes: 3 additions & 0 deletions pkg/scaling/resolver/scale_resolvers.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ func ResolveAuthRefAndPodIdentity(ctx context.Context, client client.Client, log
fmt.Errorf("roleArn can't be set if KEDA isn't identity owner, current value: '%s'", *podIdentity.IdentityOwner)
}
authParams["awsRoleArn"] = *podIdentity.RoleArn
if podIdentity.ExternalID != nil {
authParams["awsRoleExternalId"] = *podIdentity.ExternalID
}
}
if podIdentity.IsWorkloadIdentityOwner() {
value, err := resolveServiceAccountAnnotation(ctx, client, podTemplateSpec.Spec.ServiceAccountName, namespace, kedav1alpha1.PodIdentityAnnotationEKS, true)
Expand Down
Loading