Skip to content

Account security & settings ​

Castor keeps sign-in state on the server, so sessions can be listed and signed out. On top of that it offers two-step verification, password reset by email, password rules and rate limits. Features that add friction are off by default; administrators turn them on under System → Configuration → System settings.

System settings ​

The System settings page holds switches and parameters that can change at runtime. They are stored in the system_settings table and apply within a few seconds, without a restart. The page has four tabs — Security, Mail, File storage and AI; for mail, storage and AI see Configuration in System settings. This is the Security tab:

SettingPurposeDefault
security.totp_enabledTwo-step verification master switchOff
security.totp_required_rolesRoles that must use two-step verificationNone
security.password_reset_enabledPassword reset by emailOff
security.password_min_lengthMinimum password length (6–64)6
security.password_require_letters_digitsPasswords must contain letters and digitsOff
security.password_require_symbolPasswords must contain a symbolOff
security.session_ttl_hoursSession lifetime (hours, 1–720, sliding)SESSION_TTL_HOURS (8)
security.login_max_failures / security.login_lockout_minutesFailed attempts before lockout (3–1000) / lockout duration and counting window (minutes, 1–1440), see Sign-in lockout10 / 15
security.rate_limit_per_minute/api and /ws requests per IP per minute (60–100000)600
security.auth_rate_limit_per_minuteSign-in requests per IP per minute (3–1000; sign-in, 2FA codes, identity checks and password reset share it)20
security.api_tokens_enabledAllow API tokens, see Open APIOff
  • Features that add a step for users (two-step verification, password reset, API tokens) stay off until their switch is turned on. The sign-in lockout and rate limits are active from the start with the defaults above
  • A switch whose prerequisites are missing can't be turned on, and the page says why. For example, password reset needs an SMTP server and the site URL on the Mail tab, and two-step verification, password reset and API tokens can't be turned on in demo mode
  • Secrets (the SMTP password, the S3 secret key and the AI API key) are stored encrypted with a key derived from SECRET_KEY; neither the API nor the page returns them again
  • A setting whose environment variable is set (e.g. SMTP_HOST, AI_API_KEY) follows the variable and is read-only on the page. SESSION_TTL_HOURS is the exception: it only sets the default of the session lifetime and doesn't lock it. Only what the server needs before it starts (database URL, SECRET_KEY, ports …) must be an environment variable
  • Viewing needs the menu permission system_settings; saving needs the button permission system_settings_edit

How system settings are protected ​

System settings control where data goes — the mail server, file storage, the AI API. A hijacked admin account changing them could intercept password reset mails or send new uploads and AI requests to someone else's server. So:

  • Recent identity check: saving settings or using a test button needs a sign-in or identity check within the last 10 minutes; otherwise an identity check dialog asks for the current password (plus a two-step code or recovery code for enrolled accounts). A stolen session cookie alone can't change settings, and failures count toward the sign-in lockout. Creating an API token and adding or changing a webhook need the same check (see Open API). The endpoint is POST /api/admin/reauth; the backend protects endpoints with requireRecentAuth(request) from common/session.ts
  • Change notifications: every save sends all active super admins a notification naming who changed which settings (secrets only say "updated / cleared"); the operation log records it too, with secrets masked
  • No reserved or internal addresses: the SMTP server, S3 endpoint and AI API URL can't point at reserved addresses such as cloud metadata (169.254.169.254), and in production by default not at internal networks either (127.0.0.1, 10.x, 192.168.x …). Set SETTINGS_ALLOW_PRIVATE_NETWORK=true to use a MinIO or mail server on your own network. Saving and testing check this, and AI requests re-check the address actually connected to, so a hostname can't later be pointed inside. Values pinned by environment variables are the operator's choice and aren't checked
  • Few people with access: viewing needs the menu permission system_settings, changing needs the button permission system_settings_edit; by default only super admins have them

Recommended for production:

  1. Add the super admin role to "Required for roles" so every super admin uses two-step verification
  2. Pin the key settings with environment variables, e.g. APP_BASE_URL (the site URL in password reset links) and SMTP_HOST, so even a hijacked admin account can't change them
  3. Don't use a super admin account for everyday work, and give system_settings_edit only to those who need it

Adding a setting ​

Settings are defined in SETTING_DEFINITIONS in apps/api/src/common/settings.ts: group, type (boolean, integer, string, secret, enum, string list), default, bounds, the environment variable that can pin it, whether it is public (public ones are sent to signed-out pages through /api/admin/app-info) and why it may be unavailable. New environment variable names also go into common/settings-env.ts. On the frontend, add the label and description to FIELD_META in apps/web/src/modules/admin/pages/settings/form.ts, place the field on its tab in index.tsx next to it, and add the translations to that page's locales/. Code reads them with app.settings.get(); on hot paths that run for every request (like rate limiting) use app.settings.peek(), which returns the cached values without waiting for the database.

When a new feature should be "off by default, an admin can turn it on", add a setting here rather than another environment variable.

Server-side sessions ​

  • Signing in creates a row in sessions. The castor_session cookie (encrypted, HttpOnly, SameSite=Lax) holds only the session ID and the CSRF token; the row decides whether the request is signed in and as whom
  • Write requests (POST / PUT / PATCH / DELETE) under /api/ from a session must send that CSRF token in the X-CSRF-Token header; the frontend's request client does this, see Backend
  • Each request checks the session once; last activity and expiry are updated at most once a minute (sliding expiry)
  • Sessions that expired or were revoked more than a day ago are deleted hourly by the scheduler process

These actions end sessions right away; the next request gets 401:

ActionAffects
Sign outThe current session
Change passwordThe user's other sessions (this device stays signed in)
An admin changes the password, disables or deletes the userAll of that user's sessions
Password reset through an email linkAll of that user's sessions
Force sign-outThe chosen session

On the backend, check sign-in with isSignedIn(request) from common/session.ts; don't read cookie fields.

Online users ​

System → Security & audit → Online users lists the signed-in sessions (user, device, IP, sign-in time and last activity), filtered by the viewer's data scope. With the button permission system_sessions_revoke an admin can force a sign-out, except for their own current session; only super admins can sign out super admins.

Under Profile → Signed-in devices, every user sees where they are signed in and can sign out one device or all others.

Two-step verification ​

Once turned on in System settings:

  1. Users go to Profile → Two-step verification, scan the QR code with an authenticator app (Google Authenticator, Microsoft Authenticator, 1Password …) and enter the first code. They get 10 recovery codes, shown only once
  2. Enrolled users enter a 6-digit code after their password when signing in, or a recovery code instead
  3. Members of the roles listed under "Required for roles" who haven't enrolled are asked to set it up at sign-in and go straight in afterwards; they can't turn it off themselves
  4. When a user loses both phone and recovery codes, an admin resets their two-step verification from the bottom-left of the Edit user dialog under Users (needs system_users_edit; only super admins can reset super admins)

Details:

  • TOTP (6 digits, 30 s, SHA-1), with one period of clock drift either way; a code for a given period can be used only once
  • The secret is stored encrypted with AES-256-GCM, using a key derived from SECRET_KEY; changing SECRET_KEY invalidates existing enrollments
  • Recovery codes are stored as sha256 hashes and each works once
  • Wrong codes count toward the sign-in lockout just like wrong passwords
  • After the password but before the second step, the session is pending: it can't reach any endpoint that needs sign-in and expires after 5 minutes. Passing the step issues a new session ID. The "signed in" log entry and last sign-in time are recorded only then
  • Turning the master switch off only stops sign-in from asking for codes; enrollments are kept and apply again when it is turned back on

Password reset ​

First fill in the SMTP server and the site URL on the Mail tab of System settings (the "Send test mail" button checks them, see Configuration), then turn it on in the Security tab. The sign-in page then shows "Forgot password?":

  1. The user enters the email on their account. The answer is the same whether or not the email exists, so it doesn't reveal accounts
  2. The link looks like <site URL>/reset-password?token=…; it is valid for 30 minutes and works once, and a new request invalidates earlier links
  3. The new password is checked against the password rules; afterwards the user is signed out everywhere. Accounts with two-step verification still need a code at the next sign-in

The email follows the user's current interface language. In local development, MAIL_DRIVER=log prints mails to the backend log instead of sending them.

Password rules ​

Every place a password is set checks the rules from System settings: changing a password, creating and editing users, importing users and password reset. Existing passwords are not affected. Frontend forms read the same rules from app-info and validate before submitting.

Rate limits ​

  • Every /api and /ws request counts per IP; over the per-minute limit the response is 429 with a translated error and a Retry-After header. Static files and /health don't count
  • Sign-in, 2FA codes (at sign-in and when enrolling), identity checks and password reset (requesting the mail and setting the new password) share a stricter limit, security.auth_rate_limit_per_minute
  • Counters live in process memory: with several instances each one counts separately, so the effective limit can be up to the setting times the number of instances
  • RATE_LIMIT_ENABLED=false turns it off entirely (it is off in the test environment)

Rate limits and the sign-in lockout work together: one limits how often requests come in, the other how many attempts may fail.

Sign-in lockout ​

  • Failed attempts are wrong passwords (including correct passwords for a disabled account), wrong two-step or recovery codes at sign-in, and failed identity checks (wrong password or code). They are counted over the last security.login_lockout_minutes minutes, per IP and per username
  • Once either count reaches security.login_max_failures, sign-in, the second step and identity checks answer 429 until older failures leave the window. In demo mode only the IP is counted, so nobody can lock the shared demo account
  • After a successful sign-in, earlier failures no longer count for that username or that IP; they stay in the sign-in log
  • LOGIN_MAX_FAILURES / LOGIN_LOCKOUT_MINUTES pin both values, see Configuration

Released under the MIT License.