Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

SituationWhat happens
a thread is freethe attempt is checked at once
threads busy, room in the queuethe attempt waits its turn
threads busy, queue fullimmediate refusal, with no account lookup, not counted for the ban
the address already holds hash_per_address placessame refusal, whatever the load
client gone before its turnthe attempt is not hashed; its place is given back
address banned while waitingthe attempt is not hashed; banned-address response
server shutdownno 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

QuestionAnswer
why at most 2 threads by defaulteach thread can hold a whole argon2 m; 2 threads at the owasp-min profile stay under 80 MB
how many connections per second2 threads: about thirty at the rfc9106-low-mem profile (68 ms), over a hundred at the owasp-min profile (16 ms)
why 1024 placesa 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 placehash_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 budgeta 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.