Skip to content

Rate-limit the authentication surface #602

Description

@DerOetzi

Why

Guessing a credential is bounded by the request rate, not by the hash. That is the reasoning
recorded in
decision 0006:
a fast hash for API keys is correct because they carry 128 bits of token_hex and nobody guesses
them, and argon2id protects the administration password against somebody who already holds
security.json. Neither does anything about an attacker who simply keeps asking.

Today nothing limits that. There is no throttle, no lockout and no backoff anywhere in the service.
The security hardening added the missing half of the picture — a rejected API key and a rejected
administration login each write a warning naming no value, so the attempts are at least visible —
but visibility is not a control.

The administration password is the target that matters. It is human-chosen, it may be short, and the
documented fallback is a single well-known word. An unthrottled POST /api/auth/token is the whole
attack. This becomes considerably more pressing when the service is exposed as a Home Assistant
add-on.

What has to be decided

  • Where the counters live. In-process state has the same problem as the refresh tokens: it is
    per worker, so a limit of five attempts becomes five per worker. Either the counters wait for the
    SQLite persistence work, or they are explicitly in-memory and the limit is documented as
    approximate. The rest of that state moves in the persistence phase anyway.
  • What is counted, and per what. Per client address is the obvious key, but the service runs
    behind ProxyHeadersMiddleware and, as an add-on, behind Ingress — so the address is only as
    trustworthy as the proxy in front. Counting per account for the administration login avoids that
    question and introduces another: an attacker can then lock the administrator out on purpose.
  • Throttle or lock out. A delay that grows with the number of failures keeps the legitimate
    administrator working while making a guessing run useless; a hard lockout is simpler and is a
    denial-of-service lever pointed at the owner of the instance.
  • The answer to a limited request. 429 with Retry-After is the honest one, which means a new
    LearningHouseException subclass — the error classes carry their own status code and OpenAPI
    description, so a new one documents itself.

Scope

  • POST /api/auth/token (password login) — the important one.
  • PUT /api/auth/token (refresh) and API key authentication on every protected endpoint — cheaper
    targets, but they should not be the way around the limit.
  • Not in scope: anything that would rate-limit training or prediction traffic for legitimate API
    keys. A brain being fed a data point per second is normal use.

Acceptance

  • Repeated failed administration logins are slowed or refused, and the behaviour is documented
    on the security page.
  • A legitimate administrator who mistypes a password twice is not locked out for an unreasonable
    time.
  • The limit cannot be bypassed by switching between the password endpoint and API key
    authentication.
  • The chosen approach is recorded as a decision record, including what it does not protect
    against.
  • Whatever state the limiter keeps is compatible with running more than one worker, or the
    restriction is stated where workers is documented.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions