# tailwart edge — layer-4 mail proxy A custom Caddy (with the `caddy-l4` app) that pipes the public mail ports to the Stalwart sidecar over the tailnet. Pure TCP pass-through with PROXY protocol — Stalwart still terminates all the TLS. **Runs anywhere** with a public IP that's on the tailnet and tagged `tag:reverse-proxy`; doesn't need to share a host with the mailbox. ## Why layer 4 and not a normal Caddy vhost Web apps reverse-proxy at layer 7 (route by Host/SNI, Caddy terminates TLS). Mail can't: port 25 has no SNI (STARTTLS comes after connect), and you want one global `:25` listener, not per-domain routing. So the edge is a dumb L4 pipe and Stalwart owns the TLS. The novelty you spotted: this is the same `stream`-style proxying nginx/Caddy can do for *any* TCP — it just usually isn't used for it. ## Build & run ```bash docker compose up -d --build # builds the image, runs it ``` The Dockerfile doesn't compile Caddy — it pulls the prebuilt L4-enabled binary from `caddyserver.com/api/download` (the house method, see `~/docs/caddy.md` "Custom Binary"), dodging the ~1GB-RAM local `xcaddy` build this VPS can't afford. The build still fails loudly if `caddy-l4` isn't in the downloaded binary. To add plugins, append `&p=` to `CADDY_DOWNLOAD` in the Dockerfile. ## Edit the upstream `caddy.json` dials `stalwart.tail7b1641.ts.net:`. If your `STALWART_MAGIC_NAME` / `TS_TAILNET` differ, update the five `dial` lines. (JSON can't read `.env`; this is the one spot the MagicDNS name is hardcoded — same trade-off as pgAdmin's `servers.json`.) ## The HTTP side (MTA-STS / autoconfig / autodiscover) — `:443` SNI fan-out Stalwart publishes DNS that points public HTTPS names at this edge: `mta-sts.`, `autoconfig.`, `autodiscover.`. They serve the MTA-STS policy and mail-client autoconfig over **:443** — so the edge has to handle `:443` too, which is where a naive setup collides with a box that already runs a web Caddy. The fix is **not** an L7 `reverse_proxy` (terminate at Caddy). You can't: the domain's **CAA** record pins issuance to Stalwart's ACME account (`accounturi=…`), so Caddy can't obtain its own cert for `*.`. Stalwart already holds the wildcard. So we **pass TLS through** to it. The `web` server in `caddy.json` owns `:443` and fans out by SNI: - `mta-sts` / `autoconfig` / `autodiscover.` → `stalwart:443` (pass-through; Stalwart terminates with its wildcard cert — **no** proxy protocol on `:443`, unlike the mail ports). - every other SNI → `127.0.0.1:8443`, the box's own web Caddy. For that fallback to exist, move the web Caddy's HTTPS off `:443`: ```caddyfile { https_port 8443 # web vhosts now listen here; the L4 :443 forwards to them } your-web-site.example { reverse_proxy … } ``` HTTP→HTTPS redirects still resolve to `:443` correctly. A **mail-only** edge (no web vhosts on the box) omits the `web` server entirely — keep just the mail ports above. > Note: `tag:reverse-proxy → tag:stalwart` must also grant **`tcp:443`** in the > Tailscale ACL (see `../acl-snippet.hujson`), on top of the mail ports. ## Prerequisites on the host running this - Joined to the tailnet, tagged `tag:reverse-proxy` (so the ACL lets it reach `tag:stalwart`). - Public firewall opens for whichever mail ports you expose (`25` minimum). - Nothing else bound to those ports.