mirror of
https://tangled.org/evan.jarrett.net/at-container-registry
synced 2026-08-29 12:17:00 +00:00
ValidateDeviceSecret ran bcrypt.CompareHashAndPassword against every row in the devices table until one matched — no WHERE clause. At bcrypt cost 10 (~65ms on the single-core production host) and 244 registered devices, a device near the end of the scan cost ~15.8s of pure CPU per /auth/token, which is past Docker's client deadline. Measured on production: 15.7-16.0s steady state with the appview pinned at 100% CPU for the duration, while anonymous requests on the same box served in 20ms. The cost grew linearly with every device registered, and the scan ran in rowid order, so the newest devices — the ones most likely to be in active use — paid the most. This is the timeout users were reporting. Devices now carry secret_lookup = hex(sha256(secret)), indexed, and authentication fetches the single matching row. SHA-256 is the verifier here, not merely an index. Device secrets are 32 bytes from crypto/rand, so presenting a value that hashes to a stored digest requires a preimage or a 2^256 search. bcrypt's work factor only helps when the input space is small enough to enumerate, which does not apply to a random 256-bit token, and a database leak exposes no more than before. The plaintext is not recoverable from a bcrypt hash, so existing rows cannot be backfilled directly. They are migrated lazily on their next successful authentication, which any push, pull or login triggers, and the legacy scan is filtered to un-migrated rows so its cost decays as devices migrate. The backfill runs after the cursor is closed: issuing it inside the rows loop deadlocks, because the open cursor holds the connection the write needs. bcrypt now exists solely to carry legacy rows across and can be deleted once the table is fully migrated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>