Harden auth before exposing Nexus to the internet #110

Open
opened 2026-09-04 16:17:28 +02:00 by lz · 0 comments
Owner

Tracking issue. Nexus's auth is built for the deployment it has today: one operator, one passphrase, a LAN. Every item below is a reasonable choice at that scale and a real exposure the moment the instance is reachable from the internet. None of them are addressed by #108 (session expiry) or #109 (scoped API tokens).

Filed as a checklist, not a plan. Each item is independently shippable.

1. No brute-force protection on /api/auth/unlock — highest priority

routes/api/auth/unlock/+server.ts accepts unlimited unauthenticated attempts. It is in PUBLIC_API (hooks.server.ts:5) by necessity. Argon2id raises the cost per guess but does not bound the number of guesses, and it is the server's CPU paying that cost — so the same property that slows an attacker is also a DoS vector.

Wants: attempt counter with backoff, keyed on source IP and globally; a lockout window; logging of failures. The global key matters — per-IP alone is trivially defeated.

2. Sessions are in-memory and hold the vault key

SessionStore (auth/session.ts) is a Map in the process. Two consequences:

  • Every deploy or crash signs out everyone. Acceptable for one operator, not for users.
  • event.locals.masterKey is the unlocked master key. The session is not a pointer to an identity, it is a live key. This is the single deepest assumption in the codebase and the one that multi-tenancy breaks (see the multi-tenancy issue).

Note: persisting sessions is not a fix on its own. A restored session without its key is authenticated but locked — same failure, more code. Any solution has to answer where key material lives across a restart, and "on disk" defeats the vault.

3. /api/auth/lock is global

sessions.destroyAll() (auth/session.ts:38) zeroizes and drops every session. With one operator that is exactly right and is the point of the button. With more than one, any user locking signs out the whole instance. Needs to become per-session, with the global form kept as an explicit admin action.

4. Secure cookies are opt-in

force_secure_cookie defaults off and NODE_ENV=production is what otherwise sets Secure. An internet-exposed instance must not be reachable over plain HTTP at all — the flag should be a hard requirement in that posture rather than a knob, and there is currently nothing that warns an operator who exposes an instance without it.

5. No CSRF layer beyond SameSite=Lax

The API is JSON routes, not SvelteKit form actions, so SvelteKit's built-in origin check does not cover them. Lax blocks cross-site POST cookies and carries most of the weight today, but it is the only thing doing so. Worth an explicit origin check on state-changing routes.

6. No session inventory or revocation

There is no way to see active sessions or revoke one. Single-operator, /api/auth/lock is a sufficient blunt instrument. Beyond that, "sign out other devices" is table stakes — and it becomes a hard requirement once #109 lands, since a leaked API token needs revocation independent of sessions.

Out of scope here

Rate limiting the rest of the API, audit logging, and 2FA. Real, but each wants its own issue.

Tracking issue. Nexus's auth is built for the deployment it has today: one operator, one passphrase, a LAN. Every item below is a reasonable choice at that scale and a real exposure the moment the instance is reachable from the internet. None of them are addressed by #108 (session expiry) or #109 (scoped API tokens). Filed as a checklist, not a plan. Each item is independently shippable. ## 1. No brute-force protection on `/api/auth/unlock` — highest priority `routes/api/auth/unlock/+server.ts` accepts unlimited unauthenticated attempts. It is in `PUBLIC_API` (`hooks.server.ts:5`) by necessity. Argon2id raises the cost per guess but does not bound the number of guesses, and it is the *server's* CPU paying that cost — so the same property that slows an attacker is also a DoS vector. Wants: attempt counter with backoff, keyed on source IP and globally; a lockout window; logging of failures. The global key matters — per-IP alone is trivially defeated. ## 2. Sessions are in-memory and hold the vault key `SessionStore` (`auth/session.ts`) is a `Map` in the process. Two consequences: - Every deploy or crash signs out everyone. Acceptable for one operator, not for users. - `event.locals.masterKey` **is** the unlocked master key. The session is not a pointer to an identity, it is a live key. This is the single deepest assumption in the codebase and the one that multi-tenancy breaks (see the multi-tenancy issue). Note: persisting sessions is *not* a fix on its own. A restored session without its key is authenticated but locked — same failure, more code. Any solution has to answer where key material lives across a restart, and "on disk" defeats the vault. ## 3. `/api/auth/lock` is global `sessions.destroyAll()` (`auth/session.ts:38`) zeroizes and drops every session. With one operator that is exactly right and is the point of the button. With more than one, any user locking signs out the whole instance. Needs to become per-session, with the global form kept as an explicit admin action. ## 4. Secure cookies are opt-in `force_secure_cookie` defaults off and `NODE_ENV=production` is what otherwise sets `Secure`. An internet-exposed instance must not be reachable over plain HTTP at all — the flag should be a hard requirement in that posture rather than a knob, and there is currently nothing that warns an operator who exposes an instance without it. ## 5. No CSRF layer beyond `SameSite=Lax` The API is JSON routes, not SvelteKit form actions, so SvelteKit's built-in origin check does not cover them. `Lax` blocks cross-site POST cookies and carries most of the weight today, but it is the only thing doing so. Worth an explicit origin check on state-changing routes. ## 6. No session inventory or revocation There is no way to see active sessions or revoke one. Single-operator, `/api/auth/lock` is a sufficient blunt instrument. Beyond that, "sign out other devices" is table stakes — and it becomes a hard requirement once #109 lands, since a leaked API token needs revocation independent of sessions. ## Out of scope here Rate limiting the rest of the API, audit logging, and 2FA. Real, but each wants its own issue.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lz/agent-nexus#110
No description provided.