From 0ee978142aa7021dd56f7d7cbe89b4c0aaeebfc6 Mon Sep 17 00:00:00 2001 From: boxswapper Date: Sun, 13 Sep 2026 14:00:45 -0600 Subject: [PATCH] removed stoat service user --- .../ses_f63e23ebcffe7Q6sJKCl9W8sFt.json | 4 +- README.md | 39 +++++++++-------- group_vars/all/vars.yml | 9 +--- roles/stoat/tasks/main.yml | 4 +- roles/stoat/tasks/podman.yml | 27 +----------- roles/stoat/tasks/stoat.yml | 42 ++++++++++++------- .../templates/podman-compose-stoat.service.j2 | 7 +--- site.yml | 2 +- 8 files changed, 55 insertions(+), 79 deletions(-) diff --git a/.omo/run-continuation/ses_f63e23ebcffe7Q6sJKCl9W8sFt.json b/.omo/run-continuation/ses_f63e23ebcffe7Q6sJKCl9W8sFt.json index 5288853..88ce93d 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: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" } } } \ No newline at end of file diff --git a/README.md b/README.md index 4caa33b..a969602 100644 --- a/README.md +++ b/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 ` (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. \ No newline at end of file diff --git a/group_vars/all/vars.yml b/group_vars/all/vars.yml index f1af6aa..5c5078d 100644 --- a/group_vars/all/vars.yml +++ b/group_vars/all/vars.yml @@ -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 # ------------------------------------------------------------ diff --git a/roles/stoat/tasks/main.yml b/roles/stoat/tasks/main.yml index a8f18be..e7181cc 100644 --- a/roles/stoat/tasks/main.yml +++ b/roles/stoat/tasks/main.yml @@ -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 diff --git a/roles/stoat/tasks/podman.yml b/roles/stoat/tasks/podman.yml index 85108f6..888949b 100644 --- a/roles/stoat/tasks/podman.yml +++ b/roles/stoat/tasks/podman.yml @@ -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/ 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. diff --git a/roles/stoat/tasks/stoat.yml b/roles/stoat/tasks/stoat.yml index df490d4..6530b89 100644 --- a/roles/stoat/tasks/stoat.yml +++ b/roles/stoat/tasks/stoat.yml @@ -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 diff --git a/roles/stoat/templates/podman-compose-stoat.service.j2 b/roles/stoat/templates/podman-compose-stoat.service.j2 index 992fd4c..db4ec56 100644 --- a/roles/stoat/templates/podman-compose-stoat.service.j2 +++ b/roles/stoat/templates/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. diff --git a/site.yml b/site.yml index 587d3b8..a4053a1 100644 --- a/site.yml +++ b/site.yml @@ -1,7 +1,7 @@ --- # ============================================================ # Stoat.chat Fedora 44 server -# Harden the host, install rootless Podman, deploy Stoat. +# Harden the host, install Podman, deploy Stoat. # # Usage: # ansible-playbook -i inventory.ini site.yml