Menu

Wednesday, April 2, 2025

Access Azure from HCP Terraform with OIDC federation

Share


Storing long-lived Azure credentials poses a security risk. While HCP Terraform secures sensitive credentials as write-only variables, you must audit the usage of long-lived credentials to detect if they are compromised. Many organizations have a policy to block these types of credentials.

A more secure and better alternative is available for authentication: dynamic provider credentials on HCP Terraform. This feature allows Terraform to authenticate to Azure as a service principal through a native OpenID Connect (OIDC) integration. HCP Terraform obtains temporary credentials for each run, and discards the credentials when the run completes. These credentials allow you to call Azure APIs that the service principal has access to at runtime. These credentials are short-lived by design, so their usefulness to an attacker is limited.

In this blog post, we’ll explore dynamic credentials for Azure and walk you through the required steps to set this up for yourself.

»

»

azurerm, azuread, and tfe providers in your Terraform configuration:

terraform {
  required_providers {
    azuread = {
      source  = "hashicorp/azuread"
      version = "3.0.2"
    }
 
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "4.14.0"
    }
 
    tfe = {
      source  = "hashicorp/tfe"
      version = "0.62.0"
    }
  }
}
 
provider "azuread" {}
 
provider "azurerm" {
  features {}
 
  subscription_id = "xxxxxxxxxx"
}
 
provider "tfe" {
  organization = "xxxxxxxxxx"
}

All three providers perform implicit authentication with the credentials available in the environment at runtime.

You’ll authenticate to Azure and Entra ID using the Azure CLI. For guidance on setting up the Azure CLI, refer to the Azure CLI documentation. While there are alternative methods to authenticate to Azure and Entra ID, we prefer using the Azure CLI for its simplicity. For more details, check the documentation for the Azure or Entra ID providers.

Begin by creating an application in Entra ID and a corresponding service principal:

resource "azuread_application" "hcp_terraform" {
  display_name = "hcp-terraform-azure-oidc"
}
 
resource "azuread_service_principal" "hcp_terraform" {
  client_id = azuread_application.hcp_terraform.client_id
}

Below, you’ll start by establishing an OIDC trust relationship using a federated identity credentials resource. This resource is configured with an audience (api://AzureADTokenExchange), an issuer (https://app.terraform.io) and a subject.

A subject has the following format that includes details from your HCP Terraform environment:

organization::project::workspace::run_phase:

Below is a specific example of a plan operation in an HCP Terraform organization named “my-organization”. Also included is an example project named “my-project” and a workspace named “my-workspace”:

organization:my-organization:project:my-project:workspace:my-workspace:run_phase:plan

It is a good practice to use a federated credential for a single purpose. It’s also possible to use different service principals for Terraform plan and apply operations.

In this tutorial we use a single service principal with two different federated credentials, one for plan operations and one for apply operations.

First, you need to create federated credentials for the workspace’s plan operations:

# data source to reference the current hcp terraform organization
data "tfe_organization" "current" {}
 
resource "azuread_application_federated_identity_credential" "plan" {
  application_id = azuread_application.hcp_terraform.id
  display_name   = "${azuread_application.hcp_terraform.display_name}-plan"
  audiences      = ["api://AzureADTokenExchange"]
  issuer         = "
  description    = "For HCP Terraform plan operations"
 
  subject = join(":", [
    "organization",
    data.tfe_organization.current.name,
    "project",
    tfe_project.default.name,
    "workspace",
    tfe_workspace.default.name,
    "run_phase",
    "plan"
  ])
}

Next, create federated credentials for the workspace’s apply operations:

resource "azuread_application_federated_identity_credential" "apply" {
  application_id = azuread_application.hcp_terraform.id
  display_name   = "${azuread_application.hcp_terraform.display_name}-apply"
  audiences      = ["api://AzureADTokenExchange"]
  issuer         = "
  description    = "For HCP Terraform apply operations"
 
  subject = join(":", [
    "organization",
    data.tfe_organization.current.name,
    "project",
    tfe_project.default.name,
    "workspace",
    tfe_workspace.default.name,
    "run_phase",
    "apply"
  ])
}

»

Storage Account Contributor role to allow it to create and manage storage accounts on Azure. You will also set up a custom role to allow it to create resource groups on Azure.

Our custom Resource Group Creator role is defined and assigned to the service principal like this:

# data source for the current azure subscription
data "azurerm_subscription" "current" {}
 
resource "azurerm_role_definition" "resource_group_creator" {
  name  = "Resource Group Creator"
  scope = data.azurerm_subscription.current.id
 
  permissions {
    actions = [
      "*/read",
      "Microsoft.Resources/subscriptions/resourceGroups/write",
    ]
  }
 
  assignable_scopes = [
    data.azurerm_subscription.current.id,
  ]
}
 
resource "azurerm_role_assignment" "resource_group_creator" {
  scope              = data.azurerm_subscription.current.id
  principal_id       = azuread_service_principal.hcp_terraform.object_id
  role_definition_id = azurerm_role_definition.resource_group_creator.role_definition_resource_id
}

Similarly, assigning the Storage Account Contributor role to the service principal on the Azure subscription scope is done like this:

resource "azurerm_role_assignment" "storage_account_contributor" {
  scope                = data.azurerm_subscription.current.id
  principal_id         = azuread_service_principal.hcp_terraform.object_id
  role_definition_name = "Storage Account Contributor"
}

»

»

»

HCP Terraform variable set for each workspace, as described in the discussion above. The variable set for each workspace has two environment variables. These are the same two environment variables from the demo above: TFC_AZURE_PROVIDER_AUTH and TFC_AZURE_RUN_CLIENT_ID. These credentials are injected into the provider to grant access to any Azure API permitted by the service principal’s permissions.

Below is an example configuration of a variable set for a team (e.g. “Team A”).

First, create the variable set:

resource "tfe_variable_set" "oidc_team_a_dev" {
  name         = "oidc-team-a-dev"
  description  = "OIDC federation configuration for team A (dev)"
  organization = "XXXXXXXXXXXXXXX"
}
 

Next, set up the required environment variables and link them to the variable set:

resource "tfe_variable" "tfc_azure_provider_auth" {
  key             = "TFC_AZURE_PROVIDER_AUTH"
  value           = "true"
  category        = "env"
  variable_set_id = tfe_variable_set.oidc_team_a_dev.id
}
 
resource "tfe_variable" "tfc_azure_run_client_id" {
  sensitive       = true
  key             = "TFC_AZURE_RUN_CLIENT_ID"
  value           = azuread_service_principal.team_a_dev.client_id
  category        = "env"
  variable_set_id = tfe_variable_set.oidc_team_a_dev.id
}

Finally, share the variable set with Team A by connecting it to their development workspace. This ensures that the targeted workspace receives and uses the environment variables, allowing HCP Terraform to automatically obtain and inject the temporary credentials:

resource "tfe_workspace_variable_set" "oidc_team_a_dev" {
  variable_set_id = tfe_variable_set.oidc_team_a_dev.id
  workspace_id    = "ws-XXXXXXXXXXXXXXX"
}

Set up similar resources for each team that should have access to Azure. You will also need to configure Azure RBAC permissions for each service principal.

»

Use dynamic credentials with the Azure provider and the OIDC federation documentation. Find a more complete example of configuring the Azure OIDC identity provider on GitHub.



Source link

Read more

Local News