diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 50fd34f0f4b5..f3d944537393 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -313,6 +313,14 @@ func azureProvider(supportLegacyTestSuite bool) *schema.Provider { Description: "Allow Azure CLI to be used for Authentication.", }, + // Azure AKS Workload Identity fields + "use_aks_workload_identity": { + Type: schema.TypeBool, + Optional: true, + DefaultFunc: schema.EnvDefaultFunc("ARM_USE_AKS_WORKLOAD_IDENTITY", false), + Description: "Allow Azure AKS Workload Identity to be used for Authentication.", + }, + // Managed Tracking GUID for User-agent "partner_id": { Type: schema.TypeString, @@ -400,6 +408,11 @@ func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc { return nil, diag.FromErr(err) } + tenantId, err := getTenantId(d) + if err != nil { + return nil, diag.FromErr(err) + } + var ( env *environments.Environment @@ -418,13 +431,13 @@ func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc { var ( enableAzureCli = d.Get("use_cli").(bool) enableManagedIdentity = d.Get("use_msi").(bool) - enableOidc = d.Get("use_oidc").(bool) + enableOidc = d.Get("use_oidc").(bool) || d.Get("use_aks_workload_identity").(bool) ) authConfig := &auth.Credentials{ Environment: *env, ClientID: *clientId, - TenantID: d.Get("tenant_id").(string), + TenantID: *tenantId, AuxiliaryTenantIDs: auxTenants, ClientCertificateData: clientCertificateData, @@ -529,6 +542,23 @@ func getOidcToken(d *schema.ResourceData) (*string, error) { idToken = fileToken } + if d.Get("use_aks_workload_identity").(bool) && os.Getenv("AZURE_FEDERATED_TOKEN_FILE") != "" { + path := os.Getenv("AZURE_FEDERATED_TOKEN_FILE") + fileTokenRaw, err := os.ReadFile(os.Getenv("AZURE_FEDERATED_TOKEN_FILE")) + + if err != nil { + return nil, fmt.Errorf("reading OIDC Token from file %q provided by AKS Workload Identity: %v", path, err) + } + + fileToken := strings.TrimSpace(string(fileTokenRaw)) + + if idToken != "" && idToken != fileToken { + return nil, fmt.Errorf("mismatch between supplied OIDC token and OIDC token file contents provided by AKS Workload Identity - please either remove one, ensure they match, or disable use_aks_workload_identity") + } + + idToken = fileToken + } + return &idToken, nil } @@ -551,6 +581,14 @@ func getClientId(d *schema.ResourceData) (*string, error) { clientId = fileClientId } + if d.Get("use_aks_workload_identity").(bool) && os.Getenv("AZURE_CLIENT_ID") != "" { + aksClientId := os.Getenv("AZURE_CLIENT_ID") + if clientId != "" && clientId != aksClientId { + return nil, fmt.Errorf("mismatch between supplied Client ID and that provided by AKS Workload Identity - please remove, ensure they match, or disable use_aks_workload_identity") + } + clientId = aksClientId + } + return &clientId, nil } @@ -576,6 +614,20 @@ func getClientSecret(d *schema.ResourceData) (*string, error) { return &clientSecret, nil } +func getTenantId(d *schema.ResourceData) (*string, error) { + tenantId := strings.TrimSpace(d.Get("tenant_id").(string)) + + if d.Get("use_aks_workload_identity").(bool) && os.Getenv("AZURE_TENANT_ID") != "" { + aksTenantId := os.Getenv("AZURE_TENANT_ID") + if tenantId != "" && tenantId != aksTenantId { + return nil, fmt.Errorf("mismatch between supplied Tenant ID and that provided by AKS Workload Identity - please remove, ensure they match, or disable use_aks_workload_identity") + } + tenantId = aksTenantId + } + + return &tenantId, nil +} + const resourceProviderRegistrationErrorFmt = `Error ensuring Resource Providers are registered. Terraform automatically attempts to register the Resource Providers it supports to diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 5174af9c6977..bb1167f9009e 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -249,7 +249,7 @@ func testAccProvider_clientSecretAuthFromEnvironment(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - // Support only Client Certificate authentication + // Support only Client Secret authentication provider.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { envName := d.Get("environment").(string) env, err := environments.FromName(envName) @@ -312,7 +312,7 @@ func testAccProvider_clientSecretAuthFromFiles(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - // Support only Client Certificate authentication + // Support only Client Secret authentication provider.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { envName := d.Get("environment").(string) env, err := environments.FromName(envName) @@ -367,7 +367,7 @@ func TestAccProvider_genericOidcAuth(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - // Support only Client Certificate authentication + // Support only OIDC authentication provider.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { envName := d.Get("environment").(string) env, err := environments.FromName(envName) @@ -420,7 +420,7 @@ func TestAccProvider_githubOidcAuth(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() - // Support only Client Certificate authentication + // Support only GitHub OIDC authentication provider.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { envName := d.Get("environment").(string) env, err := environments.FromName(envName) @@ -452,6 +452,74 @@ func TestAccProvider_githubOidcAuth(t *testing.T) { } } +func TestAccProvider_aksWorkloadIdentityAuth(t *testing.T) { + if os.Getenv("TF_ACC") == "" { + t.Skip("TF_ACC not set") + } + if os.Getenv("AZURE_CLIENT_ID") == "" { + t.Skip("AZURE_CLIENT_ID not set") + } + if os.Getenv("AZURE_TENANT_ID") == "" { + t.Skip("AZURE_TENANT_ID not set") + } + if os.Getenv("AZURE_FEDERATED_TOKEN_FILE") == "" { + t.Skip("AZURE_FEDERATED_TOKEN_FILE not set") + } + + logging.SetOutput(t) + + provider := TestAzureProvider() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + // Support only AKS Workload Identity authentication + provider.ConfigureContextFunc = func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) { + envName := d.Get("environment").(string) + env, err := environments.FromName(envName) + if err != nil { + t.Fatalf("configuring environment %q: %v", envName, err) + } + + oidcToken, err := getOidcToken(d) + if err != nil { + return nil, diag.FromErr(err) + } + + clientId, err := getClientId(d) + if err != nil { + return nil, diag.FromErr(err) + } + + tenantId, err := getTenantId(d) + if err != nil { + return nil, diag.FromErr(err) + } + + authConfig := &auth.Credentials{ + Environment: *env, + TenantID: *tenantId, + ClientID: *clientId, + EnableAuthenticationUsingOIDC: true, + OIDCAssertionToken: *oidcToken, + } + + return buildClient(ctx, provider, d, authConfig) + } + + // Ensure we enable AKS Workload Identity else the configuration will not be detected + conf := map[string]interface{}{"use_aks_workload_identity": true} + d := provider.Configure(ctx, terraform.NewResourceConfigRaw(conf)) + if d != nil && d.HasError() { + t.Fatalf("err: %+v", d) + } + + if errs := testCheckProvider(provider); len(errs) > 0 { + for _, err := range errs { + t.Error(err) + } + } +} + func testCheckProvider(provider *schema.Provider) (errs []error) { client := provider.Meta().(*clients.Client) diff --git a/website/docs/guides/aks_workload_identity.html.markdown b/website/docs/guides/aks_workload_identity.html.markdown new file mode 100644 index 000000000000..8cae35ae1abf --- /dev/null +++ b/website/docs/guides/aks_workload_identity.html.markdown @@ -0,0 +1,148 @@ +--- +layout: "azurerm" +page_title: "Azure Provider: Authenticating via AKS Workload Identity" +description: |- + This guide will cover how to use AKS Workload Identity for pods in Azure AKS clusters as authentication for the Azure Provider. +--- + +# Azure Provider: Authenticating using managed identities for Azure Kubernetes Service with Workload Identity + +Terraform supports a number of different methods for authenticating to Azure: + +- [Authenticating to Azure using the Azure CLI](azure_cli.html) +* [Authenticating to Azure using Managed Service Identity](managed_service_identity.html) +- [Authenticating to Azure using a Service Principal and a Client Certificate](service_principal_client_certificate.html) +- [Authenticating to Azure using a Service Principal and a Client Secret](service_principal_client_secret.html) +- [Authenticating to Azure using OpenID Connect](service_principal_oidc.html) +- Authenticating to Azure using AKS Workload Identity (covered in this guide) + +--- + +We recommend using a service principal or a managed identity when running Terraform non-interactively (such as when running Terraform in a CI/CD pipeline), and authenticating using the Azure CLI when running Terraform locally. + +## What is AKS Workload Identity? + +[AKS Workload Identity](https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview) can be used to authenticate to services that support Azure Active Directory (Azure AD) authentication when running in Azure Kubernetes Service clusters. + +When a service account and pod are configured to use AKS Workload Identity, a federated identity token is injected into the pod at run-time, along with environment variables to use that identity. + +## Configuring a workload to use an AKS Workload Identity + +The (simplified) Terraform configuration below provisions a cluster with workload identity enabled, creates an identity and federated identity credential suitable for a workload identity, and then grants the Contributor role to the identity. + +```hcl +data "azurerm_subscription" "current" {} + +variable "workload_sa_name" { + type = string + description = "Kubernetes service account to permit" +} + +variable "workload_sa_namespace" { + type = string + description = "Kubernetes service account namespace to permit" +} + +resource "azurerm_kubernetes_cluster" "mycluster" { + # ... + workload_identity_enabled = true +} + +resource "azurerm_user_assigned_identity" "myworkload_identity" { + # ... + name = "myworkloadidentity" +} + +resource "azurerm_federated_identity_credential" "myworkload_identity" { + name = azurerm_user_assigned_identity.myworkload_identity.name + resource_group_name = azurerm_user_assigned_identity.myworkload_identity.resource_group_name + parent_id = azurerm_user_assigned_identity.myworkload_identity.id + audience = ["api://AzureADTokenExchange"] + issuer = azurerm_kubernetes_cluster.mycluster.oidc_issuer_url + subject = "system:serviceaccount:${workload_sa_namespace}:${workload_sa_name}" +} + +data "azurerm_role_definition" "contributor" { + name = "Contributor" +} + +resource "azurerm_role_assignment" "example" { + scope = data.azurerm_subscription.current.id + role_definition_id = "${data.azurerm_subscription.current.id}${data.azurerm_role_definition.contributor.id}" + principal_id = azurerm_user_assigned_identity.wayfinder_main.principal_id +} + +output "myworkload_identity_client_id" { + description = "The client ID of the created managed identity to use for the annotation 'azure.workload.identity/client-id' on your service account" + value = azurerm_user_assigned_identity.myworkload_identity.client_id +} +``` + +## Configuring Terraform to use an AKS workload identity + +At this point we assume that workload identity is configured on the AKS cluster being used and that permissions have been assigned via Azure's Identity and Access Management system. + +Terraform can be configured to use AKS workload identity for authentication in one of two ways: using environment variables, or by defining the field within the provider block. + +### Configuring with environment variables + +Setting the `ARM_USE_AKS_WORKLOAD_IDENTITY` environment variable (equivalent to provider block argument [`use_aks_workload_identity`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#use_aks_workload_identity)) to `true` tells Terraform to use an AKS workload identity. It is also suggested to disable Azure CLI authentication by setting the `ARM_USE_CLI` environment variable (equivalent to provider block argument [`use_cli`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#use_cli)) to `false`. + +If you have not annotated your Kubernetes service account with `azure.workload.identity/client-id`, you will need to specify the `ARM_CLIENT_ID` environment variable (equivalent to provider block argument [`client_id`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#client_id)) to the [client id](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity#client_id) of the identity. + +In addition to a properly-configured managed identity, Terraform needs to know the subscription ID to fully configure the AzureRM provider. The tenant ID will be detected from the environment provided by AKS Workload Identity. + +```shell +export ARM_USE_AKS_WORKLOAD_IDENTITY=true +export ARM_USE_CLI=false +export ARM_SUBSCRIPTION_ID=159f2485-xxxx-xxxx-xxxx-xxxxxxxxxxxx +export ARM_CLIENT_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # only necessary if the service account is not annotated with the relevant client ID +``` + +A provider block is _technically_ optional when using environment variables. Even so, we recommend defining provider blocks so that you can pin or constrain the version of the provider being used, and configure other optional settings: + +```hcl +# We strongly recommend using the required_providers block to set the +# Azure Provider source and version being used +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=3.0.0" + } + } +} + +# Configure the Microsoft Azure Provider +provider "azurerm" { + features {} +} +``` + +### Configuring with the provider block + +It's also possible to configure an AKS workload identity within the provider block: + +```hcl +# We strongly recommend using the required_providers block to set the +# Azure Provider source and version being used +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "=3.0.0" + } + } +} + +# Configure the Microsoft Azure Provider +provider "azurerm" { + features {} + + use_aks_workload_identity = true + use_cli = false + #... +} +``` + +More information on [the fields supported in the provider block can be found here](../index.html#argument-reference). diff --git a/website/docs/guides/azure_cli.html.markdown b/website/docs/guides/azure_cli.html.markdown index 1875971f06aa..741d25fbb61a 100644 --- a/website/docs/guides/azure_cli.html.markdown +++ b/website/docs/guides/azure_cli.html.markdown @@ -15,6 +15,7 @@ Terraform supports a number of different methods for authenticating to Azure: * [Authenticating to Azure using a Service Principal and a Client Certificate](service_principal_client_certificate.html) * [Authenticating to Azure using a Service Principal and a Client Secret](service_principal_client_secret.html) * [Authenticating to Azure using a Service Principal and Open ID Connect](service_principal_oidc.html) +* [Authenticating to Azure using AKS Workload Identity](aks_workload_identity.html) --- diff --git a/website/docs/guides/managed_service_identity.html.markdown b/website/docs/guides/managed_service_identity.html.markdown index 0e9f437851c3..730d5e11b069 100644 --- a/website/docs/guides/managed_service_identity.html.markdown +++ b/website/docs/guides/managed_service_identity.html.markdown @@ -14,6 +14,7 @@ Terraform supports a number of different methods for authenticating to Azure: - [Authenticating to Azure using a Service Principal and a Client Certificate](service_principal_client_certificate.html) - [Authenticating to Azure using a Service Principal and a Client Secret](service_principal_client_secret.html) - [Authenticating to Azure using OpenID Connect](service_principal_oidc.html) +- [Authenticating to Azure using AKS Workload Identity](aks_workload_identity.html) --- @@ -70,7 +71,7 @@ Terraform can be configured to use managed identity for authentication in one of ### Configuring with environment variables -Setting the`ARM_USE_MSI` environment variable (equivalent to provider block argument [`use_msi`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#use_msi)) to `true` tells Terraform to use a managed identity. +Setting the `ARM_USE_MSI` environment variable (equivalent to provider block argument [`use_msi`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#use_msi)) to `true` tells Terraform to use a managed identity. By default, Terraform will use the system assigned identity for authentication. To use a user assigned identity instead, you will need to specify the `ARM_CLIENT_ID` environment variable (equivalent to provider block argument [`client_id`](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs#client_id)) to the [client id](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity#client_id) of the identity. diff --git a/website/docs/guides/service_principal_client_certificate.html.markdown b/website/docs/guides/service_principal_client_certificate.html.markdown index e550bb319422..81a932812c99 100644 --- a/website/docs/guides/service_principal_client_certificate.html.markdown +++ b/website/docs/guides/service_principal_client_certificate.html.markdown @@ -15,6 +15,7 @@ Terraform supports a number of different methods for authenticating to Azure: * Authenticating to Azure using a Service Principal and a Client Certificate (which is covered in this guide) * [Authenticating to Azure using a Service Principal and a Client Secret](service_principal_client_secret.html) * [Authenticating to Azure using a Service Principal and OpenID Connect](service_principal_oidc.html) +* [Authenticating to Azure using AKS Workload Identity](aks_workload_identity.html) --- diff --git a/website/docs/guides/service_principal_client_secret.html.markdown b/website/docs/guides/service_principal_client_secret.html.markdown index 2a99c8fb8c30..b0c636508c06 100644 --- a/website/docs/guides/service_principal_client_secret.html.markdown +++ b/website/docs/guides/service_principal_client_secret.html.markdown @@ -15,6 +15,7 @@ Terraform supports a number of different methods for authenticating to Azure: * [Authenticating to Azure using a Service Principal and a Client Certificate](service_principal_client_certificate.html) * Authenticating to Azure using a Service Principal and a Client Secret (which is covered in this guide) * [Authenticating to Azure using a Service Principal and OpenID Connect](service_principal_oidc.html) +* [Authenticating to Azure using AKS Workload Identity](aks_workload_identity.html) --- diff --git a/website/docs/guides/service_principal_oidc.html.markdown b/website/docs/guides/service_principal_oidc.html.markdown index 741518d17f68..3038f9adc1d5 100644 --- a/website/docs/guides/service_principal_oidc.html.markdown +++ b/website/docs/guides/service_principal_oidc.html.markdown @@ -14,6 +14,7 @@ Terraform supports a number of different methods for authenticating to Azure: * [Authenticating to Azure using a Service Principal and a Client Certificate](service_principal_client_certificate.html) * [Authenticating to Azure using a Service Principal and a Client Secret](service_principal_client_secret.html) * Authenticating to Azure using a Service Principal and OpenID Connect (which is covered in this guide) +* [Authenticating to Azure using AKS Workload Identity](aks_workload_identity.html) --- diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index f9bf35d441c8..5bb4c03929bc 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -147,7 +147,7 @@ When authenticating as a Service Principal using Open ID Connect, the following * `oidc_token` - (Optional) The ID token when authenticating using OpenID Connect (OIDC). This can also be sourced from the `ARM_OIDC_TOKEN` environment Variable. -* `oidc_token_file_path` - (Optional) The path to a file containing an ID token when authenticating using OpenID Connect (OIDC). This can also be sourced from the `ARM_OIDC_TOKEN_FILE_PATH` environment Variable. +* `oidc_token_file_path` - (Optional) The path to a file containing an ID token when authenticating using OpenID Connect (OIDC). This can also be sourced from the `ARM_OIDC_TOKEN_FILE_PATH` Environment Variable. * `use_oidc` - (Optional) Should OIDC be used for Authentication? This can also be sourced from the `ARM_USE_OIDC` Environment Variable. Defaults to `false`. @@ -165,6 +165,14 @@ More information on [how to configure a Service Principal using Managed Identity --- +When authenticating using AKS Workload Identity, the following fields can be set: + +* `use_aks_workload_identity` - (Optional) Should AKS Workload Identity be used for Authentication? This can also be sourced from the `ARM_USE_AKS_WORKLOAD_IDENTITY` Environment Variable. Defaults to `false`. When set, `client_id`, `tenant_id` and `oidc_token_file_path` will be detected from the environment and do not need to be specified. + +More information on [how to configure AKS Workload Identity can be found in this guide](guides/aks_workload_identity.html). + +--- + For Azure CLI authentication, the following fields can be set: * `use_cli` - (Optional) Should Azure CLI be used for authentication? This can also be sourced from the `ARM_USE_CLI` environment variable. Defaults to `true`.