Skip to content

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 ​

bash
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, …):

    bash
    docker 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 run uses the service's environment, volumes and network, publishes no ports, and --rm removes the container afterwards:

    bash
    docker compose stop modgud
    docker compose run --rm modgud recover rebuild-projections
    docker compose start modgud

    Without Compose, start the same image with the same -e/-v/--network options: 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 --realm fails fast with error: Realm '<slug>' not found. and a non-zero exit code — it never silently acts on the wrong tenant.
  • When --realm is 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 ​

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.

bash
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 \
  --json

Issuing 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.

bash
dotnet Modgud.Api.dll recover list

Admin 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.

bash
dotnet Modgud.Api.dll recover reset-2fa alice

set-email <username> <new-email> ​

Update the user's email and append a UserUpdatedEvent so projections

  • SignalR-driven admin grids refresh live.
bash
dotnet Modgud.Api.dll recover set-email alice alice@example.com

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.

bash
dotnet Modgud.Api.dll recover magic-link alice

rebuild-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.

bash
docker compose stop modgud
docker compose run --rm modgud recover rebuild-projections   # --force to override the check
docker compose start modgud

bootstrap-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).

bash
# 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 admin

Flags:

FlagRequiredNotes
--emailyesEmail — required in both modes.
--usernamenoDefaults to the local-part of the email.
--firstnamenoOptional.
--lastnamenoOptional.
--passwordnoIf present: Direct mode. Validated against the configured Identity password rules. If absent: Invite mode.
--realm <slug>when multiple realms existInferred 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.

bash
dotnet Modgud.Api.dll recover migrate-cc-credentials --realm acme

realm-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.

bash
dotnet Modgud.Api.dll recover realm-list

realm-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.

bash
dotnet Modgud.Api.dll recover realm-add-domain \
  --slug acme \
  --domain auth.example.com

Flags:

  • --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.

bash
dotnet Modgud.Api.dll recover realm-remove-domain \
  --slug system \
  --domain old.example.com

realm-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).

bash
dotnet Modgud.Api.dll recover realm-set-primary-domain \
  --slug system \
  --domain auth.example.com

Flags:

  • --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.

bash
dotnet Modgud.Api.dll recover realm-set-public-url --slug acme --url https://auth.example.com

Flags:

  • --slug <slug> — required.
  • --url <origin> — required. Absolute http(s) URL with no path, query or fragment (https://auth.example.com, http://localhost:4300). Pass an empty value to clear it back to https://{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.

bash
dotnet Modgud.Api.dll recover control-plane list
dotnet Modgud.Api.dll recover control-plane transfer acme

Break-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).

bash
dotnet Modgud.Api.dll recover adopt-tenant acme "Acme Corp" acme.example.com

rotate-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.

bash
dotnet Modgud.Api.dll recover rotate-signing-key --realm acme

With --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.

bash
dotnet Modgud.Api.dll recover rotate-signing-key --realm acme --revoke-previous

revoke-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.

bash
dotnet Modgud.Api.dll recover revoke-retired-signing-keys --realm acme

help ​

Show the usage summary.

bash
dotnet Modgud.Api.dll recover help

Running 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.

yaml
# 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> then magic-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-list to confirm what's configured, then realm-add-domain to bind the new hostname, then realm-set-primary-domain to make it the realm's canonical primary.
  • Marten projections out of sync after a schema change → rebuild-projections.
  • Legacy client_credentials clients fail mutation guard → migrate-cc-credentials provisions the linked SA they need.
  • Routine key hygiene → rotate-signing-key issues a fresh key while honoring in-flight tokens.
  • Suspected signing-key compromise → rotate-signing-key --revoke-previous (the active key) or revoke-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.

Released under the Apache-2.0 License.