skip to content

A front door records every challenge token it issues; under a spoofed flood, why does that fail and what does the fix cost?

level: middleimportance: nice to knowfreq 30%

answer

  1. a table is state, and state is the target
  2. recompute, never look up
  3. keyed, or the attacker mints their own
  4. same key on every instance, rotated with overlap
  5. bind to the address, expire in seconds

basics

~20 s

Recording issued tokens re-creates the per-client state the challenge existed to avoid, so the flood fills that table instead of the connection queue. Make the token self-validating — a keyed hash over the source address and a timestamp — and verify it by recomputing rather than looking it up.

solid answer

~50 s

The point of a challenge is that an unproven client costs you nothing. A table of outstanding tokens breaks that: every spoofed packet mints a row, the flood is now aimed at your token store, and you have merely renamed the queue you were trying to protect. The fix is a token the front door can verify without remembering it — typically a keyed hash over the client's source address and a coarse timestamp, computed with a server key, handed back, and checked on the next request by recomputing it over the address the packet actually came from. Nothing is stored and nothing is allocated. It is not free: you pay a hash per arriving token, the same key must exist on every front-door instance and be rotated with an overlap window, and the token is replayable from its own address until it expires — so keep the lifetime in seconds.

code

text · 7 lines
text
spoofed src 203.0.113.9 -> server : Initial[ClientHello]     server allocates nothing
server -> 203.0.113.9            : Retry[token = MAC(k, 203.0.113.9 | t)]   never collected
...
real client 198.51.100.4 -> server : Initial[ClientHello]
server -> 198.51.100.4             : Retry[token = MAC(k, 198.51.100.4 | t)]
real client 198.51.100.4 -> server : Initial[token] + ClientHello
server                             : recompute MAC(k, src | t), compare, then allocate

go deeper

for a junior

Know that the whole value of a challenge is that an unproven client costs nothing, and that storing something per unproven client throws that value away.

for a middle

Be ready to describe the construction: a keyed hash over source address and a coarse timestamp, verified by recomputation, with no server-side record of any kind.

for a senior

Talk about operating it — shared key across instances, rotation with an overlap window, lifetime chosen against replay, and where in the path the verification runs.

for a principal

Be able to argue why a self-verifying token is the boundary between a control that scales under attack and one that becomes the attack surface, and where that reasoning recurs elsewhere in the edge.

## The trap A challenge is worth building only if an unproven client is cheap. The obvious implementation is not: generate a random token, store it in a map keyed by token or by client, and look it up when the client returns. Under a spoofed flood every single packet mints an entry, none of them ever return, and the entries sit there until they expire. You have not removed the per-client allocation, you have moved it from the connection queue into a hash map — and the hash map is usually the softer target, because it is in your application's heap rather than in a tuned kernel structure with its own overflow behaviour. The attacker does not have to change a thing. ## The self-validating token The construction that actually works is a token the front door can verify by recomputing rather than remembering: token = MAC(server_key, source_address | coarse_timestamp | scope) The client is handed the token and, on its next attempt, sends it back. The front door recomputes the same value over the source address of the packet that carried it and compares. If they match, the address was reachable recently and the front door may now allocate. If they do not, it costs a hash to say no. The two properties that matter are that verification requires **no lookup** (so there is nothing to fill) and that the token is **unforgeable** (so an attacker cannot mint their own and skip the trip). A random token you store fails the first test; a random token you do not store fails the second, because you cannot tell your token from a made-up one. ## The bill **CPU per verification.** A keyed hash is cheap but not zero, and an attacker can send garbage tokens purely to make you compute. It is a good trade — a hash against an allocation — but it is a real line, and it is one reason the check belongs as early in the path as you can put it. **Key distribution.** Every front-door instance must hold the same key, or a client validated by one instance is challenged again by the next and loops forever behind a load balancer. That means a shared secret, in memory, never logged, delivered the same way to every instance. **Key rotation with overlap.** Rotate on a schedule, and accept the previous key for one validity window, or every token in flight at the moment of rotation is rejected and every honest client is re-challenged at once. **A replay window.** Anyone who holds a valid token can reuse it until it expires. Two mitigations: bind it to the source address so a token minted for one address fails from another, and keep the lifetime short — seconds to a minute, not hours. Binding to the address is what stops one solved token from being distributed to a whole botnet; the residual risk is replay from the same address, which is bounded by the lifetime. **Coarse time.** The timestamp has to be coarse enough that a client re-presenting a token a moment later still recomputes to the same value, which in practice means bucketing time and accepting the current and previous bucket. ## What the token is not It is not a session cookie and it is not an identity. It authorises exactly one thing — that the front door may now spend resources on this address — and it carries no claim about who the client is or whether the request is welcome. Conflating the two produces the memorable failure where a validation token is treated downstream as an authenticated session. ## The direction of the guarantee A verified token proves that the address received something you sent recently. It does not prove the client is the same client, that it is honest, or that the request behind it is safe. Every one of those is a separate control, and the challenge exists only to make it affordable to run them.

  • What stops one solved token from being handed round an entire botnet?
    Binding it to the source address. Verification recomputes the hash over the address of the packet that carries the token, so a token minted for one address fails everywhere else. What remains is replay from that same address, and the only control on that is a short lifetime — seconds, not hours.
  • Why does an unauthenticated random token that you do not store fail?
    Because you have no way to distinguish a token you issued from one the attacker invented. Without a key, verification degenerates into accepting anything token-shaped, and the challenge is skipped entirely. Either you remember it — which is the state you were avoiding — or you make it unforgeable.
  • You rotate the server key at noon. What happens to clients mid-challenge?
    Every token issued before noon now fails to recompute, so all of them are re-challenged simultaneously — a self-inflicted burst on top of whatever traffic you already have. Accept the previous key for one validity window so rotation drains rather than cuts.

saying these in an interview costs you the question

  • Keeps a table of outstanding tokens under a flood
  • Uses an unkeyed random token the attacker can mint
  • Gives the token no expiry, so replay is unbounded
  • Uses a different key per front-door instance, looping clients
  • Treats a validation token downstream as an authenticated session

context