Skip to content

Commit

Permalink
feat: Azure AD Workload Identity support for Azure Scalers and Key Va…
Browse files Browse the repository at this point in the history
…ult (#2907)

* Azure AD Workload Identity Support - Azure Service Bus Scaler.

Signed-off-by: Vighnesh Shenoy <vshenoy@microsoft.com>
  • Loading branch information
v-shenoy authored May 10, 2022
1 parent dcb9c1e commit 98509a8
Show file tree
Hide file tree
Showing 46 changed files with 942 additions and 196 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ To learn more about our roadmap, we recommend reading [this document](ROADMAP.md

### New

- TODO ([#XXX](https://github.com/kedacore/keda/issue/XXX))
- **General:** Support for Azure AD Workload Identity as a pod identity provider. ([2487](https://github.com/kedacore/keda/issues/2487))

### Improvements

Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,11 @@ deploy: manifests kustomize ## Deploy controller to the K8s cluster specified in
$(KUSTOMIZE) edit set image ghcr.io/kedacore/keda=${IMAGE_CONTROLLER}
cd config/metrics-server && \
$(KUSTOMIZE) edit set image ghcr.io/kedacore/keda-metrics-apiserver=${IMAGE_ADAPTER}
if [ "$(AZURE_RUN_WORKLOAD_IDENTITY_TESTS)" = true ]; then \
cd config/service_account && \
$(KUSTOMIZE) edit add label --force azure.workload.identity/use:true; \
$(KUSTOMIZE) edit add annotation --force azure.workload.identity/client-id:${AZURE_SP_APP_ID} azure.workload.identity/tenant-id:${AZURE_SP_TENANT}; \
fi
# Need this workaround to mitigate a problem with inserting labels into selectors,
# until this issue is solved: https://github.com/kubernetes-sigs/kustomize/issues/1009
@sed -i".out" -e 's@version:[ ].*@version: $(VERSION)@g' config/default/kustomize-config/metadataLabelTransformer.yaml
Expand Down
18 changes: 10 additions & 8 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 Expand Up @@ -180,9 +181,10 @@ type VaultSecret struct {

// AzureKeyVault is used to authenticate using Azure Key Vault
type AzureKeyVault struct {
VaultURI string `json:"vaultUri"`
VaultURI string `json:"vaultUri"`
Secrets []AzureKeyVaultSecret `json:"secrets"`
// +optional
Credentials *AzureKeyVaultCredentials `json:"credentials"`
Secrets []AzureKeyVaultSecret `json:"secrets"`
// +optional
Cloud *AzureKeyVaultCloudInfo `json:"cloud"`
}
Expand Down
10 changes: 5 additions & 5 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.

Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ spec:
vaultUri:
type: string
required:
- credentials
- secrets
- vaultUri
type: object
Expand Down
1 change: 0 additions & 1 deletion config/crd/bases/keda.sh_triggerauthentications.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,6 @@ spec:
vaultUri:
type: string
required:
- credentials
- secrets
- vaultUri
type: object
Expand Down
1 change: 1 addition & 0 deletions config/default/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ resources:
- ../rbac
- ../manager
- ../metrics-server
- ../service_account
1 change: 0 additions & 1 deletion config/general/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
resources:
- namespace.yaml
- service_account.yaml
5 changes: 5 additions & 0 deletions config/service_account/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
resources:
- service_account.yaml

apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
File renamed without changes.
3 changes: 3 additions & 0 deletions 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.27
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.13.0
github.com/Huawei/gophercloud v1.0.21
github.com/Shopify/sarama v1.32.0
Expand Down Expand Up @@ -140,6 +141,7 @@ require (
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-stack/stack v1.8.0 // 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 @@ -185,6 +187,7 @@ require (
github.com/jpillora/backoff v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.15.0 // 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 @@ -137,6 +137,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 @@ -420,6 +422,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 @@ -795,6 +800,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 @@ -850,6 +856,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
33 changes: 33 additions & 0 deletions pkg/scalers/azure/azure_aad_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
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 "time"

// 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"`
ExpiresOnTimeObject time.Time `json:"expires_on_object"`
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"`
}
171 changes: 171 additions & 0 deletions pkg/scalers/azure/azure_aad_workload_identity.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
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"
"strings"
"time"

amqpAuth "github.com/Azure/azure-amqp-common-go/v3/auth"
"github.com/Azure/go-autorest/autorest"
"github.com/Azure/go-autorest/autorest/azure/auth"
"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{}, fmt.Errorf("error reading service account token - %w", err)
}

cred, err := confidential.NewCredFromAssertion(signedAssertion)
if err != nil {
return AADToken{}, fmt.Errorf("error getting credentials from service account token - %w", err)
}

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

result, err := confidentialClient.AcquireTokenByCredential(ctx, []string{getScopedResource(resource)})
if err != nil {
return AADToken{}, fmt.Errorf("error acquiring aad token - %w", err)
}

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

func readJWTFromFileSystem(tokenFilePath string) (string, error) {
token, err := os.ReadFile(tokenFilePath)
if err != nil {
return "", err
}
return string(token), nil
}

func getScopedResource(resource string) string {
resource = strings.TrimSuffix(resource, "/")
if !strings.HasSuffix(resource, ".default") {
resource += "/.default"
}

return resource
}

type ADWorkloadIdentityConfig struct {
ctx context.Context
Resource string
}

func NewAzureADWorkloadIdentityConfig(ctx context.Context, resource string) auth.AuthorizerConfig {
return ADWorkloadIdentityConfig{ctx: ctx, Resource: resource}
}

// Authorizer implements the auth.AuthorizerConfig interface
func (aadWiConfig ADWorkloadIdentityConfig) Authorizer() (autorest.Authorizer, error) {
return autorest.NewBearerAuthorizer(&ADWorkloadIdentityTokenProvider{ctx: aadWiConfig.ctx, Resource: aadWiConfig.Resource}), nil
}

// ADWorkloadIdentityTokenProvider is a type that implements the adal.OAuthTokenProvider and adal.Refresher interfaces.
// The OAuthTokenProvider interface is used by the BearerAuthorizer to get the token when preparing the HTTP Header.
// The Refresher interface is used by the BearerAuthorizer to refresh the token.
type ADWorkloadIdentityTokenProvider struct {
ctx context.Context
Resource string
aadToken AADToken
}

func NewADWorkloadIdentityTokenProvider(ctx context.Context, resource string) *ADWorkloadIdentityTokenProvider {
return &ADWorkloadIdentityTokenProvider{ctx: ctx, Resource: resource}
}

// OAuthToken is for implementing the adal.OAuthTokenProvider interface. It returns the current access token.
func (wiTokenProvider *ADWorkloadIdentityTokenProvider) OAuthToken() string {
return wiTokenProvider.aadToken.AccessToken
}

// Refresh is for implementing the adal.Refresher interface
func (wiTokenProvider *ADWorkloadIdentityTokenProvider) Refresh() error {
if time.Now().Before(wiTokenProvider.aadToken.ExpiresOnTimeObject) {
return nil
}

aadToken, err := GetAzureADWorkloadIdentityToken(wiTokenProvider.ctx, wiTokenProvider.Resource)
if err != nil {
return err
}

wiTokenProvider.aadToken = aadToken
return nil
}

// RefreshExchange is for implementing the adal.Refresher interface
func (wiTokenProvider *ADWorkloadIdentityTokenProvider) RefreshExchange(resource string) error {
wiTokenProvider.Resource = resource
return wiTokenProvider.Refresh()
}

// EnsureFresh is for implementing the adal.Refresher interface
func (wiTokenProvider *ADWorkloadIdentityTokenProvider) EnsureFresh() error {
return wiTokenProvider.Refresh()
}

// GetToken is for implementing the auth.TokenProvider interface
func (wiTokenProvider *ADWorkloadIdentityTokenProvider) GetToken(uri string) (*amqpAuth.Token, error) {
err := wiTokenProvider.Refresh()
if err != nil {
return nil, err
}

return amqpAuth.NewToken(amqpAuth.CBSTokenTypeJWT, wiTokenProvider.aadToken.AccessToken,
wiTokenProvider.aadToken.ExpiresOn), nil
}
Loading

0 comments on commit 98509a8

Please sign in to comment.