Compare commits

...

2 Commits

Author SHA1 Message Date
bb79b73fdd updated variables 2026-09-13 13:22:34 -06:00
4ca54b1824 updated README.md 2026-09-13 13:18:42 -06:00
4 changed files with 164 additions and 7 deletions

View File

@@ -1,10 +1,10 @@
{ {
"sessionID": "ses_f63e23ebcffe7Q6sJKCl9W8sFt", "sessionID": "ses_f63e23ebcffe7Q6sJKCl9W8sFt",
"updatedAt": "2026-09-13T19:05:07.347Z", "updatedAt": "2026-09-13T19:22:32.998Z",
"sources": { "sources": {
"background-task": { "background-task": {
"state": "idle", "state": "idle",
"updatedAt": "2026-09-13T19:05:07.347Z" "updatedAt": "2026-09-13T19:22:32.998Z"
} }
} }
} }

157
README.md
View File

@@ -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 <domain>` (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://<your-domain>` 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.

View File

@@ -13,7 +13,7 @@
# for this name, and every URL Stoat advertises uses it. # for this name, and every URL Stoat advertises uses it.
# Example: stoat.example.com (NOT the public stoat.chat domain) # Example: stoat.example.com (NOT the public stoat.chat domain)
# ------------------------------------------------------------ # ------------------------------------------------------------
stoat_domain: chat.example.com stoat_domain: chat.fpov.space
# ------------------------------------------------------------ # ------------------------------------------------------------
# Stoat application # Stoat application
@@ -36,7 +36,7 @@ stoat_video: true
# Podman (rootless) # Podman (rootless)
# ------------------------------------------------------------ # ------------------------------------------------------------
# Unprivileged system user that runs the rootless podman stack # Unprivileged system user that runs the rootless podman stack
stoat_service_user: stoat stoat_service_user: chatmanone
podman_packages: podman_packages:
- podman - podman
@@ -58,7 +58,9 @@ fail2ban_bantime: 3600 # ban length in seconds (1 hour)
# Hardening - SSH # Hardening - SSH
# ------------------------------------------------------------ # ------------------------------------------------------------
sshd_permit_root_login: no 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 # Enable ONLY if a TOTP / WebUI-2FA PAM module is already
# configured in /etc/pam.d/sshd - otherwise you can lock yourself # configured in /etc/pam.d/sshd - otherwise you can lock yourself

View File

@@ -4,7 +4,7 @@
# -------------------------------------------------------------- # --------------------------------------------------------------
[servers] [servers]
stoat-demo ansible_host=stoat-demo ansible_user=YOUR_SSH_USER stoat-demo ansible_host=stoat-demo
[servers:vars] [servers:vars]
# Elevate privileges (the playbook already sets become: true, # Elevate privileges (the playbook already sets become: true,