Password policy
Every newly set password must satisfy the server-side policy:
- Length - at least
Z4J_PASSWORD_MIN_LENGTHcharacters. The default is 12, and 8 is the lowest value you can configure. The policy rejects more than 256 characters; invitation acceptance has a stricter request-body maximum of 200 characters. - Character classes below 16 characters - a password shorter than 16 characters needs at least 3 of 4 of lowercase, uppercase, digit, and symbol. Whitespace does not count as a symbol. A password of 16 characters or more skips this composition check.
- Denylist - the case-folded password must not match the built-in weak- password list.
The denylist check still applies to passwords of 16 characters or more.
Enforcement points and existing passwords
Section titled “Enforcement points and existing passwords”The same validate_policy() implementation runs on every supported path that
sets a new password:
- First-boot form:
GET /setup, submitted toPOST /api/v1/setup/complete - CLI first-admin creation:
z4j bootstrap-admin --password-stdinorz4j createsuperuser --password-stdin - First-boot auto-provisioning through
Z4J_BOOTSTRAP_ADMIN_EMAILandZ4J_BOOTSTRAP_ADMIN_PASSWORD - Project-invitation signup:
POST /api/v1/invitations/accept - Self-service password change:
POST /api/v1/auth/change-password - Token-based password reset:
POST /api/v1/auth/password-reset/confirm - Admin user creation:
POST /api/v1/users - Admin-driven password reset:
POST /api/v1/users/{user_id}/password - CLI password reset:
z4j changepassword <email> --password-stdin
The first-boot environment variables are read during brain startup and are
honoured only while the users table is empty. They are not a recovery path
for an existing installation; use z4j changepassword instead.
Existing passwords are grandfathered when you tighten the policy. Login does
not run validate_policy() again. It may rehash the supplied password, but it
does not force the user to replace a password that no longer meets the current
policy.
Denylist composition
Section titled “Denylist composition”The current list contains roughly 6,800 entries:
- 82 hand-curated common guesses drawn from OWASP and NIST guidance, the SecLists top-100 list, and z4j's supported stack
- About 6,700 generated seasonal, year, and product-name variants
- Seasonal and year variants for the fixed 2015-2029 range; the range does not advance automatically
_generate_patterns() in z4j_brain.auth.common_passwords builds the
generated set at module import from hard-coded inputs. It is not a generated
build artefact.
This compact local list is not a breach-corpus lookup. z4j does not currently query Have I Been Pwned or another external breached-password service.
Passwords are hashed with argon2id. Defaults are:
time_cost=3memory_cost=64 MiBparallelism=4- 32-byte hash and 16-byte salt
Only the work parameters are configurable:
Z4J_ARGON2_TIME_COSTZ4J_ARGON2_MEMORY_COST(KiB)Z4J_ARGON2_PARALLELISM
Hash length and salt length are fixed in code. Other
Z4J_ARGON2_* names are ignored rather than rejected. Benchmark verification
on the hardware that will run the brain before sizing login capacity or
changing the work factors.
Rehash on login
Section titled “Rehash on login”After a successful login, z4j rehashes a stored hash whenever its parameters differ from the current configuration. This happens in either direction: raising the Argon2 work factors upgrades hashes as users log in, while lowering them also rewrites stronger hashes to the lower settings.
Error behaviour
Section titled “Error behaviour”validate_policy() uses these internal PasswordError.code values:
password_too_shortpassword_too_longpassword_too_simplepassword_in_breach_list
They are not currently an HTTP API contract. Request-schema length violations
return 422 before the policy runs. Other policy failures currently fall through
the generic error middleware and return HTTP 500 with internal_error; the
specific reason is present only in the brain log. CLI password-setting commands
print the policy message and exit non-zero. Do not make an API client branch on
the four internal codes.
Login timing
Section titled “Login timing”The absent-account branch of login verifies against a real dummy hash generated
at each brain-process boot. Login also holds success and failure responses to
the configured minimum duration (Z4J_LOGIN_MIN_DURATION_MS, default 300 ms).
These controls reduce account-existence timing differences; they are not a
latency guarantee.
Why short passwords use 3-of-4
Section titled “Why short passwords use 3-of-4”The older letter-plus-digit rule admitted low-entropy dictionary-plus-digit passwords. Requiring three character classes for passwords below 16 characters closes that short-password case while still permitting longer passphrases.
Dashboard feedback
Section titled “Dashboard feedback”The dashboard does not currently estimate password strength. Its password
fields use the minimum length returned by GET /api/v1/auth/policy; the server
remains the authority for composition and denylist checks. Because non-length
policy failures currently surface as generic internal_error responses, the
dashboard cannot display their specific policy reason.