remote_write azure auth : add workload identity support (#16788)

* initial changes

Signed-off-by: Kaveesh Dubey <kadubey@microsoft.com>

* .

Signed-off-by: Kaveesh Dubey <kadubey@microsoft.com>

* fix comments

Signed-off-by: Kaveesh Dubey <kadubey@microsoft.com>

* fix tenantid test

Signed-off-by: Kaveesh Dubey <kadubey@microsoft.com>

* style

Signed-off-by: Kaveesh Dubey <kadubey@microsoft.com>

* Update storage/remote/azuread/azuread.go

Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: bragi92 <kadubey@microsoft.com>

* Update storage/remote/azuread/azuread.go

Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: bragi92 <kadubey@microsoft.com>

* Update storage/remote/azuread/azuread.go

Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: bragi92 <kadubey@microsoft.com>

* Update storage/remote/azuread/azuread.go

Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: bragi92 <kadubey@microsoft.com>

* Update storage/remote/azuread/azuread.go

Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: bragi92 <kadubey@microsoft.com>

* Update storage/remote/azuread/azuread.go

Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: bragi92 <kadubey@microsoft.com>

* Update storage/remote/azuread/azuread.go

Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
Signed-off-by: bragi92 <kadubey@microsoft.com>

* pr feedback

Signed-off-by: Kaveesh Dubey <kadubey@microsoft.com>

---------

Signed-off-by: Kaveesh Dubey <kadubey@microsoft.com>
Signed-off-by: bragi92 <kadubey@microsoft.com>
Co-authored-by: Bartlomiej Plotka <bwplotka@gmail.com>
This commit is contained in:
bragi92 2025-08-25 23:14:47 -07:00 committed by GitHub
parent 7cf585527f
commit 20580b6ba8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 177 additions and 25 deletions

View File

@ -3021,6 +3021,12 @@ azuread:
[ managed_identity: [ managed_identity:
[ client_id: <string> ] ] [ client_id: <string> ] ]
# Azure Workload Identity.
[ workload_identity:
client_id: <string>
tenant_id: <string>
[ token_file_path: <string> | default = "/var/run/secrets/azure/tokens/azure-identity-token" ] ]
# Azure OAuth. # Azure OAuth.
[ oauth: [ oauth:
[ client_id: <string> ] [ client_id: <string> ]

View File

@ -43,12 +43,32 @@ const (
IngestionPublicAudience = "https://monitor.azure.com//.default" 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 <azure docs link>.
DefaultWorkloadIdentityTokenPath = "/var/run/secrets/azure/tokens/azure-identity-token"
)
// ManagedIdentityConfig is used to store managed identity config values. // ManagedIdentityConfig is used to store managed identity config values.
type ManagedIdentityConfig struct { type ManagedIdentityConfig struct {
// ClientID is the clientId of the managed identity that is being used to authenticate. // ClientID is the clientId of the managed identity that is being used to authenticate.
ClientID string `yaml:"client_id,omitempty"` 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. // OAuthConfig is used to store azure oauth config values.
type OAuthConfig struct { type OAuthConfig struct {
// ClientID is the clientId of the azure active directory application that is being used to authenticate. // 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 is the managed identity that is being used to authenticate.
ManagedIdentity *ManagedIdentityConfig `yaml:"managed_identity,omitempty"` 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 is the oauth config that is being used to authenticate.
OAuth *OAuthConfig `yaml:"oauth,omitempty"` 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") return errors.New("must provide a cloud in the Azure AD config")
} }
if c.ManagedIdentity == nil && c.OAuth == nil && c.SDK == nil { authenticators := 0
return errors.New("must provide an Azure Managed Identity, Azure OAuth or Azure SDK in the Azure AD config") 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 { if authenticators == 0 {
return errors.New("cannot provide both Azure Managed Identity and Azure OAuth in the Azure AD config") return errors.New("must provide an Azure Managed Identity, Azure Workload Identity, Azure OAuth or Azure SDK in the Azure AD config")
} }
if authenticators > 1 {
if c.ManagedIdentity != nil && c.SDK != nil { return errors.New("cannot provide multiple authentication methods in the Azure AD config")
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 c.ManagedIdentity != nil { 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 != nil {
if c.OAuth.ClientID == "" { if c.OAuth.ClientID == "" {
return errors.New("must provide an Azure OAuth client_id in the Azure AD config") 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") return errors.New("must provide an Azure OAuth tenant_id in the Azure AD config")
} }
var err error if _, err := uuid.Parse(c.OAuth.ClientID); err != nil {
_, err = uuid.Parse(c.OAuth.ClientID)
if err != nil {
return errors.New("the provided Azure OAuth client_id is invalid") return errors.New("the provided Azure OAuth client_id is invalid")
} }
_, err = regexp.MatchString("^[0-9a-zA-Z-.]+$", c.OAuth.TenantID) if _, err := regexp.MatchString("^[0-9a-zA-Z-.]+$", c.OAuth.TenantID); err != nil {
if err != nil {
return errors.New("the provided Azure OAuth tenant_id is invalid") return errors.New("the provided Azure OAuth tenant_id is invalid")
} }
} }
if c.SDK != nil { if c.SDK != nil {
var err error
if c.SDK.TenantID != "" { if c.SDK.TenantID != "" {
_, err = regexp.MatchString("^[0-9a-zA-Z-.]+$", c.SDK.TenantID) if _, err := regexp.MatchString("^[0-9a-zA-Z-.]+$", c.SDK.TenantID); err != nil {
if err != nil { return errors.New("the provided Azure SDK tenant_id is invalid")
return errors.New("the provided Azure OAuth tenant_id is invalid")
} }
} }
} }
@ -217,7 +259,7 @@ func (rt *azureADRoundTripper) RoundTrip(req *http.Request) (*http.Response, err
return rt.next.RoundTrip(req) 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) { func newTokenCredential(cfg *AzureADConfig) (azcore.TokenCredential, error) {
var cred azcore.TokenCredential var cred azcore.TokenCredential
var err error 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 { if cfg.OAuth != nil {
oAuthConfig := &OAuthConfig{ oAuthConfig := &OAuthConfig{
ClientID: cfg.OAuth.ClientID, ClientID: cfg.OAuth.ClientID,
@ -276,6 +330,21 @@ func newManagedIdentityTokenCredential(clientOpts *azcore.ClientOptions, managed
return azidentity.NewManagedIdentityCredential(opts) 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. // newOAuthTokenCredential returns new OAuth token credential.
func newOAuthTokenCredential(clientOpts *azcore.ClientOptions, oAuthConfig *OAuthConfig) (azcore.TokenCredential, error) { func newOAuthTokenCredential(clientOpts *azcore.ClientOptions, oAuthConfig *OAuthConfig) (azcore.TokenCredential, error) {
opts := &azidentity.ClientSecretCredentialOptions{ClientOptions: *clientOpts} opts := &azidentity.ClientSecretCredentialOptions{ClientOptions: *clientOpts}

View File

@ -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 { for _, c := range cases {
var gotReq *http.Request var gotReq *http.Request
@ -145,7 +156,7 @@ func TestAzureAdConfig(t *testing.T) {
// Missing managedidentity or oauth field. // Missing managedidentity or oauth field.
{ {
filename: "testdata/azuread_bad_configmissing.yaml", 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. // Invalid managedidentity client id.
{ {
@ -160,12 +171,32 @@ func TestAzureAdConfig(t *testing.T) {
// Invalid config when both managedidentity and oauth is provided. // Invalid config when both managedidentity and oauth is provided.
{ {
filename: "testdata/azuread_bad_twoconfig.yaml", 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. // Invalid config when both sdk and oauth is provided.
{ {
filename: "testdata/azuread_bad_oauthsdkconfig.yaml", 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. // Valid config with missing optionally cloud field.
{ {
@ -187,6 +218,10 @@ func TestAzureAdConfig(t *testing.T) {
{ {
filename: "testdata/azuread_good_sdk.yaml", filename: "testdata/azuread_good_sdk.yaml",
}, },
// Valid workload identity config.
{
filename: "testdata/azuread_good_workloadidentity.yaml",
},
} }
for _, c := range cases { for _, c := range cases {
_, err := loadAzureAdConfig(c.filename) _, err := loadAzureAdConfig(c.filename)
@ -255,6 +290,18 @@ func (s *TokenProviderTestSuite) TestNewTokenProvider() {
}, },
err: "Cloud is not specified or is incorrect: ", 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. // Valid tokenProvider for managedidentity.
{ {
cfg: &AzureADConfig{ 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 mockGetTokenCallCounter := 1
for _, c := range cases { for _, c := range cases {

View File

@ -0,0 +1,4 @@
workload_identity:
client_id: "invalidclientid"
tenant_id: "00000000-a12b-3cd4-e56f-000000000000"
cloud: "AzurePublic"

View File

@ -0,0 +1,4 @@
workload_identity:
client_id: "00000000-0000-0000-0000-000000000000"
tenant_id: "invalid-tenant-id-!"
cloud: "AzurePublic"

View File

@ -0,0 +1,3 @@
workload_identity:
tenant_id: "00000000-a12b-3cd4-e56f-000000000000"
cloud: "AzurePublic"

View File

@ -0,0 +1,3 @@
workload_identity:
client_id: "00000000-0000-0000-0000-000000000000"
cloud: "AzurePublic"

View File

@ -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"