Let the container run with nothing writable

The server writes to one path and no other: SESSION_FILE, from sessions.ts.
Everything else it touches on disk it only reads. So a container with a
read-only root filesystem already works -- except that `VOLUME ["/data"]`
quietly undid it. Docker acts on that directive: a container started without
`-v` gets an anonymous volume mounted there anyway, writable even under
`--read-only`. It persisted nothing across a redeploy, since each new container
got a fresh empty volume, and it left an orphan behind every time one was
replaced. Deployments that want the sessions to survive already say so
themselves -- docker-compose.yml and deploy.example.sh both mount a named
volume -- so removing the line changes nothing for them.

IMMUTABLE=1 asserts that this is how the instance is running. It is checked
rather than believed: the server refuses to start if SESSION_FILE is still set,
or if the filesystem it is installed on turns out to be writable. Left
unchecked the misconfiguration is silent, because persisting sessions is
best-effort -- a read-only /data costs one warning at the first sign-in and
nothing more until the instance is replaced and everyone is signed out.

SessionBackend names what the rest of the server asks of a session store, and
`sessions` in app.ts is typed as it. Nothing changes today; SessionStore is
still the only implementation. It is there so the OAuth work is written against
the interface rather than the class, and so the interface can record which of
its methods a stateless backend could satisfy alone: create, resolve, reseal
and destroy each touch one session, while listForUser and destroyAllForUser
have to reach sessions other than the caller's. The second of those carries the
guarantee that changing a password invalidates the sessions still holding the
old one, which is why it needs a registry -- Stalwart's token registry, once
sign-in goes through OAuth.
This commit is contained in:
2026-08-27 21:48:23 -07:00
parent 312a833d78
commit f72c67864e
7 changed files with 200 additions and 14 deletions
+23
View File
@@ -83,6 +83,29 @@ Full instructions, TLS, and every environment variable:
[Installing](https://docs.ihasmail.org/install/) ·
[Configuring](https://docs.ihasmail.org/configure/).
### Running immutably
The server writes to exactly one path, the optional `SESSION_FILE`. Clear it
and there is nothing left to write, so the container can run with no writable
filesystem at all:
```bash
docker run --read-only --tmpfs /tmp -e IMMUTABLE=1 -e SESSION_FILE= ...
```
`IMMUTABLE=1` is an assertion the server checks at startup rather than a switch
that changes what it does: it refuses to start if `SESSION_FILE` is still set,
or if the filesystem it is installed on turns out to be writable after all.
Without it the same misconfiguration is silent — sessions are held in memory
and persisting them is best-effort, so a read-only `/data` costs one warning at
the first sign-in and nothing else until the instance is replaced and everyone
is signed out.
That sign-out is the standing cost of this mode today, since sessions have
nowhere to live across a restart. Removing it means moving the session upstream
into a token Stalwart itself issues and can revoke, which is what the OAuth work
in [ROADMAP.md](ROADMAP.md) is for.
## Architecture
```