Recovery CLI
The recovery CLI is a break-glass tool. It runs inside the 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>]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.
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.
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.
Stop the normal application container and take a database backup before running this command. 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.
dotnet Modgud.Api.dll recover rebuild-projectionsbootstrap-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 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 →
reset-2fa <username>thenmagic-link <username>so they can log in and re-enrol. - 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. - Suspected signing-key compromise, or routine key hygiene →
rotate-signing-keyissues a fresh key while honoring in-flight tokens.
For the operational story of first-time admin setup (when there's no admin yet to invite anyone), see First-time setup.