Skip to content

Password policy

Every password on create / change must satisfy:

  1. Length - at least Z4J_PASSWORD_MIN_LENGTH characters (default 8). Max 256.
  2. Character classes - at least 3 of 4 of: lowercase, uppercase, digit, symbol.
  3. Not in denylist - a ~1,500-entry list of common passwords and breached patterns.

The same validate_policy() runs on every server-side write path that accepts a plaintext password:

  • First-boot setup form (POST /setup)
  • CLI bootstrap (z4j bootstrap-admin --password-stdin or z4j createsuperuser --password-stdin)
  • Login-time admin creation via Z4J_BOOTSTRAP_ADMIN_PASSWORD
  • Self-service signup via project invitation (POST /invitations/accept)
  • Self-service password change (POST /auth/change-password)
  • Admin-driven password reset (POST /auth/reset-password)
  • Admin user create (POST /users)
  • Admin user password set (PATCH /users/{id}/password)

There is no path that bypasses the policy. The dashboard’s client-side strength meter is advisory; the server is the authority.

  • Top 1,000 common passwords (rockyou, SecLists breach compilations).
  • Seasonal patterns (Summer2024, Winter24, etc.) with year variants for the past 10 years.
  • Product-name variants for common SaaS (<product>123, <product>!1).

Generated via _generate_patterns() in z4j_brain.auth.common_passwords. Static; baked at build time.

argon2id, OWASP 2024 defaults:

  • time_cost=3
  • memory_cost=64 MiB
  • parallelism=4
  • 32-byte hash, 16-byte salt

Tuneable via Z4J_ARGON2_* env vars. Target: 50-80 ms per verify on a 2024 server CPU.

If stored-hash parameters are weaker than current config, z4j rehashes on successful verify. Silent upgrade for users who were around before a parameter bump.

  • password_too_short (< min length)
  • password_too_long (> 256)
  • password_too_simple (< 3 of 4 classes)
  • password_in_breach_list (matches denylist entry)

All are 422-returned from the password-setting endpoints.

The wrong-username branch of login verifies against a dummy hash generated at brain boot. Takes the same wall time as a real verify, so timing doesn’t disclose account existence.

An audit flagged the old “letter + digit” rule: Summer24 and qwertyui1 both passed, both are in the top 1,000. 3-of-4 classes + denylist knocks out the long tail of dictionary-plus-one-digit passwords.

The dashboard’s password input shows a strength indicator (zxcvbn-like) as guidance - but the server is authoritative. The meter can show “strong” for a password the server rejects because of denylist match, and that’s fine.