Skip to content
PagesPermission Matrix

EIDGuard docs

On this page

Permission Matrix

Backup App (Read-Only)#

Permission Description
User.Read.All Read all users
Group.Read.All Read all groups
Application.Read.All Read all app registrations
Policy.Read.All Read all policies (CA, auth methods, authorization)
IdentityUserFlow.Read.All Read user flow configurations
IdentityProvider.Read.All Read identity providers
EventListener.Read.All Read authentication events flows (user flows)
CustomAuthenticationExtension.Read.All Read custom auth extensions
Organization.Read.All Read organization/tenant settings
RoleManagement.Read.Directory Read directory role assignments
Directory.Read.All Read directory data (also covers group settings, administrative units, devices)
APIConnectors.Read.All Read API connectors (credentials are never exportable)
Policy.Read.PermissionGrant Read permission grant policies (Graph special-cases these out of Policy.Read.All)

Restore App (Read-Write)#

Permission Description
User.ReadWrite.All Create/update users
Group.ReadWrite.All Create/update groups and memberships
Application.ReadWrite.All Create/update app registrations
Policy.Read.All Read named locations & policies (Graph requires this for GET /namedLocations — Policy.ReadWrite.ConditionalAccess only covers writes)
Policy.ReadWrite.ConditionalAccess Create/update CA policies and named locations
Policy.ReadWrite.AuthenticationMethod Update authentication methods policy; create custom authentication strengths
Policy.ReadWrite.Authorization Update authorization policy
Policy.ReadWrite.CrossTenantAccess Update cross-tenant access policy (default + partners)
Policy.ReadWrite.ApplicationConfiguration Create token lifetime / claims mapping / token issuance / HRD / app-management / activity-based-timeout policies
Policy.ReadWrite.PermissionGrant Create permission grant policies
Policy.ReadWrite.ConsentRequest Update the admin consent request policy
IdentityUserFlow.ReadWrite.All Create/update user flows
IdentityProvider.ReadWrite.All Create/update identity providers
EventListener.ReadWrite.All Create/update authentication events flows
CustomAuthenticationExtension.ReadWrite.All Create/update custom auth extensions
Organization.ReadWrite.All Update organization settings
RoleManagement.ReadWrite.Directory Create/update role assignments
APIConnectors.ReadWrite.All Recreate API connectors (placeholder credentials — update after restore)
AdministrativeUnit.ReadWrite.All Create administrative units, membership, and scoped role assignments
Directory.ReadWrite.All Create group settings; create feature rollout policies

Who needs to be an administrator, and where#

Setup needs an administrator in two places. It can be one account (e.g. an external member/guest of the External ID tenant) or separate accounts per side — the browser asks you to pick an account at each sign-in.

Where Minimum access Used for
Workforce/billing tenant (hosts the subscription) Rights to deploy the deployment, plus an Entra admin who can create app registrations and assign app roles — Cloud Application Administrator or higher Deploying from the marketplace, then the one-time browser Setup Wizard that creates the dashboard's own sign-in app
Each External ID tenant Cloud Application Administrator minimum; in practice Privileged Role Administrator or Global Administrator, see below Tenant onboarding: creating the backup/restore app registrations and consenting to their application permissions

Notes:

  • Why more than Cloud Application Administrator is usually needed. The restore app requests RoleManagement.ReadWrite.Directory. Consenting to that particular application permission requires Privileged Role Administrator or Global Administrator; Cloud Application Administrator covers everything else in both sets.
  • Consent is granted during onboarding, as app-role assignments made with the signed-in admin's own delegated token. There is no separate "Grant admin consent" click in the Azure portal afterwards.
  • In a multi-tenant setup the Graph role is needed in every External ID tenant you onboard; the Azure side is needed once (all Azure resources live in the workforce tenant's subscription).
  • The Azure resources do not live in the External ID tenant — External ID tenants typically have no subscription.

Notes#

  • All permissions are Application type (not Delegated) and require admin consent
  • The backup app is intentionally limited to .Read.All scopes so it can never modify the tenant
  • The restore app is used only by restore paths — never by a backup run
  • Onboarding uses the signed-in administrator's Delegated permissions temporarily, in the browser, and stores nothing:
    • Application.ReadWrite.All — to create the app registrations
    • AppRoleAssignment.ReadWrite.All — to grant admin consent
  • The permission names themselves are served by web/api/TenantPermissions/run.ps1, which is the single definition of both sets. Keep it and this page in sync.

Tenant Feature Requirements#

  • authenticationEventsFlows (user flows) requires the EnableMsGraphAuthenticationEventListener feature flag
  • Available in global cloud only (not US Government or China operated by 21Vianet)

Azure RBAC — runtime versus deploy-time#

Microsoft Graph permissions are covered above; this section covers the Azure control plane, where the split that matters is runtime against deploy-time.

Every grant below is defined in marketplace/bicep/modules/roles.bicep and assigned inside the deployment's own resource group. The Key Vault side is covered in web-ui.md, and the full identity picture in the architecture diagram.

Identity Automation privilege Why
Function App (runtime) Custom role: job start/read/stop, schedule and jobSchedule management, runbook read, hybrid-worker-group and hybrid-worker read Starts jobs, manages the RPO schedule, and probes worker liveness. Cannot alter what the runbooks contain
Automation account (runtime) Contributor-class roles at resource-group scope, for the private-networking feature only Deploys private endpoints, DNS, NAT and the worker VM
Deployment script (deploy-time) Automation Contributor Publishes runbook content and prunes stale schedules during deployment and upgrade

The two hybrid-worker read actions#

The marketplace custom role (marketplace/bicep/modules/roles.bicep) carries both of these, and it needs both:

Action Why
Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/read Read the worker group — is this deployment routing jobs to a hybrid worker at all?
Microsoft.Automation/automationAccounts/hybridRunbookWorkerGroups/hybridRunbookWorkers/read Read the worker child resource, whose lastSeenDateTime heartbeat is the only evidence the worker is powered on

The child action is not implied by the parent. RBAC action strings do not prefix-match — a grant of …/hybridRunbookWorkerGroups/read authorizes that operation and nothing beneath it. The heartbeat lives only on the worker resource (the group object carries just groupType/credential), so without the second action the offline-worker probe 403s and the guard is inert: exports would be accepted and then queue forever against a powered-off worker. Both are read-only.

Runbook content is written only by the deployment-script identity, and only during a deployment: the Function App's custom role carries runbooks/read and no write.

Changed with the solution template. A deny assignment used to provide a second, independent bound here — its lockingPolicy.allowedActions omitted automationAccounts/runbooks/write, so runbook modification was refused even if an RBAC grant were reintroduced by mistake. There is no deny assignment in a customer-owned resource group, so the custom role is now the only bound. It is a real reduction in defence in depth, stated plainly rather than left implied: the grant itself is unchanged and still withholds runbook write, but nothing behind it would catch a mistake.

That custom role also deliberately excludes automationAccounts/variables/*: Automation variables can hold secrets, and runbooks receive their parameters at job start rather than reading them back.

Why withholding runbook write matters. Runbooks execute as the Automation identity, which holds Key Vault and storage access. Anything able to rewrite a runbook body could therefore run code as the more privileged identity — which is why the web tier's settings capability (RPO, retention, verification, alert email) is built from a narrow custom role rather than from a broad built-in such as Automation Contributor — which matters more now that the custom role is the only thing standing there. The same reasoning keeps Microsoft.Storage/storageAccounts/* off the web identity: it carries listKeys/action, and account keys would bypass the product's AAD-only rule for blob access entirely.

Every action name in the custom role was read from the provider (Get-AzProviderOperation "Microsoft.Automation/*") and confirmed by a real deployment. ARM validates these strings — an invalid one fails the deployment with InvalidActionOrNotAction, naming the offender — so a typo cannot produce a subtly wrong role.

Raising deletion protection — extend only#

The dashboard's Apply in the Deletion protection section of Configuration → Protection lengthens the locked immutability window on the backups container. Stated by mechanism:

  • Only the runbook extends the policy. The extend is one call, Set-AzRmStorageContainerImmutabilityPolicy -ExtendPolicy -Etag, in Invoke-ImmutableFloorRaise (modules/ExternalIDBackup.psm1), which runs in the Raise-ExternalID-Immutability runbook (automation/runbook-raise-immutability.ps1) as the Automation identity. That identity holds the custom role EIDGuard Immutability Extender (marketplace/bicep/modules/roles.bicep, the automationImmutabilityExtend assignment), scoped to the backups container only, with exactly two actions:

    Action Why
    Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies/read Read the current window and its ETag, which the extend must carry
    Microsoft.Storage/storageAccounts/blobServices/containers/immutabilityPolicies/extend/action Extend the locked policy

    No immutabilityPolicies/write (which can set or shorten an unlocked policy), no /delete, no /lock/action — those stay with the deployment-script identity (EIDGuard Immutability Locker, marketplace/bicep/modules/stagingroles.bicep), which only runs during a deployment.

    The runbook also needs three reads, granted to the same identity as built-in Reader (*/read only — no key listing, no data plane), in roles.bicep beside the Extender:

    Read Why Scope (assignment)
    Microsoft.Storage/storageAccounts/managementPolicies/read The live lifecycle rules the new window is checked against. An account-level resource, which a container-scoped grant cannot reach Storage account (automationStorageReader)
    Microsoft.Storage/storageAccounts/blobServices/containers/read The container record that carries the policy's update history, which the daily limit is counted from Storage account (automationStorageReader)
    Microsoft.Automation/automationAccounts/jobs/read Its own job record, and whether another raise is still running Automation account (automationJobsReader)
  • The web tier cannot change the policy. The Function App identity holds immutabilityPolicies/read (the EIDGuard Retention Manager role, for the window the dashboard displays) and no write, extend, lock or delete on it. What it can do is start the runbook, with a number: the Admin-only handler web/api/ActionRaiseImmutability/run.ps1 (Assert-ApiRole -Role Admin) runs the same checks early for a fast answer, then calls Start-WebRunbookJob.

  • The runbook bounds that number, and the web tier cannot change the runbook. Its Automation role carries runbooks/read and no write (SEC-03). So whatever the web tier sends, the runbook allows only: 1–3650 days, strictly above the window Azure enforces now, a policy that is Locked, and at most one extension per 24 hours, counted from the policy's own update history (Get-ImmutabilityExtendHistory). Azure writes that history for every extension, whoever makes it and however the job that asked ended — a job stopped midway, or one that failed after Azure applied the extend, is still in it — and the web tier cannot write the policy at all, so it cannot forge or avoid it. The runbook refuses if the history cannot be read. The runbook also refuses a window longer than any finite retention — in the stored configuration and in the live lifecycle rules — but that is a correctness guard, not a security bound: the web tier can also change retention.

  • Worst case for a compromised web tier: one extension a day, to at most 3650 days. Recovery points are kept longer than intended — storage cost and a longer wait to uninstall — and never deleted sooner. Because Azure allows a locked policy to be extended only a limited number of times, repeated raises could also use those extensions up; the daily limit slows that down.

Stated plainly, as roles.bicep does: the Extender role and the two Reader grants are what the raise is designed to rely on, but they are not today the Automation identity's only route to any of it. That identity also holds Storage Account Contributor and Automation Contributor at resource-group scope for private networking (automationNetworkingRoles); the first's Microsoft.Storage/storageAccounts/* includes write, delete and lock on the policy. Narrowing those networking grants is recorded outstanding work; the explicit grants keep the raise working when it happens.

Right after an upgrade that adds these grants, the assignments can take a few minutes to apply; a raise started in that window refuses with an authorization error, changes nothing, and can simply be retried.

Grants the customer makes outside the deployment#

Everything above is assigned inside the deployment's own resource group, by the deployment. Two optional features need a role on a resource the deployment does not own, so the customer assigns them — and only these two.

Feature Role Role definition id Granted to Scope
Export recovery points Storage Blob Data Contributor ba92f5b4-2d11-453d-a403-e96b0029c9fe The Automation account's system-assigned managed identity The customer's destination storage account
Private networking into an existing VNet Network Contributor 4d97b98b-1d4f-4787-a291-c67834d212e7 The same Automation managed identity The customer's existing virtual network

Both share one flow (web/app/src/lib/armGrant.ts): the dashboard checks whether the identity already holds the role at that scope, checks whether the signed-in administrator is allowed to make the assignment, and then performs it browser-direct with the administrator's own ARM token. The deployment never holds the right to grant itself access to a customer's resources. Assigning either by hand in the portal works identically.

Neither grant is required to run the product: exporting is opt-in, and private networking can instead use a dedicated VNet created inside the deployment's resource group, where the deployment's identity already has rights and the customer grants nothing at all.

Export destination — Storage Blob Data Contributor#

Exporting recovery points (Configuration → Export recovery points) copies every snapshot, manifest and report to a storage account outside the deployment, so that deleting the deployment does not destroy the backups. The copy is performed by the Automation account's managed identity, AAD-only — the repo hard rule holds here as everywhere: no SAS tokens, no account keys.

GET /api/export/info (Admin) returns the principal id to grant, fetched live from the Automation account rather than from a stored copy — a stale value would have the customer grant the wrong principal.

The API refuses any destination that belongs to the deployment itself, because those accounts die with it and an export into one would look like a successful evacuation while protecting nothing. Two different rules decide that, and it is worth knowing which applies to you:

Account Refused when
The backup storage account Provenance says the deployment created it (storageAccountCreated)
The web-runtime account Provenance says the deployment created it (storageCreated)

Function-host environment variables identify accounts the deployment uses, not accounts it owns. An adopted customer account can hold webconfig or Function App host data and still be a valid destination because it survives teardown. The Marketplace API is the exception: packaged plans carry EIDB_PLAN_NAME, so only there are the host environment names treated as deployment-owned. The runbook also checks the configured source account's known resource group directly and refuses it when the group's managedBy relationship identifies a managed app; this works with the Automation identity's resource-group-scoped permissions. The actual runbook source account is authoritative; a stale webconfig account name cannot replace it. Its group comes first from the non-secret, storage-specific ExternalID_StorageResourceGroup deployment setting, with webconfig as a compatibility fallback. If neither is available for a source-account destination, the direct runbook path fails closed instead of relying on a subscription-wide resource listing the identity cannot perform. It also fails closed if the subsequent resource-group lookup is inconclusive; only a confirmed unmanaged group can make the configured source account eligible on self-host.

Separately, the backup container is refused as a destination even when the account is allowed — copying it onto itself achieves nothing.

Existing VNet — Network Contributor#

The private-networking wizard (Configuration → Networking) offers two modes. Dedicated creates the VNet inside the resource group and needs no grant at all. Existing targets a VNet the customer already runs, and the Automation identity needs Network Contributor on it to join subnets, attach private endpoints and link private DNS zones.

GET /api/network/info (Admin) returns the principal id the same way export/info does, alongside the current network state. The grant is scoped to the selected VNet resource — not the subscription or resource group — and the wizard re-checks it whenever the selected VNet changes, so a grant made against a previously selected VNet is never mistaken for one covering the new choice.

Every resource the feature creates lands in the resource group — the private endpoints and their NICs, the private DNS zones and their VNet links, the NAT gateway and its public IP, and the worker VM — so deleting the deployment removes them and leaves the customer's VNet itself intact. It does not leave it untouched: the two sections below cover what the runbook changes inside that VNet and what remains there afterwards.

What existing-VNet mode changes in your VNet#

Give EIDGuard three subnets of its own. The wizard's defaults — snet-eidb-pe, snet-eidb-hw, snet-eidb-app — are chosen to be unlikely to collide, and the safest thing you can do is leave them that way and let the runbook create all three. The rest of this section is what happens when they are not exclusively ours.

The runbook resolves each subnet by name: it creates one that is missing, and adopts one that already exists under that name. Adoption is silent, and it is the same code path whether the subnet was left by an earlier run or built by you for something else that happens to share the name.

Subnet If the runbook creates it If it already exists
snet-eidb-pe Created with private endpoint network policies disabled Adopted as-is — not modified
snet-eidb-hw Created, then associated with the deployment's NAT gateway by default Adopted; an attached NAT gateway is reused, or the deployment gateway is attached when none exists (unless you opt out)
snet-eidb-app Created delegated to Microsoft.Web/serverFarms, then associated with the NAT gateway by default Adopted; an attached NAT gateway is reused, or the deployment gateway is attached when none exists (unless you opt out). The delegation is not added, which fails the deployment unless the subnet already has it (see below)

The runbook never replaces a NAT gateway already attached to an adopted worker or app subnet. It reuses each subnet's existing gateway independently, including a gateway in another resource group; that gateway remains customer-owned and is not deleted with EIDGuard. When a selected subnet has no gateway, the default is to create the deployment-owned gateway and attach it. You may turn that off when a 0.0.0.0/0 route already sends traffic through an NVA, Azure Firewall, or another egress service. Without one of those paths, the worker and dashboard may not reach the Internet services they require.

Deleting the deployment cannot detach a deployment-owned NAT association from a subnet across the resource-group boundary. A NAT-less subnet to which EIDGuard attached its managed gateway can therefore retain a stale reference after deletion. A customer NAT that EIDGuard reused is unaffected. See private-endpoints.md for the same note from the networking side.

An adopted app subnet must already be delegated to Microsoft.Web/serverFarms. The runbook adds that delegation only on a subnet it creates; adoption returns the existing subnet untouched in shape. The dashboard's regional VNet integration requires the delegation, so if the adopted subnet lacks it the integration call is rejected and the runbook throws (Site VNet integration failed (HTTP …)). It does not silently half-work.

The failure lands at a recoverable point. Subnets, any required NAT gateway and associations, the private DNS zones and the private endpoints already exist by then; the data-plane lockdown step has not run, so Key Vault and storage are still publicly reachable and the dashboard still answers. Fix the delegation and re-run — every step is idempotent.

Practical guidance: either leave the app subnet's configured name unused so the runbook creates and delegates it, or delegate your existing subnet to Microsoft.Web/serverFarms before starting. Note that the delegation is exclusive in practice — a subnet delegated to App Service is not usable for arbitrary workloads — which is another reason to give EIDGuard its own.

So: if you point the wizard at subnet names that carry other workloads, their existing NAT egress is preserved. Dedicated subnets still avoid shared-lifecycle questions entirely.

What existing-VNet mode leaves behind#

The runbook cannot put subnets in the resource group — a subnet is a child of the VNet, and in this mode the VNet is yours. Deleting the resource group removes everything else (private endpoints and NICs, private DNS zones and their links, any EIDGuard NAT gateway and public IP, the worker VM), but the three subnets stay — along with the app subnet's Microsoft.Web/serverFarms delegation, the private-endpoint subnet's disabled network policies, and any association EIDGuard added to its managed gateway.

Empty subnets cost nothing. They do hold address space; the delegation means that subnet cannot host arbitrary workloads until it is removed; and the stale NAT association must be cleared before either subnet can route outbound through anything else.

Cleaning up by hand — check before you delete. The deployment does not record which subnets it created and which it adopted, and by deletion time that information is gone with it. Treat the three names as candidates, not as a list to remove:

  1. For each of the three, confirm it is genuinely ours — nothing else is attached to it, and it is not a subnet you recognise from before the deployment. A subnet with any other NIC, endpoint or delegation in it is not ours to delete.
  2. Clear a stale NAT association only where the subnet referenced EIDGuard's deleted eidb-natgw. Do not remove a customer NAT gateway the deployment merely reused.
  3. Remove the Microsoft.Web/serverFarms delegation from the app subnet before trying to delete it; a delegated subnet will not delete.
  4. Delete only the subnets that passed step 1.

Azure refuses to delete a subnet that anything is still attached to, which is a useful backstop — but it is not a substitute for step 1, because an adopted subnet that happens to be empty right now will delete without complaint. If in doubt, leave the subnet: an empty subnet costs nothing, and the delegation is removable on its own.

Names differ if they were overridden in the wizard. This is one of the deletion story's loose ends — the things a resource-group delete does not reach. The full list, and the order to take them in, is plans-and-limits.md. In short: the backup and restore app registrations in each External ID tenant (offboarding removes them; nothing else does), the dashboard's own EIDGuard-WebUI (<web app name>) registration in the workforce tenant (delete it by hand), and the five custom role definitions the deployment creates (EIDGuard Retention Manager, EIDGuard Automation Operator, EIDGuard Upgrade Cleanup, EIDGuard Immutability Extender in marketplace/bicep/modules/roles.bicep, and EIDGuard Immutability Locker in marketplace/bicep/modules/stagingroles.bicep): a role definition is a subscription-level object even when its only assignable scope is the resource group. Every measured teardown removed them with the group all the same, but the read plane can keep listing them for a while and a straggler is possible, so check after the group is gone. Telling a straggler from a stale read, and clearing one, is covered in deployment-troubleshooting.md. Everything else the product creates is inside the deployment's resource group and cascades — once the backups container has been emptied, which Azure only allows after each recovery point's immutable window has passed. That page covers the purge, and exporting the recovery points first.