sessiond

Server-side sessions as a service — opaque tokens in Redis, cross-node revocation with a provable staleness bound.

GoRedisLuaminiredis demo: deploy pending

What it is

Distributed browser sessions: opaque tokens whose SHA-256 hashes live in Redis, sliding idle expiry under a hard absolute cap, per-user concurrent-session limits, and global revocation. Within Meridian, idp and portal are the intended callers; sessiond trusts only bearer-authenticated services, never browsers (except its built-in /demo, which exists to make the behavior visible).

The architectural challenge

Any per-node cache reintroduces exactly the problem sessions exist to solve: a revoked session must die everywhere. And Redis pub/sub — the obvious invalidation transport — is fire-and-forget; a design that needs delivery for correctness is broken the day it ships. sessiond’s answer (ADR 0003) is a consistency argument that never mentions pub/sub:

  1. Redis is the single point of truth; the Lua scripts are its only writers. The cache never writes back.
  2. Every cache entry self-expires after CacheTTL (default 2s).
  3. Therefore a node that misses every broadcast still serves a revoked session for at most CacheTTL.

Pub/sub only tightens ”≤ 2s” to “milliseconds”. Because it’s an optimization, its failure modes degrade latency, never correctness. CacheTTL is an explicit security parameter: precisely the maximum time a revoked session can outlive its revocation on one node.

Key design decisions

-- touchScript: validate + renew atomically; the deadline re-check protects
-- against clock-skewed nodes and restored snapshots with stale TTLs.
if redis.call('EXISTS', KEYS[1]) == 0 then return {} end
local now = tonumber(ARGV[1])
local deadline = tonumber(redis.call('HGET', KEYS[1], 'deadline_ms'))
if now >= deadline then
  redis.call('DEL', KEYS[1])
  return {}
end

Security highlights

From the threat model: missing, expired, and revoked sessions are one indistinguishable 404 — the API is deliberately not a validity oracle; rotation kills the old ID and writes the new one in the same script, so there’s no fixation window; API tokens are hashed then compared in constant time, and zero configured tokens fails closed (503), never open; realm and user IDs pass an allowlist before being spliced into Redis keys; only truncated UA fingerprints are stored, not raw strings.

Verified by

Code worth reading