From 20580b6ba8884a4dbdf1f147104c4ec40829f307 Mon Sep 17 00:00:00 2001 From: bragi92 Date: Mon, 25 Aug 2025 23:14:47 -0700 Subject: [PATCH] remote_write azure auth : add workload identity support (#16788) * initial changes Signed-off-by: Kaveesh Dubey * . Signed-off-by: Kaveesh Dubey * fix comments Signed-off-by: Kaveesh Dubey * fix tenantid test Signed-off-by: Kaveesh Dubey * style Signed-off-by: Kaveesh Dubey * Update storage/remote/azuread/azuread.go Co-authored-by: Bartlomiej Plotka Signed-off-by: bragi92 * Update storage/remote/azuread/azuread.go Co-authored-by: Bartlomiej Plotka Signed-off-by: bragi92 * Update storage/remote/azuread/azuread.go Co-authored-by: Bartlomiej Plotka Signed-off-by: bragi92 * Update storage/remote/azuread/azuread.go Co-authored-by: Bartlomiej Plotka Signed-off-by: bragi92 * Update storage/remote/azuread/azuread.go Co-authored-by: Bartlomiej Plotka Signed-off-by: bragi92 * Update storage/remote/azuread/azuread.go Co-authored-by: Bartlomiej Plotka Signed-off-by: bragi92 * Update storage/remote/azuread/azuread.go Co-authored-by: Bartlomiej Plotka Signed-off-by: bragi92 * pr feedback Signed-off-by: Kaveesh Dubey --------- Signed-off-by: Kaveesh Dubey Signed-off-by: bragi92 Co-authored-by: Bartlomiej Plotka --- docs/configuration/configuration.md | 6 + storage/remote/azuread/azuread.go | 113 ++++++++++++++---- storage/remote/azuread/azuread_test.go | 64 +++++++++- ..._bad_workloadidentity_invalidclientid.yaml | 4 + ..._bad_workloadidentity_invalidtenantid.yaml | 4 + ..._bad_workloadidentity_missingclientid.yaml | 3 + ..._bad_workloadidentity_missingtenantid.yaml | 3 + .../azuread_good_workloadidentity.yaml | 5 + 8 files changed, 177 insertions(+), 25 deletions(-) create mode 100644 storage/remote/azuread/testdata/azuread_bad_workloadidentity_invalidclientid.yaml create mode 100644 storage/remote/azuread/testdata/azuread_bad_workloadidentity_invalidtenantid.yaml create mode 100644 storage/remote/azuread/testdata/azuread_bad_workloadidentity_missingclientid.yaml create mode 100644 storage/remote/azuread/testdata/azuread_bad_workloadidentity_missingtenantid.yaml create mode 100644 storage/remote/azuread/testdata/azuread_good_workloadidentity.yaml diff --git a/docs/configuration/configuration.md b/docs/configuration/configuration.md index 0a1f126284..b3ea571b80 100644 --- a/docs/configuration/configuration.md +++ b/docs/configuration/configuration.md @@ -3021,6 +3021,12 @@ azuread: [ managed_identity: [ client_id: ] ] + # Azure Workload Identity. + [ workload_identity: + client_id: + tenant_id: + [ token_file_path: | default = "/var/run/secrets/azure/tokens/azure-identity-token" ] ] + # Azure OAuth. [ oauth: [ client_id: ] diff --git a/storage/remote/azuread/azuread.go b/storage/remote/azuread/azuread.go index 1b577a56bc..ef4d6bb424 100644 --- a/storage/remote/azuread/azuread.go +++ b/storage/remote/azuread/azuread.go @@ -43,12 +43,32 @@ const ( IngestionPublicAudience = "https://monitor.azure.com//.default" ) +const ( + // DefaultWorkloadIdentityTokenPath is the default path where the Azure Workload Identity + // webhook puts the service account token on Azure environments. See . + DefaultWorkloadIdentityTokenPath = "/var/run/secrets/azure/tokens/azure-identity-token" +) + // ManagedIdentityConfig is used to store managed identity config values. type ManagedIdentityConfig struct { // ClientID is the clientId of the managed identity that is being used to authenticate. ClientID string `yaml:"client_id,omitempty"` } +// WorkloadIdentityConfig is used to store workload identity config values. +type WorkloadIdentityConfig struct { + // ClientID is the clientId of the Microsoft Entra application or user-assigned managed identity. + ClientID string `yaml:"client_id,omitempty"` + + // TenantID is the tenantId of the Microsoft Entra application or user-assigned managed identity. + // This should match the tenant ID where your application or managed identity is registered. + TenantID string `yaml:"tenant_id,omitempty"` + + // TokenFilePath is the path to the token file provided by the Kubernetes service account projected volume. + // If not specified, it defaults to DefaultWorkloadIdentityTokenPath. + TokenFilePath string `yaml:"token_file_path,omitempty"` +} + // OAuthConfig is used to store azure oauth config values. type OAuthConfig struct { // ClientID is the clientId of the azure active directory application that is being used to authenticate. @@ -72,6 +92,9 @@ type AzureADConfig struct { //nolint:revive // exported. // ManagedIdentity is the managed identity that is being used to authenticate. ManagedIdentity *ManagedIdentityConfig `yaml:"managed_identity,omitempty"` + // WorkloadIdentity is the workload identity that is being used to authenticate. + WorkloadIdentity *WorkloadIdentityConfig `yaml:"workload_identity,omitempty"` + // OAuth is the oauth config that is being used to authenticate. OAuth *OAuthConfig `yaml:"oauth,omitempty"` @@ -111,20 +134,25 @@ func (c *AzureADConfig) Validate() error { return errors.New("must provide a cloud in the Azure AD config") } - if c.ManagedIdentity == nil && c.OAuth == nil && c.SDK == nil { - return errors.New("must provide an Azure Managed Identity, Azure OAuth or Azure SDK in the Azure AD config") + authenticators := 0 + if c.ManagedIdentity != nil { + authenticators++ + } + if c.WorkloadIdentity != nil { + authenticators++ + } + if c.OAuth != nil { + authenticators++ + } + if c.SDK != nil { + authenticators++ } - if c.ManagedIdentity != nil && c.OAuth != nil { - return errors.New("cannot provide both Azure Managed Identity and Azure OAuth in the Azure AD config") + if authenticators == 0 { + return errors.New("must provide an Azure Managed Identity, Azure Workload Identity, Azure OAuth or Azure SDK in the Azure AD config") } - - if c.ManagedIdentity != nil && c.SDK != nil { - return errors.New("cannot provide both Azure Managed Identity and Azure SDK in the Azure AD config") - } - - if c.OAuth != nil && c.SDK != nil { - return errors.New("cannot provide both Azure OAuth and Azure SDK in the Azure AD config") + if authenticators > 1 { + return errors.New("cannot provide multiple authentication methods in the Azure AD config") } if c.ManagedIdentity != nil { @@ -136,6 +164,26 @@ func (c *AzureADConfig) Validate() error { } } + if c.WorkloadIdentity != nil { + if c.WorkloadIdentity.ClientID == "" { + return errors.New("must provide an Azure Workload Identity client_id in the Azure AD config") + } + if c.WorkloadIdentity.TenantID == "" { + return errors.New("must provide an Azure Workload Identity tenant_id in the Azure AD config") + } + + if _, err := uuid.Parse(c.WorkloadIdentity.ClientID); err != nil { + return errors.New("the provided Azure Workload Identity client_id is invalid") + } + if _, err := uuid.Parse(c.WorkloadIdentity.TenantID); err != nil { + return errors.New("the provided Azure Workload Identity tenant_id is invalid") + } + + if c.WorkloadIdentity.TokenFilePath == "" { + c.WorkloadIdentity.TokenFilePath = DefaultWorkloadIdentityTokenPath + } + } + if c.OAuth != nil { if c.OAuth.ClientID == "" { return errors.New("must provide an Azure OAuth client_id in the Azure AD config") @@ -147,24 +195,18 @@ func (c *AzureADConfig) Validate() error { return errors.New("must provide an Azure OAuth tenant_id in the Azure AD config") } - var err error - _, err = uuid.Parse(c.OAuth.ClientID) - if err != nil { + if _, err := uuid.Parse(c.OAuth.ClientID); err != nil { return errors.New("the provided Azure OAuth client_id is invalid") } - _, err = regexp.MatchString("^[0-9a-zA-Z-.]+$", c.OAuth.TenantID) - if err != nil { + if _, err := regexp.MatchString("^[0-9a-zA-Z-.]+$", c.OAuth.TenantID); err != nil { return errors.New("the provided Azure OAuth tenant_id is invalid") } } if c.SDK != nil { - var err error - if c.SDK.TenantID != "" { - _, err = regexp.MatchString("^[0-9a-zA-Z-.]+$", c.SDK.TenantID) - if err != nil { - return errors.New("the provided Azure OAuth tenant_id is invalid") + if _, err := regexp.MatchString("^[0-9a-zA-Z-.]+$", c.SDK.TenantID); err != nil { + return errors.New("the provided Azure SDK tenant_id is invalid") } } } @@ -217,7 +259,7 @@ func (rt *azureADRoundTripper) RoundTrip(req *http.Request) (*http.Response, err return rt.next.RoundTrip(req) } -// newTokenCredential returns a TokenCredential of different kinds like Azure Managed Identity and Azure AD application. +// newTokenCredential returns a TokenCredential of different kinds like Azure Managed Identity, Workload Identity and Azure AD application. func newTokenCredential(cfg *AzureADConfig) (azcore.TokenCredential, error) { var cred azcore.TokenCredential var err error @@ -239,6 +281,18 @@ func newTokenCredential(cfg *AzureADConfig) (azcore.TokenCredential, error) { } } + if cfg.WorkloadIdentity != nil { + workloadIdentityConfig := &WorkloadIdentityConfig{ + ClientID: cfg.WorkloadIdentity.ClientID, + TenantID: cfg.WorkloadIdentity.TenantID, + TokenFilePath: cfg.WorkloadIdentity.TokenFilePath, + } + cred, err = newWorkloadIdentityTokenCredential(clientOpts, workloadIdentityConfig) + if err != nil { + return nil, err + } + } + if cfg.OAuth != nil { oAuthConfig := &OAuthConfig{ ClientID: cfg.OAuth.ClientID, @@ -276,6 +330,21 @@ func newManagedIdentityTokenCredential(clientOpts *azcore.ClientOptions, managed return azidentity.NewManagedIdentityCredential(opts) } +// newWorkloadIdentityTokenCredential returns new Microsoft Entra Workload Identity token credential. +// +// For detailed setup instructions, see: +// https://learn.microsoft.com/en-us/azure/azure-monitor/essentials/prometheus-metrics-enable-workload-identity +func newWorkloadIdentityTokenCredential(clientOpts *azcore.ClientOptions, workloadIdentityConfig *WorkloadIdentityConfig) (azcore.TokenCredential, error) { + opts := &azidentity.WorkloadIdentityCredentialOptions{ + ClientOptions: *clientOpts, + ClientID: workloadIdentityConfig.ClientID, + TenantID: workloadIdentityConfig.TenantID, + TokenFilePath: workloadIdentityConfig.TokenFilePath, + } + + return azidentity.NewWorkloadIdentityCredential(opts) +} + // newOAuthTokenCredential returns new OAuth token credential. func newOAuthTokenCredential(clientOpts *azcore.ClientOptions, oAuthConfig *OAuthConfig) (azcore.TokenCredential, error) { opts := &azidentity.ClientSecretCredentialOptions{ClientOptions: *clientOpts} diff --git a/storage/remote/azuread/azuread_test.go b/storage/remote/azuread/azuread_test.go index 37931800f1..876c33b288 100644 --- a/storage/remote/azuread/azuread_test.go +++ b/storage/remote/azuread/azuread_test.go @@ -88,6 +88,17 @@ func (ad *AzureAdTestSuite) TestAzureAdRoundTripper() { }, }, }, + // AzureAd roundtripper with Workload Identity. + { + cfg: &AzureADConfig{ + Cloud: "AzurePublic", + WorkloadIdentity: &WorkloadIdentityConfig{ + ClientID: dummyClientID, + TenantID: dummyTenantID, + TokenFilePath: DefaultWorkloadIdentityTokenPath, + }, + }, + }, } for _, c := range cases { var gotReq *http.Request @@ -145,7 +156,7 @@ func TestAzureAdConfig(t *testing.T) { // Missing managedidentity or oauth field. { filename: "testdata/azuread_bad_configmissing.yaml", - err: "must provide an Azure Managed Identity, Azure OAuth or Azure SDK in the Azure AD config", + err: "must provide an Azure Managed Identity, Azure Workload Identity, Azure OAuth or Azure SDK in the Azure AD config", }, // Invalid managedidentity client id. { @@ -160,12 +171,32 @@ func TestAzureAdConfig(t *testing.T) { // Invalid config when both managedidentity and oauth is provided. { filename: "testdata/azuread_bad_twoconfig.yaml", - err: "cannot provide both Azure Managed Identity and Azure OAuth in the Azure AD config", + err: "cannot provide multiple authentication methods in the Azure AD config", }, // Invalid config when both sdk and oauth is provided. { filename: "testdata/azuread_bad_oauthsdkconfig.yaml", - err: "cannot provide both Azure OAuth and Azure SDK in the Azure AD config", + err: "cannot provide multiple authentication methods in the Azure AD config", + }, + // Invalid workload identity client id. + { + filename: "testdata/azuread_bad_workloadidentity_invalidclientid.yaml", + err: "the provided Azure Workload Identity client_id is invalid", + }, + // Invalid workload identity tenant id. + { + filename: "testdata/azuread_bad_workloadidentity_invalidtenantid.yaml", + err: "the provided Azure Workload Identity tenant_id is invalid", + }, + // Missing workload identity client id. + { + filename: "testdata/azuread_bad_workloadidentity_missingclientid.yaml", + err: "must provide an Azure Workload Identity client_id in the Azure AD config", + }, + // Missing workload identity tenant id. + { + filename: "testdata/azuread_bad_workloadidentity_missingtenantid.yaml", + err: "must provide an Azure Workload Identity tenant_id in the Azure AD config", }, // Valid config with missing optionally cloud field. { @@ -187,6 +218,10 @@ func TestAzureAdConfig(t *testing.T) { { filename: "testdata/azuread_good_sdk.yaml", }, + // Valid workload identity config. + { + filename: "testdata/azuread_good_workloadidentity.yaml", + }, } for _, c := range cases { _, err := loadAzureAdConfig(c.filename) @@ -255,6 +290,18 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() { }, err: "Cloud is not specified or is incorrect: ", }, + // Invalid tokenProvider for workload identity. + { + cfg: &AzureADConfig{ + Cloud: "PublicAzure", + WorkloadIdentity: &WorkloadIdentityConfig{ + ClientID: dummyClientID, + TenantID: dummyTenantID, + TokenFilePath: DefaultWorkloadIdentityTokenPath, + }, + }, + err: "Cloud is not specified or is incorrect: ", + }, // Valid tokenProvider for managedidentity. { cfg: &AzureADConfig{ @@ -284,6 +331,17 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() { }, }, }, + // Valid tokenProvider for workload identity. + { + cfg: &AzureADConfig{ + Cloud: "AzurePublic", + WorkloadIdentity: &WorkloadIdentityConfig{ + ClientID: dummyClientID, + TenantID: dummyTenantID, + TokenFilePath: DefaultWorkloadIdentityTokenPath, + }, + }, + }, } mockGetTokenCallCounter := 1 for _, c := range cases { diff --git a/storage/remote/azuread/testdata/azuread_bad_workloadidentity_invalidclientid.yaml b/storage/remote/azuread/testdata/azuread_bad_workloadidentity_invalidclientid.yaml new file mode 100644 index 0000000000..d0cc41d8b8 --- /dev/null +++ b/storage/remote/azuread/testdata/azuread_bad_workloadidentity_invalidclientid.yaml @@ -0,0 +1,4 @@ +workload_identity: + client_id: "invalidclientid" + tenant_id: "00000000-a12b-3cd4-e56f-000000000000" +cloud: "AzurePublic" diff --git a/storage/remote/azuread/testdata/azuread_bad_workloadidentity_invalidtenantid.yaml b/storage/remote/azuread/testdata/azuread_bad_workloadidentity_invalidtenantid.yaml new file mode 100644 index 0000000000..c59d42cf74 --- /dev/null +++ b/storage/remote/azuread/testdata/azuread_bad_workloadidentity_invalidtenantid.yaml @@ -0,0 +1,4 @@ +workload_identity: + client_id: "00000000-0000-0000-0000-000000000000" + tenant_id: "invalid-tenant-id-!" +cloud: "AzurePublic" diff --git a/storage/remote/azuread/testdata/azuread_bad_workloadidentity_missingclientid.yaml b/storage/remote/azuread/testdata/azuread_bad_workloadidentity_missingclientid.yaml new file mode 100644 index 0000000000..b16f094059 --- /dev/null +++ b/storage/remote/azuread/testdata/azuread_bad_workloadidentity_missingclientid.yaml @@ -0,0 +1,3 @@ +workload_identity: + tenant_id: "00000000-a12b-3cd4-e56f-000000000000" +cloud: "AzurePublic" diff --git a/storage/remote/azuread/testdata/azuread_bad_workloadidentity_missingtenantid.yaml b/storage/remote/azuread/testdata/azuread_bad_workloadidentity_missingtenantid.yaml new file mode 100644 index 0000000000..f68e7abeb3 --- /dev/null +++ b/storage/remote/azuread/testdata/azuread_bad_workloadidentity_missingtenantid.yaml @@ -0,0 +1,3 @@ +workload_identity: + client_id: "00000000-0000-0000-0000-000000000000" +cloud: "AzurePublic" diff --git a/storage/remote/azuread/testdata/azuread_good_workloadidentity.yaml b/storage/remote/azuread/testdata/azuread_good_workloadidentity.yaml new file mode 100644 index 0000000000..93f99efe3c --- /dev/null +++ b/storage/remote/azuread/testdata/azuread_good_workloadidentity.yaml @@ -0,0 +1,5 @@ +workload_identity: + client_id: "00000000-0000-0000-0000-000000000000" + tenant_id: "00000000-a12b-3cd4-e56f-000000000000" + token_file_path: "/var/run/secrets/azure/tokens/azure-identity-token" +cloud: "AzurePublic"