Password Hashing: How Salting and Peppering Work
Every security review eventually lands on the same question: "How are we storing passwords?" If the answer is "hashed," the follow-up is always "with salt?" — because an unsalted hash is closer to plaintext than most teams realize. Salting and peppering are the two classic additions that turn a weak password store into a defensible one, and modern password hashing algorithms build the first one directly into their design.
This guide explains what each mechanism defeats, how they differ, and how algorithms like bcrypt, Argon2, and PBKDF2 put the theory into practice.
Why Plain Hashing Fails
The naive approach looks reasonable: hash the password with SHA-256 and store the digest.
password: "hunter2"
SHA-256("hunter2") = 25ba7a3b37a70b5ef3f3e5b6d35c91b352e3a0f4d5d18ea0a6791cb1c12b8d21
The problems appear the moment that database leaks:
- Rainbow tables make it instant. Attackers precompute hashes of billions of common passwords once, then look up every stolen digest in a table. SHA-256 is so fast that a commodity GPU can test billions of candidates per second; the lookup takes milliseconds.
- Identical passwords produce identical hashes. If two users both chose
Summer2026!, their stored digests match — so the attacker cracks one and gets both. Patterns across the user base become visible: everyone who reused a corporate password clusters to the same value. - Fast hashes are a feature for attackers. SHA-256 was designed to be quick on hardware. That is exactly wrong for password storage, where every verification should be expensive for an attacker running billions of guesses.
Salting addresses points 1 and 2; deliberate slowness addresses point 3.
Salting: One Random Value per Password
A salt is a unique random value generated for each password at creation time:
salt (random, 16 bytes): 9f86d081884c7d659a2feaa0c55ad015
stored: salt + algorithm + hash(password + salt)
Verification recomputes the hash with the stored salt and compares the results.
What Salting Defeats
- Rainbow tables die. The precomputed table was built for unsalted hashes. With a per-password salt, an attacker must recompute the table for every salt — practical value: zero. They must instead guess each password individually.
- Identical passwords diverge. Two users with
hunter2get different salts, hence different stored hashes. Cross-account pattern analysis no longer works. - Targeted cracking slows down. Even knowing a user's password is salted, the attacker still needs the salt (which lives in the same database) — so a leak still exposes everything, but the cracking cost per hash rises dramatically because no precomputation carries over.
Salting Rules That Matter
- Uniqueness beats secrecy. The salt does not need to be secret; it needs to be different for every password. A global secret salt (one value for the whole database) provides almost no benefit — it is one more secret to steal and it restores rainbow-table viability if it leaks.
- Cryptographically random. Use a CSPRNG (
random_bytes()in PHP,secretsin Python). Timestamps or user IDs are guessable and reduce the salt's effective entropy. - Store it alongside the hash. The salt must be available at verification time, so it lives in the same row or column. This is not a weakness — the security comes from the salt being unique, not hidden.
- Never reuse a salt across users or across password rotations.
Modern libraries generate and manage salts automatically. The Salt Hash Generator demonstrates the mechanics with random or custom salts so you can see two identical inputs produce two different digests.
Peppering: The Server-Side Secret
A pepper is a secret key, shared across all passwords, stored outside the database — in an environment variable, a secrets manager, or a hardware module:
stored = hash(pepper + salt + password)
The distinction from a salt is the crucial part:
| Salt | Pepper | |
|---|---|---|
| Uniqueness | Unique per password | Shared across all passwords |
| Location | Stored with the hash (in the DB) | Stored outside the DB (env/secrets manager) |
| Purpose | Defeat precomputation & pattern analysis | Limit damage of a pure database leak |
| Lost if database stolen? | Yes (by design) | No — it was never there |
What Peppering Defeats
If an attacker exfiltrates only the database — through SQL injection, a backup leak, or a misconfigured bucket — they obtain hashes and salts but not the pepper. Cracking now requires the pepper, so the entire leak is useless until the attacker also compromises the application host or secrets manager.
Limitations to Understand
- No help against a full server compromise. If the attacker can read the environment variable, the pepper is gone along with everything else.
- Operational burden. Losing the pepper means every password becomes unverifiable — users must reset. Rotating it requires re-hashing every stored password (pepper old → pepper new, one login at a time or in a batch job).
- Not a substitute for salt. A shared pepper without per-password salts still produces identical digests for identical passwords; the two mechanisms solve different problems.
Peppering is a cheap, high-value layer for any system that treats a database leak as a realistic threat — which should be every system that stores passwords.
Password-Hashing Algorithms Do the Rest
Salts (and stretching) are built into dedicated password hashing functions, which is why security guidance says "never use SHA-256 directly for passwords":
bcrypt
Generates a random salt internally, embeds it in the output, and runs a configurable number of rounds via the cost factor. $2y$12$... means cost 12 — each increment doubles the work. The Bcrypt Generator lets you adjust the cost and watch verification time change accordingly.
Argon2
The 2015 Password Hashing Competition winner, designed to resist GPU and ASIC attacks by consuming memory as well as CPU. Parameters control memory usage (m), iterations (t), and parallelism (p). Argon2id is the recommended variant for most applications. The Argon2 Hash Generator exposes all three parameters.
PBKDF2
Standardized in NIST SP 800-132, it applies HMAC repeatedly (-WithSHA256, 600,000+ iterations per current OWASP guidance). No memory hardness, which makes it weaker than Argon2 against dedicated hardware, but universally available — including inside many database products via CREATE USER ... IDENTIFIED WITH.
scrypt An earlier memory-hard alternative, still found in legacy systems and cryptocurrency wallets.
Parameter Guidance (2026 Baseline)
| Algorithm | Baseline setting | Notes |
|---|---|---|
| bcrypt | cost 12+ | Raise until ~250–500 ms per hash on production hardware |
| Argon2id | 64 MB, t=3, p=4 | OWASP minimum; more memory = fewer GPU gains |
| PBKDF2-SHA256 | ≥600,000 iterations | NIST/OWASP floor; prefer Argon2 where available |
Tune against your own hardware every year — what takes 300 ms today takes 100 ms on tomorrow's server, which also speeds up the attacker by the same factor.
Common Mistakes
- Double-hashing myths — "SHA-256 then bcrypt" adds little; bcrypt already includes a salt and stretching. Extra layers mostly complicate migrations.
- Fast hashes for passwords — SHA-256/MD5/SHA-1 are for integrity checks and tokens, not credentials. The Hash Type Identifier can tell you what algorithm produced a digest you found in a legacy table.
- Salt in the username or email — deterministic, guessable, and reused if the user changes their address. Use randomness.
- Static pepper committed to code — a pepper in your Git repository is not a pepper; it leaked with the first clone. Secrets managers and environment injection exist for this.
- Never re-hashing — as hardware improves, parameters must rise. Plan a transparent re-hash-on-login strategy from the start.
- Ignoring password strength on top of hashing — a perfectly hashed
123456is still cracked in seconds. Pair storage hardening with a strength meter like the Password Strength Checker.
Frequently Asked Questions
Is a salt the same as a pepper? No. A salt is unique, stored with the hash, and defeats precomputation. A pepper is a shared secret stored outside the database and limits the value of a database-only leak. Use both.
Should I store the salt in the same column as the hash?
Yes — that is standard practice. Format like algorithm$params$salt$hash (bcrypt embeds this automatically). The salt's value is its uniqueness, not its secrecy.
Can I use one salt for the whole database? It would stop naive rainbow tables, but it reintroduces cross-user pattern analysis and becomes a single high-value secret. Per-password random salts are cheap; there is no reason to skip them.
Does salting make stolen hashes safe? No — it raises the cost of offline cracking and kills precomputation, but a weak password with a salt is still a weak password. Salting, strong algorithms, high parameters, and password policy work together, not separately.
Conclusion
Salts make every stored password unique and defeat precomputed attacks; peppers ensure a database leak alone is not enough to crack a single credential; modern algorithms like bcrypt and Argon2 bundle salting with deliberate slowness. Review your storage format today — if your hashes have no salt or no stretching, migrate to Argon2id or bcrypt with current parameters, and use the Salt Hash Generator and Bcrypt Generator to compare outputs before you commit code. For the algorithm-selection side of the decision, Choosing a Hash Algorithm for Data Security covers how password hashing differs from general-purpose hashing.