initial commit
This commit is contained in:
88
group_vars/all/vars.yml
Normal file
88
group_vars/all/vars.yml
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# ============================================================
|
||||||
|
# Stoat.chat Fedora 44 - variables
|
||||||
|
# ============================================================
|
||||||
|
# Single source of truth for every tunable used by the stoat role.
|
||||||
|
# Override on the command line like:
|
||||||
|
# ansible-playbook -i inventory.ini site.yml -e stoat_domain=chat.example.com
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# >>> REQUIRED <<<
|
||||||
|
# The domain that has an A/AAAA record pointing to this server.
|
||||||
|
# Let's Encrypt certificates (via the built-in Caddy) are issued
|
||||||
|
# for this name, and every URL Stoat advertises uses it.
|
||||||
|
# Example: stoat.example.com (NOT the public stoat.chat domain)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
stoat_domain: chat.example.com
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Stoat application
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
stoat_repo: https://github.com/stoatchat/self-hosted
|
||||||
|
stoat_install_dir: /opt/stoat/self-hosted
|
||||||
|
stoat_version: main
|
||||||
|
|
||||||
|
# Place Stoat behind an existing reverse proxy?
|
||||||
|
# false (recommended): the built-in Caddy terminates TLS on 80/443
|
||||||
|
# true: Caddy is moved to localhost:8880 and you must
|
||||||
|
# provide your own front proxy + certificate
|
||||||
|
stoat_reverse_proxy: false
|
||||||
|
|
||||||
|
# Enable camera / screen sharing via LiveKit?
|
||||||
|
# Requires firewall ports 7881/tcp and 50000-50100/udp (see below)
|
||||||
|
stoat_video: true
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Podman (rootless)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Unprivileged system user that runs the rootless podman stack
|
||||||
|
stoat_service_user: stoat
|
||||||
|
|
||||||
|
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
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
fail2ban_port: "22"
|
||||||
|
fail2ban_maxretry: 3 # failed attempts before a ban
|
||||||
|
fail2ban_bantime: 3600 # ban length in seconds (1 hour)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Hardening - SSH
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
sshd_permit_root_login: no
|
||||||
|
sshd_password_authentication: no
|
||||||
|
|
||||||
|
# Enable ONLY if a TOTP / WebUI-2FA PAM module is already
|
||||||
|
# configured in /etc/pam.d/sshd - otherwise you can lock yourself
|
||||||
|
# out. Leave false unless you know what you are doing.
|
||||||
|
sshd_mfa_required: false
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Hardening - firewall (firewalld, default deny)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Services: ssh for administration, http/https for Caddy TLS
|
||||||
|
firewall_services:
|
||||||
|
- ssh
|
||||||
|
- http
|
||||||
|
- https
|
||||||
|
|
||||||
|
# Ports: LiveKit voice/video (signaling tcp + media udp range)
|
||||||
|
firewall_ports:
|
||||||
|
- "7881/tcp"
|
||||||
|
- "50000-50100/udp"
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Base packages
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
base_packages:
|
||||||
|
- git # clone the stoat repo
|
||||||
|
- curl
|
||||||
|
- openssl # generate_config.sh secret generation
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
---
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# Ansible playbook: Harden a Fedora 44 server & install Fail2Ban
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# Requirements:
|
|
||||||
# - Ansible >= 2.14
|
|
||||||
# - A reachable Fedora 44 host with SSH access (password or key)
|
|
||||||
# - `sudo` rights for the control user (via become: true)
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
|
|
||||||
- name: Harden Fedora 44 & install Fail2Ban
|
|
||||||
hosts: all
|
|
||||||
become: true # elevates to root for package/service ops
|
|
||||||
vars:
|
|
||||||
# ---- Tunables -------------------------------------------------
|
|
||||||
# Change these to match your environment
|
|
||||||
fail2ban_port: "22" # default SSH jail; can be "http","https",etc.
|
|
||||||
fail2ban_maxretry: 3 # how many attempts before ban
|
|
||||||
fail2ban_bantime: 3600 # seconds (1 h)
|
|
||||||
# -----------------------------------------------------------------
|
|
||||||
# Ensure we are on Fedora 44 (you can relax this if you wish)
|
|
||||||
fedora_release: "44"
|
|
||||||
tasks:
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# Verify OS version (informational, can be removed)
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
- name: Gather os fact
|
|
||||||
ansible.builtin.debug:
|
|
||||||
var: ansible_distribution_version
|
|
||||||
|
|
||||||
- name: Ensure we are running Fedora {{ fedora_release }}
|
|
||||||
ansible.builtin.assert:
|
|
||||||
that:
|
|
||||||
- ansible_distribution == "Fedora"
|
|
||||||
- ansible_distribution_version == fedora_release
|
|
||||||
fail_msg: "This playbook targets Fedora {{ fedora_release }} only."
|
|
||||||
success_msg: "Running on the correct Fedora version."
|
|
||||||
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# Update / upgrade all packages
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
- name: Update all packages to latest versions
|
|
||||||
ansible.builtin.dnf:
|
|
||||||
name: "*"
|
|
||||||
state: latest
|
|
||||||
update_cache: true
|
|
||||||
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# Install essential hardening packages
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
- name: Install fail2ban, firewalld, and sudo
|
|
||||||
ansible.builtin.dnf:
|
|
||||||
name:
|
|
||||||
- fail2ban
|
|
||||||
- firewalld
|
|
||||||
- sudo
|
|
||||||
state: present
|
|
||||||
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# Start & enable firewalld (default-deny all, allow SSH)
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
- name: Start and enable firewalld
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: firewalld
|
|
||||||
state: started
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
- name: Allow SSH service through firewalld
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: firewall-cmd --permanent --add-service=ssh
|
|
||||||
changed_when: false # idempotent check handled by firewall_cmd
|
|
||||||
|
|
||||||
- name: Reload firewalld to apply rules
|
|
||||||
ansible.builtin.command:
|
|
||||||
cmd: firewall-cmd --reload
|
|
||||||
changed_when: false
|
|
||||||
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# Harden SSH (disable root login, enforce key‑only auth)
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
- name: Ensure SSH config directory exists
|
|
||||||
ansible.builtin.file:
|
|
||||||
path: /etc/ssh/sshd_config.d
|
|
||||||
state: directory
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Disable root login via SSH
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: /etc/ssh/sshd_config.d/00-disable-root.conf
|
|
||||||
create: true
|
|
||||||
mode: "0644"
|
|
||||||
line: "PermitRootLogin no"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Enforce password-auth disabled (key-only)
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: /etc/ssh/sshd_config.d/00-key-only.conf
|
|
||||||
create: true
|
|
||||||
mode: "0644"
|
|
||||||
line: "PasswordAuthentication no"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Require MFA (optional) — add if you use Duo, etc.
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: /etc/ssh/sshd_config.d/00-mfa.conf
|
|
||||||
create: true
|
|
||||||
mode: "0644"
|
|
||||||
line: "AuthenticationMethods publickey,keyboard-interactive:pam"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Restart sshd to apply changes
|
|
||||||
ansible.builtin.service:
|
|
||||||
name: sshd
|
|
||||||
state: restarted
|
|
||||||
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# Configure Fail2Ban
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
- name: template fail2ban jail.local
|
|
||||||
ansible.builtin.template:
|
|
||||||
src: jail.local.j2
|
|
||||||
dest: /etc/fail2ban/jail.local
|
|
||||||
mode: "0644"
|
|
||||||
notify: Restart fail2ban
|
|
||||||
|
|
||||||
- name: Ensure fail2ban is started & enabled
|
|
||||||
ansible.builtin.service:
|
|
||||||
name: fail2ban
|
|
||||||
state: started
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# (Optional) Install & configure unattended‑security updates
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
- name: Install dnf-automatic (security-only)
|
|
||||||
ansible.builtin.dnf:
|
|
||||||
name: dnf-automatic
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Enable security-only automatic updates
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: /etc/dnf/automatic.conf
|
|
||||||
regexp: "^update_type"
|
|
||||||
line: "update_type = security"
|
|
||||||
state: present
|
|
||||||
|
|
||||||
- name: Enable and start dnf-automatic.timer
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: "@dnf-automatic.timer"
|
|
||||||
state: started
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# Log rotation for security‑relevant logs
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
- name: Ensure rsyslog is running
|
|
||||||
ansible.builtin.service:
|
|
||||||
name: rsyslog
|
|
||||||
state: started
|
|
||||||
enabled: true
|
|
||||||
|
|
||||||
- name: Add logrotate rule for fail2ban logs
|
|
||||||
ansible.builtin.lineinfile:
|
|
||||||
path: /etc/logrotate.d/fail2ban
|
|
||||||
create: true
|
|
||||||
mode: "0644"
|
|
||||||
content: |
|
|
||||||
/var/log/fail2ban/*.log {
|
|
||||||
daily
|
|
||||||
rotate 7
|
|
||||||
compress
|
|
||||||
missingok
|
|
||||||
notifempty
|
|
||||||
postrotate
|
|
||||||
/usr/bin/fail2ban-client reload >/dev/null 2>&1 || true
|
|
||||||
endscript
|
|
||||||
}
|
|
||||||
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
# 9 Handlers
|
|
||||||
# --------------------------------------------------------------
|
|
||||||
handlers:
|
|
||||||
- name: Restart fail2ban
|
|
||||||
ansible.builtin.service:
|
|
||||||
name: fail2ban
|
|
||||||
state: restarted
|
|
||||||
10
roles/stoat/handlers/main.yml
Normal file
10
roles/stoat/handlers/main.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
# Handlers for the stoat role
|
||||||
|
- name: Reload firewalld
|
||||||
|
ansible.builtin.command: firewall-cmd --reload
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Restart fail2ban
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: fail2ban
|
||||||
|
state: restarted
|
||||||
130
roles/stoat/tasks/harden.yml
Normal file
130
roles/stoat/tasks/harden.yml
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
---
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Firewall (firewalld) - default deny, allow only what is needed
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
- name: Start and enable firewalld
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: firewalld
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
- name: Check which firewall services are already allowed
|
||||||
|
ansible.builtin.command: "firewall-cmd --query-service={{ item }}"
|
||||||
|
loop: "{{ firewall_services }}"
|
||||||
|
register: fw_service_check
|
||||||
|
changed_when: false
|
||||||
|
failed_when: fw_service_check.rc not in [0, 1]
|
||||||
|
|
||||||
|
- name: Allow firewall services (ssh, http, https)
|
||||||
|
ansible.builtin.command: "firewall-cmd --permanent --add-service={{ item.item }}"
|
||||||
|
loop: "{{ fw_service_check.results }}"
|
||||||
|
when: item.rc | int != 0
|
||||||
|
notify: Reload firewalld
|
||||||
|
|
||||||
|
- name: Check which firewall ports are already allowed
|
||||||
|
ansible.builtin.command: "firewall-cmd --query-port={{ item }}"
|
||||||
|
loop: "{{ firewall_ports }}"
|
||||||
|
register: fw_port_check
|
||||||
|
changed_when: false
|
||||||
|
failed_when: fw_port_check.rc not in [0, 1]
|
||||||
|
|
||||||
|
- name: Allow firewall ports (LiveKit voice/video)
|
||||||
|
ansible.builtin.command: "firewall-cmd --permanent --add-port={{ item.item }}"
|
||||||
|
loop: "{{ fw_port_check.results }}"
|
||||||
|
when: item.rc | int != 0
|
||||||
|
notify: Reload firewalld
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# SSH hardening (Drop-in config, validated before restart)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
- name: Deploy sshd hardening drop-in
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: sshd_hardening.conf.j2
|
||||||
|
dest: /etc/ssh/sshd_config.d/00-hardening.conf
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
register: sshd_conf
|
||||||
|
|
||||||
|
- name: Validate sshd configuration
|
||||||
|
ansible.builtin.command: sshd -t
|
||||||
|
changed_when: false
|
||||||
|
when: sshd_conf is changed
|
||||||
|
|
||||||
|
- name: Restart sshd
|
||||||
|
ansible.builtin.service:
|
||||||
|
name: sshd
|
||||||
|
state: restarted
|
||||||
|
when: sshd_conf is changed
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Fail2Ban - brute force protection
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
- name: Install fail2ban
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: fail2ban
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Deploy fail2ban jail.local
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: jail.local.j2
|
||||||
|
dest: /etc/fail2ban/jail.local
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
notify: Restart fail2ban
|
||||||
|
|
||||||
|
- name: Enable and start fail2ban
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: fail2ban
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Automatic security updates (dnf5-automatic)
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
- name: Install dnf5-plugin-automatic
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: dnf5-plugin-automatic
|
||||||
|
state: present
|
||||||
|
|
||||||
|
- name: Enable automatic installation of updates
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/dnf/automatic.conf
|
||||||
|
insertafter: "^\\[commands\\]"
|
||||||
|
regexp: "^#?\\s*apply_updates\\s*="
|
||||||
|
line: "apply_updates = yes"
|
||||||
|
|
||||||
|
- name: Restrict automatic updates to security errata
|
||||||
|
ansible.builtin.lineinfile:
|
||||||
|
path: /etc/dnf/automatic.conf
|
||||||
|
insertafter: "^\\[commands\\]"
|
||||||
|
regexp: "^#?\\s*upgrade_type\\s*="
|
||||||
|
line: "upgrade_type = security"
|
||||||
|
|
||||||
|
- name: Enable and start dnf5-automatic.timer
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: dnf5-automatic.timer
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Log rotation for fail2ban logs
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
- name: Add logrotate rule for fail2ban
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: /etc/logrotate.d/fail2ban
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
content: |
|
||||||
|
/var/log/fail2ban/*.log {
|
||||||
|
daily
|
||||||
|
rotate 7
|
||||||
|
compress
|
||||||
|
missingok
|
||||||
|
notifempty
|
||||||
|
postrotate
|
||||||
|
/usr/bin/fail2ban-client reload >/dev/null 2>&1 || true
|
||||||
|
endscript
|
||||||
|
}
|
||||||
17
roles/stoat/tasks/main.yml
Normal file
17
roles/stoat/tasks/main.yml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
# 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
|
||||||
|
# stoat -> clone repo, generate config, systemd-managed compose stack
|
||||||
|
- name: System update and base packages
|
||||||
|
ansible.builtin.import_tasks: system.yml
|
||||||
|
|
||||||
|
- name: Harden the host
|
||||||
|
ansible.builtin.import_tasks: harden.yml
|
||||||
|
|
||||||
|
- name: Install rootless podman
|
||||||
|
ansible.builtin.import_tasks: podman.yml
|
||||||
|
|
||||||
|
- name: Deploy Stoat
|
||||||
|
ansible.builtin.import_tasks: stoat.yml
|
||||||
33
roles/stoat/tasks/podman.yml
Normal file
33
roles/stoat/tasks/podman.yml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
---
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Podman - rootless engine, compose support, unprivileged 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
|
||||||
75
roles/stoat/tasks/stoat.yml
Normal file
75
roles/stoat/tasks/stoat.yml
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# Stoat deployment - rootless podman-compose, managed by systemd
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
- name: Clone Stoat self-hosted repository
|
||||||
|
ansible.builtin.git:
|
||||||
|
repo: "{{ stoat_repo }}"
|
||||||
|
dest: "{{ stoat_install_dir }}"
|
||||||
|
version: "{{ stoat_version }}"
|
||||||
|
force: true
|
||||||
|
become: true
|
||||||
|
become_user: "{{ stoat_service_user }}"
|
||||||
|
become_method: su
|
||||||
|
environment:
|
||||||
|
HOME: "/home/{{ stoat_service_user }}"
|
||||||
|
|
||||||
|
- name: Ensure generate_config.sh is executable
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ stoat_install_dir }}/generate_config.sh"
|
||||||
|
mode: "0755"
|
||||||
|
become: true
|
||||||
|
become_user: "{{ stoat_service_user }}"
|
||||||
|
become_method: su
|
||||||
|
|
||||||
|
# The compose file bind-mounts these directories; create them up
|
||||||
|
# front so rootless podman 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
|
||||||
|
- rabbit
|
||||||
|
- minio
|
||||||
|
- caddy-data
|
||||||
|
- caddy-config
|
||||||
|
|
||||||
|
# generate_config.sh is interactive; it reads two questions from
|
||||||
|
# stdin: reverse-proxy? [y/N] and camera/screen sharing? [Y/n].
|
||||||
|
# `creates` makes this idempotent - the script refuses to overwrite
|
||||||
|
# existing config without --overwrite, which is a destructive
|
||||||
|
# operation we never want Ansible to do by accident.
|
||||||
|
- name: Generate Stoat configuration and secrets
|
||||||
|
ansible.builtin.shell: >-
|
||||||
|
printf '{{ 'y' if stoat_reverse_proxy else 'n' }}\n{{ 'y' if stoat_video else 'n' }}\n'
|
||||||
|
| ./generate_config.sh {{ stoat_domain }}
|
||||||
|
args:
|
||||||
|
chdir: "{{ stoat_install_dir }}"
|
||||||
|
creates: "{{ stoat_install_dir }}/Revolt.toml"
|
||||||
|
become: true
|
||||||
|
become_user: "{{ stoat_service_user }}"
|
||||||
|
become_method: su
|
||||||
|
environment:
|
||||||
|
HOME: "/home/{{ stoat_service_user }}"
|
||||||
|
|
||||||
|
- name: Deploy podman-compose systemd unit
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: podman-compose-stoat.service.j2
|
||||||
|
dest: /etc/systemd/system/podman-compose-stoat.service
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
|
# First start pulls ~2 GB of images + initializes MongoDB, which can
|
||||||
|
# take several minutes. TimeoutStartSec covers this inside the unit.
|
||||||
|
- name: Enable and start the Stoat service (first pull may take minutes)
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: podman-compose-stoat
|
||||||
|
state: started
|
||||||
|
enabled: true
|
||||||
|
daemon_reload: true
|
||||||
|
timeout: 900
|
||||||
14
roles/stoat/tasks/system.yml
Normal file
14
roles/stoat/tasks/system.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
# System: bring the host up to date and install baseline tools
|
||||||
|
# ------------------------------------------------------------
|
||||||
|
- name: Update all packages to latest versions
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: "*"
|
||||||
|
state: latest
|
||||||
|
update_cache: true
|
||||||
|
|
||||||
|
- name: Install base packages
|
||||||
|
ansible.builtin.dnf:
|
||||||
|
name: "{{ base_packages }}"
|
||||||
|
state: present
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# --------------------------------------------------------------
|
# --------------------------------------------------------------
|
||||||
# Fail2Ban jail.local - default settings for Fedora 44
|
# Fail2Ban jail.local - default settings for Fedora 44
|
||||||
# Values like {{ fail2ban_port }} come from the playbook vars
|
# Values like {{ fail2ban_port }} come from the vars file
|
||||||
|
# (group_vars/all/vars.yml)
|
||||||
# --------------------------------------------------------------
|
# --------------------------------------------------------------
|
||||||
|
|
||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
@@ -25,13 +26,14 @@ maxretry = {{ fail2ban_maxretry }}
|
|||||||
bantime = {{ fail2ban_bantime }}
|
bantime = {{ fail2ban_bantime }}
|
||||||
|
|
||||||
# --------------------------------------------------------------
|
# --------------------------------------------------------------
|
||||||
# HTTP auth jail - uncomment if you run a web server
|
# HTTP auth jail - Caddy (Stoat's proxy) logs are inside the
|
||||||
|
# podman container; enable only if you mount /var/log for it.
|
||||||
# --------------------------------------------------------------
|
# --------------------------------------------------------------
|
||||||
#[http-auth]
|
#[http-auth]
|
||||||
#enabled = true
|
#enabled = true
|
||||||
#port = http,https
|
#port = http,https
|
||||||
#filter = http-auth
|
#filter = http-auth
|
||||||
#logpath = /var/log/nginx/access.log
|
#logpath = /var/log/caddy/access.log
|
||||||
#maxretry = {{ fail2ban_maxretry }}
|
#maxretry = {{ fail2ban_maxretry }}
|
||||||
#bantime = {{ fail2ban_bantime }}
|
#bantime = {{ fail2ban_bantime }}
|
||||||
|
|
||||||
24
roles/stoat/templates/podman-compose-stoat.service.j2
Normal file
24
roles/stoat/templates/podman-compose-stoat.service.j2
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Stoat - self-hosted chat (podman-compose)
|
||||||
|
Documentation=https://github.com/stoatchat/self-hosted
|
||||||
|
After=network-online.target
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
# oneshot + RemainAfterExit: the unit tracks compose state while
|
||||||
|
# the containers themselves run detached under rootless 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.
|
||||||
|
TimeoutStartSec=900
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
8
roles/stoat/templates/sshd_hardening.conf.j2
Normal file
8
roles/stoat/templates/sshd_hardening.conf.j2
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Managed by Ansible - stoat role (sshd hardening drop-in)
|
||||||
|
# Values come from group_vars/all/vars.yml
|
||||||
|
|
||||||
|
PermitRootLogin {{ sshd_permit_root_login }}
|
||||||
|
PasswordAuthentication {{ sshd_password_authentication }}
|
||||||
|
{% if sshd_mfa_required %}
|
||||||
|
AuthenticationMethods publickey,keyboard-interactive:pam
|
||||||
|
{% endif %}
|
||||||
14
site.yml
Normal file
14
site.yml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
# ============================================================
|
||||||
|
# Stoat.chat Fedora 44 server
|
||||||
|
# Harden the host, install rootless Podman, deploy Stoat.
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ansible-playbook -i inventory.ini site.yml
|
||||||
|
# ============================================================
|
||||||
|
- name: Harden Fedora 44 and deploy Stoat (podman)
|
||||||
|
hosts: servers
|
||||||
|
become: true
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- stoat
|
||||||
Reference in New Issue
Block a user