Skip to content

Two-step sign-in (MFA) ​

Users who sign in with a password can add a second factor: an authenticator app (TOTP), a passkey, or both. Recovery codes come with the first one. auth.mfa says who must have one. Users who sign in through an OIDC provider get their provider's MFA instead, and reportd never asks them.

How it works ​

  • A user with MFA signs in with their password, then answers a second step: a code from their app, their passkey, or a recovery code.
  • The second step has to come within 5 minutes, and allows 5 wrong answers. Wrong answers count with wrong passwords in the sign-in throttle: 5 for a username, or 20 from one address, in 15 minutes.
  • On state.driver: postgres (state.md), a ticket made on one node can be answered on another, and its 5 wrong answers count across all of them, as the throttle does.
  • A browser is never remembered. Sessions last 24 hours (auth.sessions.lifetime), so a user is asked at most once a day.
  • API keys don't use MFA. Scripts and agents use keys, and reportd mcp takes a token, so neither changes.

Config ​

yaml
publicURL: https://reports.example.com   # passkeys need it
auth:
  mfa: admins                            # optional (the default) | admins | required
  mfaKey: secret:env:REPORTD_MFA_KEY     # optional; see Keys and backups
auth.mfaWho must have a second factor
optionalNobody. Anyone with a password may add one.
adminsAnyone who holds user.manage in any environment, counting roles from a provider's role rules, and always admin.username.
requiredEvery user who has a password.

A user who signs in only through an OIDC or SAML provider has no password, so MFA never applies to them: their IdP owns their second factor. The Account dialog shows them no password or two-step sign-in, only where they're signed in.

A user who must have MFA and has none can't do anything after their password except set it up. The studio opens the Account dialog for them, and it can't be closed except by signing out. The viewer asks them to set it up in the studio first. A user who must change their password does that first, then sets up MFA.

Passkeys need publicURL. Its host is the passkeys' relying party, and it must be the address people use. A passkey made for one host doesn't work on another, so changing publicURL's host means adding passkeys again. The host must be a name, not an IP address: browsers refuse passkeys on http://127.0.0.1:43123, but http://localhost:43123 works. Without publicURL, or with an IP address, reportd offers only authenticator apps, logs why, and the Account dialog says which.

We recommend passkeys for administrators: a passkey can't be phished, and a code from an app can.

Setting up a factor ​

In the studio, a user opens Account from their name in the header. Every change asks for their current password, so a stolen session can't add or remove a factor.

  • Authenticator app. Scan the QR code, or type the secret, into any app (Microsoft Authenticator, Google Authenticator, 1Password, Bitwarden and the rest). Type the code it shows to turn it on.
  • Passkey. Choose "Add a passkey" and follow the browser: Windows Hello, Touch ID, a phone or a security key. Give it a name, such as "Laptop", to tell passkeys apart.
  • Recovery codes. The first factor comes with 10 codes, shown once. Save them somewhere safe. Each works once, instead of the app or passkey. The dialog warns when 3 or fewer are left; "Make new codes" replaces them all.

Adding or removing a factor ends the user's other sessions. When MFA is required, the last factor can't be removed.

Getting back in ​

  • A recovery code. "Use a recovery code" on the second step.

  • An administrator. DELETE /v1/users/{name}/mfa (user.manage) removes every factor and recovery code and ends the user's sessions. They sign in with their password alone, and set up MFA again when auth.mfa requires it.

  • The break-glass administrator. When admin.username has lost their factor and their codes, stop reportd and run:

    sh
    reportd mfa reset admin --config /etc/reportd/config.yaml

    It refuses while reportd answers on listen. Start reportd again and sign in with the password. On state.driver: postgres it works while the nodes run, and they see the change at once.

Keys and backups ​

TOTP secrets have to be readable to check a code, so auth.json keeps them sealed with AES-256-GCM. The key is auth.mfaKey, a secret: reference to at least 32 bytes. Without it, reportd makes a random key on first start and keeps it in dataDir/mfa.key (mode 0600). The file holds the key as text, so it can move into secrets.dir and be named as secret:file:mfa.key.

On state.driver: postgres, auth.mfaKey is required: every node checks codes, so every node reads the same key, and reportd refuses to start without it. A TOTP secret shown and not yet confirmed waits in the database sealed under it too. Move mfa.key into your secret store before reportd state import (state.md).

Backups keep auth.json with its MFA data, and leave the key out. A backup alone therefore can't make codes. Restoring on the same machine keeps working. On a new server, set the same auth.mfaKey, or copy mfa.key across before the first start. Otherwise codes from apps fail there: users sign in with a recovery code or a passkey, or an administrator resets them. Recovery codes and passkeys don't need the key.

Audit ​

ActionWhen
user.sign-in <name> (password, totp)A sign-in with a password and a code from an app. (password, passkey) and (password, recovery-code) for the others, and (password, break-glass, …) while passwords are off.
user.mfa-add <name> totp|passkeyA user added a factor.
user.mfa-remove <name> totp|passkeyA user removed one.
user.mfa-recovery-codes <name>A user made new recovery codes.
user.mfa-reset <name>An administrator reset a user's MFA. (cli) when reportd mfa reset did.

Trying it on a workstation ​

Check a real authenticator app and a real passkey by hand before relying on them:

  1. In config.yaml, set publicURL: http://localhost:43123 and auth.mfa: admins. Browsers allow passkeys on localhost over plain HTTP, but not on 127.0.0.1, so open the studio at http://localhost:43123.
  2. Start reportd and sign in as the administrator. The Account dialog opens and says your administrator requires two-step sign-in.
  3. Set up an authenticator app on your phone from the QR code. Save the recovery codes.
  4. Sign out and in again. The second step takes a code from the app. A code you used once is refused the second time.
  5. Add a passkey from the Account dialog with Windows Hello, Touch ID or your phone. Sign out and in again with "Use a passkey".
  6. Sign in with a recovery code. The Account dialog shows 9 left.
  7. Try to remove both factors: the last one is refused.
  8. Stop reportd, run reportd mfa reset admin, start it, and sign in with the password alone. The Account dialog opens again.
  9. The audit log has each of these.