Skip to content

Commit

Permalink
Azure AD Workload Identity support for Azure Service Bus scaler.
Browse files Browse the repository at this point in the history
Signed-off-by: Vighnesh Shenoy <[email protected]>
  • Loading branch information
v-shenoy committed Apr 13, 2022
1 parent 9f0ee58 commit eff0e2d
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 32 deletions.
13 changes: 7 additions & 6 deletions apis/keda/v1alpha1/triggerauthentication_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,13 @@ type PodIdentityProvider string
// PodIdentityProviderNone specifies the default state when there is no Identity Provider
// PodIdentityProvider<IDENTITY_PROVIDER> specifies other available Identity providers
const (
PodIdentityProviderNone PodIdentityProvider = "none"
PodIdentityProviderAzure PodIdentityProvider = "azure"
PodIdentityProviderGCP PodIdentityProvider = "gcp"
PodIdentityProviderSpiffe PodIdentityProvider = "spiffe"
PodIdentityProviderAwsEKS PodIdentityProvider = "aws-eks"
PodIdentityProviderAwsKiam PodIdentityProvider = "aws-kiam"
PodIdentityProviderNone PodIdentityProvider = "none"
PodIdentityProviderAzure PodIdentityProvider = "azure"
PodIdentityProviderAzureWorkload PodIdentityProvider = "azure-workload"
PodIdentityProviderGCP PodIdentityProvider = "gcp"
PodIdentityProviderSpiffe PodIdentityProvider = "spiffe"
PodIdentityProviderAwsEKS PodIdentityProvider = "aws-eks"
PodIdentityProviderAwsKiam PodIdentityProvider = "aws-kiam"
)

// PodIdentityAnnotationEKS specifies aws role arn for aws-eks Identity Provider
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/Azure/azure-storage-queue-go v0.0.0-20191125232315-636801874cdd
github.com/Azure/go-autorest/autorest v0.11.24
github.com/Azure/go-autorest/autorest/azure/auth v0.5.11
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0
github.com/DataDog/datadog-api-client-go v1.8.0
github.com/Huawei/gophercloud v1.0.21
github.com/Shopify/sarama v1.31.1
Expand All @@ -28,6 +29,7 @@ require (
github.com/go-playground/validator/v10 v10.10.0
github.com/go-redis/redis/v8 v8.11.4
github.com/go-sql-driver/mysql v1.6.0
github.com/gobwas/glob v0.2.3
github.com/gocql/gocql v0.0.0-20211222173705-d73e6b1002a7
github.com/golang/mock v1.6.0
github.com/golang/protobuf v1.5.2
Expand Down Expand Up @@ -131,8 +133,8 @@ require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-stack/stack v1.8.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang-jwt/jwt/v4 v4.2.0 // indirect
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe // indirect
github.com/golang-sql/sqlexp v0.0.0-20170517235910-f1bb20e5a188 // indirect
Expand Down Expand Up @@ -177,6 +179,7 @@ require (
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.14.2 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/leodido/go-urn v1.2.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect
Expand Down
7 changes: 7 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@ github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZ
github.com/Azure/go-autorest/tracing v0.5.0/go.mod h1:r/s2XiOKccPW3HrqB+W0TQzfbtp2fGCgRFtBroKn4Dk=
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0 h1:WVsrXCnHlDDX8ls+tootqRE87/hL9S/g4ewig9RsD/c=
github.com/AzureAD/microsoft-authentication-library-for-go v0.4.0/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4=
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
Expand Down Expand Up @@ -408,6 +410,9 @@ github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zV
github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.2.0 h1:besgBTC8w8HjP6NzQdxwKH9Z5oQMZ24ThTrHp3cZ8eU=
Expand Down Expand Up @@ -777,6 +782,7 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
Expand Down Expand Up @@ -830,6 +836,7 @@ github.com/pierrec/lz4 v2.5.2+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4/go.mod h1:4OwLy04Bl9Ef3GJJCoec+30X3LQs/0/m4HFRt/2LUSA=
github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down
30 changes: 30 additions & 0 deletions pkg/scalers/azure/azure_aad_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Copyright 2022 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 azure

// AADToken is the token from Azure AD
type AADToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn string `json:"expires_in"`
ExpiresOn string `json:"expires_on"`
NotBefore string `json:"not_before"`
Resource string `json:"resource"`
TokenType string `json:"token_type"`
GrantedScopes []string `json:"grantedScopes"`
DeclinedScopes []string `json:"DeclinedScopes"`
}
11 changes: 0 additions & 11 deletions pkg/scalers/azure/azure_aad_podidentity.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,3 @@ func GetAzureADPodIdentityToken(ctx context.Context, httpClient util.HTTPDoer, a

return token, nil
}

// AADToken is the token from Azure AD
type AADToken struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn string `json:"expires_in"`
ExpiresOn string `json:"expires_on"`
NotBefore string `json:"not_before"`
Resource string `json:"resource"`
TokenType string `json:"token_type"`
}
88 changes: 88 additions & 0 deletions pkg/scalers/azure/azure_aad_workload_identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
Copyright 2022 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 azure

import (
"context"
"fmt"
"os"
"strconv"

"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
)

// Azure AD Workload Identity Webhook will inject the following environment variables.
// * AZURE_CLIENT_ID - Client id set in the service account annotation
// * AZURE_TENANT_ID - Tenant id set in the service account annotation. If not defined, then tenant id provided via
// azure-wi-webhook-config will be used.
// * AZURE_FEDERATED_TOKEN_FILE - Service account token file path
// * AZURE_AUTHORITY_HOST - Azure Active Directory (AAD) endpoint.
const (
azureClientIDEnv = "AZURE_CLIENT_ID"
azureTenantIDEnv = "AZURE_TENANT_ID"
azureFederatedTokenFileEnv = "AZURE_FEDERATED_TOKEN_FILE"
azureAuthrityHostEnv = "AZURE_AUTHORITY_HOST"
)

// GetAzureADWorkloadIdentityToken returns the AADToken for resource
func GetAzureADWorkloadIdentityToken(ctx context.Context, resource string) (AADToken, error) {
clientID := os.Getenv(azureClientIDEnv)
tenantID := os.Getenv(azureTenantIDEnv)
tokenFilePath := os.Getenv(azureFederatedTokenFileEnv)
authorityHost := os.Getenv(azureAuthrityHostEnv)

signedAssertion, err := readJWTFromFileSystem(tokenFilePath)
if err != nil {
return AADToken{}, err
}

cred, err := confidential.NewCredFromAssertion(signedAssertion)
if err != nil {
return AADToken{}, err
}

authorityOption := confidential.WithAuthority(fmt.Sprintf("%s%s/oauth2/token", authorityHost, tenantID))
confidentialClient, err := confidential.New(
clientID,
cred,
authorityOption,
)
if err != nil {
return AADToken{}, err
}

result, err := confidentialClient.AcquireTokenByCredential(ctx, []string{resource})
if err != nil {
return AADToken{}, err
}

return AADToken{
AccessToken: result.AccessToken,
ExpiresOn: strconv.FormatInt(result.ExpiresOn.Unix(), 10),
GrantedScopes: result.GrantedScopes,
DeclinedScopes: result.DeclinedScopes,
Resource: resource,
}, nil
}

func readJWTFromFileSystem(tokenFilePath string) (string, error) {
token, err := os.ReadFile(tokenFilePath)
if err != nil {
return "", fmt.Errorf("error reading service account token")
}
return string(token), nil
}
40 changes: 26 additions & 14 deletions pkg/scalers/azure_servicebus_scaler.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const (
subscription entityType = 2
messageCountMetricName = "messageCount"
defaultTargetMessageCount = 5
// Service bus resource id is "https://servicebus.azure.net/" in all cloud environments
serviceBusResource = "https://servicebus.azure.net/"
)

var azureServiceBusLog = logf.Log.WithName("azure_servicebus_scaler")
Expand Down Expand Up @@ -155,7 +157,7 @@ func parseAzureServiceBusMetadata(config *ScalerConfig) (*azureServiceBusMetadat
if len(meta.connection) == 0 {
return nil, fmt.Errorf("no connection setting given")
}
case kedav1alpha1.PodIdentityProviderAzure:
case kedav1alpha1.PodIdentityProviderAzure, kedav1alpha1.PodIdentityProviderAzureWorkload:
if val, ok := config.TriggerMetadata["namespace"]; ok {
meta.namespace = val
} else {
Expand Down Expand Up @@ -224,24 +226,32 @@ func (s *azureServiceBusScaler) GetMetrics(ctx context.Context, metricName strin
}

type azureTokenProvider struct {
httpClient *http.Client
ctx context.Context
httpClient *http.Client
ctx context.Context
podIdentity kedav1alpha1.PodIdentityProvider
}

// GetToken implements TokenProvider interface for azureTokenProvider
func (a azureTokenProvider) GetToken(uri string) (*auth.Token, error) {
ctx := a.ctx
// Service bus resource id is "https://servicebus.azure.net/" in all cloud environments
token, err := azure.GetAzureADPodIdentityToken(ctx, a.httpClient, "https://servicebus.azure.net/")

var token azure.AADToken
var err error

switch a.podIdentity {
case kedav1alpha1.PodIdentityProviderAzure:
token, err = azure.GetAzureADPodIdentityToken(ctx, a.httpClient, serviceBusResource)
case kedav1alpha1.PodIdentityProviderAzureWorkload:
scopedResource := fmt.Sprintf("%s%s", serviceBusResource, ".default")
token, err = azure.GetAzureADWorkloadIdentityToken(ctx, scopedResource)
default:
err = fmt.Errorf("unknown pod identity provider")
}
if err != nil {
return nil, err
}

return &auth.Token{
TokenType: auth.CBSTokenTypeJWT,
Token: token.AccessToken,
Expiry: token.ExpiresOn,
}, nil
return auth.NewToken(auth.CBSTokenTypeJWT, token.AccessToken, token.ExpiresOn), nil
}

// Returns the length of the queue or subscription
Expand All @@ -267,19 +277,21 @@ func (s *azureServiceBusScaler) getServiceBusNamespace(ctx context.Context) (*se
var namespace *servicebus.Namespace
var err error

if s.podIdentity == "" || s.podIdentity == kedav1alpha1.PodIdentityProviderNone {
switch s.podIdentity {
case "", kedav1alpha1.PodIdentityProviderNone:
namespace, err = servicebus.NewNamespace(servicebus.NamespaceWithConnectionString(s.metadata.connection))
if err != nil {
return namespace, err
}
} else if s.podIdentity == kedav1alpha1.PodIdentityProviderAzure {
case kedav1alpha1.PodIdentityProviderAzure, kedav1alpha1.PodIdentityProviderAzureWorkload:
namespace, err = servicebus.NewNamespace()
if err != nil {
return namespace, err
}
namespace.TokenProvider = azureTokenProvider{
ctx: ctx,
httpClient: s.httpClient,
ctx: ctx,
httpClient: s.httpClient,
podIdentity: s.podIdentity,
}
namespace.Name = s.metadata.namespace
}
Expand Down

0 comments on commit eff0e2d

Please sign in to comment.