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>]The --realm flag defaults to system. Commands that don't need a tenant context (the realm-*, control-plane, and adopt-tenant commands carry their own --slug) ignore it.
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 CLI prints anote:to stderr naming the realm it defaulted to, so a multi-realm operator is never surprised. With a single realm the default 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
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.
dotnet Modgud.Api.dll recover rebuild-projectionsbootstrap-admin
Create the first admin in a realm. Default realm: system. 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> | no | Defaults to system. |
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 systemrealm-list
List every active realm with its slug, display name, primary domain, and configured domains (the control-plane realm is marked [CP]). Useful first probe after a fresh deploy — shows the system realm's seeded localhost domains so you know which Host header to use.
dotnet Modgud.Api.dll recover realm-listrealm-add-domain
Add a domain to an active realm's Domains list. Typically used once after a fresh deploy to add the production hostname to the system realm.
dotnet Modgud.Api.dll recover realm-add-domain \
--slug system \
--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. The PrimaryDomain is the single domain (out of the realm's Domains) that Modgud uses for every outbound link (magic-links, bootstrap-invites) and as the WebAuthn relying-party ID. 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.
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 the master + system tenant are provisioned — 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.