initial commit
This commit is contained in:
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
|
||||
46
roles/stoat/templates/jail.local.j2
Normal file
46
roles/stoat/templates/jail.local.j2
Normal file
@@ -0,0 +1,46 @@
|
||||
# --------------------------------------------------------------
|
||||
# Fail2Ban jail.local - default settings for Fedora 44
|
||||
# Values like {{ fail2ban_port }} come from the vars file
|
||||
# (group_vars/all/vars.yml)
|
||||
# --------------------------------------------------------------
|
||||
|
||||
[DEFAULT]
|
||||
# Ban IPs after maxretry failures for bantime seconds
|
||||
bantime = {{ fail2ban_bantime }}
|
||||
maxretry = {{ fail2ban_maxretry }}
|
||||
# Raise ban time incrementally for repeat offenders
|
||||
bantime.increment = true
|
||||
bantime.factor = 2
|
||||
bantime.maxtime = 24h
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# SSH jail - protects against brute-force login attempts
|
||||
# --------------------------------------------------------------
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = {{ fail2ban_port }}
|
||||
filter = sshd
|
||||
logpath = /var/log/secure
|
||||
backend = systemd
|
||||
maxretry = {{ fail2ban_maxretry }}
|
||||
bantime = {{ fail2ban_bantime }}
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# HTTP auth jail - Caddy (Stoat's proxy) logs are inside the
|
||||
# podman container; enable only if you mount /var/log for it.
|
||||
# --------------------------------------------------------------
|
||||
#[http-auth]
|
||||
#enabled = true
|
||||
#port = http,https
|
||||
#filter = http-auth
|
||||
#logpath = /var/log/caddy/access.log
|
||||
#maxretry = {{ fail2ban_maxretry }}
|
||||
#bantime = {{ fail2ban_bantime }}
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Add more jails below as needed, e.g.:
|
||||
# [vsftpd] - FTP brute force
|
||||
# [postfix] - mail smtp auth failures
|
||||
# [dovecot] - IMAP/POP3 brute force
|
||||
# Each jail needs: enabled = true, filter, logpath
|
||||
# --------------------------------------------------------------
|
||||
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 %}
|
||||
Reference in New Issue
Block a user