Managing Environment-Specific Access in Azure with Entra ID and Terraform
Applications and Services need multiple environments. Therefore you can’t get away from the practical realities of managing multi-environment cloud infrastructure. One such challenge that emerges is managing machine-to-machine authentication securely and efficiently.
Using Azure Entra ID (formerly Azure Active Directory) with Terraform, we can enforce environment-specific access control while adhering to the principle of least privilege.
This article outlines how to dynamically configure access for different Entra ID service principals across environments such as development (DEV) and production (PROD), using Terraform variables and symbolic references for portability — meaning the same root module can be reused in a different context: a new environment, a different subscription, or even an entirely separate Entra ID tenant.
Defining Client Access Per Environment
To determine which clients can authenticate against a service, we use a combination of a primary application identity and additional clients that vary based on the environment. These client IDs are passed as variables to the Terraform module.
locals {
all_clients = concat([var.application_id], var.additional_clients)
}
The application_id represents the main service identity, while additional_clients is a variable list of Entra ID application IDs that should also have access. This variable allows flexible, environment-specific configurations. For example:
In dev.tfvars:
additional_clients = ["x", "y"]
In prod.tfvars:
additional_clients = ["a", "b"]
By varying additional_clients, each environment can permit different service principals, enforcing a finely tuned access policy.
Applying Least Privilege with Entra ID Applications
Every instance of the application, whether in development, staging, or production, should use its own set of Entra ID applications for authentication. This ensures that credentials and access scopes are not shared unnecessarily across environments, aligning with the principle of least privilege.
With this setup, developers can configure access using infrastructure as code without hardcoding access permissions, maintaining clean separation of concerns and security contexts.
Enabling Authentication in Azure Function Apps
The Azure Function App resource azurerm_linux_function_app includes an auth_settings_v2 block, which defines how the app authenticates incoming requests. Within this, the active_directory_v2 block configures Entra ID integration.
Here’s an example configuration:
auth_settings_v2 {
auth_enabled = true
require_authentication = true
unauthenticated_action = "Return401"
forward_proxy_convention = "NoProxy"
http_route_api_prefix = "/.auth"
require_https = true
runtime_version = "~1"
active_directory_v2 {
client_id = var.application_id
tenant_auth_endpoint = "https://login.microsoftonline.com/${data.azurerm_client_config.current.tenant_id}/v2.0"
allowed_applications = local.all_clients
allowed_audiences = ["api://${var.application_id}"]
www_authentication_disabled = false
}
login {
token_store_enabled = true
}
}
In this configuration:
- client_id is the main application’s Entra ID.
- allowed_applications is the full list of authorized clients, built from both the primary and additional ones.
- tenant_auth_endpoint is dynamically derived from the current client configuration, improving module portability.
Avoiding Static GUIDs with Symbolic References
If the Terraform module had access to Entra ID (via the azuread Terraform provider), it could replace hardcoded application IDs with symbolic lookups via the AzureAD provider. For example, it could retrieve the application_id by name, sparing developers from maintaining GUIDs manually—a common source of frustration and error.
At least we don’t have to hardcode Tenant ID. Use of the azurerm_client_config data source is perfect for this use case.
data.azurerm_client_config.current.tenant_id
This approach makes even root modules more portable, especially useful for teams that span multiple tenants. However, it’s important to understand the limitations of this symbolic contextualization. While tenant and subscription IDs are generally stable, the identity under which Terraform runs can vary — particularly between developers and CI pipelines. More on that in another article.
Conclusion
Using environment-specific variables and symbolic context in Terraform helps build secure, portable infrastructure with minimal operational overhead.
By combining dynamic Entra ID authentication with scoped permissions via a parameterized allowed_applications input variable, you gain fine-grained access control that scales across environments.
Avoiding static GUIDs and hardcoded settings ensures that your infrastructure remains maintainable and adaptable as team structures and environments evolve.