Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
| Proof verification updates the credential/factor counter in the same logical | ||
| operation as the verification result. A failed proof increments the counter and | ||
| records the failure time. A successful proof resets the counter for that proof | ||
| type and records the successful verification time. |
There was a problem hiding this comment.
Refine "operation" a bit further. Do you mean Request or DB Transaction? I'm asking because we need to be careful here. Given:
- A secret like a password is obtained from the database before it can be verified.
- Verification is a hash operation which is typically bound to CPU time
- The counter needs to be updated on failure.
If this all happens in the same transaction, we risk DB pool exhaustion as the a connection stays occupied mid-transaction for the duration of the crypto operation. Hence we should specify a behavior that reading a hashed secret should happen in a first transaction that gets terminated. Post-check updates should happen in a second transaction.
Developing further on this I would propose to update the counter when the secret is read for verification, within the same round-trip if possible. This way we can protect against parallel brute-forcing. Updating the counter post-failure allows a short time window of sending multiple password attempts in parallel requests, which may then result in exceeding the policy.
My proposed request flow would be:
- Initial transaction:
- Secret is obtained and counter is increased. The query returns the old value of the counter.
- Policy check on counter -> lock user / block request if policy is exceeded.
- Commit
- Perform verification / crypto operation
- Second transaction:
- Reset counter on success
- Push related success / failure events (audit log ADR is not merged yet, but here is where that should go, IMO)
- Commit
CC @adlerhurst?
| The credential/factor counter drives progressive controls: | ||
|
|
||
| - after each failed proof, calculate an exponential backoff delay from the | ||
| current failure count, bounded by the fixed initial maximum delay | ||
|
|
||
| The initial backoff defaults are fixed service behavior, not customer-facing | ||
| configuration: | ||
|
|
||
| - failures 1-4: no artificial delay | ||
| - failure 5: 10 second delay | ||
| - each later failure doubles the delay, capped at 5 minutes |
There was a problem hiding this comment.
How should the back-off be enforced?
- Do we flat-out deny the request with a 423 or 429? (cheapest, easy to implement)
- Current Zitadel delays the response using a tarpit. I'm not a big fan of this because it keeps the HTTP connection open for the duration of tarpit. Platforms like GCP have a max of concurrent requests on a container, so it is very easy to exceed the budget by sending invalid passwords and sending all available connections into the tarpit.
- Use tech like ALTCHA.1 They provide both a Go and NPM package which allows setting a PoW target which we can increase on failure, moving the cost of brute-forcing entirely to the client. More failed attempts will result in higher CPU consumption for the client. (little more expensive to implement)
Footnotes
-
Originally proposed on slack: https://zitadel.slack.com/archives/C098X56G06R/p1777366864101389 ↩
There was a problem hiding this comment.
Can we add a list of positives / negatives (risks) for the decisions taken in this ADR?
There was a problem hiding this comment.
Pull request overview
Adds a new Architecture Decision Record (ADR 041) defining an initial approach for abuse prevention (rate limiting, brute-force mitigation, CAPTCHA escalation) and consistency requirements for auth-critical reads, and registers it in the ADR index.
Changes:
- Add ADR 041: “Abuse Prevention and Operational Resiliency”.
- Update
docs/adrs/README.mdto include ADR 041 in the ADR index table.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| docs/adrs/README.md | Adds ADR 041 to the ADR index table. |
| docs/adrs/041-abuse-prevention-operational-resiliency.md | Introduces the new ADR content for abuse prevention and operational resiliency decisions. |
| | [038](038-user-credential-migration-and-recovery.md) | User Credential Migration and Recovery | Proposed | Transparent password rehash-on-verify via passwap; passkey migration (soft second-passkey nudge, ROR for domain/`rpId` changes) and single-secret TOTP rotation; last-factor deregistration safeguard; email magic-link / one-time code as both a primary auth method and a recovery path, alongside recovery codes. | | ||
| | [039](039-signing-key-rotation-and-incident-response.md) | Signing Key Rotation and Incident Response | Proposed | Per-project signing keys with `active_from`/`retired_at`, a propagation window and a grace period, auto-rotating every 30 days and retired by supersession rather than by an expiry clock; emergency signing-key and KEK/DEK compromise handling, where KEK-and-database compromise means rotating every secret rather than re-wrapping it. | | ||
| | [040](040-tenant-login-templates-editable-config.md) | Tenant Login Templates as Editable Config | Proposed | Login templates become an editable-config resource: immutable per-project branding revisions, latest-revision resolution on flow responses, CLI-authoritative LiquidJS validation with a lexical Go gate, sibling-`.liquid` local dialect, and an ejectable design catalog. | | ||
| | [041](041-abuse-prevention-operational-resiliency.md) | Abuse Prevention and Operational Resiliency | **Proposed.** Edge-first rate limiting, durable brute-force counters, explicit CAPTCHA/risk gates, and consistency-safe auth reads for PostgreSQL and Spanner. | |
| operational resiliency before implementation. The current POC has auth-attempt | ||
| state and CAPTCHA design surfaces, but no cohesive policy for rate limits or | ||
| bruteforce mitigation. |
Summary
Validation
Release notes / changeset
Related