Levelrail
Skip to content

Security center ​

The security center is one page, Security center in the sidebar (/security), that answers three questions: how exposed is this instance right now, what should I fix first, and who is signed in. It reads the control plane's own state (accounts, tokens, sessions, settings, certificates, the exposure audit) and never calls anything outside your servers.

Security score and checklist ​

GET /api/v1/security/posture returns a score from 0 to 100, a grade, and counts by severity. Every check that fails costs points: 25 for critical, 15 for high, 8 for medium and 3 for low. A check that could not be read shows as unknown and costs nothing, so a missing data source never makes the score look worse or better than it is.

Anyone with read gets the score and counts, plus a short checklist for their own account (two-factor or passkey, recovery codes, a reset flag). Admins (root) also get the full platform checklist with the names of the accounts, tokens, domains and nodes behind each failing check. The result is cached for APP_SECURITY_POSTURE_CACHE_TTL (default 30s); the exposure audit inside it, which may ask remote agents for their containers, refreshes every APP_SECURITY_POSTURE_EXPOSURE_TTL (default 5m). Changing the policy, revoking sessions or a "this wasn't me" report refreshes it at once.

CheckSeverityData sourceFix
Admins without two-factor or a passkeycriticalusers with root, TOTP status, registered passkeysSettings > Security
Accounts flagged for a new passwordcritical"this wasn't me" reportschange the password
The Docker API is reachable from the internetcriticalexposure audit findings for ports 2375 and 2376Firewall, firewall exposure
API tokens that hold roothighlive tokensSettings > API tokens
New browser approval is offhighAPP_AUTH_NEW_DEVICE_APPROVALset it back to true
Container ports open to the internethighexposure auditFirewall, firewall exposure
Off-box control plane backups are offhighdisaster recovery statusControl plane backup
Domains without a publicly trusted certificatehighrouted domains, stored certificates, ACME settingDomains
Bursts of failed sign-inshighfailed sign-in counter, see belowAudit log
Accounts with no recovery codes leftmediumTOTP statusSettings > Security
API tokens that never expiremediumlive tokensone click: limit new tokens to 90 days
API tokens that can approve sign-insmediumlive tokens holding signin:approveSettings > API tokens
Admins can sign in with a codemediumauth.code_loginone click: turn it off
HSTS is off while real certificates are in placemediumHSTS setting and certificatesDomains
Node agents older than the minimum versionmediumeach node's reported agent versionNodes
No admin has a passkeylowregistered passkeysSettings > Security
API tokens unused for a long timelowlast use, warn_unused_daysSettings > API tokens
Old sessions still signed inlowsessions older than APP_SECURITY_SESSION_MAX_AGE_DAYS (default 7)Sessions tab
Only password sign-ins need new browser approvallowauth.approval_scopeone click: every method
The master key is missing or overdue for rotationlowrotation history, APP_DOCTOR_MASTER_KEY_ROTATION_WARN_DAYSsecrets rotate-master-key
Secrets older than 90 dayslowsecret update timesrotate the secret

A domain counts as served without public TLS when ACME is off, or when its only certificate is from the control plane's internal issuer or has expired. A proxy in front that terminates TLS (tls_terminated_upstream) passes the check.

From the CLI:

bash
levelrail-cli security posture
levelrail-cli security posture --json

Sessions and devices ​

The Sessions and devices tab, GET /api/v1/security/sessions, and levelrail-cli security sessions list show:

  • every live session of the account: browser (a short label like "Firefox on Linux" built from a fixed list, never the raw User-Agent), the network it signed in from (the /24 or /48 of its address, which is the only location this platform knows: no GeoIP database is used), when it signed in, when it was last active, and which one is this browser;
  • trusted browsers, which skip new browser approval;
  • the account's API tokens with when each was last used.

Sign out ends one session (DELETE /api/v1/security/sessions/{id}, security sessions revoke <id>). Sign out all other sessions (POST /api/v1/security/sessions/revoke-others, security sessions revoke-others) keeps the browser you are using and also resets trusted browsers, waiting sign-ins and signin:approve tokens, like changing the password does.

An admin can pick another account in the tab, or pass --user <user id> (?user_id= on the API), to see and end its sessions. Every revoke is audited (security.session_revoked, security.sessions_revoked), with the target account when it is not the caller's own. Anyone else asking for a session that is not theirs gets a not found answer, so session ids cannot be probed. A token used for these routes must belong to a user and hold write:sensitive or signin:approve.

Sign-in protection ​

Approval scope ​

New browser approval (see Identity and access) always applies to password sign-ins. auth.approval_scope decides whether it also applies to passkey and OAuth sign-ins:

ValuePasswordPasskeyOAuth / OIDC
password_only (default)heldnot heldnot held
all_methodsheldheldheld

A sign-in is only held when the account already has another live session, so the last way in is never blocked. A held OAuth sign-in returns to the sign-in page, which shows the number to type in the approving session. Set it in the Policy tab, with levelrail-cli security policy set --approval-scope all_methods, or PUT /api/v1/security/policy (root). APP_AUTH_APPROVAL_SCOPE sets the default before anyone saves one. If the policy cannot be read, the sign-in is held, never waved through.

Each account can also turn on Require approval for new browsers on my account in the Policy tab (PUT /api/v1/security/account). That applies approval to every method for that account whatever the platform scope is. Turning it off needs a signed-in dashboard session; a token can only turn it on.

Break-glass. APP_AUTH_NEW_DEVICE_APPROVAL=false on the control plane, then a restart, turns approval off for every method and every account, whatever the scope and account switches say. Use it only when nobody can approve; set it back afterwards. recover-admin on the server still works.

New sign-in alerts ​

When an account signs in from a browser and network it has not used before, its owner gets an email with the browser, network and time, and channels opted in to sign-in notices (the same switch as CLI login notices) get a short message. The first browser an account ever uses raises nothing, and a sign-in you approved yourself, or a session link, does not alert. At most APP_SIGNIN_ALERTS_PER_ACCOUNT_PER_HOUR (default 5) emails go out per account per hour. Known browsers are remembered as a hash of the browser label and network for APP_KNOWN_BROWSER_RETENTION (default 180 days).

The email carries a this wasn't me link. Opening it changes nothing; the page has one button. Pressing it (POST /api/v1/auth/sign-in-alert/disown) signs that session out, removes every trusted browser of the account, cancels its waiting sign-ins and flags the account for a new password. The flag shows in the checklist and the attention center until the password is changed or reset. The link:

  • is an HMAC over a random id with a key derived from the control plane's encryption key, so it cannot be forged or altered;
  • works once, recorded in the database before anything is revoked;
  • expires after APP_SIGNIN_ALERT_LINK_TTL (default 6 hours);
  • is rate limited per address and only accepted as a same-origin JSON request;
  • is sent only to the account's own email, never to a channel, since anyone reading a channel could use it.

Failed sign-in signals ​

Failed password sign-ins are counted per typed account name and per address over APP_SECURITY_FAILED_LOGIN_WINDOW (default 15 minutes). Reaching APP_SECURITY_FAILED_LOGIN_ACCOUNT_THRESHOLD (default 10) for one account or APP_SECURITY_FAILED_LOGIN_IP_THRESHOLD (default 20) from one address writes one audit row (security.failed_logins), posts one notice to opted-in channels, and adds an attention item (to admins, and to the account owner for their own account). A burst raises one alert per window, not one per attempt. The counts live in memory, so a restart starts them again. This is a signal only: the login throttle and account lockout in the auth engine are what slow guessing down. There is no "new country" signal, since the control plane has no GeoIP data.

API token hygiene ​

Three policy settings, each in the Policy tab, security policy set, or PUT /api/v1/security/policy:

SettingEnv defaultWhat it does
max_token_lifetime_daysAPP_TOKEN_MAX_LIFETIME_DAYS (none)new tokens must set expires_in_days from 1 to this; existing tokens are only flagged in the checklist
warn_unused_daysAPP_TOKEN_WARN_UNUSED_DAYS (30)tokens unused this long are listed in the checklist
disable_unused_daysAPP_TOKEN_DISABLE_UNUSED_DAYS (off)tokens unused this long are disabled after a notice

The sweep runs every APP_TOKEN_HYGIENE_SWEEP_INTERVAL (default 1h). A token unused for disable_unused_days first gets a notice: an audit row (security.token_unused_notice), a channel message and an attention item for its owner and admins. If it is still unused APP_TOKEN_HYGIENE_NOTICE_GRACE (default 7 days) later, it is revoked (security.token_unused_disabled). Using the token at any point clears the notice. Tokens that belong to no user (minted by the platform itself) are never swept. A value of 0 turns a setting off.

bash
levelrail-cli security policy get
levelrail-cli security policy set --max-token-lifetime-days 90 --disable-unused-days 120

Audit events ​

security.session_revoked, security.sessions_revoked, security.policy_updated, security.device_approval_changed, security.new_browser_sign_in, security.sign_in_disowned, security.token_unused_notice, security.token_unused_disabled and security.failed_logins.

Next steps ​

Released under the Apache 2.0 License.