6. Integrations
RBAC is the least standalone thing in Azure. It has no purpose of its own — it exists to make other services reachable by the right callers, so every integration on this page is really an answer to the question "how do these two resources talk to each other without a secret?"
Two glue mechanisms recur so often that they're worth stating before the table, because between them they answer most of those questions:
Managed identity + a role assignment — the keyless way one Azure resource authenticates to another. The identity provides who, the role assignment provides may. This pairing replaces connection strings, account keys, and stored passwords across essentially the whole catalogue.
Private endpoint + Private DNS zone — the way one Azure resource reaches another without
traversing the public internet. Orthogonal to RBAC and frequently confused with it: a private endpoint
controls reachability, RBAC controls authorisation. A caller needs both, and neither implies the other.
Say which sub-resource the endpoint targets (blob, vault, sqlServer), because getting it wrong
produces a DNS resolution that succeeds and a connection that hangs.
[Image Prompt: 2D minimalistic hub-and-spoke diagram with Azure RBAC at the centre connected by labelled edges to Microsoft Entra ID, managed identities, Key Vault, Azure Storage, Azure Policy, Azure Kubernetes Service, Azure SQL Database, Azure Monitor, and Terraform, each edge labelled with its glue mechanism, flat design, clean vector art style, white background]
The pairings that matter
| Pairs with | Why | The glue |
|---|---|---|
| Microsoft Entra ID | Supplies every principal RBAC can reference; nothing works without it | The oid claim in the token is the object ID the role assignment points at. Group membership arrives as a token claim |
| Managed identities | Gives a workload an identity with no credential to leak | identity {} on the resource, then one narrow dataActions role assignment. Prefer user-assigned so replacement doesn't orphan the grant |
| Key Vault | Holds the secrets that remain after you've eliminated the avoidable ones | Set enableRbacAuthorization = true, then Key Vault Secrets User on the vault or a single secret. This retires access policies — the legacy parallel model |
| Blob Storage | The canonical control-plane/data-plane demonstration | Storage Blob Data Reader at container scope, plus allowSharedKeyAccess = false so keys stop being a bypass |
| Azure Policy | Enforces the shape of access that RBAC merely expresses | Policy at a management group auditing or denying Owner assignments at subscription scope; DeployIfNotExists to place a required assignment |
| Azure Monitor / Log Analytics | The only record of who granted what | A diagnostic setting routing the activity log to a workspace; KQL over AzureActivity for assignment writes |
| AKS | Two RBAC systems in one product, and they can be wired together | Azure RBAC for Kubernetes authorisation maps Azure role assignments to in-cluster permissions; workload identity federates a pod service account to a managed identity |
| Azure SQL Database | Database access without a password | Entra-only authentication, an Entra group as the database admin, and CREATE USER … FROM EXTERNAL PROVIDER for the workload's identity |
| GitHub Actions | The pipeline needs the most dangerous permission in Azure | Workload identity federation to an Entra app registration, granted Role Based Access Control Administrator — never Owner, never a client secret |
| Azure Functions / App Service | The most common consumer of a data-plane role | Managed identity plus a Key Vault reference in app settings, resolved at startup — which is why propagation timing matters here |
| Entra PIM | Adds the time dimension to a role assignment | roleEligibilityScheduleRequests through ARM, so eligibility is IaC and activation is an event |
| Terraform / the pipeline identity | The thing that manages RBAC is itself governed by RBAC | Federated credential scoped to one repo and branch; the identity holds roleAssignments/write and must not be able to widen itself |
Managed identity + role assignment, in full
The pattern behind most of the table. Three moving parts, and the ordering of the third is where people get caught.
# 1. The identity — user-assigned, so replacement of the consumer doesn't orphan the grant
resource "azurerm_user_assigned_identity" "app" {
name = "id-payments-prod"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
}
# 2. The consumer, wearing that identity
resource "azurerm_linux_function_app" "app" {
# …
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.app.id]
}
app_settings = {
# Tells the SDK which identity to use when several are attached
AZURE_CLIENT_ID = azurerm_user_assigned_identity.app.client_id
}
}
# 3. The grant — narrowest scope that works
resource "azurerm_role_assignment" "app_reads_secrets" {
scope = azurerm_key_vault.this.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_user_assigned_identity.app.principal_id
principal_type = "ServicePrincipal"
description = "Function app reads connection strings at startup"
}
AZURE_CLIENT_ID is the detail that saves an afternoon. When a resource has multiple identities
attached, DefaultAzureCredential cannot guess which to use and the failure is a confusing 403 rather
than a clear error. With one user-assigned identity it's optional; set it anyway.
On the consuming side there is no credential code at all:
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
client = SecretClient(
vault_url="https://kv-payments-prod.vault.azure.net",
credential=DefaultAzureCredential(),
)
conn = client.get_secret("db-connection-string").value
DefaultAzureCredential walks a chain — environment variables, managed identity, az login, and
others. Convenient, and occasionally maddening: it can succeed locally using your generous
permissions and fail in Azure where the managed identity has less. When debugging a 403, establish
which identity the token belongs to before touching roles.
Key Vault — the migration that makes RBAC real
Key Vault shipped with access policies: a per-vault list of principals and permitted operations, entirely separate from RBAC. It still works, and while it's enabled you have two authorisation systems on the same vault.
resource "azurerm_key_vault" "this" {
name = "kv-payments-prod"
resource_group_name = azurerm_resource_group.this.name
location = azurerm_resource_group.this.location
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
enable_rbac_authorization = true # retires access policies for this vault
purge_protection_enabled = true
soft_delete_retention_days = 90
}
Two things about this block are worth flagging.
enable_rbac_authorization = true is one-way in practice. Flipping it makes every existing access
policy inert immediately — so migrate by adding the equivalent role assignments first, verifying, then
flipping. Doing it in the other order is an outage.
purge_protection_enabled = true cannot be turned off. It's the right setting for production and it
has an RBAC-adjacent consequence: a deleted vault keeps its name reserved for the retention period, so a
recreate fails, and so does every role assignment your module wanted to make against it. The apply error
is about the vault; the incident gets reported as "RBAC broke".
The role mapping, since the names don't line up with the old verbs:
| Access-policy permission | RBAC role |
|---|---|
| Secrets: get, list | Key Vault Secrets User |
| Secrets: all | Key Vault Secrets Officer |
| Keys: crypto operations | Key Vault Crypto User |
| Certificates: manage | Key Vault Certificates Officer |
| Manage the vault resource itself | Key Vault Contributor — and it reads no secrets |
That last row is the control-plane/data-plane line in one sentence, in the service where it surprises people most.
Azure Policy — enforcing the access model
RBAC expresses who may do what; Policy is how you stop someone expressing the wrong thing. The three policies worth having, in rough order of value:
Audit or deny privileged assignments at subscription scope. Owner and User Access Administrator
at a subscription are the grants that matter. There are built-in definitions in this area ⚠️ verify the
current built-in set against current Azure docs.
Require the description field on new role assignments. Turns the drift query in
Deployment from heuristic to reliable: anything without a description wasn't made
by your pipeline.
DeployIfNotExists for assignments that must always exist. The security team's Security Reader
on every subscription, for instance — a remediation task places it on new subscriptions automatically,
so a new landing zone is governed on day one rather than when someone notices.
The interaction to remember: a DeployIfNotExists policy needs its own managed identity with enough
permission to make the assignment it deploys. Policy remediation is itself an RBAC principal, and it
needs roleAssignments/write — which means the policy assignment's identity is privileged and belongs
in the same review category as the pipeline's.
AKS — two RBAC systems, wired together
AKS is the most confusing integration because Kubernetes has its own RBAC and the names collide. Three independent things, and it's worth naming which is which:
| Layer | Governs | Configured by |
|---|---|---|
| Azure RBAC on the cluster resource | Managing the cluster: scale it, upgrade it, read the kubeconfig |
Role assignment on the AKS resource. Azure Kubernetes Service Cluster User gets you a kubeconfig and nothing inside |
| Azure RBAC for Kubernetes authorisation | Permissions inside the cluster, expressed as Azure role assignments | azure_rbac_enabled = true; then roles like Azure Kubernetes Service RBAC Reader scoped to the cluster or a namespace |
| Kubernetes RBAC | Permissions inside the cluster, expressed as Role/RoleBinding objects |
kubectl, and bypassed entirely by the admin kubeconfig |
resource "azurerm_kubernetes_cluster" "this" {
# …
azure_active_directory_role_based_access_control {
azure_rbac_enabled = true
admin_group_object_ids = [data.azuread_group.aks_admins.object_id]
}
local_account_disabled = true # retires the admin kubeconfig bypass
oidc_issuer_enabled = true # required for workload identity
workload_identity_enabled = true
}
local_account_disabled = true is the AKS equivalent of allowSharedKeyAccess = false. Until it's
set, az aks get-credentials --admin hands out a certificate that bypasses every Azure role assignment
on the cluster.
Workload identity is the pod-level half: a Kubernetes service account is federated to an Entra managed identity via the cluster's OIDC issuer, so a pod gets a token with no secret mounted. It replaces the older pod-identity approach ⚠️ verify current guidance and any deprecation timelines against current Azure docs.
The most common AKS RBAC failure worth pre-empting: AcrPull on the wrong scope. The cluster's
kubelet identity — not the cluster identity, and not the control-plane identity — needs AcrPull on the
registry. Three identities, similar names, and the symptom is ImagePullBackOff, which nobody
immediately reads as a role assignment problem.
Azure SQL Database — access with no password
-- Run as the Entra admin, against the target database
CREATE USER [id-payments-prod] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [id-payments-prod];
The important structural point: Azure RBAC does not grant access to data inside a SQL database.
There is no dataActions role for table reads. RBAC governs the server and database resources;
inside the database, permissions are SQL's own role model, granted to an Entra principal that RBAC never
sees. Two authorisation systems in sequence rather than in parallel, which is different from the
storage pattern and catches people who generalised from it.
Set azuread_authentication_only = true on the server to retire SQL logins, the same move as
allowSharedKeyAccess and local_account_disabled.
Azure Monitor — the only record of who granted what
Role assignment writes appear in the activity log, which is retained briefly by default and must be routed somewhere to be useful. Diagnostic settings are not on by default; this one is worth having on day one.
az monitor diagnostic-settings subscription create \
--name activity-to-law \
--location uksouth \
--workspace "$(az monitor log-analytics workspace show -g rg-platform-obs -n law-platform --query id -o tsv)" \
--logs '[{"category":"Administrative","enabled":true},{"category":"Security","enabled":true},{"category":"Policy","enabled":true}]'
Then the query that answers the audit question:
AzureActivity
| where OperationNameValue in~ (
"MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/WRITE",
"MICROSOFT.AUTHORIZATION/ROLEASSIGNMENTS/DELETE",
"MICROSOFT.AUTHORIZATION/ROLEDEFINITIONS/WRITE",
"MICROSOFT.AUTHORIZATION/ELEVATEACCESS/ACTION")
| where ActivityStatusValue == "Success"
| project TimeGenerated, OperationNameValue, Caller, CallerIpAddress,
scope = tostring(parse_json(Properties).entity)
| order by TimeGenerated desc
ELEVATEACCESS/ACTION in that list is the one to alert on unconditionally — it's a Global Administrator
granting themselves User Access Administrator at the root scope, which is legitimate as a break-glass
move and should never be silent. More in Production.
The two mistakes that span every integration
Granting the control-plane role and expecting data access. Storage Account Contributor for
something that reads blobs. Key Vault Contributor for something that reads secrets. Azure Service Bus Contributor for something that sends messages. All three fail, all three look like a bug, and the
fix is always a role with Data in the name.
Leaving the parallel path open. Every integration above has a legacy credential that bypasses RBAC entirely, and each has a switch:
az storage account update -g rg -n stapp --allow-shared-key-access false
az keyvault update -g rg -n kv-payments --enable-rbac-authorization true
az aks update -g rg -n aks-payments --disable-local-accounts
az sql server ad-only-auth enable -g rg -n sql-payments
Until those are set, your role assignments are documentation.
Next: Production →