A service authenticates every API call with HTTP Basic and verifies the supplied password against a bcrypt hash on each request. What operational consequences does that create, and how would you design around them?
answer
- stateless Basic → KDF on every request
- throughput ≈ cores / hash_time
- bad credentials = cheapest attack, most expensive path
- high-entropy machine secret → SHA-256 is enough
- short-TTL bounded cache; isolate pool; rate-limit before hashing
basics
~20 sBasic is stateless, so a deliberately slow password hash runs on every request. Throughput becomes cores divided by hash time, and unauthenticated traffic can exhaust CPU. Fix with short-TTL verification caching, fast hashes for high-entropy machine secrets, isolation and rate limits.
solid answer
~60 sBecause Basic carries no session, the credential is re-verified per request. A password KDF is intentionally expensive — bcrypt at cost 12 is roughly 100 ms of CPU — so peak authenticated throughput is about `cores / hash_time`: eight cores gives ~80 requests per second **before any business logic**. Worse, an attacker gets CPU amplification: a cheap garbage request forces a full hash, so a trickle of traffic saturates the box, and if verification runs on the request pool it starves legitimate work. Design responses, roughly in order: 1. **Distinguish credential types.** Machine-to-machine secrets should be high-entropy random strings; those need only SHA-256 plus constant-time compare, since a KDF exists to slow guessing of low-entropy human passwords. 2. **Cache verification** keyed by a fast hash of the presented credential, with a short TTL, a bounded size and invalidation on rotation. 3. **Isolate** verification in its own bounded pool or service so it cannot consume the request pool. 4. **Rate-limit and lock out** per user and per source before the hash runs. 5. Strategically, move to token auth where verification is a signature check.
code
bash · 4 lines# Each request costs the attacker microseconds and the server a full bcrypt run
for i in $(seq 1 10000); do
curl -s -o /dev/null -u probe:wrongpassword https://api.example.com/v1/ping &
donego deeper
Recognise that Basic sends the password every request, so the server checks it every time, and that password hashing is intentionally slow.
Do the arithmetic — throughput is roughly cores divided by hash time — and propose caching verified credentials briefly plus rate limiting.
Diagnose the amplification and pool-starvation failure modes, design the cache (keyed hash, short TTL, bounded, invalidation) and isolate verification behind a bounded executor with load shedding.
Separate credential classes by entropy to remove the KDF entirely for machine secrets, derive the cost factor from a stated security budget, and lay out the migration to short-lived tokens with the revocation and rollout trade-offs named.
## Why the cost exists at all Password hashing functions — bcrypt, scrypt, Argon2, PBKDF2 — are deliberately slow and, for the newer ones, deliberately memory-hungry. That slowness is the security property: it makes offline brute force of a stolen hash database expensive. A cost factor is chosen so a single verification takes on the order of 50–250 ms of CPU. That cost was designed for a **login event** — something a user does once per session. HTTP Basic has no sessions. It is stateless by construction, so the client replays the password on every request and the server, if it is naive, runs the KDF every time. You have taken a once-per-session cost and put it on the hot path of every API call. ## The capacity arithmetic The model is simple enough to do out loud in an interview. Password verification is CPU-bound and essentially unparallelisable within a single check, so: `max auth throughput ≈ cores / verification_time` Eight cores with a 100 ms bcrypt gives ~80 verifications per second, saturating the machine with zero cycles left for TLS, JSON, database work or GC. Doubling the cost factor halves that. Argon2id with a 64 MB memory parameter adds a second wall: 80 concurrent verifications want 5 GB of RAM, so memory, not CPU, caps concurrency. This also breaks common autoscaling signals: latency rises long before request-count-based scaling reacts, and adding replicas scales verification linearly but at high cost. ## The amplification problem The attacker's request is cheap; yours is expensive. A single TCP connection replaying `Authorization: Basic <garbage>` forces a full KDF run per request — a self-inflicted denial of service with an amplification factor of thousands. Two aggravations: - **Failed verifications are the most expensive path**, because there is no cache hit and no early exit. - If verification runs on the same thread pool that serves requests, a burst of bad credentials **starves legitimate traffic**: healthy requests queue behind hashing work, health checks time out, and the orchestrator restarts pods that were merely busy. So the first structural fix is not about speed at all: **bound and isolate** the work. ## Design responses **1. Separate credential classes.** The crucial insight is that a KDF exists to compensate for low-entropy secrets. A 256-bit randomly generated API secret has no guessable structure; there is nothing to brute force even with a fast hash. So for machine-to-machine credentials, store a plain SHA-256 (or HMAC with a server-side pepper) of the secret and compare in constant time — microseconds instead of milliseconds. Reserve bcrypt/Argon2 for human-chosen passwords, and do not let humans use Basic on a hot API path in the first place. Getting this distinction right usually removes the entire problem. **2. Verification caching.** If human passwords must be verified per request, cache the outcome: key on a fast keyed hash (HMAC-SHA-256 with a process-lifetime key) of the full presented credential, value = principal plus expiry. TTL of seconds to a couple of minutes, bounded LRU size, negative results cached only briefly or not at all (or a wrong password stays wrong after a fix, and cached negatives become their own DoS surface). Never store the plaintext credential as the cache key. Invalidate on password change, role change and account disable — accept that the TTL is your revocation lag and state it explicitly as a risk you are choosing. Keep the cache in-process; putting credential material in a shared Redis widens the blast radius considerably. **3. Isolation.** Run verification in a dedicated bounded executor with a queue limit and shed load (`503` with `Retry-After`) when full. The request pool stays available for cached hits and unauthenticated routes. In a larger system, a dedicated authentication service with its own capacity plan does the same at the fleet level. **4. Cheap rejection before expensive work.** Rate-limit per source and per user id, apply account lockout or exponential backoff after repeated failures, and reject malformed or oversized `Authorization` headers before decoding. Every request rejected before the KDF is CPU you keep. Beware of doing this only per user id: an attacker cycling user ids evades it, so limit per source too. **5. Strategic move to tokens.** The durable fix is to stop putting a password verification on every request: authenticate once, issue a short-lived signed token, and let each subsequent request cost a signature verification (tens of microseconds) or a cached introspection lookup. That is exactly the trade the industry made, and saying so shows you understand *why* bearer schemes exist rather than treating them as fashion. ## What good judgment sounds like A strong answer measures rather than guesses: benchmark verification cost on production-class hardware, derive the per-core ceiling, compare it to the traffic profile, and pick the cost factor from a stated security budget ("offline cracking must cost X") rather than a copied default. It also names the accepted risks — cache TTL as revocation lag, fast hashing valid *only* for high-entropy secrets — instead of pretending the mitigations are free.
- Is it ever acceptable to store an API secret with a fast hash such as SHA-256 instead of bcrypt?Yes, when the secret is generated with high entropy — say 256 random bits — rather than chosen by a human. A slow KDF exists to make guessing low-entropy secrets expensive; there is nothing to guess in a random 256-bit value, so a single SHA-256 (ideally HMAC with a server-side pepper) plus a constant-time comparison is sufficient. The rule fails the moment humans can set the secret.
- What are the risks of caching successful password verifications, and how do you bound them?The cache TTL becomes your revocation lag: a disabled account or rotated password keeps working until entries expire. It also concentrates credential-derived material in memory. Bound it with a short TTL measured in seconds, an in-process bounded LRU rather than a shared store, keys that are keyed hashes rather than the credential itself, and explicit invalidation hooks on password change, role change and deactivation.
- How would you keep credential verification from starving normal request handling?Run it on a dedicated bounded executor with a queue limit rather than the request pool, and shed load with 503 plus Retry-After when that queue fills. Add per-source and per-user rate limiting and lockout so most abusive traffic is rejected before any hashing happens, and make sure health checks are served on a path that does not authenticate.
It is like requiring a full identity background check at the door for every single time an employee walks through it, rather than once at hire — the check was priced for a rare event and is now on the busiest path in the building.
saying these in an interview costs you the question
- Not noticing that stateless Basic means the KDF runs on every single request
- Lowering the bcrypt cost factor as the primary fix, weakening offline-cracking resistance to buy throughput
- Caching credentials with the plaintext credential as the key, or in a shared external store
- Claiming the cache has no downside, without naming revocation lag
- Applying fast hashing to human-chosen passwords because 'it worked for API keys'