Recovery CLI
The recovery CLI is a break-glass tool. It runs from the Modgud image itself (next to the server or in a one-off container), using the configured database connection — there's no network surface, no auth bypass. It exists for the situations where the admin UI cannot help: no admin can sign in, the projections desynced after a schema change, an old OAuth client needs to be migrated onto a linked Service Account, etc.
Most invocations write an entry to the security audit log (see Audit trail below); a few read-only commands don't.
Entry point
dotnet Modgud.Api.dll recover <command> [args...] [--realm <slug>]Running it with Docker
The image's entrypoint is dotnet Modgud.Api.dll, so arguments after the image name go straight to Modgud. When they start with recover, the container runs that one command and exits with its exit code; it never starts the web server. There are two ways to use it:
Next to the running server, for most commands (
bootstrap-admin,reset-2fa,magic-link,list, …):bashdocker exec <container> dotnet Modgud.Api.dll recover <command> [args...]In a one-off container while the server is stopped, for commands that must not race a running server (
rebuild-projections).compose runuses the service's environment, volumes and network, publishes no ports, and--rmremoves the container afterwards:bashdocker compose stop modgud docker compose run --rm modgud recover rebuild-projections docker compose start modgudWithout Compose, start the same image with the same
-e/-v/--networkoptions:docker run --rm … <image> recover rebuild-projections. With more than one replica, stop all of them first.
Where you cannot pass a command at all (Portainer and similar), use STARTUP_COMMAND.
Tenant-scoped commands infer the realm only when exactly one active realm exists. With multiple realms, --realm <slug> is required. With zero realms, only deployment-wide commands such as install-link can run.
For tenant-scoped commands the named realm is resolved up front:
- A misspelled or unknown
--realmfails fast witherror: Realm '<slug>' not found.and a non-zero exit code — it never silently acts on the wrong tenant. - When
--realmis omitted and more than one realm exists, the command fails and asks for an explicit target. With a single realm the target is unambiguous and stays quiet.
Every command exits 0 on success and a non-zero code on failure (a validation error, an unknown realm, or an unknown command); error text is written to stderr.
Commands
install-link
Issue the short-lived, single-use authorization for the initial installation. This command works while the deployment has zero realms. The browser installation form and CI both submit the resulting token to /api/install/complete.
dotnet Modgud.Api.dll recover install-link \
--base-url https://auth.example.com \
--minutes 30
# Machine-readable final output line for CI
dotnet Modgud.Api.dll recover install-link \
--base-url https://auth.test.localhost \
--minutes 10 \
--jsonIssuing a new link revokes older unconsumed links. The plaintext token is shown only in CLI output; the Global Store contains its SHA-256 hash. See First-time setup.
list
List every active user with UserName · Email · Active · Admin · 2FA · Passkeys.
dotnet Modgud.Api.dll recover listAdmin means the user holds realm:admin (typically via the System Admin role inside the seeded Administrators group).
reset-2fa <username>
Disable TOTP and Email-OTP, delete every stored passkey credential, and clear the grace-period stamp so the user gets a fresh secure-setup window on next login. It also ends every existing access of the account: browser sessions, issued tokens and staffing sessions (consent grants stay), so whoever held the old factors is signed out and the account signs in afresh.
dotnet Modgud.Api.dll recover reset-2fa aliceset-email <username> <new-email>
Update the user's email and append a UserUpdatedEvent so projections
- SignalR-driven admin grids refresh live.
dotnet Modgud.Api.dll recover set-email alice alice@example.commagic-link <username>
Issue a one-time magic-link URL and print it to stdout. Useful for nudging a locked-out user back in without resetting their password. The link replaces the password only: a second factor the sign-in policy requires is still asked for (run reset-2fa first when the user lost it), and an account under an administrative lock is not signed in.
dotnet Modgud.Api.dll recover magic-link alicerebuild-projections
Rebuild all Marten projections (inline + async). Bootstrap path for the first migration after a breaking schema change — runs without any admin authentication.
The set is the same as the admin rebuild endpoint's, and both print or return it: the views (UserView, inbox items), the Principal subtypes, roles, Apps, OAuth clients, scopes and APIs, login providers, external identity links, position grants, terminal enrollments, staffing sessions and the audit view (which replays archived events too, so erased users' de-identified audit rows survive). Earlier releases rebuilt only the views, the Principal subtypes and the roles, although it said "all".
Rebuilding empties the read models and replays them from the events. A running Modgud instance keeps writing the same projections and their progress meanwhile, so stop every instance and take a database backup first, then run the command in a one-off container (see Running it with Docker). The command refuses when it finds other live instances in the cluster node table and names them; --force runs it anyway. If the node table cannot be read, it prints a warning and continues. While an admin can still sign in, the admin rebuild endpoint is the simpler route: it pauses the projection processing of its own instance. Do not run it with Modgud v0.9.1: that version can remove Principal subtypes from their shared table during replay. Upgrade to a newer patch release first. The fixed command rebuilds Person and Group together while preserving directly stored Service Accounts.
docker compose stop modgud
docker compose run --rm modgud recover rebuild-projections # --force to override the check
docker compose start modgudbootstrap-admin
Create or recover an admin in an existing realm. Two modes — Direct (password set immediately) and Invite (a magic-link URL is printed and emailed if SMTP is configured).
# Direct mode
dotnet Modgud.Api.dll recover bootstrap-admin \
--email admin@example.com \
--username admin \
--firstname Admin \
--lastname User \
--password 'ChangeMe1!'
# Invite mode (no --password)
dotnet Modgud.Api.dll recover bootstrap-admin \
--email admin@example.com \
--username adminFlags:
| Flag | Required | Notes |
|---|---|---|
--email | yes | Email — required in both modes. |
--username | no | Defaults to the local-part of the email. |
--firstname | no | Optional. |
--lastname | no | Optional. |
--password | no | If present: Direct mode. Validated against the configured Identity password rules. If absent: Invite mode. |
--realm <slug> | when multiple realms exist | Inferred when exactly one active realm exists. |
migrate-cc-credentials
For every OAuth client that still has the client_credentials grant without a linked Service Account, auto-provision a Service Account named legacy.{clientId} and backfill the link so the standard SA-managed mutation guard applies.
Idempotent — already-linked clients are skipped; existing legacy.* SAs are re-used.
dotnet Modgud.Api.dll recover migrate-cc-credentials --realm acmerealm-list
List every active realm with its slug, display name, primary domain, and configured domains (the control-plane realm is marked [CP]). A fresh, uninitialized deployment returns an empty list.
dotnet Modgud.Api.dll recover realm-listrealm-add-domain
Add a domain to an active realm's Domains list. This is useful when adding a hostname after installation or preparing a reverse-proxy change.
dotnet Modgud.Api.dll recover realm-add-domain \
--slug acme \
--domain auth.example.comFlags:
--slug <slug>— required.--domain <hostname>— required. Stored verbatim; case-insensitive match at request time.
realm-remove-domain
Remove a domain from an active realm's Domains list. No-op if not present. Guarded: you cannot remove a realm's last domain, nor its PrimaryDomain — re-point the primary with realm-set-primary-domain first.
dotnet Modgud.Api.dll recover realm-remove-domain \
--slug system \
--domain old.example.comrealm-set-primary-domain
Re-point a realm's PrimaryDomain — its canonical public host name. It is the WebAuthn relying-party ID and the cookie domain, which is why it is a bare host: neither may carry a scheme or port. (Where users actually reach the realm is a separate value — see realm-set-public-url.) The new primary must already be in the realm's Domains; add it with realm-add-domain first (there is no silent add).
dotnet Modgud.Api.dll recover realm-set-primary-domain \
--slug system \
--domain auth.example.comFlags:
--slug <slug>— required.--domain <hostname>— required. Must already be one of the realm's domains.
Changing the primary invalidates passkeys
Because the PrimaryDomain is the WebAuthn relying-party ID, changing it invalidates every passkey registered for the realm — affected users must re-register their passkeys on next sign-in. Other login methods (password, TOTP, Email OTP, magic-link) are unaffected. The CLI prints this warning and writes it to the audit log.
realm-set-public-url
Set a realm's public origin — the absolute base URL users actually reach it at, port included. Every outbound link (magic link, password reset, email verification, invites, the login-provider callback URLs shown in the admin UI) is built against it, and it is an accepted WebAuthn origin.
First installation records the origin its installation link was issued for, so a fresh deployment already has the right value. Use this command for a realm created before the field existed, or when the deployment moves.
dotnet Modgud.Api.dll recover realm-set-public-url --slug acme --url https://auth.example.comFlags:
--slug <slug>— required.--url <origin>— required. Absolutehttp(s)URL with no path, query or fragment (https://auth.example.com,http://localhost:4300). Pass an empty value to clear it back tohttps://{PrimaryDomain}.
Passkeys are not invalidated: the relying-party ID is the PrimaryDomain, which this command does not touch.
control-plane list / control-plane transfer <slug>
Inspect or relocate the control-plane role (the realm that hosts cross-realm administration). list prints the current holder; transfer moves the stored IsControlPlane flag to another realm, clearing every other holder in one transaction.
dotnet Modgud.Api.dll recover control-plane list
dotnet Modgud.Api.dll recover control-plane transfer acmeBreak-glass for when the control-plane realm has no usable admin: the target realm's existing realm:admin users gain cross-realm administration. There is deliberately no grant subcommand — authority is realm:admin within the flag-holding realm, so there is nothing to grant, only the flag to move. Restart the running container afterwards so its in-process realm cache picks up the change.
adopt-tenant <slug> <displayName> [domain]
Register an already-existing tenant database (<master-db>_<slug>) as a realm — the migration counterpart to creating a realm via the API. It does not CREATE DATABASE; restore the dump into the target DB first, then adopt it. Errors if the database is missing or a realm with the slug already exists. Schema is applied idempotently (existing data is kept).
dotnet Modgud.Api.dll recover adopt-tenant acme "Acme Corp" acme.example.comrotate-signing-key
Rotate a realm's OpenIddict signing key: generates a fresh RSA keypair and retires the previous active key into a 30-day verification-overlap window so tokens already issued stay valid until they expire. Running API instances pick up the new key within about a minute.
dotnet Modgud.Api.dll recover rotate-signing-key --realm acmeWith --revoke-previous the previous key is revoked instead of retired: it leaves the verification set and the JWKS at once, and every token it signed (access, refresh, ID and logout tokens) stops working, so users and clients have to sign in again. Use it when the active key is suspected compromised — a plain rotation keeps the old key trusted for 30 days.
dotnet Modgud.Api.dll recover rotate-signing-key --realm acme --revoke-previousrevoke-retired-signing-keys
Revoke every retired key that is still inside its overlap window, for a compromise discovered after an ordinary rotation. The active key is untouched.
dotnet Modgud.Api.dll recover revoke-retired-signing-keys --realm acmehelp
Show the usage summary.
dotnet Modgud.Api.dll recover helpRunning a command at container startup (STARTUP_COMMAND)
For orchestrators where overriding the container's command/entrypoint is awkward (Portainer, some Compose setups), set the STARTUP_COMMAND environment variable to a recover command. On boot — after deployment-wide storage is ready — the value is split into argv and run; the process then idles (it never starts Kestrel and never exits) so a restart policy can't crash-loop it.
# docker-compose.yml (excerpt)
environment:
STARTUP_COMMAND: 'recover control-plane transfer acme'Check the logs, then remove the variable and redeploy to resume normal web serving. STARTUP_COMMAND is only consulted when no CLI command args are present, and is a raw environment variable (not a Cocoar.Configuration key). Multi-word arguments work when double-quoted (e.g. a realm display name).
Audit trail
Most recovery commands write a Recovery <command>. ... entry to the security audit log, surfaced in the admin UI's auth log (GET /api/admin/auth-log). These entries are logged at Warning level, including failures — there is no separate Error level for a failed recovery command. Purely read-only commands (list, realm-list) don't write an audit entry, and most usage/validation failures (unknown realm, bad flags, guard violations) are only printed to the console, not recorded.
When to reach for the CLI
- No admin can sign in →
bootstrap-admin(Direct mode) creates a fresh admin in one shot. - A user lost their 2FA device and cannot get back in themselves →
reset-2fa <username>thenmagic-link <username>so they can log in and re-enrol. A user with a password usually does not need this: the e-mail code stands in for the lost factor (see When the second factor is gone), unless the realm switched that off or the account has no password. - Production hostname doesn't route to a realm →
realm-listto confirm what's configured, thenrealm-add-domainto bind the new hostname, thenrealm-set-primary-domainto make it the realm's canonical primary. - Marten projections out of sync after a schema change →
rebuild-projections. - Legacy
client_credentialsclients fail mutation guard →migrate-cc-credentialsprovisions the linked SA they need. - Routine key hygiene →
rotate-signing-keyissues a fresh key while honoring in-flight tokens. - Suspected signing-key compromise →
rotate-signing-key --revoke-previous(the active key) orrevoke-retired-signing-keys(a key already rotated out). Tokens signed with a revoked key stop working at once.
For the operational story of first-time admin setup (when there's no admin yet to invite anyone), see First-time setup.