Track HiveGrid Cloud planning reference
This commit is contained in:
1 parent
b3d5ea6764
commit
e341c5938a
5784 files changed
+1104929
-3
No files matched your search
@@ -0,0 +1,104 @@
|
||||
# Password Policy
|
||||
|
||||
Server-side password validation that every password-write endpoint goes
|
||||
through. Configurable via env without a code deploy. Implementation lives in
|
||||
`apps/web/app/services/auth/password-policy.server.ts`.
|
||||
|
||||
## Defaults (OWASP 2024 aligned)
|
||||
|
||||
| Env var | Default | Effect |
|
||||
|---|---|---|
|
||||
| `AUTH_PASSWORD_MIN_LENGTH` | `12` | Minimum character count. |
|
||||
| `AUTH_PASSWORD_REQUIRE_UPPER` | `true` | Requires at least one A–Z character. |
|
||||
| `AUTH_PASSWORD_REQUIRE_LOWER` | `true` | Requires at least one a–z character. |
|
||||
| `AUTH_PASSWORD_REQUIRE_DIGIT` | `true` | Requires at least one 0–9 character. |
|
||||
| `AUTH_PASSWORD_REQUIRE_SPECIAL` | `false` | Requires at least one non-alphanumeric character. Off by default — adds friction without much entropy beyond the length floor. |
|
||||
| `AUTH_PASSWORD_HIBP_CHECK` | `true` | Consults `api.pwnedpasswords.com` via SHA-1 k-anonymity. The plaintext password never leaves the server; only the first 5 characters of its SHA-1 are sent. |
|
||||
| `AUTH_PASSWORD_HIBP_MAX_OCCURRENCES` | `0` | Accept passwords seen at most this many times in HIBP. `0` = reject any leak. |
|
||||
|
||||
Also enforced regardless of env:
|
||||
|
||||
- An absolute max length of 1024 characters, so the validate-then-hash chain
|
||||
can't be amplified into a DoS via 100 MB password strings.
|
||||
- If the caller supplies `ctx.email`, the password must not contain the
|
||||
email's local-part (case-insensitive), provided the local-part is ≥3
|
||||
characters. Short local-parts (`x@example.com`) are skipped to avoid
|
||||
false positives.
|
||||
|
||||
## Enforcement points
|
||||
|
||||
Every endpoint that writes a password runs the policy:
|
||||
|
||||
| Route | Caller |
|
||||
|---|---|
|
||||
| `apps/web/app/utils/services/auth.service.server.ts` (`AuthService.validateRegistration`) | `validatePassword` before `users.db.server.ts` hashes/stores the password |
|
||||
| `apps/web/app/routes/reset.tsx` | UserValidationUtils.validatePasswords |
|
||||
| `apps/web/app/routes/invitation.$id.tsx` | UserValidationUtils.validatePasswords |
|
||||
| `apps/web/app/routes/admin/settings/profile.tsx` (change password) | validatePassword directly |
|
||||
| `apps/web/app/routes/app.$tenant/settings/profile.tsx` (change password) | validatePassword directly |
|
||||
| `apps/web/app/routes/admin/accounts/users.tsx` (admin reset) | validatePassword directly |
|
||||
| `apps/web/app/routes/app.$tenant/__portals/portals.$portal/users.tsx` (portal user creation / reset) | validatePassword directly |
|
||||
|
||||
Login (`apps/web/app/modules/users/services/AuthService.ts`) deliberately
|
||||
does NOT run the strength policy. Enforcing 12-char + class requirements
|
||||
there would lock out every user with a pre-policy password instead of
|
||||
prompting them to rotate. The plain "is the field a non-empty 4+ char
|
||||
string" presence-check in `UserValidationUtils.validatePassword` is what
|
||||
guards the login form. The dashboard-banner soft-check from the detail-MD
|
||||
is a follow-up.
|
||||
|
||||
## HIBP fail-open
|
||||
|
||||
If the HIBP API times out (>3 s), returns a 5xx, or refuses the connection,
|
||||
the validation result carries `hibpBypassed: true` and the password is
|
||||
accepted (the other policy rules still apply). Caller code can log the
|
||||
bypass so operators have a signal when api.pwnedpasswords.com is having a
|
||||
bad day. We chose fail-open over fail-closed because the alternative
|
||||
("can't register new users while HIBP is down") is worse for availability
|
||||
than the alternative ("a newly-leaked password might slip through during
|
||||
an HIBP outage").
|
||||
|
||||
To force fail-closed during a known-bad incident: set
|
||||
`AUTH_PASSWORD_HIBP_CHECK=false` to skip HIBP entirely until the API
|
||||
recovers. (Yes, that's the same as the bypass — but the env switch makes
|
||||
it explicit and avoids the per-request 3 s timeout cost.)
|
||||
|
||||
## Error model
|
||||
|
||||
Each failure produces a bounded i18n key plus optional non-secret args:
|
||||
|
||||
```ts
|
||||
{
|
||||
valid: false,
|
||||
errors: [
|
||||
{ code: "auth.password.errors.tooShort", args: { minLength: 12 } },
|
||||
{ code: "auth.password.errors.missingDigit" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Codes:
|
||||
- `auth.password.errors.empty` — input was empty / wrong type
|
||||
- `auth.password.errors.tooShort` — below `AUTH_PASSWORD_MIN_LENGTH`
|
||||
- `auth.password.errors.tooLong` — above the absolute max (1024)
|
||||
- `auth.password.errors.missingUpper`
|
||||
- `auth.password.errors.missingLower`
|
||||
- `auth.password.errors.missingDigit`
|
||||
- `auth.password.errors.missingSpecial`
|
||||
- `auth.password.errors.containsEmail`
|
||||
- `auth.password.errors.pwned`
|
||||
|
||||
Routes are expected to surface the first error to the user; the i18n
|
||||
catalog should map every code above.
|
||||
|
||||
## Operator playbook
|
||||
|
||||
- **Relax the policy** (e.g. temporary lower bound during a migration):
|
||||
set `AUTH_PASSWORD_MIN_LENGTH=10` + restart. The change takes effect on
|
||||
next request; no code deploy.
|
||||
- **Tighten the policy**: set `AUTH_PASSWORD_REQUIRE_SPECIAL=true` and
|
||||
bump `AUTH_PASSWORD_MIN_LENGTH=16`. Existing users keep their old
|
||||
passwords until they next rotate; the new policy applies only to writes.
|
||||
- **HIBP-Outage handling**: monitor `hibpBypassed` in the audit log; if
|
||||
the rate sustains, set `AUTH_PASSWORD_HIBP_CHECK=false` to drop the 3 s
|
||||
per-write timeout. Re-enable when api.pwnedpasswords.com recovers.
|
||||
Reference in new issue
Block a user