Grants a principal eligible (activate-on-demand) rights to a role at a scope — a just-in-time, least-privilege alternative to a standing active assignment. Targets
hashicorp/azurerm ~> 4.0.
This module manages exactly one thing, well:
- 🎯 A PIM eligible role assignment — a principal becomes eligible to activate a role at a scope, rather than holding it as a standing, always-active grant.
- 🧑🤝🧑 Works with any principal — a user, an Entra group (preferred), a service principal, or a managed identity — referenced by object ID.
- 🧱 Works at any scope — management group, subscription, resource group, or an individual resource.
- ⏳ Supports a bounded eligibility window — a start time and an expiration expressed in days, hours, or an explicit end date.
- 🧾 Records governance metadata — a business justification, an optional ABAC condition, and an optional change-management ticket, so activation history and access reviews are meaningful.
💡 Why it matters: Standing (active) role assignments are permanent attack surface — the rights exist whether or not anyone is using them. An eligible assignment inverts that: the principal holds nothing by default and must activate the role just-in-time, leaving an audit trail each time. Time-boxing the eligibility and attaching a justification turns privileged access into a reviewed, expiring, accountable event instead of a forgotten grant.
If this module saves you time, please consider supporting its continued development:
- ⭐ Star the repository on GitHub.
- 🤝 Connect on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
This module pairs with the identity family: it consumes a principal, a role definition, and a scope, and it is
the just-in-time counterpart to the standing role-assignments module.
flowchart LR
rg["terraform-azurerm-resource-group"]
uai["terraform-azurerm-user-assigned-identity: NOT eligible-capable"]
rd["terraform-azurerm-role-definition"]
ra["terraform-azurerm-role-assignments"]
active["terraform-azurerm-pim-active-role-assignment"]
this["terraform-azurerm-pim-eligible-role-assignment"]
key["azurerm_pim_eligible_role_assignment.this"]
uai -->|"principal_id -- ACTIVE assignments only"| active
rd -->|"role_definition_id"| this
rg -->|"scope"| this
this -->|"manages"| key
this -.->|"standing active counterpart"| active
ra -.->|"non-PIM permanent alternative"| this
classDef me fill:#0078D4,color:#fff,stroke:#004578;
classDef keystone fill:#004578,color:#fff,stroke:#002B4D;
classDef sib fill:#EEF2F6,color:#1A1A1A,stroke:#B8C2CC;
class this me;
class key keystone;
class rg,uai,rd,ra,active sib;
One keystone resource, fed by three identifiers, producing eligibility that is activated on demand rather than held continuously.
flowchart LR
principal["Principal: user / group / service principal"]
scope["Target scope: MG / subscription / RG / resource"]
role["Role definition"]
this["azurerm_pim_eligible_role_assignment.this"]
jit["Just-in-time activation: bounded, on demand"]
principal -->|"principal_id"| this
role -->|"role_definition_id"| this
scope -->|"scope"| this
this -->|"eligible, not standing active"| jit
classDef me fill:#0078D4,color:#fff,stroke:#004578;
classDef keystone fill:#004578,color:#fff,stroke:#002B4D;
classDef sib fill:#EEF2F6,color:#1A1A1A,stroke:#B8C2CC;
class this keystone;
class principal,scope,role,jit sib;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_pim_eligible_role_assignment.this |
1 | The eligible role assignment (keystone). |
↳ ticket block |
0–1 | Change-management ticket recorded with the request. |
↳ schedule block (+ nested expiration) |
0–1 | The eligibility window (start and bounded end). |
↳ timeouts block |
0–1 | Create / read / delete operation timeouts. |
| Requirement | Value |
|---|---|
| Terraform floor | >= 1.12.0 |
| Provider | hashicorp/azurerm, pinned ~> 4.0 |
| Provider block | None in this module — the caller configures provider "azurerm" { features {} }, authentication, and subscription. |
Schema notes that bite (verified against the live provider schema):
- 🔁
scope,role_definition_id, andprincipal_idare force-new — changing any one replaces the eligible assignment. - 🚫 This resource type supports neither
tagsnorlocation— those variables are intentionally absent. - ⏱️ The
timeoutsblock supports create / read / delete only — there is no update timeout. - 🧮
principal_typeis computed by the platform, not an input. - ♾️ Omitting
schedule(or itsexpiration) yields permanent eligibility — supply a bounded expiration to time-box it. - ⚖️
schedule.expirationaccepts at most one ofduration_days,duration_hours, orend_date_time; the module rejects more than one at parse time. - 🔗
condition_versionis required by the provider only whenconditionis set.
User Access AdministratororOwnerat the targetscope— or a custom role carryingMicrosoft.Authorization/roleEligibilityScheduleRequests/writeplus the corresponding read actions.- Grant the caller's identity this permission at the smallest scope that satisfies the assignment (a resource group or resource rather than the whole subscription wherever possible).
- Microsoft Entra ID P2 (or a Microsoft Entra ID Governance license) on the tenant — PIM eligible assignments require it.
- The target
scope, theprincipal_id, and therole_definition_idalready exist and are resolvable by the caller's identity. - The caller configures the
provider "azurerm" { features {} }block, authentication, and subscription — the module declares none of these.
terraform-azurerm-pim-eligible-role-assignment/
├── providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
├── variables.tf # deeply-typed object() schemas; ticket + schedule/expiration; timeouts tail
├── main.tf # keystone azurerm_pim_eligible_role_assignment.this; dynamic blocks + try()
├── outputs.tf # id first, then principal_type and the resolved identifiers
├── README.md # this document
├── SCOPE.md # the cross-module contract
├── LICENSE # MIT, Copyright (c) 2026 Casey Wood
└── .gitignore # the canonical library ignore set
The smallest real call — make a principal eligible for a role at a scope. The caller configures the provider,
authentication, and the mandatory features {} block.
provider "azurerm" {
features {}
}
module "eligible_reader" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-platform"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" # Reader
principal_id = "11111111-1111-1111-1111-111111111111"
justification = "Standing read access replaced with just-in-time eligibility."
}🔒 Pin
?ref=v1.0.0— never a branch. This library is plan-only during authoring; a human applies from CI.
Consumes
| Input | Type | Typical source |
|---|---|---|
scope |
string |
terraform-azurerm-resource-group (id), a subscription / management-group ID, or any resource id |
role_definition_id |
string |
terraform-azurerm-role-definition or a built-in role ID |
principal_id |
string |
A user or group object ID from Entra ID. Not a managed identity or service principal — those cannot hold an eligible assignment; use terraform-azurerm-pim-active-role-assignment for them. |
Emits
| Output | Description |
|---|---|
id |
The eligible role assignment Resource ID (emitted first). |
principal_type |
Platform-resolved principal type (User / Group / ServicePrincipal). |
scope |
The scope at which eligibility is granted. |
role_definition_id |
The role the principal is eligible to activate. |
principal_id |
The principal made eligible. |
justification |
The recorded justification (provider-computed when not supplied). |
1 · Minimal call — eligible Reader at a resource group
module "eligible_reader" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" # Reader
principal_id = "11111111-1111-1111-1111-111111111111"
}ℹ️ With no
schedule, eligibility is permanent. Fine for a read-only role, but prefer a bounded window for anything that can change state.
2 · Contributor eligibility bounded by days
module "eligible_contributor" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" # Contributor
principal_id = "22222222-2222-2222-2222-222222222222"
justification = "Quarter-bounded contributor eligibility for the platform team."
schedule = {
expiration = {
duration_days = 90
}
}
}💡 A bounded expiration forces the grant to be re-affirmed on renewal — eligibility is reviewed, not forgotten.
3 · Short-lived eligibility bounded by hours
module "eligible_break_glass" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-prod"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" # Contributor
principal_id = "33333333-3333-3333-3333-333333333333"
justification = "Time-boxed incident-response eligibility."
schedule = {
expiration = {
duration_hours = 8
}
}
}🔒 Hour-scoped eligibility suits break-glass and on-call windows: the eligibility itself lapses quickly, not just the activation.
4 · Eligibility bounded by an explicit end date
module "eligible_until" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-migration"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" # Contributor
principal_id = "44444444-4444-4444-4444-444444444444"
schedule = {
expiration = {
end_date_time = "2026-12-31T23:59:59Z"
}
}
}
⚠️ Supply only one ofduration_days,duration_hours, orend_date_time— the module rejects more than one at parse time.
5 · Scheduled future start
module "eligible_onboarding" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" # Reader
principal_id = "55555555-5555-5555-5555-555555555555"
schedule = {
start_date_time = "2026-08-01T00:00:00Z"
expiration = {
duration_days = 180
}
}
}ℹ️ A future
start_date_timepre-provisions eligibility for a known onboarding date without granting it early.
6 · Permanent eligibility (documented exception)
module "eligible_permanent" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-shared"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" # Reader
principal_id = "66666666-6666-6666-6666-666666666666"
justification = "Permanent read eligibility for the audit team; reviewed annually."
}
⚠️ Omittingscheduleyields permanent eligibility. Prefer a bounded expiration; when you must go permanent, record why injustificationso access reviews have context.
7 · Ticketed justification
module "eligible_ticketed" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-prod"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" # Contributor
principal_id = "77777777-7777-7777-7777-777777777777"
justification = "Change-controlled contributor eligibility."
ticket = {
number = "CHG0012345"
system = "ServiceNow"
}
schedule = {
expiration = {
duration_days = 30
}
}
}🔒 Tying the grant to an approved change ticket strengthens the audit trail and satisfies change-management controls.
8 · ABAC-conditioned eligibility
module "eligible_conditioned" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe" # Storage Blob Data Contributor
principal_id = "88888888-8888-8888-8888-888888888888"
condition = "((!(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/write'})) OR (@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringEquals 'landing'))"
condition_version = "2.0"
schedule = {
expiration = {
duration_days = 60
}
}
}💡 An ABAC
conditionnarrows what the activated role can touch — an additional least-privilege lever.condition_versionis required wheneverconditionis set.
9 · Group principal (preferred over individual users)
module "eligible_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" # Contributor
principal_id = "99999999-9999-9999-9999-999999999999" # Entra group object ID
justification = "Team-level eligibility; membership governs who can activate."
schedule = {
expiration = {
duration_days = 90
}
}
}💡 Assign eligibility to a group and manage membership separately — activation history and reviews stay auditable, and joiners/leavers need no Terraform change.
10 · Management-group scope
module "eligible_mg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/providers/Microsoft.Management/managementGroups/mg-landing-zones"
role_definition_id = "/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" # Reader
principal_id = "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
justification = "Cross-subscription read eligibility at the management-group tier."
}
⚠️ Management-group eligibility is broad by nature. Keep the role narrow (Reader here) and time-box it.
11 · Subscription scope
module "eligible_subscription" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" # Contributor
principal_id = "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"
schedule = {
expiration = {
duration_days = 30
}
}
}🔒 Prefer the smallest scope that works — reach for a subscription-wide role only when a resource-group scope genuinely cannot serve.
12 · Custom role definition
module "eligible_custom_role" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-ops"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/cccccccc-cccc-cccc-cccc-cccccccccccc" # custom role
principal_id = "dddddddd-dddd-dddd-dddd-dddddddddddd"
justification = "Eligibility for a purpose-built least-privilege operations role."
schedule = {
expiration = {
duration_days = 90
}
}
}💡 A purpose-built custom role plus eligibility is the tightest combination — narrow permissions, held only on demand.
13 · Many eligible assignments with for_each
locals {
sub = "/subscriptions/00000000-0000-0000-0000-000000000000"
eligibilities = {
reader_app = {
scope = "${local.sub}/resourceGroups/rg-app"
role = "acdd72a7-3385-48ef-bd42-f606fba81ae7" # Reader
principal_id = "11111111-1111-1111-1111-111111111111"
}
contributor_ops = {
scope = "${local.sub}/resourceGroups/rg-ops"
role = "b24988ac-6180-42a0-ab88-20f7382dd24c" # Contributor
principal_id = "22222222-2222-2222-2222-222222222222"
}
}
}
module "eligible" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
for_each = local.eligibilities
scope = each.value.scope
role_definition_id = "${local.sub}/providers/Microsoft.Authorization/roleDefinitions/${each.value.role}"
principal_id = each.value.principal_id
schedule = {
expiration = {
duration_days = 90
}
}
}ℹ️ A stable map key per assignment keeps state addressing stable — adding or removing one entry never re-indexes the rest.
14 · Custom operation timeouts
module "eligible_timeouts" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app"
role_definition_id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" # Reader
principal_id = "eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee"
timeouts = {
create = "30m"
read = "5m"
delete = "30m"
}
}
⚠️ This resource has no update timeout — onlycreate,read, anddeleteare accepted.
15 · 🏗️ End-to-end composition
Create a resource group and a user-assigned identity, then make that identity eligible for the built-in Reader role on the group — a complete, wired example.
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-platform"
location = "eastus"
}
# A managed identity CANNOT hold an eligible assignment -- see the callout below. The principal
# here is a human, supplied as an object ID because this library is azurerm-only and does not
# manage Entra users.
variable "platform_oncall_group_object_id" {
description = "Object ID of the Entra GROUP whose members carry the on-call rotation."
type = string
}
data "azurerm_subscription" "current" {}
module "eligible_reader" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-pim-eligible-role-assignment.git?ref=v1.0.0"
scope = module.rg.id
role_definition_id = "${data.azurerm_subscription.current.id}/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7" # Reader
principal_id = var.platform_oncall_group_object_id
justification = "Just-in-time read eligibility for the platform on-call rotation."
ticket = {
number = "CHG0042000"
system = "ServiceNow"
}
schedule = {
expiration = {
duration_days = 90
}
}
}
output "eligible_assignment_id" {
value = module.eligible_reader.id
}💡 The role is referenced by the built-in role's stable GUID under the subscription — no standing active assignment is created anywhere in this composition.
Primary identity (all required, all force-new)
| Name | Type | Description |
|---|---|---|
scope |
string |
The scope at which the principal becomes eligible (MG / subscription / RG / resource ID). |
role_definition_id |
string |
The role the principal may activate (built-in or custom role ID). |
principal_id |
string |
The Entra object ID of the principal. A user, or a group whose members are users. Azure accepts a service-principal or managed-identity object ID here and creates the eligibility, but it can never be activated. |
Governance metadata (optional)
| Name | Type | Default | Description |
|---|---|---|---|
justification |
string |
null |
Business justification recorded with the assignment. |
condition |
string |
null |
Optional ABAC condition constraining the activated role. |
condition_version |
string |
null |
ABAC condition-language version; required when condition is set. |
ticket |
object |
null |
Change-management ticket (number, system). |
schedule |
object |
null |
Eligibility window (start_date_time, expiration). Omit for permanent eligibility. |
timeouts |
object |
null |
Create / read / delete timeouts (no update timeout for this resource). |
Full object() schemas
variable "scope" {
type = string # force-new
}
variable "role_definition_id" {
type = string # force-new
}
variable "principal_id" {
type = string # force-new
}
variable "justification" {
type = string
default = null
}
variable "condition" {
type = string
default = null
}
variable "condition_version" {
type = string
default = null
}
variable "ticket" {
type = object({
number = optional(string)
system = optional(string)
})
default = null
}
variable "schedule" {
type = object({
start_date_time = optional(string)
expiration = optional(object({
duration_days = optional(number)
duration_hours = optional(number)
end_date_time = optional(string)
}))
})
default = null
# validation: schedule.expiration accepts at most one of
# duration_days, duration_hours, end_date_time.
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
delete = optional(string)
})
default = null
}ℹ️ There is no
tagsvariable and nolocationvariable — this resource type supports neither.
| Output | Description | Notes |
|---|---|---|
id |
The eligible role assignment Resource ID. | Emitted first. |
principal_type |
Platform-resolved principal type (User / Group / ServicePrincipal). | Computed. |
scope |
The scope at which eligibility is granted. | |
role_definition_id |
The role the principal is eligible to activate. | |
principal_id |
The principal made eligible. | |
justification |
The recorded justification. | Provider-computed when not supplied. |
scope_kind |
managementGroup, subscription, resourceGroup or resource, derived from scope. |
Derived. Blast-radius signal. |
grants_no_standing_access |
Constant true. The principal holds nothing until it activates. |
Constant. |
eligibility_is_permanent |
Whether the eligibility itself carries no expiry. | Derived. |
activation_controls_live_elsewhere |
Constant true. MFA, approval, justification and duration are all on the role management policy. |
Constant. |
requires_per_user_pim_license |
Constant true. Entra ID P2 / Governance, counted per user. |
Constant. |
cannot_be_used_for_a_service_principal_or_managed_identity |
Constant true. |
Constant. |
eligibility_at_resource_scope_is_narrower_than_it_looks |
Constant true. The provider accepts resource scope; the portal's assignment path does not. |
Constant. |
- Eligible, not active. The whole point of this resource is that the principal holds nothing until it activates the role just-in-time. It is the least-privilege counterpart to a standing active assignment.
- Three force-new keys.
scope,role_definition_id, andprincipal_idare immutable — Terraform replaces the assignment if any changes. Treat these as the resource's identity. - No
tags, nolocation. Neither is part of this resource's schema, so neither is a module variable. This is one of the rare azurerm resources without the near-universaltagsfield. timeoutsis create/read/delete only. The resource has no update lifecycle, so there is no update timeout; the variable mirrors the schema exactly.- Permanent by omission. Leave
scheduleunset and eligibility is permanent. The module keeps this possible but steers you toward a boundedexpiration; the single-mechanism rule onexpirationis enforced at parse time so a conflicting window fails before any API call. - Computed fields render cleanly.
justification,start_date_time, and theexpirationfields are optional-and-computed; when omitted, thedynamicblocks andtry(...)guards render them as absent and the platform computes the effective value. features {}dependence. The provider will not initialize without a caller-sideprovider "azurerm" { features {} }block. That belongs to the root module — this module never declares a provider block.
The empty call already produces the least-privilege posture; every relaxation is something the caller must type.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Standing vs. just-in-time access | Eligible assignment (activate on demand) | use terraform-azurerm-role-assignments for a standing active grant |
| Eligibility window | bounded expiration recommended and enforced single-mechanism |
omit schedule for permanent eligibility |
| Accountability | justification and ticket available for the audit trail |
omit them |
| Blast radius | narrow scope + narrow role encouraged |
broaden the scope or role |
| Fine-grained limits | ABAC condition available |
omit the condition |
| Secret handling | none accepted or emitted | — |
# From the module folder — offline, no backend, no cloud calls:
terraform init -backend=false
terraform validate
terraform fmt -check- Pin
?ref=v1.0.0in thesource— never a branch. - This library is plan-only during authoring; a human runs
terraform plan/applyfrom CI against real credentials.
The offline proof gate is what this module guarantees:
terraform init -backend=falseresolves the pinnedazurerm ~> 4.0provider with no backend.terraform validateproves the configuration is type-correct against the provider schema — the deeply-typedobject()inputs surface a malformedscheduleorticketas a parse-time error, and theexpirationsingle-mechanism rule is checked here.terraform fmt -checkenforces canonical formatting.
What the gate does not do: it never calls Azure. Only terraform plan (run by a human from CI) resolves
the real scope, principal_id, and role_definition_id and exercises the PIM API.
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
eligible_assignment_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-platform/providers/Microsoft.Authorization/roleEligibilityScheduleRequests/9f3b2c1a-..."
principal_type = "ServicePrincipal"
| Symptom | Cause | Fix |
|---|---|---|
Error: Insufficient privileges on create |
Caller identity lacks User Access Administrator / Owner at the scope |
Grant the required role at the target scope (smallest that works). |
| Plan wants to replace the assignment | You changed scope, role_definition_id, or principal_id |
These are force-new; a change means a new eligible assignment by design. |
Error: Unsupported argument for tags or location |
This resource supports neither | Remove them — they are intentionally not module variables. |
Error: Unsupported argument for a timeouts.update |
This resource has no update timeout | Use only create, read, delete. |
Validation error on schedule.expiration |
More than one of duration_days / duration_hours / end_date_time set |
Supply exactly one expiration mechanism. |
| Eligibility never expires | schedule omitted → permanent eligibility |
Add a bounded schedule.expiration. |
condition_version required |
condition set without a version |
Set condition_version = "2.0". |
| Provider fails to initialize | Missing caller-side features {} block |
Add provider "azurerm" { features {} } to the root module. |
| PIM operation rejected at tenant level | Microsoft Entra ID P2 / Governance license not present | Enable the required license before creating eligible assignments. |
- Provider resource:
azurerm_pim_eligible_role_assignment - Azure built-in roles
- What is Privileged Identity Management?
- Sibling modules:
terraform-azurerm-role-definition,terraform-azurerm-role-assignments,terraform-azurerm-pim-active-role-assignment(the correct module for a managed identity or service principal),terraform-azurerm-resource-group. - This module's
SCOPE.md.
💙 "Infrastructure as Code should be standardized, consistent, and secure."