为什么我们不信任数据库进行身份验证
Why We Don't Trust the Database with Authentication

原始链接: https://blog.sturdystatistics.com/posts/api_keys/

Sturdy Statistics 认为,将数据库作为 API 身份的唯一仲裁者会带来严重漏洞:一旦攻击者获得 SQL 注入权限,他们就可以篡改记录,从而实现对系统的完全租户接管。 为了防止这种情况,该公司采用了“深度防御”策略,将身份验证与数据库存储解耦。他们没有仅仅存储简单的哈希值,而是使用后端加密盐(pepper)来计算 HMAC-SHA512 签名。该签名在加密层面上将 API 密钥与其特定的组织 ID 和生命周期版本绑定。由于加密盐仅存在于安全的后端内存中,仅凭数据库泄露不足以伪造或修改密钥。 该系统通过以下方式进一步加强了安全性: * **单向棘轮机制:** 数据库触发器可防止版本回滚,从而挫败“僵尸密钥”攻击。 * **冗余验证:** 在路由、身份验证、应用程序逻辑和模式约束这四个层级上强制实施租户隔离。 * **平滑轮换:** 双重哈希策略允许在不引入新漏洞的情况下,进行安全且零停机的密钥轮换。 通过将信任锚点从数据库转移到应用层,Sturdy Statistics 确保了单一故障点不会导致灾难性的安全漏洞。

这篇 Hacker News 讨论聚焦于一篇主张“不应完全信任数据库进行身份验证”的文章。作者建议对身份验证数据进行签名,以便在数据库遭到入侵时,应用程序能够检测到篡改并阻止未经授权的访问。 评论者对此方法反应不一: * **实现方面的顾虑:** 有用户认为,投入精力去实现这样一套系统,不如将其用于改进数据库抽象层或进行安全审计。 * **替代策略:** 有读者建议采用更直接的防御措施,即在哈希处理前先将用户名与字符串进行加盐(salting)。 * **操作安全性:** 另有用户称赞了作者对客户端密钥轮换的关注,并指出能够实现零停机密钥轮换的系统非常罕见且极具价值。 总的来说,尽管社区认可其对稳健安全架构的重视,但对于作者提出的具体方法是否是抵御数据库级威胁最高效或最实用的方式,各方仍存在争议。
相关文章

原文

When writing an API, it’s easy to unknowingly introduce a dangerous, implicit assumption: that the database is the ultimate source of truth. If a record is in the database, an application often treats it as authoritative. I want to explain in this post why that might be a very bad idea.

Here at Sturdy Statistics, our core engineering philosophy is Defense in Depth: we work to prevent failure at every layer, but we design each layer as though the others could fail. When it comes to API authentication, trusting the database as the sole arbiter of identity is a critical vulnerability masquerading as standard practice.

Here is why we don’t trust our database with authentication, and how we structurally prevent database-level compromises from becoming full-system breaches.

The Bypass: When SQL Injection Grants System-Wide Access

Consider the dangerous implicit default for handling API keys. If you treat a machine token like a user password, the implementation seems obvious:

  1. Generate a random secret.
  2. Hash it (e.g., SHA-256).
  3. Store the hash in an api_keys table alongside an org_id.
  4. When a request comes in, hash the provided secret and look for a matching row.

This feels secure because the database only holds hashes, not plaintext secrets. But let’s look at what happens if an attacker discovers a blind SQL injection vulnerability anywhere else in the application.

The attacker doesn’t need to read the database or invert any hashes. An attacker can simply register a legitimate account and generate his own valid API key. Since the key is both legitimate and legitimately his, he knows the plaintext secret, and he knows the application will successfully verify it. So far, this is entirely licit.

Now let’s imagine the attacker finds a SQL injection exploit. By executing a simple UPDATE statement, he can copy the hash of his key and paste it over the hash of the victim’s key. When the attacker sends a request attempting to access the victim’s data, he uses his own (valid) key. The API middleware hashes it, finds the victim’s row (which now contains the attacker’s pasted hash), and grants access.

Boom. Now it doesn’t matter how many other security measures we have in place; the attacker has just bypassed the API security perimeter and the entire key-based tenant boundary layer. His attack uses a valid key. Because the application blindly trusts the database’s record of the hash, a simple database write translated into complete tenant takeover.

The Fix: Cryptographic Binding

Securing the database against SQL injection is obviously a priority, but the “bypass” nature of this attack means we need Defense in Depth comparable to what we apply at the perimeter. We must separate the security of the authentication system from the security of the database.

To achieve this, Sturdy Statistics does not store simple hashes of API keys. Instead, we use a server-side cryptographic pepper – a high-entropy secret TPM-sealed to the backend – to compute an HMAC-SHA512 signature.

Crucially, we don’t just sign the secret. We sign the structural context of the key:

Stored Hash = HMAC-SHA512(Pepper, api-key-id
                                  || rotation-version
                                  || org-id
                                  || secret)

Where do these pieces come from? The api-key-id, rotation-version, and secret are parsed directly from the incoming client token. The org-id is strictly pulled from the requested URL path parameter. The database provides the stored hash to check against, along with its record of the api-key-id, rotation-version, and org-id. Thus, the secret and stored hash each appear only once. The other identifiers are each stored in two locations. This “double-book accounting” allows us to check for consistency.

If an attacker manages to write to the database and swaps a hash from Organization A into Organization B’s row, the attack fails immediately. When our authentication middleware verifies the incoming token, it pulls Organization B’s org-id from the URL path and injects it into the HMAC calculation. Because the hash encodes the wrong org-id, the hash changes, the signature is rejected, and the attacker is locked out.

Crucially, because this signature requires the Pepper – which lives exclusively in backend memory and never touches the database – even an attacker with full DB read and write access is thwarted. It is impossible for him to mint or modify keys without the pepper. The database no longer decides identity on its own; it stores verifiers that only the backend can validate. Thus the backend’s defense layers remain intact and can’t be bypassed via the DB.

Rollback Resistance, or Zombie Keys

You might wonder why we also include the rotation-version in the hash. This binds the signature to a specific epoch in the key’s lifecycle, which is the first half of defending against rollback attacks. If a key is compromised and eventually rotated, its old V1 hash is invalidated.

But what if an attacker with DB access tries to resurrect that revoked key by overwriting the active row, setting the hash back to the V1 hash, and the rotation-version back to 1?

Because the HMAC prevents a V1 hash from being passed off as a V2 key, the attacker must roll back the version column to make the hashing math work. We stop this by pairing the cryptographic signature with a database TRIGGER. We configure the database engine to enforce a strict, one-way ratchet on the rotation_version column: it can only ever increase. When the attacker attempts their UPDATE statement to roll the version backwards, the database schema itself rejects the transaction. This combination locks out even an attacker with write access to the DB and access to an expired key from the victim’s organization.

Paranoid Tenancy: Four Layers of Verification

Cryptographically binding the API key to the tenant is the first step, but Defense in Depth requires overlapping security boundaries. In a multi-tenant environment, cross-tenant data leakage is the most common security failure identified by OWASP.

To ensure absolute isolation, we enforce tenancy checks repeatedly throughout the lifecycle of every single request:

  1. The Routing Layer: Tenancy is explicitly declared in every request path itself (as a path parameter), ensuring the routing infrastructure knows exactly which logical boundary is being accessed before any application code executes.

  2. The Authentication Layer: As detailed above, API key verification is cryptographically tied directly to the org-id.

  3. The Application Layer: Every read and write operation at the application level enforces tenant scoping. Business logic functions do not accept naked identifiers; they require the authorized tenant context to proceed.

  4. The Database Schema Layer: As the final failsafe, we enforce tenancy at the lowest possible level using database TRIGGERs. Even if a logic error in the application code attempts a cross-tenant write, the schema rejects it.

Graceful Security: Zero-Downtime Rotation

Security mechanisms that are hard to use usually end up being bypassed. Key rotation is notoriously painful, often requiring coordinated downtime between the API provider and the client.

Because we explicitly control the exact mechanics of our hashing and database schema, we built zero-downtime rotation directly into the architecture. Our api_keys table includes a prev-hash column and a grace-period-expires-at timestamp.

When a user rotates a key, we generate a new secret, increment the rotation version, and compute the new primary hash. The old hash is demoted to the prev-hash column and given a strict time-to-live (e. g., 24 hours). During this window, the backend checks will authorize either key. This allows distributed client systems to update their environment variables asynchronously without dropping a single licit request.

If you’re paying attention, this re-introduces the rollback vulnerability: an attacker with DB write access and access to an expired key could resuscitate the key by overwriting the grace period. We solve this with another TRIGGER: the grace period can only be extended when the rotation version is also being incremented.

Containment without Containers

At Sturdy Statistics, we don’t believe that adding complex layers of distributed infrastructure is the only way to achieve security.

True security comes from architectural simplicity and mathematical rigor. By moving the trust anchor out of the database and enforcing strict cryptographic boundaries at the application edge, we ensure that a failure in one layer remains precisely that: a contained failure, not a catastrophic breach.

Nota bene: Throughout this post, I used the language of SQL injection and database TRIGGERs because those are the familiar terms of relational database architecture. Under the hood, Sturdy Statistics actually uses Datomic. Datomic’s immutable ledger means authentication state is never simply overwritten in place, which adds protection against attacks that depend on rewriting stored data. While Datomic does not use SQL triggers, we enforce the same one-way ratchets and boundary conditions through Datomic transaction functions. I chose SQL terminology here for accessibility, but the core lesson is the same regardless of storage engine: database state alone should not be able to grant authentication.

联系我们 contact @ memedata.com