Day 1 Challenge — Terraform Deployment Identity
Day 1 Challenge — Terraform Deployment Identity
So far, most actions were performed with your own user account. That is useful for learning, but in real environments Terraform is usually executed by a dedicated deployment identity.
In Azure, this is often a service principal or another workload identity used by a CI/CD pipeline.
Challenge goal
Configure Terraform to deploy the lab environment using a dedicated identity.
The first attempt should fail.
That failure is intentional.
You will give Terraform enough access to create resources, but not enough access to create Azure RBAC role assignments.
This teaches an important lesson:
Terraform may need permissions to deploy resources and permissions to assign access.
Mental model
Part 1 — Create a deployment identity
Create a service principal with Contributor on the lab resource group.
az ad sp create-for-rbac `
--name "sp-cloudsec-terraform-<your-name>" `
--role Contributor `
--scopes /subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>The command returns values such as:
{
"appId": "...",
"password": "...",
"tenant": "..."
}Warning
The password is a secret.
Do not commit it to Git, paste it into screenshots, or share it.
Screenshot suggestion
Screenshot suggestion
Add a screenshot of the service principal.
Suggested location:
Microsoft Entra ID → App registrations → sp-cloudsec-terraform-<your-name>Show:
- display name
- application/client ID
- tenant ID
Do not show secrets.
Part 2 — Configure Terraform authentication
PowerShell
$env:ARM_CLIENT_ID="<APP_ID>"
$env:ARM_CLIENT_SECRET="<PASSWORD>"
$env:ARM_TENANT_ID="<TENANT_ID>"
$env:ARM_SUBSCRIPTION_ID="<SUBSCRIPTION_ID>"Bash
export ARM_CLIENT_ID="<APP_ID>"
export ARM_CLIENT_SECRET="<PASSWORD>"
export ARM_TENANT_ID="<TENANT_ID>"
export ARM_SUBSCRIPTION_ID="<SUBSCRIPTION_ID>"Run Terraform:
terraform init
terraform plan
terraform applyPart 3 — Observe the failure
Terraform may create some resources successfully.
It should fail when it reaches resources such as:
resource "azurerm_role_assignment" "..." {
...
}You may see an error mentioning:
Microsoft.Authorization/roleAssignments/writeor:
AuthorizationFailedThe important lesson:
Contributor can create many resources,
but Contributor cannot assign Azure RBAC roles.Screenshot suggestion
Screenshot suggestion
Add a screenshot of the failed Terraform apply.
Suggested screenshot:
Terminal → terraform applyHighlight the error around:
azurerm_role_assignment- missing permission
- role assignment write failure
Do not include secrets or environment variables.
Fix options
There are several ways to fix the deployment.
Each option has a different security trade-off.
Option A — Simple lab fix: Owner
Assign Owner to the Terraform deployment identity on the lab resource group.
az role assignment create `
--assignee <APP_ID> `
--role Owner `
--resource-group <RESOURCE_GROUP_NAME>Run Terraform again:
terraform applyWhy this works
Owner can manage resources and assign access.
Why this is risky
Owner is powerful.
A compromised deployment identity with Owner can change resources and grant access to other identities.
Use this only when the lab must continue quickly and the scope is limited.
Option B — Better separation: Contributor + RBAC Administrator
Keep Contributor for resource deployment.
Add Role Based Access Control Administrator for role assignments.
az role assignment create `
--assignee <APP_ID> `
--role "Role Based Access Control Administrator" `
--resource-group <RESOURCE_GROUP_NAME>Run Terraform again:
terraform applyWhy this is better
This separates two responsibilities:
Contributor = resource management
RBAC Administrator = access managementThe identity is still powerful, but the model is clearer than using Owner.
Option C — Custom roles
A more advanced option is to use custom roles.
You can split Terraform permissions into two custom roles:
| Custom role | Purpose |
|---|---|
| Custom lab resource deployer | Deploys the lab resources |
| Custom RBAC assignment manager | Creates the required role assignments |
The model becomes:
Terraform deployment identity
+ Custom lab resource deployment role
+ Custom RBAC assignment manager roleOption C mental model
Custom RBAC assignment manager
This role allows Terraform to manage role assignments.
Example:
{
"Name": "CloudSec Terraform RBAC Assignment Manager",
"IsCustom": true,
"Description": "Allows Terraform to manage Azure RBAC role assignments for the CloudSec lab.",
"Actions": [
"Microsoft.Authorization/roleAssignments/read",
"Microsoft.Authorization/roleAssignments/write",
"Microsoft.Authorization/roleAssignments/delete",
"Microsoft.Authorization/roleDefinitions/read"
],
"NotActions": [],
"DataActions": [],
"NotDataActions": [],
"AssignableScopes": [
"/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>"
]
}Create the role:
az role definition create `
--role-definition cloudsec-terraform-rbac-assignment-manager.jsonAssign it:
az role assignment create `
--assignee <APP_ID> `
--role "CloudSec Terraform RBAC Assignment Manager" `
--resource-group <RESOURCE_GROUP_NAME>More precise RBAC assignment scopes
The RBAC assignment manager does not always need to be scoped to the full resource group.
You can scope it to the actual resources where Terraform must create role assignments.
For this lab, that could be:
| Terraform must assign | Scope for RBAC assignment permission |
|---|---|
Contributor to backend identity | Backend App Service |
Key Vault Secrets User to backend identity | Key Vault |
This gives a tighter model:
Terraform deployment identity
+ Resource deployment role at resource group scope
+ RBAC assignment manager at App Service scope
+ RBAC assignment manager at Key Vault scopeTerraform examples
The intentional starting point can be scoped to the backend App Service:
resource "azurerm_role_assignment" "backend_app_contributor" {
scope = azurerm_linux_web_app.backend.id
role_definition_name = "Contributor"
principal_id = azurerm_linux_web_app.backend.identity[0].principal_id
}The fixed Key Vault permission stays scoped to the Key Vault:
resource "azurerm_role_assignment" "backend_key_vault_secrets_user" {
scope = azurerm_key_vault.main.id
role_definition_name = "Key Vault Secrets User"
principal_id = azurerm_linux_web_app.backend.identity[0].principal_id
}This still supports the lab:
| State | Role | Scope | Expected result |
|---|---|---|---|
| Before fix | Contributor | Backend App Service | /api/impact-demo/tag-self works |
| Before fix | No Key Vault role | Key Vault | /api/secret-demo fails |
| After fix | Contributor removed | Backend App Service | /api/impact-demo/tag-self fails |
| After fix | Key Vault Secrets User | Key Vault | /api/secret-demo works |
Assign the custom RBAC role at resource scope
Get the App Service resource ID:
az webapp show `
--resource-group <RESOURCE_GROUP_NAME> `
--name <BACKEND_APP_NAME> `
--query id `
--output tsvAssign the custom RBAC role at the App Service scope:
az role assignment create `
--assignee <APP_ID> `
--role "CloudSec Terraform RBAC Assignment Manager" `
--scope <BACKEND_APP_SERVICE_RESOURCE_ID>Get the Key Vault resource ID:
az keyvault show `
--resource-group <RESOURCE_GROUP_NAME> `
--name <KEY_VAULT_NAME> `
--query id `
--output tsvAssign the custom RBAC role at the Key Vault scope:
az role assignment create `
--assignee <APP_ID> `
--role "CloudSec Terraform RBAC Assignment Manager" `
--scope <KEY_VAULT_RESOURCE_ID>Custom resource deployment role
To also replace Contributor, create a second custom role for resource deployment.
This role should include only the resource provider actions needed by the lab.
The lab may need permissions for:
- Resource Groups
- App Service Plans
- Linux Web Apps
- Storage Accounts
- Key Vaults
- Log Analytics Workspaces
- Application Insights
- diagnostic settings
- identity configuration
A simplified starting point:
{
"Name": "CloudSec Terraform Lab Resource Deployer",
"IsCustom": true,
"Description": "Allows Terraform to deploy the Azure resources required for the CloudSec lab.",
"Actions": [
"Microsoft.Resources/subscriptions/resourceGroups/read",
"Microsoft.Resources/subscriptions/resourceGroups/write",
"Microsoft.Web/serverfarms/read",
"Microsoft.Web/serverfarms/write",
"Microsoft.Web/serverfarms/delete",
"Microsoft.Web/sites/read",
"Microsoft.Web/sites/write",
"Microsoft.Web/sites/delete",
"Microsoft.Web/sites/config/read",
"Microsoft.Web/sites/config/write",
"Microsoft.Storage/storageAccounts/read",
"Microsoft.Storage/storageAccounts/write",
"Microsoft.Storage/storageAccounts/delete",
"Microsoft.Storage/storageAccounts/listKeys/action",
"Microsoft.KeyVault/vaults/read",
"Microsoft.KeyVault/vaults/write",
"Microsoft.KeyVault/vaults/delete",
"Microsoft.OperationalInsights/workspaces/read",
"Microsoft.OperationalInsights/workspaces/write",
"Microsoft.OperationalInsights/workspaces/delete",
"Microsoft.Insights/components/read",
"Microsoft.Insights/components/write",
"Microsoft.Insights/components/delete",
"Microsoft.Insights/diagnosticSettings/read",
"Microsoft.Insights/diagnosticSettings/write",
"Microsoft.Insights/diagnosticSettings/delete"
],
"NotActions": [],
"DataActions": [],
"NotDataActions": [],
"AssignableScopes": [
"/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>"
]
}Warning
This custom role is a starting point.
Terraform may require additional permissions depending on the exact resources, provider behavior and future lab changes.
Additional Challenge — Custom role for the backend app
If you finish early, design a custom role for the backend managed identity.
Use minimal guidance.
The goal is to answer:
What is the smallest useful role the backend app needs?
Your task
Design a custom Azure role for the backend app.
Think about:
- what the app actually needs to do
- which Azure resource it needs to access
- whether this is management-plane or data-plane access
- which actions are required
- which actions should be excluded
- the narrowest useful scope
- how you will test the role
Role design flow
Role skeleton
{
"Name": "CloudSec Backend App Custom Role",
"IsCustom": true,
"Description": "Custom role for the backend application identity in the CloudSec lab.",
"Actions": [],
"NotActions": [],
"DataActions": [],
"NotDataActions": [],
"AssignableScopes": [
"/subscriptions/<SUBSCRIPTION_ID>/resourceGroups/<RESOURCE_GROUP_NAME>"
]
}Warning
Do not assume every permission belongs in Actions.
Azure separates management-plane permissions from data-plane permissions.
Test your role
Test both positive and negative behavior.
| Test | Expected result |
|---|---|
| Backend performs required app function | Works |
| Backend modifies unrelated resource | Fails |
| Backend reads data it should not read | Fails |
| Backend manages access or role assignments | Fails |
The expected outcome is not one perfect role.
The expected outcome is a reasoned access design.
Compare the options
| Option | Works? | Simplicity | Security fit |
|---|---|---|---|
| Contributor only | No | Easy | Too limited for role assignments |
| Owner | Yes | Easy | Broad |
| Contributor + RBAC Administrator | Yes | Medium | Better separation |
| Contributor + custom RBAC role | Yes | Advanced | More precise access management |
| Custom resource role + custom RBAC role | Maybe, after testing | Advanced | Closest to least privilege |
Reflection questions
- Why did Terraform fail with only
Contributor? - What is the difference between deploying resources and assigning access?
- Why is a Terraform identity more sensitive than an app identity?
- What is the risk of using
Owner? - Why is
Contributor + RBAC Administratorclearer thanOwner? - Why is scoping RBAC assignment permissions to the actual resources better?
- What makes custom roles harder to maintain?
- What did you choose for the backend app role, and why?
Cleanup
Remove extra access when it is no longer needed.
az role assignment delete `
--assignee <APP_ID> `
--role "<ROLE_NAME>" `
--resource-group <RESOURCE_GROUP_NAME>Optionally remove the service principal:
az ad sp delete `
--id <APP_ID>If you created custom roles:
az role definition delete `
--name "CloudSec Terraform RBAC Assignment Manager"az role definition delete `
--name "CloudSec Terraform Lab Resource Deployer"az role definition delete `
--name "CloudSec Backend App Custom Role"Final takeaway
Key takeaway
Terraform does not only need permission to create resources.
If Terraform also manages Azure RBAC role assignments, the deployment identity needs permission to manage access.
That makes the deployment identity privileged, so it must be scoped carefully.
A good deployment identity is:
specific to the deployment
limited to the required scope
powerful enough to deploy
not broader than necessary
removable after use
monitored when active