Skip to content
pwnsy
cryptographybeginner#password-hashing#salting#peppering#cryptography#authentication

Salting and Peppering Explained: Safer Password Storage

How salts and peppers protect stored passwords, why they defeat rainbow tables, and where each belongs. A plain guide to password hashing hygiene.

When a service is breached and passwords spill out, the difference between a minor incident and a catastrophe usually comes down to how those passwords were stored. Storing them in plain text is indefensible. Storing a bare hash of them is only slightly better. Salting and peppering are the two techniques that turn a leaked password store from an open buffet into a slow, expensive grind for the attacker.

Both are simple ideas layered on top of password hashing. Understanding what each one defends against, and what it does not, is the difference between security theater and storage that actually holds up under a breach.

Start with why plain hashing fails

The instinct is to hash passwords before storing them, so the database holds SHA-256(password) rather than the password itself. This helps, and it is not enough, for two reasons.

First, general-purpose hashes are fast. A modern machine computes billions of SHA-256 hashes per second, so an attacker with your hash can try billions of candidate passwords per second offline. Fast hashing is a virtue for file integrity and a liability for passwords. See How Hashing Works.

Second, hashing is deterministic. The same password always produces the same hash. That opens the door to precomputation. See Password Cracking Techniques.

Rainbow tables and the precomputation attack

Because a hash is deterministic, an attacker can compute the hashes of millions of common passwords once, store them in a giant lookup structure, and then reverse any matching hash instantly. A rainbow table is a space-efficient form of exactly this precomputed lookup from hash back to input.

With unsalted hashes, the economics favor the attacker badly. They build the table once and reuse it against every breached database forever. Worse, within a single stolen database, every user who chose password123 has the identical hash, so cracking one cracks them all at once.

This is the specific problem salting solves.

Salts: unique randomness per password

A salt is a random value, unique to each password, added before hashing. Instead of storing hash(password), you store hash(salt + password) together with the salt itself. The salt is not secret. It lives right next to the hash in the database.

That non-secrecy surprises people, so it is worth being clear about what the salt actually buys you.

  • It breaks precomputation. An attacker's rainbow table was built for bare passwords. With a unique salt per user, the attacker would need a separate table for every salt, which defeats the whole point of precomputing.
  • It de-duplicates identical passwords. Two users with the same password now have different salts and therefore different stored hashes. Cracking one tells the attacker nothing about the other.
  • It forces per-password work. The attacker can no longer attack the whole database in one pass. Each account must be attacked individually, multiplying their cost by the number of accounts.
ApproachStored valueRainbow table worksIdentical passwords collide
Plain hashhash(password)YesYes
Salted hashhash(salt + password) plus saltNoNo
Salt plus pepperhash(pepper + salt + password) plus saltNoNo, and DB alone is insufficient

A salt should be generated from a cryptographically secure random source and be long enough to be effectively unique, commonly 16 bytes. See Secure Random Number Generation.

Salts are not secret, and that is fine

The salt's job is not to be hidden. It is to be different for every password, so that no single precomputed table and no single cracked hash helps the attacker anywhere else. Storing the salt beside the hash is exactly how the standard password hashers work.

Peppers: one secret, kept apart

A pepper is a different tool for a different threat. It is a single secret value added to every password before hashing, and crucially it is stored somewhere other than the password database, for example in application configuration, a separate secrets manager, or a hardware security module.

The scenario a pepper defends against is a database-only breach. If an attacker steals just the database, they have the salts and the hashes but not the pepper. Without the pepper they cannot even begin an offline cracking run, because they are missing an input that went into every hash. The pepper turns a database leak, on its own, into a dead end.

The tradeoffs are real and worth stating plainly.

  • The pepper is shared across all passwords, so if the pepper itself leaks, it provides no protection and you are back to salted hashing.
  • Because it is applied to everyone, rotating a pepper is awkward; it usually requires versioning peppers and rehashing on next login.
  • A pepper complements salting; it does not replace it. Salts handle same-database attacks and precomputation, peppers handle the database-only leak.

A common modern approach is to apply the pepper as a keyed step, such as an HMAC of the password with a secret key, before or after the slow hash. See What Is HMAC.

The layer underneath: slow hashing

Salting and peppering both assume you are already hashing with a slow, purpose-built password function. This is the load-bearing part, and it is easy to skip.

A fast hash like SHA-256 lets an attacker try billions of guesses per second even with salting. Password hashing functions are deliberately slow and, in the best cases, memory-hard, so each guess costs meaningful time and memory. The current recommended choices are Argon2 (the modern default), scrypt, and bcrypt. NIST SP 800-132 covers the key-derivation side of this. See Password Hashing: bcrypt, scrypt, Argon2 and Key Derivation Functions Explained.

Salting a fast hash is still weak

A unique salt on top of plain SHA-256 stops rainbow tables, and it does nothing to slow down a straightforward brute-force run, because the hash is fast. Salting and slow hashing are separate defenses and you need both. Use Argon2, scrypt, or bcrypt as the base, then rely on its built-in salting.

Common mistakes

  1. Using a fast hash for passwords. SHA-256 or MD5, salted or not, is the wrong tool. Use Argon2, scrypt, or bcrypt.
  2. Reusing one salt for every user. A shared salt is close to no salt. It must be unique per password. This is why you let the library generate it.
  3. Hand-rolling the salt handling. Modern hashers such as bcrypt and Argon2 generate the salt, embed it in the output string, and read it back on verification. Managing salts manually invites bugs.
  4. Storing the pepper in the same database as the hashes. A pepper in the same store that gets breached provides no protection. Keep it in a separate secret store or HSM.
  5. Treating a pepper as a substitute for salting or slow hashing. It is an added layer for one specific threat, on top of the other two, never instead of them.

The three defenses as separate cost multipliers

It helps to think of the whole scheme as three independent multipliers on the attacker's cost, each addressing a different weakness. The slow hash multiplies the cost of every single guess. Where a fast hash lets an attacker try billions of candidates per second, a tuned slow hash might allow only a handful per second on the same hardware, so the same guessing effort covers a tiny fraction of the ground. The salt multiplies the cost across accounts, forcing the attacker to attack each account on its own rather than the whole database in one pass, and removing the shortcut of a precomputed table. The pepper multiplies the cost by an entire additional compromise, because without the separately stored secret the offline run cannot begin.

Because the three multipliers act on different weaknesses, removing any one leaves a gap the others do not cover. A slow hash without a salt still lets a precomputed effort and identical-password collisions help the attacker. A salt without a slow hash still lets a fast brute-force run tear through human-chosen passwords. A pepper without either still collapses to whatever the underlying hashing provides once the secret leaks. The scheme is strong precisely because the three are stacked, and each is doing a job the others cannot.

There is a useful way to apply the pepper that keeps it cleanly separated from the slow hash. Rather than concatenating the pepper into the password string, many systems apply it as a keyed step, computing an HMAC of the password with the secret pepper as the key, and then feeding that result into the slow hasher. The keyed construction gives the pepper a well-defined cryptographic role and makes versioning it more tractable, because the key used can be recorded as a version number alongside the stored hash. See What Is HMAC.

A worked example: one breach, three storage schemes

Picture the same stolen database under three different storage decisions, and follow what an attacker can do with each. The database holds a million accounts, and among them ten thousand people chose the same weak password.

Under plain hashing, the attacker computes the hash of that weak password once and searches the stolen file for matches. All ten thousand accounts fall in a single lookup, because identical passwords produced identical hashes. The attacker then loads a precomputed table and reverses a large fraction of the remaining accounts without computing anything new. The work is close to constant regardless of how many accounts there are.

Under salted hashing, the picture changes completely. Each of those ten thousand shared passwords now sits behind a different salt, so each produced a different stored hash. The precomputed table is useless, because it was built for bare passwords and every entry here is effectively a different input. The attacker must now attack each account separately, computing candidate hashes against that account's specific salt. The cost has multiplied by the number of accounts, and cracking one account reveals nothing about any other.

Under salted hashing with a pepper, the attacker who stole only the database cannot begin at all. Every hash was computed over the pepper as well as the salt and the password, and the pepper is not in the file. Without it, no candidate guess can reproduce a stored hash, so the offline run never starts. The attacker is stuck until they also compromise the separate location where the pepper lives.

The same breach produces three very different outcomes, and the only variables are the storage decisions made long before the incident.

Detection and operational signals

Password storage weaknesses rarely announce themselves, so the signals are found by inspection and by watching how the system behaves.

  • Identical hashes across accounts. If two users with the same password share a stored hash, salting is absent or shared. A quick scan for duplicate hash values in a store is a direct tell.
  • Fast verification times. A login that verifies a password in well under a millisecond is likely using a fast general-purpose hash rather than a deliberately slow function. Purpose-built hashers take meaningful, tunable time by design.
  • Short or fixed hash formats. A bare 64-character hexadecimal value suggests a plain SHA-256 with no algorithm identifier, cost parameter, or embedded salt. Modern hasher outputs carry a recognizable prefix that names the algorithm and its parameters.
  • No versioning path. If there is no field recording which algorithm or which pepper version produced a stored hash, the system cannot rotate parameters or a leaked pepper without a painful migration. The absence of that field is an operational warning.
  • Pepper stored beside the hashes. If the secret meant to be kept apart is readable from the same database as the hashes, it provides no protection against the exact breach it exists to stop.

Salt versus pepper at a glance

The two are easy to confuse because both are extra values mixed into a password before hashing. Their properties are different in every respect that matters.

PropertySaltPepper
UniquenessDifferent for every passwordSingle value shared across all passwords
SecrecyNot secret, stored openlySecret, stored apart from the database
Storage locationNext to the hash in the databaseApplication config, secrets manager, or HSM
Threat it addressesPrecomputation and identical-password collisionsDatabase-only breach
Effect if it leaksLittle, it was never secretFalls back to salted hashing
Who manages itGenerated automatically by the hasherProvisioned and rotated by operators

Reading the table top to bottom shows why the two stack rather than compete. The salt handles attacks that involve the database's own contents, and the pepper handles the case where the database is all the attacker has.

Common misconceptions

Several persistent beliefs lead teams to build storage that looks safe and is not.

  • The salt must be secret. It does not. Its value comes from being unique, so no single table and no single cracked hash helps anywhere else. Standard hashers store it in plain sight.
  • A long password does not need slow hashing. Length raises the cost of guessing, and a fast hash still lets an attacker try billions of candidates per second, which reaches many human-chosen passwords. Slow hashing is a separate, load-bearing defense.
  • A pepper replaces salting. It does not. A pepper is shared across everyone, so on its own it does nothing about identical-password collisions or precomputation. It is a third layer for one specific threat.
  • Salting a fast hash is enough. A unique salt on plain SHA-256 stops precomputed tables and does nothing to slow a direct brute-force run, because the hash is fast. The base must be a slow, purpose-built function.
  • Hashing and encryption are interchangeable here. Encryption is reversible with a key, which makes it the wrong model for stored passwords. A verifier only needs to confirm a match, and a one-way slow hash provides exactly that without the recoverable plaintext that a key would imply.

How password storage practice evolved

Early systems often stored passwords in plain text or under a single fast hash, and the arrival of large precomputed tables made those choices untenable. Salting became standard practice to defeat precomputation and to stop one cracked password from unlocking every account that shared it. As commodity hardware and graphics processors made fast hashes cheap to attack at enormous scale, the emphasis moved to deliberately slow, and later memory-hard, functions that resist the parallelism of specialized cracking rigs.

The current consensus reflects that history. Argon2 emerged from a public competition to design a memory-hard password hash and is the modern default recommendation, with scrypt and bcrypt as well-understood alternatives. NIST guidance on password-based key derivation and on digital identity codified the direction, and OWASP distilled it into practical storage advice. Peppering sits alongside this as an optional extra layer for organizations that can manage a separate secret safely.

Let the library own the salt

Every modern password hasher generates a fresh random salt for each password, embeds it in the output string alongside the algorithm identifier and cost parameters, and reads it back automatically on verification. That single output string is all you store. Hand-managing salts in separate columns is where avoidable bugs, such as a reused or truncated salt, tend to creep in.

Frequently asked questions

Do I still need a salt if I use bcrypt or Argon2? You already have one. These functions generate a unique salt per password and embed it in the encoded output. You store that single string, and the salt travels with it. You do not add a separate salt yourself.

Is a pepper worth the extra complexity? It depends on whether you can store and rotate a secret apart from the database safely. When you can, a pepper turns a database-only breach into a dead end, which is a meaningful gain. When you cannot store it separately, it adds operational risk without the benefit, so salted slow hashing is the sound baseline.

Can I use SHA-256 if I add a salt and many iterations? A construction that iterates a fast hash many times moves in the right direction, and it is what purpose-built functions already do in a tuned, reviewed form. Reach for Argon2, scrypt, or bcrypt rather than assembling your own iterated SHA-256, because the parameters and edge cases are easy to get wrong.

Where should the pepper live? Anywhere that is not the password database. Common homes are application configuration loaded at runtime, a dedicated secrets manager, or a hardware security module. The requirement is only that stealing the database does not also hand over the pepper.

How long should a salt be? Long enough to be effectively unique across every password you will ever store. Sixteen bytes from a cryptographically secure random source is a common and comfortable choice, and the standard hashers pick an appropriate length for you.

What happens when I need to change the hashing cost or rotate a leaked pepper? You upgrade opportunistically. On each successful login, you have the plaintext for a moment, so you recompute the hash with the new parameters or the new pepper version and store it. Recording an algorithm and version identifier alongside each hash is what makes this smooth.

Does salting protect a weak password? Not by itself. Salting stops precomputation and stops one crack from unlocking many accounts. A slow hash raises the per-guess cost. Neither rescues a password that appears near the top of a common-password list, which is why length and uniqueness of the password still matter.

How does verification actually work if the salt is random? On registration, the hasher generates a random salt, computes the hash over the salt and the password, and stores both together in one encoded string. On login, the code reads that stored string, extracts the salt and parameters it embeds, recomputes the hash over the same salt and the supplied password, and compares. The salt being random is no obstacle, because it travels with the hash and is read back at verification time.

Should I compare hashes with a plain string equality check? Use a constant-time comparison for the final match so that timing differences do not leak information about how many leading bytes matched. The standard verify functions provided by password hashers already do this internally, which is another reason to call the library's verify routine rather than comparing strings yourself.

Credential-stuffing and offline cracking follow directly from weak password storage, and related flaws surface constantly in advisories. Our Exploit Intelligence dashboard tracks live CVEs by severity and exploitation likelihood, so you can see which authentication and credential-handling flaws are under active pressure.

Salting, peppering, and slow hashing are three separate defenses that stack. The slow hash makes each guess expensive, the salt stops precomputation and forces per-account effort, and the pepper makes a database-only leak useless on its own. Reach for a modern password hasher that gives you the first two by default, add a pepper stored apart from the database for the third, and a breach becomes a slow grind rather than an instant loss.

Sources & further reading

Sharetwitterlinkedin

Related guides