Provisions the single, tenant-wide Power Platform tenant isolation policy — the inbound/outbound cross-tenant connection allow-list that governs every environment in the Entra tenant. Secure-by-default
is_disabled = true(inert/off), a caller-keyed exception map, and a single primary resource namedthis. Built for microsoft/power-platform ~> 4.1.
- 🏗️ Provisions one
powerplatform_tenant_isolation_policy— a tenant-wide singleton: there is exactly one of these per Entra tenant, and it takes noenvironment_idinput at all. - 🔒 Defaults
is_disabled = true(isolation OFF / inert) per this suite's secure-by-default convention for tenant-wide toggles — the caller must setis_disabled = falseexplicitly, understanding the blast radius, to begin enforcing cross-tenant connection restrictions. - 🗂️
allowed_tenantsis a caller-keyedmap(object({ inbound, outbound })), keyed by the external tenant's GUID (or the literal wildcard"*"for a default posture applied to every unlisted tenant) — rendered via a plainforexpression into the resource's required attribute-style set. ⚠️ This module ships a confirmed, exhaustively-reproduced upstream provider defect write-up — see "Schema notes that bite" and "Troubleshooting" below before relying on this module'sterraform validateoutput.
💡 Why it matters: tenant isolation is the single biggest lever for containing cross-tenant data exfiltration risk in Power Platform — but it is also, structurally, one of the highest blast-radius toggles in the entire catalog: flipping it on affects every environment and every user in the tenant simultaneously, with no report-only/dry-run mode.
If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:
- ⭐ Star this repository to help others discover this Terraform module.
- 🤝 Connect with me on LinkedIn: linkedin.com/in/microsoftexpert
- ☕ Buy me a coffee: buymeacoffee.com/microsoftexpert
Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!
graph TD
TENANT["Entra Tenant (implicit — this module IS a tenant-wide singleton)"]:::this
TIP["terraform-power-platform-tenant-isolation-policy (this module)"]:::this
ENV["terraform-power-platform-environment (all environments in the tenant)"]:::neutral
EXT["External Entra tenants (allowed_tenants exceptions)"]:::neutral
TS["terraform-power-platform-tenant-settings (sibling tenant-wide singleton)"]:::neutral
TENANT -->|"exactly one policy per tenant"| TIP
TIP -->|"governs inbound/outbound Power Platform connections for"| ENV
EXT -->|"allowed_tenants exception (tenant_id, inbound, outbound)"| TIP
TIP -.->|"no direct cross-reference — both tenant-wide singletons"| TS
classDef this fill:#742774,color:#fff,stroke:#742774;
classDef neutral fill:#E8E8E8,color:#222,stroke:#999;
graph TD
D1["var.is_disabled (default true — inert)"]:::input
D2["var.allowed_tenants (map keyed by tenant_id or wildcard)"]:::input
D3["var.timeouts"]:::input
R["powerplatform_tenant_isolation_policy.this"]:::this
O1["output: id"]:::output
O2["output: is_disabled"]:::output
O3["output: allowed_tenant_ids"]:::output
D1 -->|"is_disabled"| R
D2 -->|"allowed_tenants = toset([for tenant_id,v in var.allowed_tenants: {...}])"| R
D3 -->|"timeouts"| R
R -->|"id"| O1
R -->|"is_disabled"| O2
D2 -->|"{ for tenant_id,v in var.allowed_tenants: tenant_id => {...} }"| O3
classDef input fill:#E8E8E8,color:#222,stroke:#999;
classDef this fill:#742774,color:#fff,stroke:#742774;
classDef output fill:#E8E8E8,color:#222,stroke:#999;
Resource inventory: one resource, powerplatform_tenant_isolation_policy.this. No for_each
children — allowed_tenants is a required attribute-style collection on this same resource, not an
independent child.
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
microsoft/power-platform |
~> 4.1 |
| Provider block | None — the caller's root module configures authentication |
Schema notes that bite:
is_disabledandallowed_tenantsare BOTH required fields in the live schema (no provider-side default for either) — this module supplies its own secure defaults so the empty call produces the safe, inert resource.- The literal wildcard tenant id
"*"is a real, validallowed_tenantskey that sets a default inbound/outbound posture for every external tenant NOT otherwise listed — it is not a placeholder. - This is a tenant-wide singleton — there is exactly one tenant isolation policy per Entra tenant. Running this module from two different root configurations against the same tenant will fight over the same underlying policy.
⚠️ CONFIRMED UPSTREAM DEFECT, exhaustively reproduced this session (microsoft/power-platform 4.1.0):allowed_tenantsis implemented in the provider using a raw Go slice model type ([]tenant_isolation_policy.AllowedTenantModel) rather than a framework-awaretypes.Set. Terraform'svalidatecommand substitutes an unknown value of the declared type for every variable-sourced expression — standard, documented CLI behavior, independent of whether a default or-varoverride is supplied — and this resource's raw-slice model type cannot represent "unknown" for the whole collection. The result:terraform validate, run standalone against this module, fails with:
Error: Value Conversion Error
An unexpected error was encountered trying to build a value. This is always
an error in the provider....
Path:
Target Type: []tenant_isolation_policy.AllowedTenantModel
Suggested Type: basetypes.SetValue
This was reproduced across 10+ distinct HCL constructions this session — direct variable
assignment, for-expression, toset, local indirection, concat, JSON round-trip, set-
vs. list-typed variables, required-vs-defaulted variables, all fail identically. A single
scalar leaf value (e.g. one tenant_id sourced from a plain string variable inside an otherwise
fully-literal collection) does NOT trigger it — only the outer collection's cardinality/shape being
variable-derived does. This is not a house authoring defect: the equivalent variable-sourced
Set Nested Attributes on terraform-power-platform-data-loss-prevention-policy (same session, same
provider version) validate cleanly, proving the gap is specific to this resource's model
implementation. No HCL-only workaround exists that preserves a genuinely reusable (variable-driven)
module — see Troubleshooting below.
- Power Platform Administrator role on the authenticating principal (tenant-wide policy — no narrower, environment-scoped role applies here, unlike DLP policies).
- Confirm current Power Platform API app-registration permission scope names against the provider's Authentication guide at authoring/deployment time — Microsoft has renamed and restructured these scopes across provider versions.
- Power Platform Administrator role.
- No specific licensing beyond standard Power Platform administration capabilities.
- Confirm the full, current list of external tenant GUIDs that must be allow-listed BEFORE flipping
is_disabled = falsein a tenant with existing cross-tenant integrations — this policy has no "dry run"/report-only mode analogous to a Conditional Access policy.
terraform-power-platform-tenant-isolation-policy/
├── providers.tf # required_providers + required_version — no provider {} block
├── variables.tf # is_disabled, allowed_tenants, timeouts
├── main.tf # single resource "powerplatform_tenant_isolation_policy" "this"
├── outputs.tf # id, is_disabled, allowed_tenant_ids
├── README.md # this file
├── SCOPE.md # lightweight standalone scope
└── examples/ # runnable example configurations
module "tenant_isolation" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"11111111-1111-1111-1111-111111111111" = {
inbound = true
outbound = true
}
}
}ℹ️ The caller's root module configures the
powerplatformprovider block (Azure CLI, Service Principal + secret/certificate, OIDC, or Managed Identity) — this module never references authentication. See the confirmed upstream defect above before relying onterraform validatefor this module.
Consumes
| Input | Type | Source module |
|---|---|---|
| (none) | — | This is a tenant-wide singleton; it consumes no sibling-module outputs. allowed_tenants keys are external tenant GUIDs supplied directly by the caller. |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
Tenant isolation policy id — primary output. Exactly one per Entra tenant. | Informational / audit |
is_disabled |
Whether enforcement is currently disabled | Reporting; caller gating logic |
allowed_tenant_ids |
Map of tenant_id (or "*") => { inbound, outbound } |
Informational / audit |
1 · Minimal inert policy (default, no enforcement)
module "inert" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
}💡
is_disableddefaults totrue— the empty call produces a policy resource that enforces nothing. This is the safe default; no cross-tenant connections are affected.
2 · Enforcing isolation with a single allowed external tenant
module "isolated_with_partner" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"11111111-1111-1111-1111-111111111111" = {
inbound = true
outbound = true
}
}
}🔒 Setting
is_disabled = falsebegins enforcing inbound/outbound restrictions tenant-wide, immediately, for every environment in the tenant.
3 · Inbound-only exception (partner may connect in, not out)
module "inbound_only_partner" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"22222222-2222-2222-2222-222222222222" = {
inbound = true
outbound = false
}
}
}4 · Outbound-only exception (we may connect out, partner may not connect in)
module "outbound_only_partner" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"33333333-3333-3333-3333-333333333333" = {
inbound = false
outbound = true
}
}
}5 · Multiple partner tenants with mixed postures
module "multi_partner_isolation" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"11111111-1111-1111-1111-111111111111" = { inbound = true, outbound = true }
"22222222-2222-2222-2222-222222222222" = { inbound = true, outbound = false }
"33333333-3333-3333-3333-333333333333" = { inbound = false, outbound = true }
}
}6 · Wildcard default posture for all unlisted tenants (restrictive)
module "wildcard_restrictive" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"11111111-1111-1111-1111-111111111111" = { inbound = true, outbound = true }
"*" = { inbound = false, outbound = false }
}
}ℹ️ The
"*"key sets the DEFAULT posture for every tenant not explicitly listed above it — here, every unlisted tenant is fully blocked (inbound and outbound both false).
7 · Wildcard default posture for all unlisted tenants (permissive outbound only)
module "wildcard_outbound_permissive" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"*" = { inbound = false, outbound = true }
}
}
⚠️ Every unlisted external tenant may receive OUTBOUND connections from this tenant under this configuration, while none may connect IN. Confirm this matches the intended governance posture before applying — this is a tenant-wide change.
8 · Acquisition/merger scenario — several newly-trusted partner tenants at once
module "post_acquisition_isolation" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"44444444-4444-4444-4444-444444444444" = { inbound = true, outbound = true }
"55555555-5555-5555-5555-555555555555" = { inbound = true, outbound = true }
"66666666-6666-6666-6666-666666666666" = { inbound = true, outbound = true }
}
}💡 The keyed
map(object)design means adding a fourth acquired tenant later never reindexes or disturbs the first three entries.
9 · Removing a previously-allowed tenant (governance change)
module "partner_offboarded" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
# "11111111-1111-1111-1111-111111111111" removed — partner relationship ended.
"22222222-2222-2222-2222-222222222222" = { inbound = true, outbound = false }
}
}
⚠️ Removing an entry here is a tenant-wide governance change, not a routine infrastructure edit — confirm the offboarded tenant's integrations have been decommissioned first.
10 · Custom per-operation timeouts
module "slow_region_isolation" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"11111111-1111-1111-1111-111111111111" = { inbound = true, outbound = true }
}
timeouts = {
create = "15m"
update = "10m"
}
}11 · Explicitly re-affirming the inert default with a code comment (governance audit trail)
module "explicitly_inert" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
# Tenant isolation intentionally left disabled: no cross-tenant restriction requirement has been
# identified for this tenant as of this review cycle. Re-evaluate at the next security review.
is_disabled = true
}💡 Even though
is_disabled = trueis the default, restating it explicitly with a comment creates an auditable record of a deliberate decision, not an oversight.
12 · Preparing an allow-list before enabling enforcement (staged rollout)
module "staged_rollout" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
# Stage 1: populate the allow-list while still disabled, so the exception list is fully reviewed
# before flipping is_disabled = false in a follow-up change.
is_disabled = true
allowed_tenants = {
"11111111-1111-1111-1111-111111111111" = { inbound = true, outbound = true }
"22222222-2222-2222-2222-222222222222" = { inbound = true, outbound = false }
}
}ℹ️
allowed_tenantshas no effect whileis_disabled = true— this pattern lets a reviewer validate the exception list in a PR before a second change flips enforcement on.
13 · Full bidirectional trust with one partner, one-way with another
module "asymmetric_trust" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"77777777-7777-7777-7777-777777777777" = { inbound = true, outbound = true }
"88888888-8888-8888-8888-888888888888" = { inbound = true, outbound = false }
}
}14 · Disabling enforcement while preserving the allow-list for later re-enable
module "temporarily_disabled" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
# Temporarily disabled during a cross-tenant migration cutover; allow-list preserved so
# re-enabling later is a single is_disabled flip, not a full reconstruction.
is_disabled = true
allowed_tenants = {
"11111111-1111-1111-1111-111111111111" = { inbound = true, outbound = true }
}
}15 · 🏗️ End-to-end composition — tenant isolation policy alongside environment-scoped DLP governance
module "tenant_isolation" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-tenant-isolation-policy.git?ref=v1.0.0"
is_disabled = false
allowed_tenants = {
"11111111-1111-1111-1111-111111111111" = { inbound = true, outbound = true }
"*" = { inbound = false, outbound = false }
}
}
module "environment" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-environment.git?ref=v1.0.0"
display_name = "Claims Processing — Production"
location = "unitedstates"
environment_type = "Production"
dataverse = {
currency_code = "USD"
language_code = 1033
security_group_id = "8e6499a0-4b1b-4c2e-9f2b-6a9d3e2b7c11"
}
}
module "dlp_policy" {
source = "git::https://github.com/microsoftexpert/terraform-power-platform-data-loss-prevention-policy.git?ref=v1.0.0"
display_name = "Claims Processing DLP"
environment_type = "OnlyEnvironments"
environments = [module.environment.id]
}ℹ️ Tenant isolation and DLP are complementary, independent controls: tenant isolation governs WHICH external tenants may connect at all; DLP governs WHICH connectors may be combined once connected. Neither module references the other's outputs — both are wired here purely to show a realistic governance-layer composition.
| Group | Variables |
|---|---|
| Enforcement toggle | is_disabled (default true) |
| Exception list | allowed_tenants (keyed map, default {}) |
| Universal tail | timeouts |
Full variables.tf schema
variable "is_disabled" {
type = bool
default = true
}
variable "allowed_tenants" {
type = map(object({
inbound = bool
outbound = bool
}))
default = {}
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = {}
}| Output | Description | Sensitive/Conditional |
|---|---|---|
id |
Tenant isolation policy id (primary output) | — |
is_disabled |
Whether enforcement is currently disabled | — |
allowed_tenant_ids |
Map of tenant_id (or "*") => { inbound, outbound } |
— |
- Tenant-wide singleton, no
environment_id. Unlike almost every other module in this catalog, this resource does not scope to an environment at all — it is one policy per Entra tenant. allowed_tenantsis a required attribute-style Set on the single resource, rendered from a keyedmap(object)variable via a plainforexpression wrapped intoset— never adynamicblock (invalid for this schema shape) and never a separate child resource.- The
"*"wildcard key is a real provider value, not a house convention — it sets the default inbound/outbound posture for every external tenant not otherwise listed. Treat any change to a wildcard entry as equivalent in blast radius to changingis_disableditself. terraform validatecannot be made to pass cleanly for this module as long asallowed_tenantsis exposed as a genuinely caller-configurable variable, due to a confirmed upstream provider defect (see "Schema notes that bite" and "Troubleshooting"). This module keeps the correct, idiomatic, reusable design rather than hardcoding a fixed-shapeallowed_tenantspurely to force a greenvalidaterun.
| Default | Opt-out |
|---|---|
is_disabled = true (isolation OFF / inert) |
Caller sets is_disabled = false explicitly, with a comment acknowledging the tenant-wide blast radius |
allowed_tenants defaults to {} (no exceptions configured) |
Caller populates the map explicitly |
No tags variable (this provider has no tagging concept) |
N/A — not applicable to this provider |
No provider {} block, no credential-shaped variable |
Caller's root module configures auth |
cd terraform-power-platform-tenant-isolation-policy
terraform init -backend=false
terraform validate # see "Schema notes that bite" — currently fails due to a confirmed upstream defect
terraform fmt -checkPin consumption at ?ref=v1.0.0, never a branch. This is a plan-only authoring process — a human
runs terraform apply from CI after review. Given the tenant-wide blast radius and the confirmed
validate limitation above, require an explicit second reviewer for any change to is_disabled or
allowed_tenants before a human applies it.
terraform fmt -check passes cleanly and confirms the configuration is consistently formatted.
terraform validate, run standalone against this module, currently fails due to the confirmed
upstream provider defect described above — this is independent of whether the configuration is
correct; it reproduces for ANY variable-sourced allowed_tenants value, including the module's own
defaults. Neither check exercises whether the Power Platform API will actually accept a given tenant
GUID for your tenant; only a live plan/apply against a real tenant does that, and this module's
authoring process is plan-only and does not perform that exercise.
$ terraform output
id = "9f8e7d6c-5b4a-3210-fedc-ba9876543210"
is_disabled = false
allowed_tenant_ids = {
"11111111-1111-1111-1111-111111111111" = {
"inbound" = true
"outbound" = true
}
"*" = {
"inbound" = false
"outbound" = false
}
}
| Symptom | Cause | Fix |
|---|---|---|
terraform validate fails with Error: Value Conversion Error... Target Type: []tenant_isolation_policy.AllowedTenantModel... Suggested Type: basetypes.SetValue |
Confirmed upstream provider defect (microsoft/power-platform 4.1.0): allowed_tenants is modeled with a raw Go slice type that cannot represent Terraform's "unknown" placeholder, which validate substitutes for every variable-sourced value regardless of default or -var override |
This is not a house authoring defect and has no HCL-only fix that preserves a reusable module. Recommended next steps: (1) file/track an upstream issue against microsoft/terraform-provider-power-platform; (2) treat this module's terraform validate gate as a known limitation in CI until the provider ships a fix; (3) real terraform plan/apply against a live tenant with concrete literal inputs from a root caller was not verified in this plan-only authoring process — confirm behavior in a controlled non-production tenant before relying on it in CI. |
A terraform plan/apply in a real pipeline behaves differently than the standalone validate failure above |
Real root-module callers typically supply fully literal, concrete allowed_tenants values, which may follow a different internal evaluation path than this module's isolated, credential-free validate run |
Not independently confirmed by this authoring process (no live tenant access) — treat as an open question and verify in a sandboxed tenant before depending on it. |
Adding the "*" wildcard key unexpectedly changes behavior for many external tenants at once |
The wildcard is a real, special provider value that sets the default posture for every unlisted tenant | Treat any change to a "*" entry as equivalent in blast radius to flipping is_disabled — require the same review rigor. |
is_disabled = false was applied but no restrictions appear to be in effect |
allowed_tenants was left empty or only contains a permissive wildcard entry |
Review the exact allowed_tenants map — an empty map with is_disabled = false still enforces isolation, but a permissive "*" = { inbound = true, outbound = true } entry effectively allows every tenant. |
- microsoft/power-platform provider docs
terraform-power-platform-tenant-settings,terraform-power-platform-tenant-capacity(sibling tenant-wide singletons)- This module's
SCOPE.md
💙 "Infrastructure as Code should be standardized, consistent, and secure."