Web UI
A small web companion for day-2 operations: health dashboard, snapshot browsing, restores with WhatIf preview, verification, on-demand backups, certificate rotation, recovery-point export, and settings — secured by Entra ID sign-in with app roles. Tenant onboarding (app registrations, admin consent, certificate issuance) happens here too, in the browser Setup Wizard; see setup-wizard.md.
Architecture#
Everything below is provisioned by marketplace/bicep/ as part of the solution
template deployment, into the resource group you own. One Function App serves
both the SPA (a {*path} catch-all) and /api/*.
Browser
│ Function App (PowerShell 7.6, Linux App Service plan,
│ │ B1 default up to P1v3, Always On)
│ │ Bearer tokens (msal-browser PKCE); Easy Auth validates JWTs
│ │ App roles: Viewer / Operator / Admin
│ ▼
│ /api/* PowerShell functions (import ExternalIDBackup.psm1)
│ │ the SPA is served by the same app ({*path} catch-all)
│ │
│ │ managed identity:
│ │ Storage Blob Data Owner + Queue/Table Data Contributor
│ │ (keyless host storage requires all three)
│ │ A custom storage role limited to the lifecycle-management
│ │ policy — the retention setting's only control-plane call
│ │ Key Vault Certificates Officer (see the note below —
│ │ manages certificate OBJECTS, cannot export private keys)
│ │ EIDGuard Automation Operator (one custom role carrying
│ │ exactly the needed actions, including both
│ │ hybrid-worker read actions)
│ │ Website Contributor on its OWN site only, so the Setup
│ │ Wizard can flip authsettingsV2
│ ▼
└──── long-running operations run as Automation runbook jobs:
Backup-ExternalID · Restore-ExternalID-Job ·
Compare-ExternalID-Job · Rotate-ExternalID-Cert ·
Export-ExternalID-Job · Deploy-ExternalID-Networking ·
Raise-ExternalID-Immutability
(on the hybrid worker when private networking is deployed)
Automation account's own identity:
resource-group-scoped Automation Contributor, which is where its
jobs/read for the sibling-job guard comes from
Runbooks are bundled and published at deployment time by the deployment script identity, never at runtime.
Key Vault: what the web identity can actually do#
The web identity holds Key Vault Certificates Officer
(marketplace/bicep/modules/roles.bicep:177-186) — certificatecas/*,
certificates/*, certificatecontacts/write. It cannot export a
certificate's private key.
That is a statement about one specific action, so read it from the live role
definition rather than from the role name. A Key Vault certificate's private key
is retrieved by reading its backing secret, which requires
Microsoft.KeyVault/vaults/secrets/getSecret/action. Certificates Officer does
not list it, and certificates/* does not reach it, because RBAC action strings
do not prefix-match across resource types (the same rule as the
hybrid-worker actions).
Do not "simplify" this to Key Vault Certificate User. That role sounds
narrower and is not — read from the live role definitions:
| Role | dataActions |
Can export the PFX? |
|---|---|---|
Key Vault Certificates Officer (what the deployment grants) |
certificatecas/*, certificates/*, certificatecontacts/write |
No |
Key Vault Certificate User |
certificates/read, secrets/getSecret/action, secrets/readMetadata/action, keys/read |
Yes |
The two roles are different rather than nested: Certificates Officer is broader over certificate objects (create, import, update, delete — which browser onboarding needs) and narrower over secret material. Certificates Officer was granted deliberately, and the narrower-sounding alternative is the more dangerous one.
These docs have had this backwards before. An earlier version of this page described Certificates Officer as "Certificate User plus more". It is not, and the mistake points the wrong way — it makes the role that can export private keys look like the safe simplification. If you are about to change this grant, re-read both role definitions rather than their names.
What the code does is narrower still: the only web-tier Key Vault read is
Test-BackupCertificateExpiry -PublicOnly
(modules/ExternalIDBackup.psm1:224-228), which calls
Get-AzKeyVaultCertificate and uses .Expires. No web-tier path fetches a
private key.
Why the distinction is load-bearing. The vault holds the backup and restore
apps' certificates, and those certificates are those apps' Graph credentials.
Anything able to call getSecret on that vault could obtain the restore app's
PFX and authenticate to Microsoft Graph as an application holding .ReadWrite on
the customer tenant. The role boundary is what prevents it — not the absence of a
Graph permission, and not the fact that no code path tries.
Two read grants in the diagram are not decoration, and both are easy to lose by tidying a role definition:
- Web identity →
hybridRunbookWorkerGroups/read+hybridRunbookWorkers/read. The offline-worker probe (Get-HybridWorkerStatus) needs both, and neither Automation Operator built-in includes them. Without them the probe 403s and the guard silently protects nothing — in exactly the private-networking topology it exists for. The customEIDGuard Automation Operatorrole names them outright. - Automation identity →
jobs/readon its own account. The overlapping-run guard (Get-RunbookSiblingJob) reads jobs on the account it is running in. It reaches that through the resource-group-scoped Automation Contributor the identity holds for the networking feature. If that is ever narrowed, the guard fails open — it warns loudly when it does, andtests/RolesTemplate.Tests.ps1pins the grant.
Design decisions:
The API is a PowerShell Function App, and the SPA rides along on it. Azure Static Web Apps managed functions never support PowerShell, so a Function App was always going to serve
/api/*; having it serve the SPA too (via the{*path}catch-all) means one origin, one Easy Auth configuration and no CORS.The dedicated Always On plan is what removes cold starts. A scale-to-zero plan costs the first caller several seconds while the PowerShell worker spins up, which on a dashboard is the first thing anyone sees. The B1 plan is the single largest fixed cost in the deployment and buys exactly this.
The
KeepWarmtimer still runs, and still earns its place: it fires every 4 minutes and refreshes the health payload into a cache blob, so the Overview serves from one small read instead of chaining ARM and storage calls.No web API code path writes to a tenant. Restores and rotation execute inside Automation runbooks that fetch the restore app's certificate from Key Vault — the same trust boundary as the scheduled backup. The web identity holds no Graph permission of its own.
Writes that do reach an External ID tenant are made with credentials other than the web tier's identity. In the administrator's browser, on their own delegated token: onboarding and offboarding both work this way (
web/app/src/pages/Onboarding.tsxrequestsApplication.ReadWrite.AllandAppRoleAssignment.ReadWrite.All, and itsoffboard()issuesDELETE /applications/{id}with the same token), so those writes are made as the admin who is present. And inside an Automation runbook, on the restore app's certificate fetched from Key Vault. Neither writer is the web tier's own identity."No Graph permission" is not by itself the same as "cannot reach the tenant". Anything that can export the restore app's certificate holds that app's Graph credential, so the missing Graph permission would only add a hop. What closes the path is the Key Vault role: it cannot export private keys. Keep both halves of that argument together — see Key Vault: what the web identity can actually do.
Blob is the source of truth. The UI lists cloud snapshots and their manifests; reports produced by web-triggered jobs land under
backups/<tenant>/<stamp>/reports/.Export / offboarding: Configuration → Export recovery points starts
Export-ExternalID-Job, which copies every recovery point to a customer-owned storage account with the Automation managed identity (the customer grants it Storage Blob Data Contributor on the destination; AAD-only, no SAS or account keys) — so deleting a deployment does not destroy the backups. Re-runs are incremental (same path+length blobs are skipped).Webconfig: a sanitized projection of
settings.json(no secrets exist in it) is uploaded to the privatewebconfigcontainer byPublish-BackupWebConfigso the API knows the deployment shape. Deploy scripts refresh it whenever config changes; the rotation runbook patches thumbprints in place.
Deployment and access#
The deployment is created from the marketplace offer; there is no deploy script to run. First-run sign-in setup is the browser Setup Wizard — see setup-wizard.md, which also covers who needs which admin role.
The administrator who completes the Setup Wizard is auto-assigned Admin.
Assign everyone else in Entra ID → Enterprise applications →
EIDGuard-WebUI (<site>) → Users and groups (deployments set up before the
2026-09 rename carry ExternalID-Backup-WebUI (<site>) — setup runs once, so
the name never changes in place); the dashboard's Users page
is read-only by design.
| Role | Can |
|---|---|
| Viewer | Dashboard, snapshots, object browser, jobs, reports |
| Operator | + backup now, compare, restore previews, missing-only restores of users, identity providers, user attributes, custom extensions, API connectors, Conditional Access (new policies created disabled) and branding — including re-creating deleted user accounts, which can then sign in |
| Admin | + Full-mode (overwrite) restores, restores that include Groups, Applications, ServicePrincipals, UserFlows, Policies, TenantPolicies, GroupSettings, DirectoryRoles or AdministrativeUnits (see Restore modes), certificate rotation, settings, onboarding |
Settings writes — RPO, retention, verification, alert email and the time zone — are first-class UI features and always available to Admins.
Sign-in uses no secret at all. The Setup Wizard registers the dashboard as
an SPA client (web/api/SetupComplete/run.ps1:69) with no password
credential, and flips the Function App's Easy Auth to bearer token-validation
(:147), so the browser signs in with PKCE and the API validates the resulting
token. There is no client secret anywhere in the deployment — the confidential-
client flow that needed one belonged to the removed deploy script.
Backup schedule#
Configuration → Protection → Backup schedule is one row: Default RPO,
Time zone and Run hour (the Anchor hour for a sub-daily RPO), saved
together by one Save schedule button. A line under the row says what the
choice means. Only an Admin can save (PUT /api/settings/schedule); everyone
else sees the row read-only, with "Requires the Admin role."
Time zone#
The time zone is one deployment setting with two meanings:
- Every schedule runs in it. The backup schedule, the daily verification and, on a private deployment, the Hybrid Worker's start/stop window are all created in that zone. The run hour is a local hour of it — the picker reads "2:00 AM", with no UTC in brackets — and it stays at that local time through daylight saving: "Backups run at 2:00 AM New York time, all year." That is what a maintenance window usually needs.
- Every time on the dashboard is shown in it — next runs, job start and end
times, recovery points, drift reports and job logs, with the zone's short
name (for example
EDT) where a bare time could be ambiguous.
Not set (the default) keeps everything as it always was: schedules run in UTC, the run hour reads "02:00 UTC", and each viewer sees their own browser's zone. The line under the row then says where that UTC hour lands for you through the year — "Backups run at 02:00 UTC — 9:00 PM in winter, 10:00 PM in summer in New York" — and that choosing a time zone pins it instead.
Choosing a zone is a schedule change, saved with the rest of the row. It keeps
the hour number you picked (2 stays 2, now 2:00 AM local) and moves what runs
on the clock: the backup schedule and the verification schedule at the same
local hour, and — on a private deployment — the worker window around them.
Both job schedules carry the zone in their names in Azure Automation
(ExternalID-Backup-24h-02-America-New_York; no zone, no suffix — the names
you already have), so a move creates the new schedule beside the old one and
removes the old one only once the new one is linked; backups are never left
unscheduled. The worker's own start/stop schedules stay in UTC (see below). The time zone is recorded — and times
switch to it — only once all three have moved; if one did not, the page says
which and Save schedule again finishes it. Saving the schedule without
changing the zone never moves the clock. Zones are IANA names, spelled exactly; pick a
canonical one (Asia/Kolkata, not the older alias Asia/Calcutta) — a name the
server cannot place on a clock is refused with a message saying so. A version upgrade
keeps the zone (see
plans and limits).
What a local time means on the two days a year the clocks change:
- One gap is an hour longer, one an hour shorter. A daily backup is 25 hours after the previous one on the night the clocks go back (23 when they go forward); a 6-hour schedule has one 7-hour and one 5-hour gap. A tenant keeps its local slot rather than drifting to a new hour, and the Overview does not call the longer gap late. Only the clocks going back is allowed for: the shorter gap never lets a tenant go past its RPO.
- The Hybrid Worker (private deployments) stays on UTC, so its windows are about an hour longer. Its start and stop schedules never take the zone: each window is widened to cover both UTC instants a local time can fall on — 2:00 AM New York is 06:00 UTC in summer and 07:00 UTC in winter, so the VM runs from 05:45 to 09:00 UTC every day — and so it is up for every job whatever Azure does on the nights the clocks change. In a zone without daylight saving (Kolkata, for example) the window is the usual length; in one that moves by half an hour (Lord Howe), half an hour longer. The window also covers the times Azure itself reports for the schedules, and if a run Azure will fire is ever outside it, Needs attention says so ("The next backup is due while the Hybrid Worker VM is off") while the dashboard re-aims it. A zone whose Windows time zone keeps different clock changes (the server compares the two) cannot be chosen on a private deployment — the save says why; pick UTC or a nearby zone (private endpoints).
- A schedule saved just before a change starts at its next run — the same night — unless that night is the one whose run time the clocks skip: then it starts at the following night's, which is a real local time.
- A local time that does not exist (2:30 AM on the US spring-forward night) runs at the same moment read just before the jump — 3:30 AM.
- A local time that happens twice (1:30 AM on the night the clocks go back) backs each tenant up once, even if Azure fires it twice.
A deployment that chose a display-only zone before schedules could follow one keeps seeing times in that zone; its schedules stay on UTC (the line under the row says so) until the schedule is saved with a zone, which makes it both.
The Overview's backup calendar still counts UTC days; when another zone is shown, its heading and tooltips say "UTC days".
Recovery points#
A recovery point is one backup of one tenant: a read-only copy of its configuration, taken on the schedule or by Run backup. Open one from Recovery points to browse the captured files and objects, compare it, or restore from it.
Deletion protection (the immutable window)#
Recovery points are written under a version-level immutability policy. While that policy is locked, Azure refuses to delete or overwrite a recovery point for the window's length, counted from its capture — for anyone, a subscription Owner and Vambris included. Each point keeps the window that was in force when it was written: a locked window can be raised later but never lowered, so a point written before a raise keeps at least its shorter window, and a point written before the lock has no locked window at all. A recovery point's page shows the policy:
| Shown | Meaning |
|---|---|
| Locked policy · N days | Files written while this locked policy was in force cannot be deleted for N days after they were written. |
| Window ended | The point's captured files are older than today's window, so the locked policy no longer protects them. Retention and Azure permissions now decide when they go. |
| Not locked | A policy exists but was never locked, so a deployment did not finish. Azure still refuses deletes today, but anyone with full access to the subscription can shorten or remove the policy. Redeploying the template completes the lock. |
| Not immutable | No policy. Azure permissions alone govern deletion. |
| Not confirmed | The policy could not be read just now, so no protection is claimed. Reload to check again. |
The window belongs to each file, not to the recovery point as a whole: a report added later — by a comparison or a restore — is written under the point and keeps its own window, counted from when it was written.
Retention is a separate control: it decides when a recovery point is removed once its window allows it. The window also sets how long an uninstall has to wait; see Before you delete the deployment.
Raising the window#
The window ships at one day, so the permanent choice is never made for you at install. To lengthen it, an Admin opens Configuration → Protection, enters the new number of days in the Deletion protection section (it shows the current window, like Retention above it) and selects Apply. Viewers and Operators see the window read-only, and the API refuses a raise to them. Apply stays disabled while the policy cannot be read, and until the value is a whole number above the current window and within the limits below.
A raise can never be undone — not by you, not by Vambris, not by a subscription Owner. Azure lets a locked policy be extended but never shortened or removed, and only a limited number of times — so pick the window you want to keep rather than stepping up to it. Apply opens a warning that asks for the number of days to be typed again, and nothing is sent until it matches.
The dashboard does not change the policy itself. It starts the Raise-ExternalID-Immutability job, which appears on the Jobs page; the Protection page links to it while it runs and shows the new window when it completes, along with how many times the policy had already been extended. If the job refuses — for any of the reasons below — it checks everything before it touches the policy, so the page says nothing was changed and the job says why. If Azure refuses because the policy has already been extended as many times as it allows, the job says so, and the window stays where it is for good. If the job instead ends any other way (it failed or was stopped after the change was sent), the page does not guess: it re-reads the window from Azure and shows it.
- The new window must be more than the current one and at most 3650 days (ten years), the longest retention the dashboard can set.
- At most one raise a day. Counted from the policy's own record of extensions — so a raise within 24 hours of the last one is refused however that one was made, including by a redeploy.
- Retention must already be at least the new window — the default on Configuration → Protection and every tenant's own window on the Tenants page (0, keep forever, is always fine). The job also checks the storage lifecycle rules Azure actually runs. Otherwise Azure would be asked to delete recovery points it has promised to keep, so the raise is refused and the message names each retention to change first.
- The raise applies from now on, and Azure may also hold existing recovery points longer.
- Keeping recovery points longer costs a little more storage (about $0.02/GB a month; a tenant snapshot is typically well under a megabyte).
- Uninstalling waits longer. Azure will not delete the
backupscontainer while it holds a protected recovery point, so removing the deployment must wait until the newest one is older than the window.
The page shows the new window as soon as the job confirms it with Azure. Redeploying or upgrading never lowers it: the deployment keeps whichever is larger, the window Azure enforces or the one the template asks for.
Offboarded tenants#
An offboarded tenant's recovery points stay listed, marked offboarded, until retention removes them. You can browse them, but restoring or comparing needs the tenant onboarded again: offboarding deleted its backup and restore applications, so there is nothing to sign in to the tenant with.
Restoring from a recovery point#
Choose Restore on a recovery point. The wizard has four steps before
anything changes: pick the resource types, pick a mode, review a preview, then
type RESTORE to confirm. Resource types are restored in dependency order —
attributes and identity providers first, then users, groups and applications,
then user flows, Conditional Access and role assignments — so references
between them can be rewritten to the restored objects' new IDs.
Restore modes#
- Missing only (the default) recreates objects that no longer exist in the tenant. Objects that still exist keep their properties, but missing links to them are re-added: group members and owners; application owners, federated identity credentials and extension properties; application permissions granted to apps and other app role assignments (every service principal is captured, Microsoft Graph's included); user flow application links; and directory role assignments, Global Administrator included. Tenant-wide settings (the default branding, directory settings) are created only when the tenant has none, and are never overwritten. An Operator can apply it — unless the selection includes a type that can re-grant access (below).
- Full (disaster recovery) also overwrites existing objects with the recovery point's contents, tenant-wide settings included. An Operator can preview it; applying it needs Admin.
Applying a restore that includes Groups, Applications, ServicePrincipals, UserFlows, Policies, TenantPolicies, GroupSettings, DirectoryRoles or AdministrativeUnits needs Admin in either mode, because each can re-grant access even in Missing only: application permissions (including on Microsoft Graph), directory role assignments and custom role definitions, federated credentials and app owners, group members and owners (a role-assignable group carries directory roles), administrative-unit scoped role members, applications re-linked to an existing user flow (which reopens self-service sign-up into them), cross-tenant partner configurations with their inbound trust, custom permission grant policies (what users may consent to), and tenant-wide directory settings such as consent and group-creation settings. An Operator can still preview them.
What an Operator can apply is a Missing-only restore of the rest: users, identity providers, user attributes, custom authentication extensions, API connectors (created with a placeholder credential), Conditional Access (new policies are created disabled) and branding. That includes re-creating deleted user accounts, which is the core of a restore: a recreated account keeps the enabled state it was backed up with and can sign in — once an administrator resets its random password, or straight away with a social or other identity that needs no password. It gets no assigned group membership, role or app permission from that alone, though a dynamic-membership group's rule can still add it.
The API enforces this, not the page: web/api/ActionRestore/run.ps1 refuses the
apply with 403 using the list in Get-PrivilegedRestoreResourceNames
(web/api/Modules/WebApi/WebApi.psm1).
Conditional Access policies a restore has to create are created disabled. In Full mode, though, an existing policy takes its backed-up state — so a policy that was enabled in the recovery point is switched back on. Some things cannot be exported from Microsoft Graph at all — user passwords, application and identity-provider secrets, MFA registrations, branding images — so restored objects get placeholders, and the result lists each one under Manual actions required. An applied restore also saves its results and that checklist with the recovery point's reports.
Preview (WhatIf) runs#
Every restore starts with a preview: the same restore job, run against the live tenant with nothing written. A preview lists the existing objects it will keep and anything it skips. It does not list or count the objects it would create or overwrite, or the links it would re-add (the members, owners, credentials, permissions and role assignments above) — in Full mode, any object in the selected resource types that is not listed will be created if it is missing, or overwritten if it exists.
For property-level detail, the review step also shows the newest comparison of this recovery point against the live tenant that covers the selected resource types. If none does, run one from Drift first.
Verification and drift#
A comparison checks a recovery point against the live tenant, or against another recovery point, and reports what was added, modified or deleted. It uses the tenant's read-only backup application, so it never changes anything.
- Scheduled verification — when enabled under Configuration →
Protection, runs daily and compares each tenant's newest recovery point with
the live tenant. Differences do not fail the job; they raise the
driftalert (see Alerts that ship). - On-demand comparisons — Drift → Run comparison, or Compare on a
recovery point. These never raise the
driftalert; a comparison job that fails still raises the failed-job alert, like any other job.
Each report is kept with its recovery point. A report stores at most the first 500 differences; its counts include every difference, and the page says when the stored rows are only part of them.
Certificate rotation#
Rotate-ExternalID-Cert (Operations → Rotate…) rotates with zero downtime:
- issues a new self-signed version of the same Key Vault certificate (the private key never leaves the vault),
- adds it to the app registration's keyCredentials (authenticating as
the restore app, which holds
Application.ReadWrite.All), - verifies an app-only Graph token with the new certificate (retrying up to 10 minutes for key propagation),
- only then removes the old credential and disables old vault versions,
- updates the webconfig thumbprints.
If verification fails, the job rolls back: original keyCredentials are
restored and the previous vault version is re-imported as current, so every
consumer keeps using the proven-good certificate. Rotating both does the
backup app first and the restore app last (it bootstraps off its own
still-valid certificate). Local certificate-store copies on admin
workstations become stale by design — Key Vault-based auth picks up new
versions automatically.
Private networking from the UI#
POST /api/actions/deploy-networking (Admin). The Networking wizard (Configuration →
Private networking) collects the VNet choice — empty for a dedicated VNet
created inside the resource group, where the deployment identity already
has rights, or an existing VNet resource id after a one-click Network Contributor
grant made browser-direct with the signed-in admin's own ARM token — plus subnet
names/prefixes and worker VM size, and starts Deploy-ExternalID-Networking
(automation/runbook-networking.ps1, registered in
marketplace/bicep/modules/runbooks.bicep, allowlisted in
Get-WebRunbookNames). The runbook deploys the private endpoints for Key
Vault/storage/Automation — optionally the web app too — the hybrid worker, and
the data-plane lockdown. Deployment is estate-wide, so it takes the
deploy-networking/estate action lock and refuses to start while another such
job is running (409).
The worker's power jobs (Start-HybridWorkerVM / Stop-HybridWorkerVM) appear
on the Jobs page and in the Overview's recent operations, labelled Worker VM
power on / Worker VM power off, so a failed power-on shows up as a failed job.
They are listed through Get-WebVisibleRunbookNames
(web/api/Modules/WebApi/WebApi.psm1, used by JobList/JobDetail/JobOutput
and the health payload) but are not in Get-WebRunbookNames, so
Start-WebRunbookJob still refuses to start them. The one web-tier start of
Start-HybridWorkerVM is Sync-HybridWorkerWindow's direct
Start-AzAutomationRunbook call on a schedule save, which takes no runbook name
from the request.
A second, parameterless branch used to sit in front of this one for the legacy
self-host deploy, gated on webui.networkDeployEnabled. It was dead on arrival
(issue #33) — the flag was written false unconditionally and the
-EnableWebNetworkDeploy switch its error message told customers to pass never
existed — and it was removed along with the self-host deploy scripts.
Marketplace plan surface#
On solution-template deployments, GET /api/config carries an extra plan
object alongside the usual configuration projection:
{ "plan": { "name": "Standard", "tenantLimit": 5, "tenantCount": 2 } }
nameandtenantLimitcome from the verified licence when the deployment holds one — issued by the licensing service from your EIDGuard SaaS subscription at setup and refreshed every six hours — and otherwise from theEIDB_PLAN_NAME/EIDB_TENANT_LIMITFunction App settings stamped into the package, which describe the plan the package was deployed from (Get-ApiPlan: licence first, settings as the fallback).tenantCountis computed from the tenant list in the same response.tenantLimit0 means unlimited.- The plan object is attached to a shallow copy of the cached config — the
response never writes it back to the
webconfigblob. - The marketplace template is what writes the fallback settings, so the key is
absent only on a deployment that predates plan stamping. The SPA treats an absent
planon a successfully loaded config as "no plan configured" and hides the plan card; every other state (including an unlimited Enterprise plan) is treated as a capped-capable deployment, and a config refresh that fails then blocks onboarding rather than guessing (blockOnRefreshFailure,web/app/src/lib/plan.ts). Note that all of this is the SPA's display and pre-flight rule only — enforcement is server-side at the onboarding commit. - The SPA gates on it in three places that must not drift — the onboard button,
the pre-flight guard inside
onboard(), and the Configuration Plan card — all throughweb/app/src/lib/plan.ts.
Enforcement itself is server-side, at the onboarding commit only. See plans-and-limits.md for what the limit counts, what a downgrade does, and how to change plan.
Vambris announcements#
The megaphone icon at the top right, beside the Needs-attention bell, is
where announcements from Vambris appear — typically that a new EIDGuard version
is available. It is always there: with no announcement, selecting it says the
dashboard is up to date and shows the running version and build — but only
when the dashboard has recently reached the Vambris licensing service, which
is how announcements arrive; otherwise it says it cannot confirm that. When an
announcement arrives the icon gets a small dot until you read it; select it to
read the title and message, open the release notes or How to update (the
steps to redeploy the new version from the Azure Marketplace — EIDGuard does not
update itself), or Mark as read (which
removes the dot, remembered in this browser, per announcement — the
announcement itself stays there to reopen). It is carried
inside the signed licence that the existing six-hourly heartbeat to
activate.vambris.com already returns, so it adds no new outbound
connection and nothing to allow through a firewall, and it is shown only
from a licence that verifies as issued to this deployment and has not
expired. The title and message are rendered as plain text, never as HTML or
clickable links, and are refused if they contain a web address (:// or
www.). The announcement's one link is a separate button, and it can point
only at vambris.com (or a subdomain of it) or at one of four Microsoft
hosts: portal.azure.com, learn.microsoft.com,
azuremarketplace.microsoft.com and marketplace.microsoft.com. Treat the
wording itself the way you would any vendor message: it names Vambris, but a
plain-text sentence can still ask you to do something, and the product checks
where the link goes, not what the text says. Marking it as read removes the
dot in the browser you did it in; an update announcement also disappears on
its own once the update is installed, and is not shown at all
to a deployment already running that release or a newer one.
