The idea: store a slow hash, add a salt and a pepper

A server should never store your password. It stores a hash of it: a one-way value that it can recompute at login but nobody can turn back into the password. Databases leak, though. So the question is what an attacker can do with a stolen table. Two extras make the answer "very little": a salt and a pepper. They sound alike but live in different places and stop different attacks.

The password goes through HMAC with the pepper from the secrets manager, then argon2id with the user's salt; the users table stores user, salt, hash and pepper id, while the pepper stays outside the database.
The salt is stored in the user's row; the pepper never touches the database.
hash = argon2id( HMAC-SHA256(pepper, password), salt, m=19456 KiB, t=2, p=1 )

On the canvas the same four users sign up under each scheme: alice and carol both chose password1, bob letmein, and dave Tr0ub4dor&3. Then the table leaks to an attacker with a six-word wordlist.

What a salt does

A salt is a random value, made fresh for each user at sign-up (16 bytes is common) and stored next to the hash. It is not a secret. It changes two things:

  • Equal passwords no longer look equal. Without a salt, alice and carol get the same hash, and anyone who reads the table sees it (Demo 1: no salt). With a salt their hashes differ.
  • Precomputed tables stop working. A rainbow table maps hashes of common passwords back to the password. It is built once and used against every unsalted site. On the canvas it cracks three of four users with no hashing at all (Demo 2: rainbow table). With a salt, the attacker must hash each guess with each user's own salt: work done for alice does not help with carol (Demo 3: add a salt).

A salt does not stop guessing. The salt is in the leaked row, so the attacker simply tries the wordlist for each user: 16 slow hashes and password1 and letmein still fall.

What a pepper does

A pepper is one secret for the whole site, kept outside the database: in a secrets manager, an HSM or the service's environment. The server mixes it in with an HMAC before the slow hash. If only the database leaks (SQL injection, a stolen backup), the attacker cannot compute HMAC(pepper, guess), so it cannot check a single guess: nobody is cracked (Demo 4: salt + pepper, DB leaks).

A pepper only helps while it stays secret. If the attacker owns the app server, it reads the pepper from memory and the attack is back to the salt-only case (Demo 5: server hacked). And a pepper is no replacement for a salt: with the same pepper for everyone, equal passwords give equal hashes again, and once the pepper leaks one pass over the wordlist cracks every user at once (Demo 6: pepper without salt).

A grid of four schemes by two leaks. Plain MD5 and salt only: 3 of 4 cracked for both leaks. Pepper only and salt plus pepper: 0 cracked when only the database leaks, 3 of 4 once the server and its pepper leak.
The pepper decides whether a database-only leak cracks anyone; the salt decides how much work each user costs.

Salt and pepper side by side

saltpepper
secret?noyes
how manyone per user, new at every sign-up and password changeone per site (with a version id)
stored wherein the user's row, next to the hashsecrets manager / HSM / environment, never the DB
stopsrainbow tables, spotting equal passwords, one attack for all userscracking after a database-only leak
does not stopguessing weak passwords one user at a timeanything, once the server itself is taken

Why the hash must be slow

Neither a salt nor a pepper makes password1 hard to guess. What limits guessing is the cost of one guess. A general-purpose hash like MD5 or SHA-256 is built to be fast; a password hash is built to be slow and to need memory, which GPUs have little of per core. Rough speeds on one high-end GPU:

hashguesses per second (order of magnitude)
MD5~1011
SHA-256~1010
bcrypt, cost 12~103
argon2id, 19 MiB, t=2~103 or less

OWASP's minimums: argon2id with 19 MiB of memory, 2 iterations, 1 lane; or scrypt; or bcrypt with cost ≥ 10; or PBKDF2-HMAC-SHA256 with 600,000 iterations where FIPS is required. Raise them as far as your login latency allows.

Checking a login

Login runs the same pipeline (Demo 7: login): read the user's salt, hash and pepper id from the row, compute argon2id(HMAC(pepper, typed password), salt) and compare it with the stored hash. Use a constant-time compare so the time taken does not reveal how many leading bytes matched, and answer "wrong user or password" either way. The stored string normally carries its own parameters ($argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>), so after a successful login the server can rehash with stronger settings.

Rotating a pepper

A hash cannot be "re-peppered" without the password. So each row keeps a pepper_id; new and changed passwords use the new pepper, and old rows are rehashed the next time their user logs in. Rows still on the old pepper after a while can be forced to reset. An alternative design keeps the normal salted hash and encrypts it with a key from a KMS; rotating that key only needs a decrypt and re-encrypt of each row.

See also Encryption vs Hashing (why passwords are hashed, not encrypted), OWASP Cryptographic Failures (unsalted MD5 as a real-world bug), OWASP Authentication Failures (brute force and credential stuffing against the login form), How a Digital Signature Works (hashes and keys), How Browser Cookies Work (the session after login) and How OAuth 2.0 Works (logging in without the site seeing your password).

Hashes as names rather than secrets: Git stores every object under the SHA-1 of its content, see Git objects.

Kerberos stores a key derived from the password (string2key) and never sends the password over the network: see How Kerberos Works.

How the cost factor and memory-hardness decide the GPU's guesses per second is animated on Password Hashing: Fast vs Slow Hashes.

What the page leaves out

Real hashes are 32 bytes and salts 16; the page shows 6 and 4 hex characters from a toy hash, and the salts are fixed so the demos repeat. It also skips bcrypt's 72-byte input limit, real cracking rules (mangling, masks, huge breached-password lists), rate limiting and lockouts on the login form, checking new passwords against breach lists, multi-factor authentication, and how the pepper itself is protected (HSM, access policies, audit logs).