Password Hashing and Salting: What a Salt Can and Cannot Do

Guilliano MolaireGuilliano MolaireUpdated 21 min read

Password hashing turns a password into a fixed-length, one-way fingerprint that cannot be turned back into the original, so a stolen database does not hand an attacker every user’s credentials. Salting in security means generating a unique random value for each password and feeding it into the hash alongside the password, so two users with the same password get two different hashes. The salt is stored in the clear next to the hash and is not a secret: its job is to defeat precomputed lookup tables and crack-once-unlock-thousands attacks, not to make a weak password strong.

OWASP says passwords must only ever be stored as salted hashes from a modern, adaptive algorithm like Argon2id, and the breach record shows why: RockYou stored plaintext, LinkedIn used unsalted SHA-1, Adobe encrypted instead of hashing, and each mistake turned a single leak into tens or hundreds of millions of compromised accounts.

Every serious breach post-mortem eventually lands on the same question: how were the passwords stored? This post covers the three famous ways to get it wrong, the GPU math that explains why fast hashes fail, the exact parameters OWASP recommends in 2026, what a salt does and does not do (with a worked example and a salt versus pepper table), and how a production identity provider like Keycloak stores the salt and the hash parameters for each credential and upgrades old hashes without locking anyone out.

Key takeaways

  • Hashing is one-way. Encryption is reversible with a key. Encoding (Base64) is reversible by anyone. Only the first belongs anywhere near password storage.
  • A salt is a per-password random value stored in the clear beside the hash. It is public by design, and it stops precomputed tables and shared-hash cracking, but it does nothing against guessing a weak password.
  • A single consumer RTX 4090 guesses MD5 at 164.1 GH/s but bcrypt at just 184 kH/s, roughly a million-fold difference (Hashcat v6.2.6 benchmarks).
  • OWASP’s 2026 baseline: Argon2id at m=19456, t=2, p=1 minimum, bcrypt at work factor 10 or higher, or PBKDF2-HMAC-SHA256 at 600,000 iterations where FIPS applies.
  • Keycloak defaults to Argon2 since version 25.0, stores each credential’s salt and hash parameters as JSON on the credential record, and re-hashes legacy credentials automatically the next time each user logs in.

What is password hashing (and how is it different from encryption)?

A hash function takes an input of any length and produces a fixed-length output, the digest. The operation only runs one way: you can compute the digest from the password, but you cannot compute the password from the digest. So you never store the password at all. At login, you hash whatever the user typed and compare digests. A match means the password was correct, and the original never needs to exist in your database.

Encryption is a different tool for a different job. It is designed to be reversed by whoever holds the key, which is exactly what you want for data you need to read back later, and exactly what you do not want for passwords. If the key exists, it can be stolen, and one stolen key unlocks every password at once. The OWASP Password Storage Cheat Sheet is blunt about it: passwords should be stored hashed with modern adaptive algorithms, never encrypted, never plaintext.

Encoding is not even in the conversation, though it keeps showing up in breach write-ups anyway. Base64 transforms data for transport and anyone can undo it with zero secrets involved. That is why the payload of a JWT is readable by design: paste any token into our JWT token analyzer and the “encoded” claims come straight out. Base64 is a transport format, not a security measure.

Technique Reversible? Secret involved The right job The wrong job
Hashing No, one-way None Password storage, integrity checks Anything you need to read back
Encryption Yes, with the key Encryption key Data you must recover (messages, PII at rest) Password storage
Encoding Yes, by anyone None Transport formats (Base64 in JWTs) Any security purpose at all

One catch before moving on: MD5, SHA-1, and SHA-256 are all genuine hash functions, and they are still the wrong choice for passwords. They were engineered to be fast, and for password storage, fast is the enemy. More on that below.

What happens when password storage goes wrong?

There are three classic failure modes: storing plaintext, using a weak or unsalted hash, and encrypting when you should have hashed. Conveniently for anyone teaching this topic, each one has a famous breach attached.

RockYou (2009): plaintext

RockYou, a social gaming company, was hit through a SQL injection flaw and lost over 32 million passwords stored in plaintext (TechCrunch, 2009). No cracking was required. The attacker read them the way you would read a spreadsheet.

The aftermath is the part that stings. That dump became rockyou.txt, a wordlist of 14,344,392 lines that Kali Linux ships in its wordlists package (Kali Linux tools) and that gets loaded in nearly every password audit and CTF to this day. A storage decision made before 2009 is still compromising accounts in 2026, because every password in that file is permanently in every attacker’s opening dictionary.

LinkedIn (2012): unsalted SHA-1

LinkedIn hashed its passwords, which sounds responsible until you see the details: SHA-1, no salt. In 2012, 6.5 million hashes appeared online. In 2016 the true scope surfaced when 117 million email and password combinations from the same breach went up for sale, and roughly 90% of the hashes were cracked within 72 hours (TechCrunch, 2016). LinkedIn had to reset passwords for affected users a second time, four years after the original incident (Krebs on Security, 2016).

There are two lessons in one breach. Unsalted means identical passwords produce identical hashes, so attackers crack a hash once and unlock every account sharing it, and precomputed lookup tables work at full strength. And SHA-1 is a speed-optimized hash, so even the salted version would have fallen fast. The 72-hour number is what “we hashed the passwords” looks like when both the salt and the algorithm are wrong. We come back to the LinkedIn numbers in the salting section below, because they show exactly which half of the problem a salt fixes.

Adobe (2013): encryption instead of hashing

Adobe lost a database that Sophos counted at “not just 38,000,000 breached records, but 150,000,000 of them”, and the passwords were not hashed at all (Sophos Naked Security, 2013, archived). Adobe confirmed that the system “used Triple DES encryption”, and the choice was “symmetric key encryption over hashing, selecting ECB mode, and using the same key for every password” (CSO Online, 2013). The password hints were stored in plaintext right alongside, and Sophos, analysing the dump, kept them “because they were very handy indeed” (Sophos Naked Security, 2013, archived).

ECB mode encrypts identical inputs to identical outputs, so every user who picked the same password had the same ciphertext. Group the duplicates, read the hints next to them (“my usual”, “name of my dog”, “123456”), and the dataset turns into what the Sophos write-up, quoting xkcd, called “the greatest crossword puzzle in the history of the world”, and the internet solved it together. And because encryption is reversible, a single leaked key would have decrypted all 150 million records in one shot, with no hint-guessing needed.

Plaintext, weak hashing, and encryption in place of hashing are three different engineering decisions with one identical outcome. Assume your database will leak eventually. How you stored passwords decides whether that leak is an incident report or a company-ending event.

Why are fast hashes like MD5 and SHA-256 useless for passwords?

Attackers do not reverse hashes, because they cannot. They guess. They take a candidate password, hash it, compare against the stolen digest, and repeat a few billion times per second. The only question is how expensive each guess is, and that is where general-purpose hashes collapse.

Here is what a single RTX 4090, a consumer gaming card, achieves under Hashcat v6.2.6 (Chick3nman’s published benchmarks, corroborated by Tom’s Hardware):

Algorithm Guesses per second (1x RTX 4090)
NTLM (Windows) 288.5 GH/s
MD5 164.1 GH/s
SHA-1 50.6 GH/s (listed as 50,638.7 MH/s)
bcrypt (cost 5) 184 kH/s

Read the units carefully. MD5 falls at 164.1 billion guesses per second. bcrypt, even at a deliberately weak cost factor of 5 (the $2a$05$ cost of Hashcat’s example bcrypt hash), holds the same card to 184 thousand (Hashcat benchmarks). That is roughly a million-fold difference in attacker cost from a single software choice, on identical hardware.

Run the arithmetic and the difference becomes concrete. The full 95-character printable keyboard set has 95 to the power of 8, about 6.6 quadrillion, possible 8-character passwords. Divide that by the 164.1 billion MD5 guesses per second from the Hashcat benchmarks and one 4090 exhausts every single one in about 11 hours. Divide it by the 184 thousand bcrypt guesses per second instead and the same exhaustive search runs past a thousand years, and production bcrypt deployments use cost 10 or higher, doubling the work with every increment.

This is what “adaptive” means and why it matters. Algorithms like bcrypt and Argon2 have a tunable cost you raise as hardware improves, and Argon2 adds memory-hardness on top, forcing each guess to occupy real RAM. GPUs have thousands of cores but nowhere near enough fast memory per core, so the massively parallel guessing that shreds MD5 does not scale against a memory-hard function. We go deeper on the attacker’s side of this in how to prevent password cracking in IAM.

Which algorithm and parameters should you use in 2026?

You do not need to guess at settings. OWASP publishes exact minimums and keeps them current (Password Storage Cheat Sheet, 2026), in this order of preference:

Algorithm OWASP minimum parameters When to use it
Argon2id m=19456 (19 MiB), t=2, p=1 First choice for anything new
scrypt OWASP’s published fallback settings When Argon2id is not available in your stack
bcrypt Work factor 10 or higher Legacy systems already built on it
PBKDF2-HMAC-SHA256 600,000 iterations (SHA-512: 220,000) When FIPS-140 compliance is mandatory

Two footnotes are worth knowing. First, OWASP notes that bcrypt “has a maximum length input length of 72 bytes” in most implementations (OWASP), which bites people who pre-hash or use long passphrases. We cover that quirk, the work factor math, and where bcrypt still earns its keep in our bcrypt deep-dive.

Second, these numbers have a shelf life. OWASP’s point about a work factor is that “it can be increased over time as hardware becomes more powerful and cheaper” (OWASP), so treat work factors as a dial you revisit yearly, not a constant you set at launch and forget.

Patched on the day, not on your next maintenance window

Keycloak 26.7.2 fixed CVE-2026-18963, an account takeover through password reset. Skycloak had it available for auto-upgrade and told customers the same day upstream shipped it. Self-hosted teams schedule that work themselves.

What is salting in security?

Salting is the practice of generating a unique random value, the salt, for every password and feeding it into the hash function together with the password. The hash you store is the digest of the salt plus the password, and the salt itself is stored in the clear right next to that digest, so the verifier can repeat the calculation at login. Wikipedia defines a salt as “random data fed as an additional input to a one-way function that hashes data, a password or passphrase”, and OWASP describes it as “a unique, randomly generated string that is added to each password as part of the hashing process”.

Many readers assume the salt is a secret, but it is not. It sits in the same database row as the hash, and an attacker who steals the table gets every salt along with every digest. That is fine, because the salt’s job was never to hide anything. As Wikipedia puts it, “the salt does not need to be encrypted, because knowing the salt would not help the attacker”. What the salt does is make each stored hash unique, even when the passwords behind them are not, and that single property breaks two attacks:

  • Precomputed lookup tables. Without salts, an attacker can hash a dictionary of common passwords once, keep the table, and look up every stolen digest in it. Rainbow tables are a compressed version of the same idea. With a unique salt per password, the attacker would need a separate table per salt, which Wikipedia notes “is not viable because of the computational cost of doing so”.
  • Crack once, unlock thousands. Without salts, every user who chose 123456 has the same digest, so one successful guess exposes all of them at once. With salts, those users have different digests, and the attacker has to guess each account separately.

This is exactly the half of the LinkedIn problem that a salt would have fixed. The 117 million unsalted SHA-1 hashes could be attacked as one job, with every guess checked against the whole set and every duplicate password falling together (TechCrunch, 2016). With a salt per user, the same attacker would have had 117 million separate jobs. It would not have made SHA-1 slow, which is the other half of the problem and the reason the algorithm table above exists.

There are two practical details to know. NIST SP 800-63B requires that the salt “SHALL be at least 32 bits in length and chosen to minimize salt value collisions among stored hashes”, and in practice libraries use 16 bytes (128 bits) or more. And you should almost never generate or concatenate a salt yourself: bcrypt, scrypt, Argon2 and PBKDF2 implementations generate a random salt and store it inside the output string or alongside the digest, so if you find hand-rolled salt handling in a 2026 codebase, something upstream went wrong. Early Unix used a 12-bit salt, “4,096 possible salt values”, with the crypt function (Wikipedia), which was reasonable in the 1970s and is nowhere near enough today.

Same password, two salts: a worked example

Here is the effect in real digests. Two users both choose the password correcthorse. We give each a random 8-byte salt (shown in hex), prepend it to the password, and hash with SHA-256. We use SHA-256 only because its output is short and easy to reproduce in a few lines, and the three digests in the table are the output of the Node snippet below it, which you can run yourself. For storage, you would use Argon2id, and the salt would be generated for you.

User Salt (hex) SHA-256(salt + password)
alice a3f9c2e17b4d8e60 1e3fa3e997388ddea0d0f9fe8b9872f3c4b11e62402d105b415bd3426b396366
bob 5c01e7b2f94a3d8c e3cbcb35090c7e67b727550f639abc03466f21266f4841f87eb8328ee48e8473
no salt (none) 6ea09c25a7f1dcbd677077edaadfacdc41202e5be88e4ccf4c35207e343990bd

The same password produced two completely unrelated digests, and nothing about the two rows reveals that alice and bob share a password. A precomputed table of SHA-256("correcthorse") matches neither of them. The third row is what an unsalted table looks like: every correcthorse user has that exact digest, and the attacker only has to find it once.

You can reproduce the table with Node’s built-in crypto module:

const { createHash } = require("node:crypto");
const password = "correcthorse";
for (const saltHex of ["a3f9c2e17b4d8e60", "5c01e7b2f94a3d8c"]) {
  const salt = Buffer.from(saltHex, "hex");
  const digest = createHash("sha256")
    .update(Buffer.concat([salt, Buffer.from(password)]))
    .digest("hex");
  console.log(saltHex, digest);
}

Same password, two salts: alice and bob both type correcthorse, each gets a different random salt, and the stored digests differ

Both users type the same password (1), each password is joined with its own random salt (2), the hash function turns each pair into a digest (3), and the two stored digests are unrelated, while the salts sit in the clear beside them (4).

At login, the server looks up alice’s salt, prepends it to whatever she typed, hashes, and compares with the stored digest. The salt being public changes nothing here, because the attacker still has to guess the password to produce a matching digest.

Salt vs pepper

A pepper is the other seasoning you will hear about, and people mix the two up because both are extra inputs to the hash. They are opposites in almost every respect:

Salt Pepper
Unique or shared One per password One for the whole system
Secret No, stored in the clear Yes
Where it lives In the database, next to the hash Outside the database: a secrets manager, HSM or application config
Protects against Precomputed tables and crack-once-unlock-thousands Offline cracking of a stolen database dump, as long as the pepper did not leak with it
Who generates it The hashing library, per password You, once, with a rotation plan
Required? Always Optional extra layer

OWASP’s description is the cleanest: “a pepper is shared between stored passwords, rather than being unique to an individual password like a password salt”, and “the pepper should not be public and should not be stored along with the generated hash” (OWASP). The recommended way to apply it is after hashing: hash the password as usual, then run the result through an HMAC such as HMAC-SHA256 keyed with the pepper. NIST says the same thing in standards language: verifiers “SHOULD perform an additional iteration of a keyed hashing or encryption operation using a secret key known only to the verifier”, and “the secret key value SHALL be stored separately from the hashed passwords” (NIST SP 800-63B).

The threat model a pepper covers is a database-only leak: SQL injection, a stolen backup, a misconfigured replica. If the attacker gets the rows but not the application host, the hashes are useless offline because every candidate guess would also need the pepper. If the attacker gets the host too, the pepper is gone and you are back to relying on the algorithm’s cost. That is why OWASP positions the pepper as a layer on top of proper hashing and salting, never a substitute for either.

What salting cannot stop

A salt fixes the economics of attacking many hashes at once. It does nothing about attacking one hash, so it is worth being precise about what is still your problem after you salt:

  • Dictionary and brute-force guessing of weak passwords. The attacker reads alice’s salt from the stolen row, prepends it to each candidate, and hashes. If alice’s password is password1, it falls on about the first try regardless of the salt. The salt only forces the attacker to repeat that work per account. The defence here is the algorithm’s cost (the benchmark table above) and a sensible password policy, which we cover in Keycloak password policy best practices.
  • Fast hashes. A salted MD5 or SHA-1 hash is still an MD5 or SHA-1 hash: billions of guesses per second on one consumer card. Salting LinkedIn’s hashes would have turned one 72-hour job into 117 million small jobs, and a weak password in any one of them would still have cracked quickly. Salt and slowness are separate requirements, and you need both.
  • Credential stuffing. When an attacker already has a user’s password from another site’s breach, they log in through your front door with it. No hash is involved on your side at all, so salt, pepper and Argon2 are irrelevant. Credential stuffing is stopped by MFA, breached-password checks and rate limiting, not by storage.
  • Online guessing against the login form. Likewise, an attacker trying passwords against your live login endpoint never sees the hash. That is a brute-force detection and rate-limiting problem, and Keycloak’s built-in protection has gaps you should know about, which we walk through in Keycloak brute-force protection’s IP blind spot.

Read the list as a division of labour. The salt protects the stored table as a whole, the algorithm protects each individual hash, the password policy and MFA protect the accounts whose passwords would fall anyway, and nothing on this list protects a password the user has already reused somewhere that leaked.

How does Keycloak handle password hashing?

This is where theory meets a production identity provider. Since version 25.0, Keycloak’s default password hashing algorithm is Argon2 for non-FIPS deployments (Keycloak 25.0.0 release announcement). The announcement states the honest cost: “the defaults for Argon2 in Keycloak requires 7MB per-hashing request”. That is not overhead to grumble about, it is the entire point. Every guess costs an attacker’s GPU the same memory, and that cost is what turns the earlier benchmark table in your favor. It does mean login-heavy deployments should budget real memory for authentication bursts, and the Argon2 provider factory limits parallel Argon2 computation with a semaphore sized to the available processors by default, because, as its source puts it, “it doesn’t make sense to hash more values concurrently than there are cores on the machine” (Argon2PasswordHashProviderFactory).

The server administration guide lists the supported algorithms under the Hashing Algorithm password policy: argon2 (the default for non-FIPS deployments), pbkdf2-sha512 (the default for FIPS deployments), pbkdf2-sha256, and pbkdf2 with SHA-1, which is deprecated. The guide’s default iteration counts are 5 for Argon2, 210,000 for pbkdf2-sha512, 600,000 for pbkdf2-sha256 and 1,300,000 for the deprecated SHA-1 variant, and it recommends Argon2 “when possible as it has significantly less CPU requirements compared to PBKDF2, while at the same time being more secure”. Full transparency though: 210,000 for pbkdf2-sha512 sits just under the 220,000 iterations OWASP currently lists for PBKDF2-HMAC-SHA512 (OWASP). If you are staying on PBKDF2, closing that gap is a one-line change to the realm’s password policy: in the Admin Console, the guide says to click Authentication, then the Policies tab, then add the Hashing Iterations policy with the value you want.

FIPS deployments are the exception to the Argon2 default. In FIPS mode, Keycloak stays on PBKDF2: the FIPS guide states that Argon2 “is not supported for FIPS 140-2” and warns that users whose passwords were hashed with Argon2 “will not be able to login after switch to the FIPS environment”. If you are in a regulated environment, that is the configuration to verify, along with the iteration count above.

The pleasant part is how little of this surfaces day to day. Hashing happens invisibly behind every password create and reset, whether that is an admin action or self-service through user management. The algorithm choice only becomes visible in two scenarios: when you look at how a credential is actually stored, and when you migrate.

How Keycloak stores the salt and the hash parameters per credential

Keycloak does not keep one global “hash settings” record. Each password credential carries its own description of how it was hashed, which is what lets a hash stored under an older policy stay valid until that user’s next login. The server developer guide, in its section on extending the CredentialModel class, describes the two fields on a credential: secret_data “contains a static json with the information that cannot be transmitted outside of Keycloak”, and credential_data “contains a json with the static information of the credential that can be shared in the Admin Console or via the REST API”. The same section says that for a salted hash, “as is the case for passwords in Keycloak”, the secret data “would also have to contain a field for the salt, and the credential data information about the algorithm such as the type of algorithm used and the number of iterations used”. Keycloak’s source code (rather than its documentation) shows how this is organised for a password: PasswordSecretData declares a value field (the digest), a salt field (a byte array) and an additionalParameters map, while PasswordCredentialData declares algorithm, hashIterations and its own additionalParameters map, which is where algorithm-specific settings such as Argon2’s memory and parallelism go.

Those two blobs surface on the REST API as the secretData and credentialData string fields of a CredentialRepresentation, next to the credential’s type, createdDate and userLabel. The older top-level fields on that representation, hashedSaltedValue, salt, hashIterations and algorithm, carry a @Deprecated annotation in Keycloak’s CredentialRepresentation class, kept “just for backwards compatibility”, because the per-credential JSON replaced them. In the source, the admin API’s endpoint that lists a user’s credentials sets each representation’s secret data to null before returning it (UserResource), which is the point of the split: the salt is public in the cryptographic sense, but there is no reason to serve it to an admin console either.

Here is an illustrative sketch of the two sides of an Argon2 credential, with the digest and salt shortened. The parameter names are the constants defined in the source of Keycloak’s Argon2 provider factory (type, version, hashLength, memory and parallelism), which the Argon2 provider checks against the stored credential data, and the salt is one the provider generates with Salt.generateSalt() for each password:

{
  "credentialData": "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}",
  "secretData": "{\"value\":\"...base64 digest...\",\"salt\":\"...base64 salt...\",\"additionalParameters\":{}}"
}

Because the salt and the parameters travel with the credential, this is also the mechanism that lets Keycloak accept imported hashes. In Keycloak’s source, the PasswordCredentialProvider class reads the algorithm from the stored credential data, asks the session for the PasswordHashProvider registered under that name, and fails the check if none is installed. After a successful check, it asks the realm’s current hash provider whether the credential still matches the password policy, and queues a re-hash if not. That is the basis for the migration path below.

How do you migrate legacy hashes without a mass reset?

When the default changes, nothing happens to stored hashes immediately. The administration guide states it in one sentence: “if you change the hashing algorithm, password hashes in storage will not change until the user logs in” (Keycloak server administration guide). The next time a user logs in with the correct password, Keycloak verifies against the old hash using the parameters stored on that credential, then re-hashes the now-known plaintext with the current default. That lazy re-hash pattern is the answer to a question every team with an aging user table eventually asks: how do we get from MD5-era hashes to Argon2 without emailing three million people a reset link? You do not need to. You need the plaintext, and the only moment you legitimately have it is during a successful login, so that is when the upgrade happens.

The honest caveat: dormant accounts never log in, so their weak hashes persist indefinitely. Pair lazy re-hashing with an expiry policy for stale credentials, or a forced reset for accounts inactive past a threshold. And if the legacy hashes live in a different system entirely, the same login-time principle applies through user federation. We compare the approaches in federating vs migrating legacy users into Keycloak.

What to check in your own user store

Before you worry about anything fancier, confirm four things about wherever your passwords live today: the algorithm is one from the OWASP table above and not MD5, SHA-1 or plain SHA-256; the parameters meet the 2026 minimums; every credential has its own random salt, generated by the library rather than by hand; and old credentials get re-hashed on login rather than left behind. If you want a pepper on top, keep it outside the database and plan its rotation before you deploy it.

If the answer to the first check is “bcrypt, from the identity provider we are leaving”, you do not have to reset everyone. Skycloak imports existing bcrypt hashes into managed Keycloak: the Auth0 import takes Auth0’s password file, matched to users by their Auth0 identity, and the bcrypt password hash provider you install from the cluster’s Extensions page verifies them (importing from Auth0); users imported without that file set a new password with Forgot password. Credentials from an existing Keycloak deployment, already hashed with Argon2 or PBKDF2, come across unchanged in the database dump a migration from self-hosted Keycloak restores, because each credential carries its own salt and parameters, so those users keep their passwords.

Talk to us about your password storage and how a move to managed Keycloak would carry your existing hashes across.

Frequently asked questions

What is salting in security?

Salting is adding a unique random value, the salt, to each password before hashing it, and storing that salt in the clear next to the resulting hash. It makes identical passwords produce different hashes, which defeats precomputed lookup tables such as rainbow tables and stops one cracked hash from unlocking every account that shares the password. The salt is not a secret and does not need to be hidden.

Is a password salt supposed to be secret?

No. The salt is stored openly alongside the hash and the verifier needs it to check a login. Knowing the salt does not help an attacker, as Wikipedia and OWASP both note, because the attacker still has to guess the password for each account separately. A secret value applied to all passwords is a different thing, a pepper, and it is stored outside the database.

What is the difference between a salt and a pepper?

A salt is a unique random value per password, stored openly beside the hash, and it exists to make identical passwords produce different hashes and defeat precomputed tables. A pepper is one shared secret applied to all passwords and kept outside the database, in a secrets manager or HSM, so a database-only leak stays uncrackable offline. OWASP and NIST both treat the pepper as an optional extra layer, never a replacement for salting and a slow algorithm.

Does salting stop brute-force attacks?

Not on its own. A salt forces an attacker to attack each account separately instead of all at once, but a weak password with a salt is still a weak password, and a salted MD5 hash is still guessed billions of times per second on one GPU. Slowing each guess is the job of an adaptive algorithm like Argon2id or bcrypt, and salting and slowness are separate requirements you need together.

What is the difference between hashing and encrypting a password?

Hashing is one-way: there is no key and no way to recover the original password from the digest, so login works by re-hashing and comparing. Encryption is reversible by anyone holding the key, which makes it wrong for passwords: Adobe encrypted more than 150 million records with a single 3DES key, and OWASP’s guidance is to store passwords only as hashes.

Why is MD5 or SHA-256 bad for password hashing?

They are built for speed, and speed is exactly what an attacker wants. One consumer RTX 4090 computes 164.1 billion MD5 guesses and 50.6 billion SHA-1 guesses per second under Hashcat v6.2.6. Password storage needs deliberately slow, memory-hard algorithms like Argon2id that cut those rates by roughly a million-fold.

What is the best password hashing algorithm in 2026?

Argon2id, per OWASP, with minimum parameters of m=19456 (19 MiB memory), t=2 iterations, and p=1 parallelism. scrypt is the fallback where Argon2id is not available, bcrypt at work factor 10 or higher remains acceptable for systems built on it, and FIPS environments should use PBKDF2-HMAC-SHA256 at 600,000 iterations.

How does Keycloak store password salts?

Each password credential in Keycloak carries its own JSON: the secret data holds the digest and the salt, and the credential data holds the algorithm, the iteration count and any extra parameters such as Argon2’s memory setting. The salt is generated per password by the hashing provider, and the admin API strips the secret data when it lists a user’s credentials. Because the parameters are stored per credential, Keycloak can verify an old hash and re-hash it to the current default on the user’s next login.

Can hashed passwords be cracked?

Yes, by guessing rather than reversing: attackers hash candidate passwords and compare results against the stolen digests. How fast depends entirely on the algorithm and on whether the hashes were salted, which is why roughly 90% of LinkedIn’s unsalted SHA-1 hashes cracked within 72 hours. Modern parameters (Argon2id, bcrypt at cost 10 or higher) with a salt per password make each guess so expensive that strong, unique passwords stay out of practical reach.

Patched on the day, not on your next maintenance window

Keycloak 26.7.2 fixed CVE-2026-18963, an account takeover through password reset. Skycloak had it available for auto-upgrade and told customers the same day upstream shipped it. Self-hosted teams schedule that work themselves.

Guilliano Molaire
Written by
Founder, Skycloak

Guilliano is the founder of Skycloak and a cloud infrastructure specialist with deep expertise in product development and scaling SaaS products. He discovered Keycloak while consulting on enterprise IAM and built Skycloak to make managed Keycloak accessible to teams of every size.

© 2026 Skycloak. All Rights Reserved. Design by Yasser SolimanKeycloak™ is a trademark of the Linux Foundation. Skycloak is not affiliated with the Keycloak project.