Stoat (Revolt fork) — Lockdown Playbook

Stoat (Revolt fork) — Lockdown Playbook (Two Edge Options + Invite-Only Registration)

Target deployment: Linux VM on FreeBSD host, Stoat self-host stack with Caddy + api/events/autumn/january/gifbox.
Org goal: closed invite-only (Star Citizen org), ~12 active / 100+ registered.

Key outcomes

  • Pick one authoritative edge (Apache-on-FreeBSD or Caddy-on-VM)
  • Ensure WebSockets are stable (/ws)
  • Make the instance invite-only (disable open registration)
  • Close internal service exposure and tighten secrets

0) Your Current Routing (Ground Truth)

Your Caddyfile defines the correct Stoat routing. Keep these paths stable; everything else builds on them.

Confirmed routes
/api*      → api:14702      (strip /api)
/ws        → events:14703   (strip /ws)  [WebSockets]
/autumn*   → autumn:14704   (strip /autumn)
/january*  → january:14705  (strip /january)
/gifbox*   → gifbox:14706   (strip /gifbox)
/          → web:5000
Important
/ws is a dedicated WebSocket route. If the edge proxy does not support WebSocket upgrades, Stoat will look “mostly fine” but behave flaky.

Option A1 (Recommended): Apache on FreeBSD is the Edge

Architecture: Internet → FreeBSD PF → Apache :443 → VM (private IP) → Caddy → Stoat services.
This centralizes control (TLS, headers, rate-limit/WAF, audit logs) on the FreeBSD host.

A1.1 Compose change: stop publishing VM ports to the internet

In compose.yml, remove public port publishing for the caddy service (or bind it only to a private interface).
The goal: the VM is not directly reachable from the internet on 80/443.

Before
caddy:
  ports:
    - "80:80"
    - "443:443"
Best practice
Prefer a private VM IP and no published ports. If you must publish, bind to a private interface only (not 0.0.0.0).

A1.2 Apache vhosts: redirect HTTP and proxy HTTPS + WebSockets

Apache :80 redirect
<VirtualHost 207.158.15.93:80>
  ServerName revolt.yaws.com
  RewriteEngine On
  RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [R=301,L]
</VirtualHost>
Apache :443 proxy (matches your Caddy routes)
<VirtualHost 207.158.15.93:443>
  ServerName revolt.yaws.com

  SSLEngine On
  # Keep your existing Let's Encrypt cert/key directives here

  ProxyPreserveHost On
  ProxyRequests Off
  RequestHeader set X-Forwarded-Proto "https"
  RequestHeader set X-Forwarded-Port "443"
  ProxyTimeout 300

  # WebSocket endpoint (your Caddyfile: route /ws → events)
  ProxyPass        /ws  ws://VM_PRIVATE_IP/ws
  ProxyPassReverse /ws  ws://VM_PRIVATE_IP/ws

  # Everything else goes to Caddy; Caddy routes /api, /autumn, /january, /gifbox, /
  ProxyPass        /    http://VM_PRIVATE_IP/
  ProxyPassReverse /    http://VM_PRIVATE_IP/
</VirtualHost>

Apache modules required: ssl, proxy, proxy_http, proxy_wstunnel, headers, rewrite.

Option A2 (Chosen): Caddy on the VM is the Edge (Harden VM + Caddy)

Architecture: Internet → FreeBSD PF (pass-through) → VM Caddy :443 → Stoat services.
This is what your current compose file implements (ports: "80:80", "443:443").

A2.1 Enforce HTTPS-only (redirect HTTP → HTTPS)

Add this near the top of your site block in Caddyfile:

Caddy redirect snippet
revolt.yaws.com {
  @http {
    protocol http
  }
  redir @http https://{host}{uri} permanent

  # ...existing routes...
}

A2.2 Firewall the VM: expose only what you intend

  • Allow inbound: tcp/443
  • Optional: allow inbound tcp/80 only to support redirect
  • Block everything else inbound (SSH restricted to trusted IP/VPN)
Compose sanity
Your compose.yml does not publish the internal service ports (14702–14706), which is good. Keep it that way.

A2.3 Harden internal secrets (strongly recommended)

Even if internal services are not public, weak credentials enable total takeover after any foothold.
Update these to long random values and store them in a restricted env file.

  • RabbitMQ: replace rabbituser/rabbitpass
  • MinIO: replace MINIO_ROOT_PASSWORD=minioautumn
  • Files: set a strong [files].encryption_key in Revolt.toml

Registration Lockdown: Make the Instance Invite-Only

Stoat/Revolt self-host deployments commonly support an invite-only toggle in Revolt.toml under
[api.registration] as invite_only = true. :contentReference[oaicite:0]{index=0}

1) Update Revolt.toml (add these sections)

Revolt.toml — invite-only + file encryption key
[hosts]
app = "https://revolt.yaws.com"
api = "https://revolt.yaws.com/api"
events = "wss://revolt.yaws.com/ws"
autumn = "https://revolt.yaws.com/autumn"
january = "https://revolt.yaws.com/january"

[api]
# (keep other api settings here as needed)

[api.registration]
# Disable open signup: users must join via invite.
invite_only = true

[files]
# REQUIRED for production: use a long, random value and keep it safe.
encryption_key = "REPLACE_WITH_LONG_RANDOM_SECRET"
Note
Your current [files].encryption_key is empty. Set it before you treat the system as production.

2) Restart the stack

Restart
docker compose up -d

3) Validate invite-only is truly enforced

  • Open https://revolt.yaws.com/login and attempt to register without an invite.
  • Confirm the server blocks registration (not just “UI hidden”).
  • Create/issue invites only to trusted recruiters/admins; use expirations + limited uses.

Client Config Notes (.env.web)

Your current .env.web is minimal:

.env.web (as provided)
HOSTNAME=revolt.yaws.com
REVOLT_PUBLIC_URL=https://revolt.yaws.com/api

Keep these consistent with [hosts] in Revolt.toml. Mismatched public URLs commonly cause login/WS issues.

Quick Checklist (Do These in Order)

  1. Edge choice: A1 (Apache edge) or A2 (VM edge). Don’t run both as public edges.
  2. Invite-only: set [api.registration].invite_only = true. :contentReference[oaicite:1]{index=1}
  3. Encryption key: set [files].encryption_key to a long random secret.
  4. Secrets: replace RabbitMQ + MinIO weak defaults.
  5. Network: confirm only intended ports are reachable from WAN (443; optionally 80 redirect).
Next step (optional)

If you want, I can generate a hardened compose.yml variant for A2 (env-secrets file, no weak defaults, safer bucket creation),
plus a “recruit intake” role/channel blueprint tuned for a Star Citizen org.