Skip to content

Commit

Permalink
Azure: Add support for Workload Identity authentication (#75681)
Browse files Browse the repository at this point in the history
* Update Azure Monitor

* Update Prometheus

* Update README

* Update docs/sources/datasources/azure-monitor/_index.md

Co-authored-by: Andrew Hackmann <5140848+bossinc@users.noreply.github.com>

* Update docs/sources/datasources/azure-monitor/_index.md

Co-authored-by: Beverly <131809838+BeverlyJaneJ@users.noreply.github.com>

* Update docs/sources/datasources/azure-monitor/_index.md

Co-authored-by: Beverly <131809838+BeverlyJaneJ@users.noreply.github.com>

* Update docs/sources/datasources/azure-monitor/_index.md

Co-authored-by: Beverly <131809838+BeverlyJaneJ@users.noreply.github.com>

* README updates

* Fix prettier

* memoize options

---------

Co-authored-by: Andrew Hackmann <5140848+bossinc@users.noreply.github.com>
Co-authored-by: Beverly <131809838+BeverlyJaneJ@users.noreply.github.com>
  • Loading branch information
3 people committed Sep 29, 2023
1 parent 9d44fef commit 5796836
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 85 deletions.
63 changes: 60 additions & 3 deletions docs/sources/datasources/azure-monitor/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ For more information, refer to [Azure documentation for role assignments](https:
If you host Grafana in Azure, such as in App Service or Azure Virtual Machines, you can configure the Azure Monitor data source to use Managed Identity for secure authentication without entering credentials into Grafana.
For details, refer to [Configuring using Managed Identity](#configuring-using-managed-identity).

You can configure the Azure Monitor data source to use Workload Identity for secure authentication without entering credentials into Grafana if you host Grafana in a Kubernetes environment, such as AKS, and require access to Azure resources.
For details, refer to [Configuring using Workload Identity](#configuring-using-workload-identity).

| Name | Description |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Authentication** | Enables Managed Identity. Selecting Managed Identity hides many of the other fields. For details, see [Configuring using Managed Identity](#configuring-using-managed-identity). |
Expand Down Expand Up @@ -114,6 +117,21 @@ datasources:
version: 1
```

**Workload Identity:**

```yaml
apiVersion: 1 # config file version

datasources:
- name: Azure Monitor
type: grafana-azure-monitor-datasource
access: proxy
jsonData:
azureAuthType: workloadidentity
subscriptionId: <subscription-id> # Optional, default subscription
version: 1
```

#### Supported cloud names

| Azure Cloud | `cloudName` Value |
Expand All @@ -124,8 +142,8 @@ datasources:

### Configure Managed Identity

If you host Grafana in Azure, such as an App Service or with Azure Virtual Machines, and have managed identity enabled on your VM, you can use managed identity to configure Azure Monitor in Grafana.
This lets you securely authenticate data sources without manually configuring credentials via Azure AD App Registrations for each.
You can use managed identity to configure Azure Monitor in Grafana if you host Grafana in Azure (such as an App Service or with Azure Virtual Machines) and have managed identity enabled on your VM.
This lets you securely authenticate data sources without manually configuring credentials via Azure AD App Registrations.
For details on Azure managed identities, refer to the [Azure documentation](https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview).

**To enable managed identity for Grafana:**
Expand All @@ -141,7 +159,46 @@ For details on Azure managed identities, refer to the [Azure documentation](http

This hides the directory ID, application ID, and client secret fields, and the data source uses managed identity to authenticate to Azure Monitor Metrics and Logs, and Azure Resource Graph.

{{< figure src="/media/docs/grafana/data-sources/screenshot-managed-identity.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor Metrics screenshot showing Dimensions" >}}
{{< figure src="/media/docs/grafana/data-sources/screenshot-managed-identity-2.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor screenshot showing Managed Identity authentication" >}}

3. You can set the `managed_identity_client_id` field in the `[azure]` section of the [Grafana server configuration][configure-grafana-azure] to allow a user-assigned managed identity to be used instead of the default system-assigned identity.

```ini
[azure]
managed_identity_enabled = true
managed_identity_client_id = USER_ASSIGNED_IDENTITY_CLIENT_ID
```

### Configure Workload Identity

You can use workload identity to configure Azure Monitor in Grafana if you host Grafana in a Kubernetes environment, such as AKS, in conjunction with managed identities.
This lets you securely authenticate data sources without manually configuring credentials via Azure AD App Registrations.
For details on workload identity, refer to the [Azure workload identity documentation](https://azure.github.io/azure-workload-identity/docs/).

**To enable workload identity for Grafana:**

1. Set the `workload_identity_enabled` flag in the `[azure]` section of the [Grafana server configuration][configure-grafana-azure].

```ini
[azure]
workload_identity_enabled = true
```

2. In the Azure Monitor data source configuration, set **Authentication** to **Workload Identity**.

This hides the directory ID, application ID, and client secret fields, and the data source uses workload identity to authenticate to Azure Monitor Metrics and Logs, and Azure Resource Graph.

{{< figure src="/media/docs/grafana/data-sources/screenshot-workload-identity.png" max-width="800px" class="docs-image--no-shadow" caption="Azure Monitor screenshot showing Workload Identity authentication" >}}

3. There are additional configuration variables that can control the authentication method.`workload_identity_tenant_id` represents the Azure AD tenant that contains the managed identity, `workload_identity_client_id` represents the client ID of the managed identity if it differs from the default client ID, `workload_identity_token_file` represents the path to the token file. Refer to the [documentation](https://azure.github.io/azure-workload-identity/docs/) for more information on what values these variables should use, if any.

```ini
[azure]
workload_identity_enabled = true
workload_identity_tenant_id = IDENTITY_TENANT_ID
workload_identity_client_id = IDENTITY_CLIENT_ID
workload_identity_token_file = TOKEN_FILE_PATH
```

## Query the data source

Expand Down
16 changes: 11 additions & 5 deletions pkg/tsdb/azuremonitor/credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ func getAuthType(cfg *setting.Cfg, jsonData *types.AzureClientSettings) string {
return azcredentials.AzureAuthClientSecret
}

// For newly created datasource with no configuration, managed identity is the default authentication type
// if they are enabled in Grafana config
// For newly created datasource with no configuration the order is as follows:
// Managed identity is the default if enabled
// Workload identity is the next option if enabled
// Client secret is the final fallback
if cfg.Azure.ManagedIdentityEnabled {
return azcredentials.AzureAuthManagedIdentity
} else if cfg.Azure.WorkloadIdentityEnabled {
return azcredentials.AzureAuthWorkloadIdentity
} else {
return azcredentials.AzureAuthClientSecret
}
Expand Down Expand Up @@ -84,8 +88,8 @@ func normalizeAzureCloud(cloudName string) (string, error) {
func getAzureCloud(cfg *setting.Cfg, jsonData *types.AzureClientSettings) (string, error) {
authType := getAuthType(cfg, jsonData)
switch authType {
case azcredentials.AzureAuthManagedIdentity:
// In case of managed identity, the cloud is always same as where Grafana is hosted
case azcredentials.AzureAuthManagedIdentity, azcredentials.AzureAuthWorkloadIdentity:
// In case of managed identity and workload identity, the cloud is always same as where Grafana is hosted
return getDefaultAzureCloud(cfg)
case azcredentials.AzureAuthClientSecret:
if cloud := jsonData.CloudName; cloud != "" {
Expand All @@ -106,7 +110,9 @@ func getAzureCredentials(cfg *setting.Cfg, jsonData *types.AzureClientSettings,
case azcredentials.AzureAuthManagedIdentity:
credentials := &azcredentials.AzureManagedIdentityCredentials{}
return credentials, nil

case azcredentials.AzureAuthWorkloadIdentity:
credentials := &azcredentials.AzureWorkloadIdentityCredentials{}
return credentials, nil
case azcredentials.AzureAuthClientSecret:
cloud, err := getAzureCloud(cfg, jsonData)
if err != nil {
Expand Down
60 changes: 60 additions & 0 deletions pkg/tsdb/azuremonitor/credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,66 @@ func TestCredentials_getAuthType(t *testing.T) {
assert.Equal(t, azcredentials.AzureAuthClientSecret, authType)
})
})

t.Run("when workload identities enabled", func(t *testing.T) {
cfg.Azure.WorkloadIdentityEnabled = true

t.Run("should be client secret if auth type is set to client secret", func(t *testing.T) {
jsonData := &types.AzureClientSettings{
AzureAuthType: azcredentials.AzureAuthClientSecret,
}

authType := getAuthType(cfg, jsonData)

assert.Equal(t, azcredentials.AzureAuthClientSecret, authType)
})

t.Run("should be workload identity if datasource not configured and managed identity is disabled", func(t *testing.T) {
jsonData := &types.AzureClientSettings{
AzureAuthType: "",
}

authType := getAuthType(cfg, jsonData)

assert.Equal(t, azcredentials.AzureAuthWorkloadIdentity, authType)
})

t.Run("should be client secret if auth type not specified but credentials configured", func(t *testing.T) {
jsonData := &types.AzureClientSettings{
AzureAuthType: "",
TenantId: "9b9d90ee-a5cc-49c2-b97e-0d1b0f086b5c",
ClientId: "849ccbb0-92eb-4226-b228-ef391abd8fe6",
}

authType := getAuthType(cfg, jsonData)

assert.Equal(t, azcredentials.AzureAuthClientSecret, authType)
})
})

t.Run("when workload identities disabled", func(t *testing.T) {
cfg.Azure.WorkloadIdentityEnabled = false

t.Run("should be workload identity if auth type is set to workload identity", func(t *testing.T) {
jsonData := &types.AzureClientSettings{
AzureAuthType: azcredentials.AzureAuthWorkloadIdentity,
}

authType := getAuthType(cfg, jsonData)

assert.Equal(t, azcredentials.AzureAuthWorkloadIdentity, authType)
})

t.Run("should be client secret if datasource not configured", func(t *testing.T) {
jsonData := &types.AzureClientSettings{
AzureAuthType: "",
}

authType := getAuthType(cfg, jsonData)

assert.Equal(t, azcredentials.AzureAuthClientSecret, authType)
})
})
}

func TestCredentials_getAzureCloud(t *testing.T) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import AzureCredentialsForm, { Props } from './AzureCredentialsForm';
const setup = (propsFunc?: (props: Props) => Props) => {
let props: Props = {
managedIdentityEnabled: false,
workloadIdentityEnabled: false,
credentials: {
authType: 'clientsecret',
azureCloud: 'azuremonitor',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ChangeEvent } from 'react';
import React, { ChangeEvent, useMemo } from 'react';

import { SelectableValue } from '@grafana/data';
import { ConfigSection } from '@grafana/experimental';
Expand All @@ -9,39 +9,64 @@ import { AzureAuthType, AzureCredentials } from '../types';

export interface Props {
managedIdentityEnabled: boolean;
workloadIdentityEnabled: boolean;
credentials: AzureCredentials;
azureCloudOptions?: SelectableValue[];
onCredentialsChange?: (updatedCredentials: AzureCredentials) => void;
onCredentialsChange: (updatedCredentials: AzureCredentials) => void;
disabled?: boolean;
children?: JSX.Element;
}

const authTypeOptions: Array<SelectableValue<AzureAuthType>> = [
{
value: 'msi',
label: 'Managed Identity',
},
{
value: 'clientsecret',
label: 'App Registration',
},
];

export const AzureCredentialsForm = (props: Props) => {
const { credentials, azureCloudOptions, onCredentialsChange, disabled, managedIdentityEnabled } = props;
const {
credentials,
azureCloudOptions,
onCredentialsChange,
disabled,
managedIdentityEnabled,
workloadIdentityEnabled,
} = props;

const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
if (onCredentialsChange) {
const updated: AzureCredentials = {
...credentials,
authType: selected.value || 'msi',
};
onCredentialsChange(updated);
const authTypeOptions = useMemo(() => {
let opts: Array<SelectableValue<AzureAuthType>> = [
{
value: 'clientsecret',
label: 'App Registration',
},
];

if (managedIdentityEnabled) {
opts.push({
value: 'msi',
label: 'Managed Identity',
});
}

if (workloadIdentityEnabled) {
opts.push({
value: 'workloadidentity',
label: 'Workload Identity',
});
}

return opts;
}, [managedIdentityEnabled, workloadIdentityEnabled]);

const onAuthTypeChange = (selected: SelectableValue<AzureAuthType>) => {
const defaultAuthType = managedIdentityEnabled
? 'msi'
: workloadIdentityEnabled
? 'workloadidentity'
: 'clientsecret';
const updated: AzureCredentials = {
...credentials,
authType: selected.value || defaultAuthType,
};
onCredentialsChange(updated);
};

const onAzureCloudChange = (selected: SelectableValue<string>) => {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
if (credentials.authType === 'clientsecret') {
const updated: AzureCredentials = {
...credentials,
azureCloud: selected.value,
Expand All @@ -51,7 +76,7 @@ export const AzureCredentialsForm = (props: Props) => {
};

const onTenantIdChange = (event: ChangeEvent<HTMLInputElement>) => {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
if (credentials.authType === 'clientsecret') {
const updated: AzureCredentials = {
...credentials,
tenantId: event.target.value,
Expand All @@ -61,7 +86,7 @@ export const AzureCredentialsForm = (props: Props) => {
};

const onClientIdChange = (event: ChangeEvent<HTMLInputElement>) => {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
if (credentials.authType === 'clientsecret') {
const updated: AzureCredentials = {
...credentials,
clientId: event.target.value,
Expand All @@ -71,7 +96,7 @@ export const AzureCredentialsForm = (props: Props) => {
};

const onClientSecretChange = (event: ChangeEvent<HTMLInputElement>) => {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
if (credentials.authType === 'clientsecret') {
const updated: AzureCredentials = {
...credentials,
clientSecret: event.target.value,
Expand All @@ -81,7 +106,7 @@ export const AzureCredentialsForm = (props: Props) => {
};

const onClientSecretReset = () => {
if (onCredentialsChange && credentials.authType === 'clientsecret') {
if (credentials.authType === 'clientsecret') {
const updated: AzureCredentials = {
...credentials,
clientSecret: '',
Expand All @@ -92,7 +117,7 @@ export const AzureCredentialsForm = (props: Props) => {

return (
<ConfigSection title="Authentication">
{managedIdentityEnabled && (
{authTypeOptions.length > 1 && (
<Field
label="Authentication"
description="Choose the type of authentication to Azure services"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const MonitorConfig = (props: Props) => {
<>
<AzureCredentialsForm
managedIdentityEnabled={config.azure.managedIdentityEnabled}
workloadIdentityEnabled={config.azure.workloadIdentityEnabled}
credentials={credentials}
azureCloudOptions={azureClouds}
onCredentialsChange={onCredentialsChange}
Expand Down
Loading

0 comments on commit 5796836

Please sign in to comment.