Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Authentication: Support Azure Federated Identity environment variables directly #23965

Merged
merged 7 commits into from
Dec 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 54 additions & 2 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Expand All @@ -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,
Expand Down Expand Up @@ -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
}

Expand All @@ -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
}

Expand All @@ -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
Expand Down
76 changes: 72 additions & 4 deletions internal/provider/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

Expand Down
148 changes: 148 additions & 0 deletions website/docs/guides/aks_workload_identity.html.markdown
Original file line number Diff line number Diff line change
@@ -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).
1 change: 1 addition & 0 deletions website/docs/guides/azure_cli.html.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down Expand Up @@ -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.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)

---

Expand Down
Loading
Loading