updated README.md
This commit is contained in:
@@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"sessionID": "ses_f63e23ebcffe7Q6sJKCl9W8sFt",
|
"sessionID": "ses_f63e23ebcffe7Q6sJKCl9W8sFt",
|
||||||
"updatedAt": "2026-09-13T19:05:07.347Z",
|
"updatedAt": "2026-09-13T19:18:39.904Z",
|
||||||
"sources": {
|
"sources": {
|
||||||
"background-task": {
|
"background-task": {
|
||||||
"state": "idle",
|
"state": "idle",
|
||||||
"updatedAt": "2026-09-13T19:05:07.347Z"
|
"updatedAt": "2026-09-13T19:18:39.904Z"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
157
README.md
157
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 <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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user