PIM の Role Management Policy を作成する際の InsufficientPermissions を解消する

Assign Azure resource roles in Privileged Identity Management で言及されているように、Azure resource role に関する PIM の設定の管理者権限を付与するには subscription scope の Owner role や User Access Administrator role が必要です。

Users or members of a group assigned to the Owner or User Access Administrator subscription roles, and Microsoft Entra Global Administrators that enable subscription management in Microsoft Entra ID have Resource administrator permissions by default. These administrators can assign roles, configure role settings, and review access using Privileged Identity Management for Azure resources.

また、Azure portal 上で Owner role や User Access Administrator role を assign する際には次のような condition を指定することができます。

  • Allow user to only assign selected roles to selected principals (fewer privileges)
  • Allow user to assign all roles except privileged administrator roles Owner, UAA, RBAC (Recommended)
  • Allow user to assign all roles (highly privileged)

PIM の設定を Terraform で管理して、service principal に terraform apply させる場合、”highly privileged” を避けて “Recommended” となっている condition を選択したいところですが、この condition で User Access Administrator role を付与しても、role management policy の作成には InsufficientPermissions というエラーで失敗してしまいます。

というわけで、このエラーを回避するために試行錯誤した結果について説明します。

結論

試行錯誤の結果、Owner role や User Access Administrator role を付与する際に “Allow user to assign all roles except privileged administrator roles Owner, UAA, RBAC” を維持しつつ、role management policy を更新する権限を付与することはできないことがわかりました。
つまり、Allow user to assign all roles (highly privileged) を選択する必要があります。

何とも味気ない結論ですが、以降検証した内容について説明します。

最小再現コード

“Allow user to assign all roles except privileged administrator roles Owner, UAA, RBAC (Recommended)” の condition 付きで User Access Administrator role を付与した service principal で次の Terraform file を apply すると InsufficientPermissions のエラーになります。

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "= 4.71.0"
    }
  }
}

provider "azurerm" {
  features {}
}

data "azurerm_subscription" "current" {}

data "azurerm_role_definition" "owner" {
  name  = "Owner"
  scope = data.azurerm_subscription.current.id
}

data "azurerm_role_assignments" "this" {
  scope = data.azurerm_subscription.current.id
}

locals {
  owner_id = [for a in data.azurerm_role_assignments.this.role_assignments : a if a.role_definition_id == data.azurerm_role_definition.owner.role_definition_id && a.principal_type == "User"][0].principal_id
}

resource "azurerm_role_management_policy" "owner" {
  scope              = data.azurerm_subscription.current.id
  role_definition_id = data.azurerm_role_definition.owner.id

  activation_rules {
    approval_stage {
      primary_approver {
        object_id = local.owner_id
        type      = "User"
      }
    }
  }
}

具体的には次のようなエラーになります。

╷
│ Error: updating Scoped Role Management Policy (Scope: "/subscriptions/<subscription-id>"
│ Role Management Policy Name: "8e3af657-a8ff-443c-a75c-2fe8c4bcb635"): unexpected status 400 (400 Bad Request) with error: InsufficientPermissions: The requestor 681e20cf-3e2e-4352-bebd-7b9369bca77a does not have permissions for this request. Please use $filter=asTarget() to filter on the requestor's assignments.
│
│   with azurerm_role_management_policy.owner,
│   on main.tf line 29, in resource "azurerm_role_management_policy" "owner":
│   29: resource "azurerm_role_management_policy" "owner" {
│
│ updating Scoped Role Management Policy (Scope: "/subscriptions/<subscription-id>
│ Role Management Policy Name: "8e3af657-a8ff-443c-a75c-2fe8c4bcb635"): unexpected status 400 (400 Bad Request) with error: InsufficientPermissions: The requestor 681e20cf-3e2e-4352-bebd-7b9369bca77a does
│ not have permissions for this request. Please use $filter=asTarget() to filter on the requestor's assignments.
╵

このエラーメッセージからはわかりませんが、activity log を見ると Microsoft.Authorization/roleManagementPolicies/write action に失敗していることがわかります。

$ az monitor activity-log list \
  --caller 681e20cf-3e2e-4352-bebd-7b9369bca77a \
  --status Failed \
  --offset 24h \
  --query "[?contains(properties.responseBody, 'InsufficientPermissions')] | [0].{authorization: authorization, operationName: operationName, resourceId: resourceId, resourceType: resourceType}"
{
  "authorization": {
    "action": "Microsoft.Authorization/roleManagementPolicies/write",
    "scope": "/subscriptions/<subscription-id>/providers/Microsoft.Authorization/roleManagementPolicies/8e3af657-a8ff-443c-a75c-2fe8c4bcb635"
  },
  "operationName": {
    "localizedValue": "Write Role management policy",
    "value": "Microsoft.Authorization/roleManagementPolicies/write"
  },
  "resourceId": "/subscriptions/<subscription-id>/providers/Microsoft.Authorization/roleManagementPolicies/8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
  "resourceType": {
    "localizedValue": "Microsoft.Authorization/roleManagementPolicies",
    "value": "Microsoft.Authorization/roleManagementPolicies"
  }
}

当然 condition にはこの action に対する制限も /subscriptions/<subscription-id>/providers/Microsoft.Authorization/roleManagementPolicies/* に対する制限もありません。

検証

次のように様々な condition で User Access Administrator role を付与した service principal を用意します。

provider "azurerm" {
  features {}
}

data "azurerm_subscription" "current" {}

resource "random_uuid" "unknown" {}

locals {
  service_principals = {
    # Allow user to assign all roles (highly privileged)
    highly_privileged = {
      condition = null
    }
    # Allow user to assign all roles except privileged administrator roles Owner, UAA, RBAC (Recommended)
    # - 8e3af657-a8ff-443c-a75c-2fe8c4bcb635: Owner
    # - 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9: User Access Administrator
    # - f58310d9-a9f6-439a-9e8d-f62e7b41a168: Role Based Access Control Administrator
    recommended = {
      condition = <<-EOF
        (
          (
            !(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
          )
          OR (
            @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168}
          )
        )
        AND (
          (
            !(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'})
          )
          OR (
            @Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168}
          )
        )
      EOF
    }
    recommended_minus_delete_cond = {
      condition = <<-EOF
        (
          (
            !(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
          )
          OR (
            @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168}
          )
        )
      EOF
    }
    recommended_minus_write_cond = {
      condition = <<-EOF
        (
          (
            !(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'})
          )
          OR (
            @Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168}
          )
        )
      EOF
    }
    exclude_write = {
      condition = <<-EOF
        !(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
      EOF
    }
    exclude_some_role_requests = {
      condition = <<-EOF
        @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168}
      EOF
    }
    exclude_unknown_role_request = {
      condition = <<-EOF
        @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {${random_uuid.unknown.result}}
      EOF
    }
    recommended_rev = {
      condition = <<-EOF
        (
          (
            !(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
          )
          OR (
            @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168}
          )
          OR (
            Not Exists @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId]
          )
        )
        AND (
          (
            !(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'})
          )
          OR (
            @Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168}
          )
        )
      EOF
    }
    recommended_rev = {
      condition = <<-EOF
        (
          (
            !(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
          )
          OR (
            @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168}
          )
          OR (
            Not Exists @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId]
          )
        )
        AND (
          (
            !(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'})
          )
          OR (
            @Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168}
          )
        )
      EOF
    }
  }
}

resource "azuread_application" "this" {
  for_each = local.service_principals

  display_name = each.key
}

resource "azuread_service_principal" "this" {
  for_each = local.service_principals

  client_id = azuread_application.this[each.key].client_id
}

resource "azuread_application_password" "this" {
  for_each = local.service_principals

  application_id = azuread_application.this[each.key].id
}

resource "azurerm_role_assignment" "this" {
  for_each = local.service_principals

  principal_id         = azuread_service_principal.this[each.key].object_id
  role_definition_name = "User Access Administrator"
  scope                = data.azurerm_subscription.current.id

  condition = each.value.condition
}

# condition によっては read 権限すらなくなるので Reader role を付与する
resource "azurerm_role_assignment" "reader" {
  for_each = local.service_principals

  principal_id         = azuread_service_principal.this[each.key].object_id
  role_definition_name = "Reader"
  scope                = data.azurerm_subscription.current.id
}

output "client_id" {
  value = { for k, v in azuread_application.this : k => v.client_id }
}

output "client_secret" {
  value     = { for k, v in azuread_application_password.this : k => v.value }
  sensitive = true
}

terraform apply 後、次のように対象 service principal を変更しつつ最小再現コードに対して terraform apply してみます。

ARM_CLIENT_ID=$(terraform output -json client_id | jq -r .highly_privileged) \
ARM_CLIENT_SECRET=$(terraform output -json client_secret | jq -r .highly_privileged) \
ARM_TENANT_ID=$(az account show --query homeTenantId -o tsv) \
ARM_SUBSCRIPTION_ID=$(az account show --query id -o tsv) \
terraform -chdir=/path/to/reproducible-code-dir apply -auto-approve

その結果、最小再現コードの apply の成否は次のようになりました。

service principal terraform apply
highly_privileged 成功
recommended 失敗
recommended_minus_delete_cond 失敗
recommended_minus_write_cond 成功
exclude_write 失敗
exclude_some_role_requests 失敗
exclude_unknown_role_request 失敗
recommended_rev 成功

上記の結果から、role management policy を作成する際には次の条件が false になっていることがわかります。

!(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
@Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {8e3af657-a8ff-443c-a75c-2fe8c4bcb635, 18d7d88d-d35e-4fb5-a5c3-7773c20a72d9, f58310d9-a9f6-439a-9e8d-f62e7b41a168}
@Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals {${random_uuid.unknown.result}}

つまり次のことが言えます。

  • actioin は Microsoft.Authorization/roleAssignments/write
  • @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAllValues:GuidNotEquals には何を指定しても false

このことから、recommended_rev では recommended の条件に次の OR 条件を追加したところ、terraform apply に成功することがわかりました。

Not Exists @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId]

ところが、この条件を追加することで、Owner などの role も付与できるようになってしまうようで、実質 highly_privileged と同じになってしまうようです。

Exists operator の説明に関係するんでしょうが、Exists operator は RoleDefinitionId をサポートしていません。

また、次のどちらの条件を追加しても結果は同じでした。

  • Exists @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId]
  • Not Exists @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId]

おそらく、サポートしていない attribute に対して Exists operator を使用したことで、condition の評価をする際にエラーになって、無条件で condition を満たしているものとみなされるんじゃないかと思いますが、深追いはしていません。