Track HiveGrid Cloud planning reference

This commit is contained in:
david committed 2026-06-26 20:48:01 +02:00
1 parent b3d5ea6764
commit e341c5938a
5784 files changed
+1104929 -3

No files matched your search

+104
View File
@@ -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.