tailwart/CLAUDE.md

95 lines
5.1 KiB
Markdown
Raw Normal View History

# CLAUDE.md — tailwart
Guidance for Claude Code in this repo. Read before editing.
## What this is
A play deployment of **Stalwart** (all-in-one mail/JMAP/IMAP/SMTP server) wired,
gratuitously, into **three** shared backends — Postgres, Redis, and Garage S3 —
to see how far the federatedSocial Tailscale-sidecar pattern stretches past the
fediverse apps. Target domain: `infinidim.net` (may become real later).
It is **self-contained and outside** `/opt/federatedSocial` on purpose: that's
an upstream clone that `git pull` overwrites. tailwart owns its own `.env`,
compose, config, ACL snippet, and Caddy build, and only *reads from the tailnet*
(shared infra over MagicDNS) at runtime.
## Architecture — two ends of one wire
```
public IP host (tag:reverse-proxy) tailnet-only mailbox
┌───────────────────────────┐ ┌────────────────────────┐
│ caddy/ (caddy-l4) │ tailnet │ ts-stalwart sidecar │
│ :25 :465 :587 :143 :993 ──┼───WireGuard───▶│ stalwart (no WAN, no │
│ PROXY protocol v2 │ │ host ports) │
└───────────────────────────┘ └───────────┬────────────┘
L7 JMAP vhost on the main Caddy │
mail.infinidim.net → :8080 ┌───────┴───────┐
▼ ▼ ▼
Postgres Redis Garage S3
(the-record)(slo-time)(garage)
```
- **Mailbox** (`docker-compose.yml`): Stalwart in a Tailscale sidecar via
`network_mode: service:ts-stalwart`. Binds nothing on the host. All mail
ports listen on the tailnet only.
- **Edge** (`caddy/`): a layer-4 TCP proxy. Pure pass-through; Stalwart owns
TLS. **Can run on a different machine** than the mailbox — the key idea.
- **Backends**: data+fts → Postgres, blob → Garage S3, lookup/in-memory →
Redis. One stalwart role/db, one Garage bucket, one Redis logical DB.
## The `.env` contract
`.env` (gitignored) is the whole operator surface; `.env.example` is the
template. Both compose files read it. Secrets reach Stalwart as env vars and are
referenced from `config/config.toml` via `%{env:NAME}%` so the toml stays
commit-safe. Never hardcode a value that belongs in `.env` — except the two
spots a static file forces it: `caddy/caddy.json` dial targets and any
MagicDNS host in the toml.
## Sidecar boilerplate
Identical to federatedSocial's (TS_ACCEPT_DNS true, kernel networking, 127.0.0.1
healthcheck, ephemeral OAuth auth). Don't drift it. Tag: `tag:stalwart`.
## Prerequisites (shared tailnet infra — already running for the fediverse)
1. Postgres role + db: `stalwart` / `STALWART_DB_NAME`. Create via the
federatedSocial `bootstrap.sh` flow or a one-off `CREATE ROLE … LOGIN; CREATE
DATABASE … OWNER …`.
2. Garage bucket `stalwart-mail` + grant the shared access key access to it.
3. Redis: nothing to create — just use a dedicated logical DB index
(`STALWART_REDIS_DB`) so we don't collide with the apps.
4. Admin console: assign `tag:stalwart` to the OAuth client (Devices/Core +
Keys/AuthKeys) and add `acl-snippet.hujson` to the policy.
## Pitfalls (some learned the hard way next door)
- **Mail edge is layer 4, not layer 7.** Don't try to give the L4 ports a
normal Caddy vhost. SNI/Host routing doesn't apply to `:25`.
- **PROXY protocol or your mail reputation dies.** Without it Stalwart sees the
proxy's tailnet IP as every client → SPF/DNSBL/greylisting break. Both ends
must agree (caddy.json `proxy_protocol: v2` ↔ config `[server.proxy]
trusted-networks`).
- **Stalwart config drifts between versions and migrates into the admin store
after first boot.** `config/config.toml` is a strawman — verify keys against
the pinned image tag before trusting them. Pin the tag once it works.
- **`POSTGRES_PASSWORD`/role passwords only apply on an empty volume.** If a
password "doesn't work," the stored credential drifted — `ALTER USER`, don't
re-init. And never test a password over `127.0.0.1` against these Postgres
containers: pg_hba `trust`s loopback and accepts ANY password. Test over the
tailnet (scram) or you'll fool yourself.
- **Outbound :25 is usually blocked on VPS.** Set `STALWART_SMARTHOST`.
- **Mail forces WAN ports.** `:25` must be world-reachable for inbound
federation — this is the one place the tailnet-only model can't hold. Keep
submission/IMAP tailnet-only if you want a tighter surface.
## What not to do
- Don't put files in `/opt/federatedSocial`. Read its `.env` if you must; never
write there.
- Don't add `ports:` to the Stalwart container — the edge proxy is the only
public surface, and it lives in `caddy/`.
- Don't commit `.env` or a built Caddy binary (see `.gitignore`).
- Don't break the sidecar netns boundary with bridge networks or host ports.