removed stoat service user
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"sessionID": "ses_f63e23ebcffe7Q6sJKCl9W8sFt",
|
||||
"updatedAt": "2026-09-13T19:48:15.726Z",
|
||||
"updatedAt": "2026-09-13T20:00:12.252Z",
|
||||
"sources": {
|
||||
"background-task": {
|
||||
"state": "idle",
|
||||
"updatedAt": "2026-09-13T19:48:15.726Z"
|
||||
"updatedAt": "2026-09-13T20:00:12.252Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
39
README.md
39
README.md
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
Ansible automation that **hardens a Fedora 44 server**, installs **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`
|
||||
|
||||
@@ -17,7 +17,7 @@ Ansible automation that **hardens a Fedora 44 server**, installs **rootless Podm
|
||||
│ ├── 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
|
||||
│ ├── podman.yml # podman, compose, SELinux container policy
|
||||
│ └── stoat.yml # clone repo → generate config → systemd unit → start
|
||||
├── handlers/main.yml
|
||||
└── templates/
|
||||
@@ -60,8 +60,8 @@ Ansible automation that **hardens a Fedora 44 server**, installs **rootless Podm
|
||||
| **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 |
|
||||
| **SELinux** | keeps mode enforcing; installs `container-selinux` (container policy) + `policycoreutils-python-utils` / `setools-console` (`semanage`, `audit2why`) |
|
||||
| **Podman** | engine + `podman-compose` + `podman-docker`; runs as root (no unprivileged service user, no linger) |
|
||||
| **SELinux** | keeps mode enforcing; installs `container-selinux` (container policy) + `policycoreutils-python-utils` / `setools-console` (`semanage`, `audit2why`); registers the deployment tree as `container_file_t` so rootful containers can use the bind mounts |
|
||||
| **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` |
|
||||
|
||||
---
|
||||
@@ -86,8 +86,6 @@ Ansible automation that **hardens a Fedora 44 server**, installs **rootless Podm
|
||||
| `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 |
|
||||
| `selinux_mode` | `enforcing` | SELinux mode the role enforces (config + setenforce) |
|
||||
| `fail2ban_port` / `fail2ban_maxretry` / `fail2ban_bantime` | `22` / `3` / `3600` | sshd jail tuning |
|
||||
| `firewall_services` / `firewall_ports` | ssh,http,https / 7881, UDP range | firewalld allow rules |
|
||||
@@ -114,7 +112,7 @@ ansible-playbook -i inventory.ini site.yml -k -e "stoat_domain=chat.example.org
|
||||
|
||||
## Day-to-day operations
|
||||
|
||||
All commands run on the server, mostly as the `stoat` user (`sudo -u stoat` when needed):
|
||||
All commands run on the server as root:
|
||||
|
||||
```bash
|
||||
# Stack status
|
||||
@@ -122,14 +120,14 @@ systemctl status podman-compose-stoat
|
||||
podman ps
|
||||
|
||||
# Logs (per service: api / events / web / livekit / database …)
|
||||
sudo -u stoat podman logs -f api
|
||||
podman logs -f api
|
||||
|
||||
# Restart the whole stack
|
||||
sudo systemctl restart podman-compose-stoat
|
||||
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
|
||||
# Update Stoat to new images (run in /opt/stoat/self-hosted)
|
||||
podman-compose pull
|
||||
podman-compose up -d
|
||||
|
||||
# Fail2Ban status
|
||||
sudo fail2ban-client status sshd
|
||||
@@ -142,7 +140,7 @@ sudo firewall-cmd --list-all
|
||||
|
||||
## SELinux notes
|
||||
|
||||
Fedora 44 ships SELinux **enforcing**; the role pins that mode. The stack runs in **rootless** podman, where containers run in the user's own SELinux domain and can read/write the `stoat` user's files — bind mounts under `/opt/stoat/self-hosted/data` generally need no relabeling or `:Z` flags.
|
||||
Fedora 44 ships SELinux **enforcing**; the role pins that mode. Containers run as root under the `container_t` domain, so the role registers `/opt/stoat/self-hosted` as **`container_file_t`** (via `semanage fcontext` + `restorecon`) — that's what lets the rootful containers read/write the bind-mounted `data/` volumes and config files without per-volume `:Z` flags, and the registration is persistent (survives `restorecon`).
|
||||
|
||||
If a service misbehaves, check for denials before anything else:
|
||||
|
||||
@@ -159,9 +157,10 @@ Common knobs if a denial shows up (only set what the denial actually requires):
|
||||
sudo setsebool -P container_connect_any 1
|
||||
# container needs to manage cgroups (systemd inside a container)
|
||||
sudo setsebool -P container_manage_cgroup 1
|
||||
# give a specific bind-mounted volume its container context (last resort,
|
||||
# only if audit2why points at file labeling):
|
||||
sudo chcon -R -t container_file_t /opt/stoat/self-hosted/data
|
||||
# re-apply file contexts if you add bind mounts outside the
|
||||
# deployment tree (the role only labels /opt/stoat/self-hosted):
|
||||
sudo semanage fcontext -a -t container_file_t '/opt/stoat/moredata(/.*)?'
|
||||
sudo restorecon -RF /opt/stoat/moredata
|
||||
```
|
||||
|
||||
---
|
||||
@@ -169,8 +168,8 @@ sudo chcon -R -t container_file_t /opt/stoat/self-hosted/data
|
||||
## 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, fail2ban, firewalld default-deny, security-only auto-updates. SSH configuration is left exactly as you set it. (Aligned with the upstream guide's recommendations.)
|
||||
- **Rootless bind mounts**: data persists in `/opt/stoat/self-hosted/data/` (mongodb, rabbitmq, minio, caddy).
|
||||
- **Podman, not Docker**: the upstream stack is docker-compose based; this deployment runs it with `podman-compose` as root. 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` (after backing up secrets).
|
||||
- **Security posture**: fail2ban, firewalld default-deny, security-only auto-updates, SELinux enforcing with `container_file_t` labels. SSH configuration is left exactly as you set it. (Aligned with the upstream guide's recommendations.)
|
||||
- **Data persistence**: everything persists in `/opt/stoat/self-hosted/data/` (mongodb, rabbitmq, minio, caddy) — keep it out of the git clone/reset.
|
||||
- System hardening + Stoat are one role run; to re-apply only config changes, re-running `site.yml` is idempotent.
|
||||
@@ -33,20 +33,13 @@ stoat_reverse_proxy: false
|
||||
stoat_video: true
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Podman (rootless)
|
||||
# Podman (runs as root)
|
||||
# ------------------------------------------------------------
|
||||
# Unprivileged system user that runs the rootless podman stack
|
||||
stoat_service_user: chatmanone
|
||||
|
||||
podman_packages:
|
||||
- podman
|
||||
- podman-compose # compose file runner (supports service_healthy conditions)
|
||||
- podman-docker # `docker` CLI compatibility, backed by podman
|
||||
|
||||
# Rootless containers cannot bind ports < 1024 by default.
|
||||
# Caddy needs 80/443, so lower the unprivileged port floor.
|
||||
podman_unprivileged_port_start: 80
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Hardening - Fail2Ban
|
||||
# ------------------------------------------------------------
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# stoat role - task order matters:
|
||||
# system -> update + baseline packages (git needed by stoat tasks)
|
||||
# harden -> firewall, ssh, fail2ban, automatic updates, logrotate
|
||||
# podman -> rootless podman + service user
|
||||
# podman -> podman + compose + SELinux container policy
|
||||
# stoat -> clone repo, generate config, systemd-managed compose stack
|
||||
- name: System update and base packages
|
||||
ansible.builtin.import_tasks: system.yml
|
||||
@@ -10,7 +10,7 @@
|
||||
- name: Harden the host
|
||||
ansible.builtin.import_tasks: harden.yml
|
||||
|
||||
- name: Install rootless podman
|
||||
- name: Install podman
|
||||
ansible.builtin.import_tasks: podman.yml
|
||||
|
||||
- name: Deploy Stoat
|
||||
|
||||
@@ -1,37 +1,12 @@
|
||||
---
|
||||
# ------------------------------------------------------------
|
||||
# Podman - rootless engine, compose support, unprivileged user
|
||||
# Podman - engine, compose support (runs as root, no service user)
|
||||
# ------------------------------------------------------------
|
||||
- name: Install podman stack
|
||||
ansible.builtin.dnf:
|
||||
name: "{{ podman_packages }}"
|
||||
state: present
|
||||
|
||||
# Rootless podman cannot bind host ports < 1024 without this.
|
||||
# Caddy (Stoat's TLS reverse proxy) needs 80/443.
|
||||
- name: Allow rootless containers to bind privileged ports
|
||||
ansible.builtin.sysctl:
|
||||
name: net.ipv4.ip_unprivileged_port_start
|
||||
value: "{{ podman_unprivileged_port_start }}"
|
||||
sysctl_file: /etc/sysctl.d/99-podman.conf
|
||||
state: present
|
||||
reload: true
|
||||
|
||||
- name: Create dedicated service user for rootless podman
|
||||
ansible.builtin.user:
|
||||
name: "{{ stoat_service_user }}"
|
||||
system: true
|
||||
shell: /bin/bash
|
||||
create_home: true
|
||||
comment: "Stoat rootless podman service user"
|
||||
register: stoat_system_user
|
||||
|
||||
# Keeps /run/user/<uid> alive and lets rootless containers start
|
||||
# on boot without an interactive login.
|
||||
- name: Enable lingering for the service user
|
||||
ansible.builtin.command: "loginctl enable-linger {{ stoat_service_user }}"
|
||||
changed_when: false
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# SELinux - keep enforcing, ensure the container policy and
|
||||
# diagnostic tools are present for resolving any denials.
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
---
|
||||
# ------------------------------------------------------------
|
||||
# Stoat deployment - rootless podman-compose, managed by systemd
|
||||
# Stoat deployment - podman-compose (as root), managed by systemd
|
||||
# ------------------------------------------------------------
|
||||
# These tasks run as root (play-level become), NOT as the stoat
|
||||
# user: becoming an unprivileged user trips an ACL bug on Linux
|
||||
# ("chmod A+user:..."). Ownership is handed to the service user
|
||||
# afterwards via chown, and the systemd unit runs podman-compose
|
||||
# as the stoat user via User=.
|
||||
# The whole stack runs as root (play-level become): the repo and
|
||||
# config are generated as root and the systemd unit runs
|
||||
# podman-compose without dropping privileges.
|
||||
# The deployment dir may already be owned by another user from
|
||||
# earlier runs; register it so git-as-root accepts it.
|
||||
- name: Allow git to operate on the deployment directory
|
||||
ansible.builtin.command:
|
||||
cmd: "git config --global --add safe.directory {{ stoat_install_dir }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Clone Stoat self-hosted repository
|
||||
ansible.builtin.git:
|
||||
repo: "{{ stoat_repo }}"
|
||||
@@ -20,13 +25,11 @@
|
||||
mode: "0755"
|
||||
|
||||
# The compose file bind-mounts these directories; create them up
|
||||
# front so rootless podman does not fail on missing sources.
|
||||
# front so podman-compose does not fail on missing sources.
|
||||
- name: Create data directories referenced by compose bind mounts
|
||||
ansible.builtin.file:
|
||||
path: "{{ stoat_install_dir }}/data/{{ item }}"
|
||||
state: directory
|
||||
owner: "{{ stoat_service_user }}"
|
||||
group: "{{ stoat_service_user }}"
|
||||
mode: "0755"
|
||||
loop:
|
||||
- db
|
||||
@@ -48,16 +51,27 @@
|
||||
chdir: "{{ stoat_install_dir }}"
|
||||
creates: "{{ stoat_install_dir }}/Revolt.toml"
|
||||
|
||||
# The generator and clone run as root; hand the whole deployment
|
||||
# tree to the service user so rootless podman can read/write it.
|
||||
- name: Hand ownership of the deployment tree to the service user
|
||||
# Rootful podman containers run as container_t; on enforcing
|
||||
# SELinux they cannot read/write bind mounts labeled usr_t, so
|
||||
# register the deployment tree as container_file_t (persistent).
|
||||
- name: Ensure the deployment tree is root-owned
|
||||
ansible.builtin.file:
|
||||
path: "{{ stoat_install_dir }}"
|
||||
state: directory
|
||||
owner: "{{ stoat_service_user }}"
|
||||
group: "{{ stoat_service_user }}"
|
||||
owner: root
|
||||
group: root
|
||||
recurse: true
|
||||
|
||||
- name: Register the deployment directory as a container file context
|
||||
community.general.sefcontext:
|
||||
target: "{{ stoat_install_dir }}(/.*)?"
|
||||
setype: container_file_t
|
||||
state: present
|
||||
|
||||
- name: Relabel the deployment tree with restorecon
|
||||
ansible.builtin.command: "restorecon -RF {{ stoat_install_dir }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Deploy podman-compose systemd unit
|
||||
ansible.builtin.template:
|
||||
src: podman-compose-stoat.service.j2
|
||||
|
||||
@@ -6,15 +6,10 @@ Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
# oneshot + RemainAfterExit: the unit tracks compose state while
|
||||
# the containers themselves run detached under rootless podman.
|
||||
# the containers themselves run detached under the root podman.
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
User={{ stoat_service_user }}
|
||||
Group={{ stoat_service_user }}
|
||||
WorkingDirectory={{ stoat_install_dir }}
|
||||
# Rootless podman needs the per-user runtime directory; %U expands
|
||||
# to the UID of `User=`. Lingering (enable-linger) keeps it alive.
|
||||
Environment=XDG_RUNTIME_DIR=/run/user/%U
|
||||
ExecStart=/usr/bin/podman-compose up -d --remove-orphans
|
||||
ExecStop=/usr/bin/podman-compose down
|
||||
# First start pulls ~2 GB of images; be generous.
|
||||
|
||||
Reference in New Issue
Block a user