04.01 Local Authentication and Password Policies
SimpleRisk's built-in local authentication uses bcrypt-hashed passwords with configurable policies (length, complexity, expiration, history, account lockout). Configure the policy at Configure → Settings → Password Policy and enforce it on every local login.
Why this matters
Every SimpleRisk install ships with local username/password authentication active. Even when you later add SSO via the Authentication Extra (SAML or LDAP/AD), the local-auth path remains available for at least the admin account — losing access to the IdP shouldn't lock you out of the application. Configuring the password policy is the lowest-effort, highest-payoff hardening step on a fresh install: most of the auth-related findings in a security audit boil down to "passwords aren't long enough" or "no lockout on failed attempts."
The honest scope to know up front: password policy applies only to local users. Users who authenticate via SAML or LDAP have their password posture determined by the IdP, not by SimpleRisk. The settings on the Password Policy page only constrain type='simplerisk' users (the Core local-auth users). Don't expect SimpleRisk to enforce the same rules on users coming in through SSO; that enforcement belongs to the IdP.
The other thing worth knowing: password storage uses bcrypt at cost 15 ($2y$15$ prefix). This is conservative on the strong end of the cost spectrum; it adds noticeable latency per login (hundreds of milliseconds per attempt on modest hardware) and that's intentional — it makes offline cracking expensive even if a password hash were exfiltrated. Don't lower the cost.
Before you start
Have these in hand:
- Admin access to Configure → Settings → Password Policy (the policy page) and to Configure → User Management (for unlocking accounts and resetting passwords).
- An organizational password policy to translate into the SimpleRisk settings. Common starting points: NIST SP 800-63B (the modern federal guidance, which prefers length over arbitrary complexity), CIS Benchmarks, or your enterprise's existing AD policy. Map the requirements to the available toggles below.
- A communication plan for users if you're tightening an existing policy. Tightening the policy with a reduced max age (
pass_policy_max_age) will force a wave of password changes at the next login; tightening complexity rules will reject existing passwords on next change. Stagger or pre-announce.
Step-by-step
1. Enable the password policy
Sidebar: Configure → Settings → Password Policy opens the policy page. The master toggle is Enable Password Policy (pass_policy_enabled in the settings table). Without this enabled, the policy fields are inert — Core accepts whatever password the user sets.
Enable the policy first, then configure the individual rules. The settings UI updates the corresponding settings row when you save.
2. Set the length and complexity rules
The relevant settings on the page:
- Minimum Length (
pass_policy_min_chars) — default8. NIST SP 800-63B recommends a minimum of 8; most current guidance recommends 12 or higher for any account that isn't ephemeral. - Require Alphabetic Characters (
pass_policy_alpha_required) — at least one letter. - Require Uppercase Characters (
pass_policy_upper_required) — at least one A-Z. - Require Lowercase Characters (
pass_policy_lower_required) — at least one a-z. - Require Numeric Characters (
pass_policy_digits_required) — at least one 0-9. - Require Special Characters (
pass_policy_special_required) — at least one non-alphanumeric.
Modern guidance (NIST SP 800-63B, OWASP) is biased toward length over complexity: a 16-character passphrase is materially stronger than an 8-character mixed-case-symbol-digit jumble, and easier for users to remember (so they don't write it down). If you can move the program in that direction, push the minimum length up and relax the complexity flags. Where compliance frameworks demand complexity (PCI DSS still requires it, for example), set the flags as the framework requires.
3. Configure expiration and history
- Maximum Password Age (
pass_policy_max_age) — days before a password must be changed;0disables expiration. NIST SP 800-63B recommends not expiring passwords on a fixed cadence (forced rotation produces predictable per-user variations likeSpring2024!→Summer2024!). PCI DSS and several internal audit programs still require it. If your program needs expiration, 90 days is the conventional setting; longer periods (180, 365) are defensible on the NIST-aligned argument. - Reuse Limit (
pass_policy_reuse_limit) — how many prior password versions a user must cycle through before reusing one. Stored in thepass_historytable. Higher values prevent the "rotate through 5 throwaway passwords back to my favorite" pattern; values of 5–10 are common.
When a password expires, the user is forced into a password-change flow on their next login attempt; they can't proceed to the application until they set a new compliant password.
4. Configure the lockout policy
- Lockout Attempts (
pass_policy_attempt_lockout) — number of failed login attempts before the account is locked. Default varies;5is a reasonable starting point. - Lockout Duration (
pass_policy_attempt_lockout_time) — minutes after which the lockout auto-expires.0means the lockout is permanent until an admin explicitly unlocks the account.
Failed attempts are logged in the failed_login_attempts table; once the threshold is hit, block_user() sets user.lockout = 1 and the user can't authenticate until either the duration expires (and check_expired_lockouts() clears it on the next login attempt) or an admin manually unlocks the account.
The right choice between "auto-expire after 30 minutes" and "permanent until admin unlocks" depends on the program's risk appetite. Auto-expiring lockouts reduce help-desk volume; permanent lockouts force a ticket per lockout event, which is better for high-security environments where the lockout itself is the signal that warrants investigation. Most programs land at 15–30 minute auto-expiry as the operational compromise.
5. Verify the policy with a test account
Don't trust the settings page; verify by trying to set a non-compliant password.
- Pick a non-admin test user.
- Open Configure → User Management → [user] → Reset Password.
- Try to set a password that violates one of the rules (too short, no digit, etc.).
- Confirm the page rejects it with the rule-specific error message.
- Try a compliant password; confirm it's accepted.
If a non-compliant password is accepted, the policy didn't save (check the master toggle and the individual flag) or the settings row didn't update (rare; check the settings table directly).
6. Communicate the policy to users
Users encounter the policy at three points:
- First login on a new account — they're forced through a password change after their initial admin-set password.
- Password change — the rules are enforced on every change.
- Login after the policy tightens or the password expires — they're forced into the change flow before they can use the application.
Tell users in advance when the policy tightens. The change-on-next-login surprise produces a help-desk wave the day after the policy update; pre-announcing it reduces that wave and gives users time to pick a compliant password rather than typing the first thing that satisfies the rules.
7. Manage account lockouts
When a user's account is locked, the operator path:
- Open Configure → User Management → [user].
- The user record shows the locked state.
- Click Unlock (or set
user.lockout = 0directly in the database). - Optionally clear the
failed_login_attemptsrows for that user; otherwise the next failed attempt picks up from the existing count.
For programs with frequent lockouts, the auto-expiry duration (pass_policy_attempt_lockout_time) is the lever to reduce help-desk friction. Be deliberate about it; auto-expiring lockouts are also a defense-in-depth posture for a brute-force attempt that the auto-expiry erases the evidence of.
8. Handle "I forgot my password" (operator path)
Core ships a "Forgot Your Password?" link on the login page that triggers a password-reset email if email is configured (see Email and the Notification Extra). Without email configured, the user-self-serve reset path is unavailable and only the admin-initiated reset works.
The admin reset:
- Configure → User Management → [user] → Reset Password.
- Enter the new password (must satisfy the policy if active).
- Save.
- Communicate the new password to the user out-of-band.
- The user is forced to change the password on their next login (the admin-reset flag triggers this).
Don't share passwords in email; use a secure messaging channel or have the user hit the self-serve "Forgot Your Password?" path if email is configured.
Common pitfalls
A handful of patterns recur with local auth and password policies.
-
Forgetting to enable the master toggle. The individual policy fields are inert until Enable Password Policy is on. The settings page accepts changes silently even with the master toggle off, which makes it look like the policy is configured. Verify with a test account.
-
Aggressive complexity without length. A required-uppercase-required-digit-required-special policy with an 8-character minimum produces predictable patterns (
Password1!,Welcome2024!). Push length up; relax the complexity flags where the program allows. -
Setting
pass_policy_attempt_lockout = 0. A0here means no lockout ever, not "lock immediately." Verify the value is the threshold (e.g.,5for "lock after 5 failures") not a disabled-state misread. -
Setting
pass_policy_max_agevery low. Forced changes every 30 days produce password fatigue and write-it-down behavior. NIST guidance is to avoid scheduled expiration entirely; if your program requires it, 90+ days is the common compromise. -
Locking out admin accounts and then having no recovery path. The admin can be unlocked from the database (
UPDATE user SET lockout = 0 WHERE id =) but a non-technical operator might not know that. Document the recovery path; consider having a second admin account specifically as a break-glass. -
Confusing local-auth policy with SSO password policy. SAML/LDAP users have their password posture determined by the IdP. The SimpleRisk policy doesn't apply to them. Don't claim "all users have a 12-character minimum" if half the users are SSO; the SSO half are governed by the IdP.
-
Not testing changes with a non-admin account. Admin bypasses some checks (the password expiration flow may behave differently for admins; the lockout typically does not). Test with a non-admin account to see what regular users experience.
-
Keeping the bcrypt cost at the default but assuming it's lower. SimpleRisk uses cost 15 (
$2y$15$). This is intentionally slow per login. If you're benchmarking login latency, the bcrypt cost is the dominant cost. Don't lower it as an optimization; the latency is the point. -
Reusing the admin password as the API key. API keys are separate (see API Key Management); generate a per-user API key for integration accounts rather than reusing the password.
Related
- Multi-Factor Authentication
- The Authentication Extra Overview
- API Key Management
- Session Management and Timeout
- Managing Users, Teams, and Roles
Reference
- Permission required:
check_adminfor the password policy and user-management surfaces. - API endpoint(s): None for password policy directly; user-management endpoints under
/api/v2/usersaccept password changes for admins. - Implementing files:
simplerisk/includes/authenticate.php(is_valid_simplerisk_user(),block_user(),check_expired_lockouts());simplerisk/admin/password_policy.php(the policy UI);simplerisk/account/profile.phpandsimplerisk/account/change_password.php(user-self password change);simplerisk/admin/user_management.php(admin reset). - Database tables:
user(password,lockout,change_passwordcolumns);pass_history(prior password hashes for reuse-limit enforcement);failed_login_attempts(rate-limit / lockout tracking). config_settingskeys:pass_policy_enabled;pass_policy_min_chars;pass_policy_alpha_required,pass_policy_upper_required,pass_policy_lower_required,pass_policy_digits_required,pass_policy_special_required;pass_policy_max_age;pass_policy_reuse_limit;pass_policy_attempt_lockout;pass_policy_attempt_lockout_time.- Password hashing:
password_hash($pwd, PASSWORD_BCRYPT, ['cost' => 15])— bcrypt with cost 15, prefix$2y$15$. Legacy crypt-based hashes are transparently rehashed on successful login. - External dependencies: None (PHP's built-in
password_hash()/password_verify()).