Checking passwords
[auth]
hash_workers = 2 # threads that compute the hashes
hash_queue = 1024 # attempts waiting for a thread
hash_per_address = 4 # slots one address can hold at once
Options
The three keys are read at startup; a reload does not change the
size of the pool. All the keys: Reference [auth].
How it works
An argon2id, bcrypt or sha512-crypt hash is pure computation: from 16 ms
(owasp-min) to more than a second (heavy argon2). Checks run
on dedicated threads (craft-file-gate-hash-0, -1… in ps -L), off the
runtime that serves transfers, the API and the probes.
| Situation | What happens |
|---|---|
| a thread is free | the attempt is checked at once |
| threads busy, room in the queue | the attempt waits its turn |
| threads busy, queue full | immediate refusal, with no account lookup, not counted for the ban |
the address already holds hash_per_address places | same refusal, whatever the load |
| client gone before its turn | the attempt is not hashed; its place is given back |
| address banned while waiting | the attempt is not hashed; banned-address response |
| server shutdown | no new attempt admitted; those on a thread finish |
An attempt takes its place before the account lookup: a known name and an unknown name get the same response. Rate limiters and bans (Bans) bound what a burst costs the queue.
Behind a reverse proxy, declare it in trusted_proxies: each client
counts at its own address. A NAT address goes in the door’s whitelist_ips
([sftp.ban], [api.ban], [admin.ban]), which is not capped. Console
sign-in (POST /admin/login) goes through the same pool.
Choosing the sizes
| Question | Answer |
|---|---|
| why at most 2 threads by default | each thread can hold a whole argon2 m; 2 threads at the owasp-min profile stay under 80 MB |
| how many connections per second | 2 threads: about thirty at the rfc9106-low-mem profile (68 ms), over a hundred at the owasp-min profile (16 ms) |
| why 1024 places | a place is a waiting connection (~70 KiB over SSH, ~42 KiB with Basic), not an argon2 allocation; 100 clients reconnecting together wait instead of being refused |
| wait of the last place | hash_queue / hash_workers checks: 6.5 s with 2 threads at the owasp-min profile; stay under sftp.login_grace_secs (120 s) |
| tight memory budget | a shorter queue, sized to the expected burst (hash_queue = 32); the pool holds hash_workers times the largest m in the file: Memory |
What you will see
At startup, one line gives each number and its source:
INFO password hashing pool started: passwords are checked on these threads, off the async runtime, ...
workers=2 workers_source="detected (8 CPUs ...), capped at 2 by default: ..."
queue=1024 queue_source="default (1024, ...)"
per_address=4 per_address_source="default (4 per address, ...)"
peak_memory="2 threads × 19 MiB = 38 MiB"
tokio_workers=8 tokio_workers_source="detected (std::thread::available_parallelism)"
tokio_workers: the threads of the async runtime, from the same detection with no
cap, or TOKIO_WORKER_THREADS.
A reload of users.toml that changes the largest m says so:
INFO password hashing peak memory changed with the users file.
Kubernetes
A pod without limits.cpu sees the node’s CPUs (32, 64). The cap at 2 protects
the default pool, not the tokio runtime. To follow what the pod requests,
pass requests.cpu through the Downward API (rounded up to the next integer):
env:
- name: CRAFT_FILE_GATE_HASH_WORKERS
valueFrom:
resourceFieldRef:
containerName: craft-file-gate
resource: requests.cpu
divisor: "1"
# same for TOKIO_WORKER_THREADS
An explicit value has no cap: requests.cpu: 8 gives 8 threads and
8 x m of memory. The Helm chart does it with passwordHashing.workers (empty:
requests.cpu) and passwordHashing.queue.