Repository navigation
Login Security
oCMS includes built-in protection against brute force attacks and DDoS on the login form.
Successful login and visits to /login with an existing session use the same
role-based destinations:
| Role | Destination | Access |
|---|---|---|
Administrator (admin) |
/admin |
Full administration |
Editor (editor) |
/admin |
Content management; administrator-only routes remain forbidden |
Public (public) |
/account |
Own account overview; no administration access |
| Unrecognized role | / |
No account dashboard or administration access |
Destinations are fixed internal paths. Login query parameters and form fields cannot override them. The Account Dashboard shares the admin layout with account-only navigation, English/Russian language selection and POST logout.
Account access reloads the user and role from the database. Demoting an editor
to public removes administration access immediately and changes the login
destination to /account. Deleted users and sessions invalidated by password
changes must sign in again. See Security for session invalidation details.
The login protection system provides two layers of defense:
- IP-based Rate Limiting — Limits the number of login attempts per IP address
- Account Lockout — Temporarily locks accounts after multiple failed login attempts
Rate limiting prevents rapid-fire login attempts from a single IP address.
| Setting | Value | Description |
|---|---|---|
| Rate | 0.5 req/s | One request per 2 seconds (sustained) |
| Burst | 5 | Initial burst allowance before rate limiting kicks in |
- Each IP address has a token bucket with 5 tokens
- Tokens regenerate at 0.5 per second
- Each login attempt consumes 1 token
- When tokens are exhausted, requests receive HTTP 429 (Too Many Requests)
HTTP/1.1 429 Too Many Requests
Content-Type: text/plain
Too many requests. Please try again later.
Account lockout prevents password guessing attacks by temporarily blocking login attempts after multiple failures.
| Setting | Value | Description |
|---|---|---|
| Max Failed Attempts | 5 | Failures from one address before that email + address pair is locked |
| Account-wide Threshold | 25 | Failures across all addresses before the account is locked for everyone |
| Attempt Window | 15 minutes | Time window for counting failures |
| Base Lockout Duration | 15 minutes | Initial lockout period |
| Max Lockout Duration | 24 hours | Maximum lockout after repeated lockouts |
- Failed login attempts are tracked per email address and client address, and separately per email address alone
- After 5 failed attempts within 15 minutes from one address, that email + address pair is locked; the account owner can still sign in from another address. For IPv6 clients the "address" is the /64 block. After 25 failed attempts within the window across all addresses, the account is locked for everyone, which stops a distributed guessing campaign; an attacker with many addresses therefore gets at most five times the single-address budget before the account lock, and that lock doubles with each repetition. Knowing a victim's email is not enough to lock them out from their own connection.
- Lockout duration doubles with each subsequent lockout (exponential backoff):
- 1st lockout: 15 minutes
- 2nd lockout: 30 minutes
- 3rd lockout: 1 hour
- 4th lockout: 2 hours
- ...up to 24 hours maximum
| Attempt | Message |
|---|---|
| 1 | "Invalid email or password" |
| 2-4 | "Invalid email or password. N attempts remaining." |
| 5 | "Too many failed attempts. Account locked for 15 minutes." |
| During lockout | "Account is temporarily locked. Try again in N minutes." |
- Non-existent email protection: Failed attempts are tracked even for non-existent email addresses to prevent user enumeration attacks
- Successful login reset: A successful login clears the failed attempt counters in both scopes. Confirming the password for a sensitive change (a user change, a theme import, the first authenticator setup) does not
- Automatic cleanup: Stale tracking entries are automatically removed
oCMS has a built-in second factor: a six-digit code from an authenticator app (TOTP: 1Password, Bitwarden, Google Authenticator, Microsoft Authenticator and others), with ten single-use recovery codes for a lost device.
| Variable | Default | Description |
|---|---|---|
OCMS_MFA_KEY |
— | Seals the authenticator secrets at rest (min 32 bytes, distinct from OCMS_SESSION_SECRET). openssl rand -base64 32
|
OCMS_REQUIRE_MFA |
true in production |
Every account (admin, editor, public) must pass a second factor; needs OCMS_MFA_KEY
|
Back OCMS_MFA_KEY up with the database. Without it, nobody can set up an
authenticator app, and enrolled users sign in with a recovery code and set
their app up again (with the account password, if that was their last code).
Demo mode turns two-factor authentication off, authenticators enrolled before
the switch included, because the demo accounts are shared.
After the password, an account with an authenticator enters its code at
/login/mfa; an account without one, where a second factor is required, sets
one up at /login/mfa/setup and sees its recovery codes once. The session is
signed in only after that step, so /admin, /account and module routes treat
it as anonymous until then. The pending sign-in lasts 10 minutes and ends after
5 wrong codes; wrong codes also count toward the account lockout. Each code is
accepted once. Switching OCMS_REQUIRE_MFA on signs out sessions that started
without a second factor.
/account/security (user menu and account sidebar) sets up, replaces or turns
off the authenticator and generates new recovery codes. The first setup asks for
the password and every later change for a current code, and setting up, replacing or turning it off
signs out the user's other sessions. Turning it off is not offered where the
site requires it.
- An administrator resets another user's second factor on
/admin/users/{id}with Reset two-factor authentication, confirmed with their own current password; the user is signed out and, where required, sets up a new authenticator at the next sign-in. - An administrator with nobody to reset them runs
ocms mfa-reset [-db ./data/ocms.db] admin@example.comon the server.
API keys are not affected: they use bearer tokens, not sessions.
The user form asks for the acting administrator's current password for a new password or email for any user (the email keys the lockout), giving or taking away the admin role, and a second-factor reset. Wrong attempts count toward the lockout. A content import that overwrites users never grants or removes the admin role. A stolen administrator session therefore cannot set a password of its own, reset an account's lockout by changing its email, create an administrator it controls, or demote the real ones. Activating the DB Manager module asks for the password too, since its raw SQL reaches every table; once active its queries need none, so switch it off when done.
Sessions are stored server-side; the session row holds the data and the cookie only carries its token. A session ends after 24 hours regardless of activity, and after 2 hours without any request; each request renews the idle window. This applies to every session, admin and public alike.
All login-related events are logged to the events system:
| Event | Level | Description |
|---|---|---|
| Login failed: user not found | Warning | Attempt with non-existent email |
| Login failed: invalid password | Warning | Wrong password for existing user |
| Login attempt on locked account | Warning | Attempt during lockout period, at the password step, the code step or authenticator setup during sign-in |
| Account locked due to failed attempts | Warning | Account just got locked |
| Login failed: invalid two-factor code | Warning | Wrong, expired or reused code at /login/mfa
|
| Recovery code used to sign in | Warning | Sign-in with a recovery code |
| Two-factor authentication enabled / disabled | Info / Warning | Changed on the account security page |
| Two-factor change refused: invalid code / invalid password / no password / account locked | Warning | Wrong current code, or wrong or missing password for the first setup, on the account security page, or any second-factor change tried while the account is locked |
| User change refused: wrong current password / account locked / no current password | Warning | Acting administrator's password refused on the user form or a second-factor reset (a missing one is not counted) |
| Module activation refused: … / Module activated with password confirmation | Warning, Error / Info | DB Manager activation refused or allowed by the password check |
| Theme import refused: … | Warning, Error | Theme import refused by the password check (a missing password is logged as "no password" and not counted) |
| … refused: password could not be verified | Error | A stored password hash could not be checked at the user form, the first two-factor setup, theme import or module activation (sign-in writes it to the server log only) |
| Recovery codes regenerated | Info | New recovery codes issued |
| Two-factor authentication reset by an administrator / from the command line | Warning | Reset on the user page or with ocms mfa-reset
|
| User logged in | Info | Successful login; second_factor metadata names the method |
View these events at Admin > Events.
When running behind a reverse proxy, ensure the proxy passes the real client IP. See Reverse Proxy for configuration.
The login protection middleware uses the client IP resolved by oCMS. Forwarding headers are honored only when the direct peer is listed in OCMS_TRUSTED_PROXIES (see the checklist below); any other peer is identified by its connection address. For a trusted peer the order is:
-
X-Forwarded-For(the first address from the right that is not a trusted proxy) -
X-Real-IP, only whenX-Forwarded-Foris absent -
RemoteAddr(direct connection, or no usable header)
- Use strong passwords: Enforce minimum password requirements
- Monitor events: Regularly check the events log for suspicious activity
- Use HTTPS: Always run behind HTTPS in production
- Enable hCaptcha: Add bot protection on the login form
-
Configure trusted proxies: Set
OCMS_TRUSTED_PROXIESfor correct IP detection -
Keep two-factor authentication required: production requires it by default; back up
OCMS_MFA_KEY
Before exposing oCMS to the public internet, work through this list:
-
Rotate the seed admin password.
OCMS_DO_SEED=trueprovisionsadmin@example.comwith the public passwordchangeme1234. Log in and change both the email and password on/admin/usersbefore anything else. Production startup blocks seeding unless demo mode is explicitly enabled, so the exposure window is limited to initial-setup runs — but the first admin session still has to rotate. Once any administrator account exists, seeding no longer recreatesadmin@example.com, so renaming or replacing it is permanent even if the flag stays on. - Set
OCMS_SESSION_SECRETto a freshly generated 32-byte value (openssl rand -base64 32). - Set
OCMS_ENV=productionso all security defaults apply. - Set
OCMS_MFA_KEYto another freshly generated 32-byte value and back it up with the database. Production requires a second factor for every account and refuses to start without the key unlessOCMS_REQUIRE_MFA=false. - Configure
OCMS_TRUSTED_PROXIESif running behind a reverse proxy — otherwise client IPs used by rate limiting collapse to a single bucket. In production, startup fails while it is unset unlessOCMS_REQUIRE_TRUSTED_PROXIES=false. See Reverse-Proxy. - Consider enabling HSTS preload (
OCMS_HSTS_PRELOAD=true) once every subdomain has a valid TLS certificate; see the HSTS Preload section in Reverse-Proxy.
- Wait for the lockout period to expire, OR
- Delete the account's rows from the
login_protectiontable. Lockout state is stored in SQLite, so restarting the application does not clear it:sqlite3 ./data/ocms.db "DELETE FROM login_protection WHERE email IN ('user@example.com', 'account' || char(31) || 'user@example.com') OR email GLOB 'pair' || char(31) || 'user@example.com' || char(31) || '*'"
- Increase burst allowance for more initial requests
- Check if users are behind a shared NAT (many users with same IP)
High volumes of "user not found" warnings may indicate:
- Legitimate users mistyping email addresses
- Automated scanning/attacks (expected and blocked)
- Misconfigured client applications
Getting Started
Core Features
- Content Management
- Media Library
- Taxonomy
- Menu Builder
- Forms
- Multi-Language
- SEO
- Feeds
- Video Embedding
- Agent-Ready
Administration
API & Integrations
Security
Theme Development
Module Development
Built-in Modules
- Module: Sentinel
- Module: Developer
- Module: DB Manager
- Module: Migrator
- Module: Informer
- Module: Demo Mode
- Module: Internal Analytics
- Module: External Analytics
- Module: Privacy
- Module: Embed
- Module: MCP Server
Deployment