Satellite v2: upstream-minted scoped sync tokens, per-session contexts
Registration simplified to a single sync URL: minted in the upstream's admin UI (Satellite access section of the domain dialog), carrying the token as userinfo — https://<token>@<auth-host>. Tokens live hashed in the upstream database, scoped to one rp-id, revocable (drops live connections). PASKIA_SYNC_TOKENS env is gone. Data minimization: the sync feed now carries only the scope's sessions and credentials; the user graph never leaves the upstream. Each session upsert embeds its master-computed ApiSessionContext (identity display data + effective permissions for the session's host); user-graph mutations eagerly re-push affected sessions with fresh contexts, so revocations land within ~1 RTT. /check and /user-info are proxied to the upstream (they need the user graph). Satellite side: replicas keyed by rp-id (no more shared-replica config conflicts — each remote domain has its own token), the replica refuses to go live on a ready-message scope mismatch, write-backs are confined to the token's scope server-side. Admin dialog: one sync-URL paste field (token split from userinfo, write-only), uplink verification required to save (reachability, rp-id match, no nesting, auth host present); a verified uplink takes over the domain's origins (**.<rp-id> + upstream auth host) and auth host. New domains adopt the upstream's rp-id and name.
This commit is contained in:
1 parent
da0b3e0e8b
commit
2209e2395c
14 files changed
+1159
-330
No files matched your search
+67
-39
@@ -20,9 +20,25 @@ alternatives is at the end of this document.
|
||||
|
||||
## Configuration
|
||||
|
||||
A domain becomes remote in the admin domains UI (master admin, on the
|
||||
primary server's auth host — this configuration itself never touches a
|
||||
remote): enable *Remote instance* and set the remote URL and sync token.
|
||||
Setup is a single paste. On the **upstream** (the authoritative instance),
|
||||
open the domain in the admin UI and use *Satellite access → Generate sync
|
||||
URL*. That mints a token scoped to the domain's rp-id and returns one URL
|
||||
carrying everything the satellite needs:
|
||||
|
||||
```
|
||||
https://<token>@auth.example.com
|
||||
```
|
||||
|
||||
On the **satellite**, open (or add) the same domain in its admin UI,
|
||||
enable *Remote instance*, and paste the sync URL. The dialog probes the
|
||||
upstream's `/auth/api/settings` as you type: it must be reachable, serve
|
||||
this very rp-id authoritatively (a satellite cannot upstream another
|
||||
satellite), and have an auth host configured — only such an instance can
|
||||
serve satellites at all. Once the uplink verifies, the remote
|
||||
configuration takes over completely: the domain's origins become
|
||||
`**.<rp-id>` plus the upstream's auth host, and saving requires the
|
||||
verified uplink.
|
||||
|
||||
In the stored config (`DomainConfig.remote`):
|
||||
|
||||
```json
|
||||
@@ -34,32 +50,31 @@ In the stored config (`DomainConfig.remote`):
|
||||
}
|
||||
```
|
||||
|
||||
- `url` — the remote instance's base URL. The satellite connects to
|
||||
`{url}/auth/api/sync/ws`; the connection is server-to-server and not
|
||||
host-dispatched, so internal addresses work. The admin dialog probes
|
||||
`{url}/auth/api/settings` as you type: it must be reachable, serve this
|
||||
very rp-id, and not be a satellite for it itself (nested satellites are
|
||||
not supported — always point at the authoritative instance). Probe
|
||||
findings are warnings; the server-side validation is authoritative.
|
||||
- `token` — bearer token for the sync channel. The **remote accepts tokens
|
||||
via its `PASKIA_SYNC_TOKENS` environment variable** (comma-separated);
|
||||
nothing is stored in the remote's database, and with the variable unset
|
||||
the sync endpoint stays closed. The token is write-only over the admin
|
||||
API (an empty field keeps the stored one).
|
||||
- `url` — the upstream's auth host URL (the userinfo token is split off
|
||||
before storing). The satellite connects to `{url}/auth/api/sync/ws`;
|
||||
the connection is server-to-server and not host-dispatched, so internal
|
||||
addresses work.
|
||||
- `token` — bearer token for the sync channel, stored **hashed in the
|
||||
upstream's database** and scoped to one rp-id. Managed in the upstream
|
||||
admin UI (the domain's *Satellite access* section: generate, list,
|
||||
revoke); the raw token is shown once, inside the sync URL. On the
|
||||
satellite it is write-only over the admin API (a sync URL without
|
||||
userinfo keeps the stored token). Revoking a token drops the satellite's
|
||||
live connections immediately.
|
||||
- `cache_ttl` — seconds the replica remains trusted after the sync channel
|
||||
goes down; then checks fail closed (503). Set it large (up to the 24 h
|
||||
session lifetime) for fail-open behavior. Minimum 1.
|
||||
- `refresh_interval` — seconds between reconnects; every connect starts
|
||||
from a full snapshot, which reconciles any drift. Minimum 30.
|
||||
|
||||
Several domains may share one remote (they share a single replica); their
|
||||
remote settings must then be identical — conflicting configurations are
|
||||
rejected at validation and aligned with a warning at load.
|
||||
Each remote domain gets its own replica and its own token; several domains
|
||||
may point at the same upstream instance independently.
|
||||
|
||||
A remote domain **must mark an auth host** (validated cross-domain and in
|
||||
the UI): the profile, admin and sign-in pages live there, so browsers and
|
||||
WebSockets go directly to the remote. Other domains on the same satellite
|
||||
remain fully local — the multi-domain config mixes both kinds freely.
|
||||
A remote domain **must mark an auth host** (the uplink's own — the dialog
|
||||
derives it): the profile, admin and sign-in pages live there, so browsers
|
||||
and WebSockets go directly to the upstream. Other domains on the same
|
||||
satellite remain fully local — the multi-domain config mixes both kinds
|
||||
freely.
|
||||
|
||||
## How it works
|
||||
|
||||
@@ -73,12 +88,20 @@ write-behind for remote, `db.update_session` for local;
|
||||
`satellite.evict_session` on logout). `satellite.forward_request(request)`
|
||||
returns the proxied response for remote domains or `None` for local ones.
|
||||
|
||||
**The replica** is a plain `DB` struct instance in RAM, never persisted.
|
||||
On connect the remote sends a snapshot of the replicated tables
|
||||
(permissions, orgs, roles, users, credentials, sessions), then live
|
||||
upsert/delete events emitted from the struct `store()`/`delete()` hooks
|
||||
(which also cover cascade deletes) and field-mutating operations. A
|
||||
single ordered WebSocket cannot gap; a slow subscriber is dropped and
|
||||
**The replica** is a `ReplicaDB` instance in RAM, never persisted. On
|
||||
connect the upstream sends a snapshot of the replicated tables
|
||||
(the domain's credentials and sessions), then live upsert/delete events
|
||||
emitted from the struct `store()`/`delete()` hooks (which also cover
|
||||
cascade deletes) and field-mutating operations. The feed is scoped to the
|
||||
token's rp-id, and the user graph never leaves the upstream: every session
|
||||
upsert carries an embedded `ctx` — the same `ApiSessionContext` sent to
|
||||
users — holding the session's user/org/role display data and effective
|
||||
permissions, computed for the session's bound host. Mutations of users,
|
||||
orgs, roles or permissions are not published; instead the upstream eagerly
|
||||
re-pushes the affected sessions with freshly computed contexts, so renames
|
||||
and permission revocations take effect within ~1 RTT. The replica refuses
|
||||
to go live when the scope in the ready message does not match its domain.
|
||||
A single ordered WebSocket cannot gap; a slow subscriber is dropped and
|
||||
resyncs. The feed carries no usable secrets: sessions are keyed by
|
||||
`hash_secret` output, credentials carry public keys only, and the OIDC
|
||||
signing key is never replicated.
|
||||
@@ -87,8 +110,9 @@ signing key is never replicated.
|
||||
|
||||
| Endpoint | Handling |
|
||||
|---|---|
|
||||
| `GET /auth/api/forward`, `GET /check`, `GET /user-info`, `GET /settings` | served from the replica (sub-ms) |
|
||||
| `GET /auth/api/forward`, `GET /settings` | served from the replica (sub-ms) |
|
||||
| `POST /auth/api/validate` | verified from the replica; the throttled refresh updates the replica and is written back over the sync channel; cookie renewed locally |
|
||||
| `GET /check`, `GET /user-info` | proxied — these need the user graph (per-session contexts do not cover arbitrary user lookups or full profile data) |
|
||||
| `POST /auth/api/logout` | proxied to the remote (original Host preserved) and evicted from the replica immediately |
|
||||
| `POST /auth/api/set-session`, `GET /token-info` | proxied (the exchange code/reset token lives on the remote); the session arrives via sync event |
|
||||
| `/auth/oidc/*` | proxied (signing key and OIDC sessions stay on the remote) |
|
||||
@@ -112,18 +136,22 @@ Freshness hierarchy:
|
||||
|
||||
## The remote side
|
||||
|
||||
Strictly additive and RAM-only: `syncfeed` (a subscriber set fed by the
|
||||
commit hooks) and the token-gated `/auth/api/sync/ws` endpoint serving
|
||||
snapshot + live events and accepting `session_refresh` write-backs. With
|
||||
no satellites connected, the hooks are a no-op.
|
||||
Strictly additive and RAM-only besides the token table: `syncfeed` (a
|
||||
subscriber set fed by the commit hooks) and the `/auth/api/sync/ws`
|
||||
endpoint serving snapshot + live events and accepting `session_refresh`
|
||||
write-backs within the token's scope. With no satellites connected, the
|
||||
hooks are a no-op.
|
||||
|
||||
## Trust and caveats
|
||||
|
||||
- The satellite host holds a full copy of the remote's auth data in RAM
|
||||
(minus the OIDC key) — treat it as trusted as the remote.
|
||||
- Avatars are stored on the remote's disk; `user-info` from a replica
|
||||
reports no avatar URL.
|
||||
- OIDC sessions in a replica-backed `user-info` show the client UUID
|
||||
- The satellite host holds only its own domain's sessions (with their
|
||||
identity contexts) and credentials in RAM — it learns the users that
|
||||
actually sign in to its domain, never the directory, and never the OIDC
|
||||
key. Still treat it as trusted: it is authoritative for session checks
|
||||
within `cache_ttl`.
|
||||
- Avatars are stored on the remote's disk; profile data such as
|
||||
`user-info` is proxied to the remote rather than answered locally.
|
||||
- OIDC sessions in a proxied `user-info` show the client UUID
|
||||
rather than its name (OIDC clients are not replicated).
|
||||
|
||||
---
|
||||
@@ -175,7 +203,7 @@ within one RTT instead of at TTL.
|
||||
meaning shifts under the caller. Dispatch is instead keyed explicitly
|
||||
by the request host.)
|
||||
- A sync protocol (snapshot + live events + reconnect reconciliation).
|
||||
- A trusted satellite host (full data copy in RAM).
|
||||
- A trusted satellite host (its domain's sessions and credentials in RAM).
|
||||
- Additive remote code (sync endpoint + commit hooks), where A needs
|
||||
none.
|
||||
- Replica housekeeping (expiry sweeper, write-behind, optimistic
|
||||
|
||||
Reference in new issue
Block a user