diff --git a/.omo/run-continuation/ses_f63e23ebcffe7Q6sJKCl9W8sFt.json b/.omo/run-continuation/ses_f63e23ebcffe7Q6sJKCl9W8sFt.json index cf3e6d3..72b0704 100644 --- a/.omo/run-continuation/ses_f63e23ebcffe7Q6sJKCl9W8sFt.json +++ b/.omo/run-continuation/ses_f63e23ebcffe7Q6sJKCl9W8sFt.json @@ -1,10 +1,10 @@ { "sessionID": "ses_f63e23ebcffe7Q6sJKCl9W8sFt", - "updatedAt": "2026-09-13T19:05:07.347Z", + "updatedAt": "2026-09-13T19:18:39.904Z", "sources": { "background-task": { "state": "idle", - "updatedAt": "2026-09-13T19:05:07.347Z" + "updatedAt": "2026-09-13T19:18:39.904Z" } } } \ No newline at end of file diff --git a/README.md b/README.md index 8760ef4..cd0b647 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,157 @@ -# stoat +# Stoat — Fedora 44 self-hosted chat deployment +Ansible automation that **hardens a Fedora 44 server**, installs **rootless Podman**, and deploys a self-hosted **[Stoat](https://stoat.chat)** instance (open-source community chat, formerly Revolt) using the official [`stoatchat/self-hosted`](https://github.com/stoatchat/self-hosted) stack. + +**Target host:** `stoat-demo` + +--- + +## Project layout + +``` +├── site.yml # main playbook — applies the role +├── inventory.ini # server inventory (stoat-demo) +├── group_vars/all/vars.yml # ★ all deployment variables live here +└── roles/stoat/ + ├── tasks/ + │ ├── main.yml # imports, in order + │ ├── system.yml # dnf full update + base packages + │ ├── harden.yml # firewalld, sshd, fail2ban, dnf5-automatic, logrotate + │ ├── podman.yml # rootless podman, compose, service user, linger + │ └── stoat.yml # clone repo → generate config → systemd unit → start + ├── handlers/main.yml + └── templates/ + ├── jail.local.j2 # fail2ban config + ├── sshd_hardening.conf.j2 # sshd drop-in + └── podman-compose-stoat.service.j2 # systemd unit for the stack +``` + +--- + +## Before you run + +1. **Set the real domain** in `group_vars/all/vars.yml`: + + ```yaml + stoat_domain: chat.yourdomain.com # ← A/AAAA record must point at the server + ``` + + Let's Encrypt certificates are issued for this name, and every URL Stoat advertises (web client, API, WebSocket, LiveKit) uses it. Do **not** use the public `stoat.chat` domain for your own instance. + +2. **Set the SSH user** in `inventory.ini`: + + ```ini + stoat-demo ansible_host=stoat-demo ansible_user=YOUR_SSH_USER + ``` + +3. **SSH password login is intentionally enabled** (`sshd_password_authentication: yes`). + - Root login stays disabled — use your SSH user + `sudo`. + - Fail2Ban is the brute-force defense: `3` failed attempts → `1 h` ban (tunable). + - If you ever want key-only auth again, flip the var to `no`. + - Ansible connects with a password via `-k`: + + ```bash + ansible-playbook -i inventory.ini site.yml -k + ``` + +--- + +## What the playbook does + +| Stage | Actions | +|---|---| +| **System** | `dnf update` all packages; install `git`, `curl`, `openssl` | +| **Firewall** | firewalld default-deny; allow `ssh`, `http`, `https`, `7881/tcp`, `50000-50100/udp` (LiveKit voice) | +| **SSH** | drop-in `00-hardening.conf`: `PermitRootLogin no`, `PasswordAuthentication yes`; config validated with `sshd -t` before restart | +| **Fail2Ban** | `sshd` jail (3 retries → 1 h ban, incremental for repeat offenders) | +| **Auto-updates** | `dnf5-plugin-automatic`, security-only, `apply_updates = yes`, started timer | +| **Logs** | logrotate rule for fail2ban logs (daily, 7 rotations, compressed) | +| **Podman** | rootless engine + `podman-compose` + `podman-docker`; `net.ipv4.ip_unprivileged_port_start=80` so Caddy can bind 80/443; dedicated `stoat` service user with lingering enabled | +| **Stoat** | clone `stoatchat/self-hosted` to `/opt/stoat/self-hosted`; run `generate_config.sh ` (answers: built-in Caddy, video enabled); start stack under `podman-compose-stoat.service` | + +--- + +## Network / ports + +| Port | Purpose | +|---|---| +| 22/tcp | SSH | +| 80/tcp, 443/tcp | Caddy reverse proxy (automatic Let's Encrypt TLS) | +| 7881/tcp | LiveKit WebRTC signaling | +| 50000–50100/udp | LiveKit voice/video media (cannot be proxied) | + +--- + +## Variables reference (`group_vars/all/vars.yml`) + +| Variable | Default | Meaning | +|---|---|---| +| `stoat_domain` | `chat.example.com` | **Required** — public domain for this instance | +| `stoat_install_dir` | `/opt/stoat/self-hosted` | where the stack lives on the server | +| `stoat_version` | `main` | git ref of `self-hosted` repo | +| `stoat_reverse_proxy` | `false` | `true` = move Caddy to `:8880`, bring your own front proxy/TLS | +| `stoat_video` | `true` | enable camera/screen sharing (LiveKit) | +| `stoat_service_user` | `stoat` | unprivileged user running rootless podman | +| `podman_unprivileged_port_start` | `80` | lowest port rootless containers may bind | +| `fail2ban_port` / `fail2ban_maxretry` / `fail2ban_bantime` | `22` / `3` / `3600` | sshd jail tuning | +| `sshd_permit_root_login` | `no` | root SSH access | +| `sshd_password_authentication` | `yes` | password login (fail2ban guards it) | +| `sshd_mfa_required` | `false` | only enable if a 2FA PAM module exists, else you lock yourself out | +| `firewall_services` / `firewall_ports` | ssh,http,https / 7881, UDP range | firewalld allow rules | + +Override any variable per-run, without editing files: + +```bash +ansible-playbook -i inventory.ini site.yml -k -e "stoat_domain=chat.example.org fail2ban_maxretry=5" +``` + +--- + +## After deployment + +1. **First account = instance owner.** Open `https://` and register. +2. **Make it invite-only** (recommended for a private instance): in `/opt/stoat/self-hosted/Revolt.toml` set `invite_only = true`, then restart the API service. +3. **Back up `secrets.env`** — it holds the VAPID push keys, file-encryption key, and LiveKit credentials. Losing it means losing access to **all uploaded files** and breaking existing sessions. Copy it somewhere safe: + ```bash + scp stoat-demo:/opt/stoat/self-hosted/secrets.env ~/backups/ + ``` +4. **Caddy terminates TLS itself** — do not put Cloudflare proxy (orange cloud) in front of the domain; ACME needs direct access to the server (grey cloud / direct DNS only). + +--- + +## Day-to-day operations + +All commands run on the server, mostly as the `stoat` user (`sudo -u stoat` when needed): + +```bash +# Stack status +systemctl status podman-compose-stoat +podman ps + +# Logs (per service: api / events / web / livekit / database …) +sudo -u stoat podman logs -f api + +# Restart the whole stack +sudo systemctl restart podman-compose-stoat + +# Update Stoat to new images (run in /opt/stoat/self-hosted as stoat) +sudo -u stoat podman-compose pull +sudo -u stoat podman-compose up -d + +# Fail2Ban status +sudo fail2ban-client status sshd + +# Open firewall UI changes +sudo firewall-cmd --list-all +``` + +--- + +## Caveats & notes + +- **First start pulls ~2 GB of images** and initializes MongoDB — allow a few minutes; the systemd unit tolerates up to 15 min (`TimeoutStartSec=900`). +- **Podman, not Docker**: the upstream stack is docker-compose based; this deployment runs it with `podman-compose` (rootless). It supports the compose features this stack needs (`depends_on: condition: service_healthy`, bind mounts, port ranges). +- **`generate_config.sh` is guarded** by `creates:` — it never overwrites existing `secrets.env`/`Revolt.toml` unintentionally. To regenerate config deliberately, run it manually with `--overwrite` as the `stoat` user (after backing up secrets). +- **Security posture**: rootless containers, non-root SSH user, fail2ban, firewalld default-deny, security-only auto-updates. (Aligned with the upstream guide's recommendations.) +- **Rootless bind mounts**: data persists in `/opt/stoat/self-hosted/data/` (mongodb, rabbitmq, minio, caddy). +- System hardening + Stoat are one role run; to re-apply only config changes, re-running `site.yml` is idempotent. \ No newline at end of file diff --git a/group_vars/all/vars.yml b/group_vars/all/vars.yml index a3d78ee..619497b 100644 --- a/group_vars/all/vars.yml +++ b/group_vars/all/vars.yml @@ -58,7 +58,9 @@ fail2ban_bantime: 3600 # ban length in seconds (1 hour) # Hardening - SSH # ------------------------------------------------------------ sshd_permit_root_login: no -sshd_password_authentication: no +# Keep password login enabled on this server. Fail2Ban (see below) +# still guards against brute-force attempts. +sshd_password_authentication: yes # Enable ONLY if a TOTP / WebUI-2FA PAM module is already # configured in /etc/pam.d/sshd - otherwise you can lock yourself diff --git a/inventory.ini b/inventory.ini index 043f84d..e38349d 100644 --- a/inventory.ini +++ b/inventory.ini @@ -4,7 +4,7 @@ # -------------------------------------------------------------- [servers] -stoat-demo ansible_host=stoat-demo ansible_user=YOUR_SSH_USER +stoat-demo ansible_host=stoat-demo [servers:vars] # Elevate privileges (the playbook already sets become: true,