Post

Password-Hash Migrations Under Real Load

Upgrade password storage with bounded verification costs, compare-and-swap updates, and a plan for inactive accounts.

Password-Hash Migrations Under Real Load

Upgrading password hashing is both a security change and a capacity change. A stronger work factor raises the cost of offline guessing, but it also raises the CPU or memory cost of legitimate login attempts. An attacker can target the same verification path to exhaust the service.

I would design the migration around three questions: how to recognize old hashes, how to upgrade them safely, and how to keep authentication available under hostile load.

Use the library’s encoded format

A modern password-hashing library typically stores algorithm and cost parameters in its encoded output. Use the library’s verification and rehash-check functions rather than inventing a parser or comparing derived values manually.

OWASP’s password storage guidance describes suitable password-hashing algorithms, cost parameters, and migration considerations. I would choose supported parameters using that baseline and measurements on the actual production instance class, not copy an isolated benchmark.

Illustrative application flow:

1
2
3
4
5
6
7
load encoded hash and credential version
verify password with the supported library
if verification fails: return the generic login failure
if the hash needs an upgrade:
    derive a new encoded hash from the verified password
    update only if the stored credential version is unchanged
continue the session-issuance checks

The conditional update prevents a slow login from overwriting a concurrent password reset with a hash of the old password. If the version changed, session issuance must follow the application’s reset and revocation policy; do not silently treat the stale verification as current authority.

Bound expensive work before it begins

Apply admission controls before the expensive hash operation, with both global protection and account-aware abuse handling. Avoid a design where attackers can trivially lock out a victim by sending bad passwords. Concurrency limits should account for the memory cost of each verification, not only CPU utilization.

Unknown-user handling also deserves load testing. A dummy verification can reduce obvious timing differences, but it consumes real resources. Generic error messages, bounded verification work, and layered abuse controls need to operate together.

Never log plaintext passwords, derived hashes, or password-bearing request bodies during migration debugging. Instrument algorithm version and outcome categories instead.

Plan for accounts that never log in

Opportunistic rehashing upgrades active users but leaves inactive accounts unchanged. Decide when those accounts must reset their credentials, and how that requirement interacts with account recovery and customer communication. Track remaining legacy hashes as a population with an owner and retirement date.

I would roll out verification support before issuing new-format hashes, then canary the rehash path. Preserve compatibility across rolling deployments. A rollback that cannot verify newly written hashes can turn an otherwise successful migration into an authentication outage.

Security owns the target policy, identity engineering owns correctness, and reliability engineering validates capacity. Product and support own the reset experience. Completion means the old format is retired without losing legitimate access or exceeding the service’s resource budget, not merely that new registrations use a better algorithm.

This post is licensed under CC BY 4.0 by the author.