Files
monotrope/infra/ansible/playbook.yml
Louis Simoneau a9e063867a Harden SSH, add fail2ban, remove redundant setup.sh
Disable password auth, restrict root login, limit auth retries.
Add fail2ban with SSH jail (3 retries, 1hr ban). Remove setup.sh
which predated Ansible and was no longer used.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 08:29:15 +10:00

436 lines
12 KiB
YAML

---
- name: Provision monotrope.au server
hosts: all
become: true
vars:
site_dir: /var/www/monotrope
deploy_user: deploy
deploy_pubkey: "{{ lookup('file', lookup('env', 'HOME') + '/.ssh/id_ed25519.pub') }}"
miniflux_db_password: "{{ lookup('env', 'MINIFLUX_DB_PASSWORD') }}"
miniflux_admin_user: "{{ lookup('env', 'MINIFLUX_ADMIN_USER') | default('admin') }}"
miniflux_admin_password: "{{ lookup('env', 'MINIFLUX_ADMIN_PASSWORD') }}"
gitea_db_password: "{{ lookup('env', 'GITEA_DB_PASSWORD') }}"
goatcounter_version: "2.7.0"
goatcounter_admin_email: "{{ lookup('env', 'GOATCOUNTER_ADMIN_EMAIL') }}"
goatcounter_admin_password: "{{ lookup('env', 'GOATCOUNTER_ADMIN_PASSWORD') }}"
tasks:
# ── System ──────────────────────────────────────────────────────────────
- name: Update apt cache and upgrade packages
apt:
update_cache: true
upgrade: dist
cache_valid_time: 3600
- name: Install common dependencies
apt:
name:
- debian-keyring
- debian-archive-keyring
- apt-transport-https
- curl
- ufw
state: present
# ── Caddy ───────────────────────────────────────────────────────────────
- name: Add Caddy GPG key
shell: |
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
args:
creates: /usr/share/keyrings/caddy-stable-archive-keyring.gpg
- name: Add Caddy apt repository
shell: |
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| tee /etc/apt/sources.list.d/caddy-stable.list
args:
creates: /etc/apt/sources.list.d/caddy-stable.list
- name: Install Caddy
apt:
name: caddy
state: present
update_cache: true
- name: Install Caddyfile
copy:
src: ../Caddyfile
dest: /etc/caddy/Caddyfile
owner: root
group: caddy
mode: '0640'
notify: Restart Caddy
tags:
- miniflux
- gitea
- goatcounter
- name: Enable and start Caddy
systemd:
name: caddy
enabled: true
state: started
# ── Site directory ───────────────────────────────────────────────────────
- name: Create www system user
user:
name: www
system: true
create_home: false
shell: /usr/sbin/nologin
state: present
# ── SSH hardening ───────────────────────────────────────────────────────
- name: Harden SSH configuration
copy:
dest: /etc/ssh/sshd_config.d/hardening.conf
owner: root
group: root
mode: '0644'
content: |
PasswordAuthentication no
PermitRootLogin prohibit-password
MaxAuthTries 3
notify: Restart sshd
# ── Fail2ban ────────────────────────────────────────────────────────────
- name: Install fail2ban
apt:
name: fail2ban
state: present
- name: Configure fail2ban SSH jail
copy:
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: '0644'
content: |
[sshd]
enabled = true
port = ssh
maxretry = 3
bantime = 3600
findtime = 600
notify: Restart fail2ban
- name: Enable and start fail2ban
systemd:
name: fail2ban
enabled: true
state: started
# ── UFW ─────────────────────────────────────────────────────────────────
- name: Set UFW default incoming policy to deny
ufw:
default: deny
direction: incoming
- name: Set UFW default outgoing policy to allow
ufw:
default: allow
direction: outgoing
- name: Allow SSH
ufw:
rule: allow
name: OpenSSH
- name: Allow HTTP
ufw:
rule: allow
port: '80'
proto: tcp
- name: Allow HTTPS
ufw:
rule: allow
port: '443'
proto: tcp
- name: Allow Gitea SSH
ufw:
rule: allow
port: '2222'
proto: tcp
- name: Enable UFW
ufw:
state: enabled
# ── Docker ──────────────────────────────────────────────────────────────
- name: Create Docker keyring directory
file:
path: /etc/apt/keyrings
state: directory
mode: '0755'
- name: Add Docker GPG key
shell: |
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| gpg --dearmor -o /etc/apt/keyrings/docker.gpg
chmod a+r /etc/apt/keyrings/docker.gpg
args:
creates: /etc/apt/keyrings/docker.gpg
- name: Add Docker apt repository
shell: |
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| tee /etc/apt/sources.list.d/docker.list
args:
creates: /etc/apt/sources.list.d/docker.list
- name: Install Docker
apt:
name:
- docker-ce
- docker-ce-cli
- containerd.io
- docker-buildx-plugin
- docker-compose-plugin
state: present
update_cache: true
- name: Enable Docker
systemd:
name: docker
enabled: true
state: started
# ── Miniflux ────────────────────────────────────────────────────────────
- name: Create Miniflux directory
file:
path: /opt/miniflux
state: directory
owner: root
group: root
mode: '0750'
tags: miniflux
- name: Copy Miniflux docker-compose.yml
copy:
src: ../miniflux/docker-compose.yml
dest: /opt/miniflux/docker-compose.yml
owner: root
group: root
mode: '0640'
tags: miniflux
- name: Write Miniflux .env
copy:
dest: /opt/miniflux/.env
owner: root
group: root
mode: '0600'
content: |
MINIFLUX_DB_PASSWORD={{ miniflux_db_password }}
MINIFLUX_ADMIN_USER={{ miniflux_admin_user }}
MINIFLUX_ADMIN_PASSWORD={{ miniflux_admin_password }}
no_log: true
tags: miniflux
- name: Pull and start Miniflux
command: docker compose up -d --pull always
args:
chdir: /opt/miniflux
tags: miniflux
# ── Gitea ───────────────────────────────────────────────────────────────
- name: Create Gitea directory
file:
path: /opt/gitea
state: directory
owner: root
group: root
mode: '0750'
tags: gitea
- name: Copy Gitea docker-compose.yml
copy:
src: ../gitea/docker-compose.yml
dest: /opt/gitea/docker-compose.yml
owner: root
group: root
mode: '0640'
tags: gitea
- name: Write Gitea .env
copy:
dest: /opt/gitea/.env
owner: root
group: root
mode: '0600'
content: |
GITEA_DB_PASSWORD={{ gitea_db_password }}
no_log: true
tags: gitea
- name: Pull and start Gitea
command: docker compose up -d --pull always
args:
chdir: /opt/gitea
tags: gitea
# ── GoatCounter ─────────────────────────────────────────────────────────
- name: Create goatcounter system user
user:
name: goatcounter
system: true
create_home: false
shell: /usr/sbin/nologin
state: present
tags: goatcounter
- name: Create GoatCounter data directory
file:
path: /var/lib/goatcounter
state: directory
owner: goatcounter
group: goatcounter
mode: '0750'
tags: goatcounter
- name: Download GoatCounter binary
get_url:
url: "https://github.com/arp242/goatcounter/releases/download/v{{ goatcounter_version }}/goatcounter-v{{ goatcounter_version }}-linux-amd64.gz"
dest: /tmp/goatcounter.gz
mode: '0644'
tags: goatcounter
- name: Decompress GoatCounter binary
shell: gunzip -f /tmp/goatcounter.gz && mv /tmp/goatcounter /usr/local/bin/goatcounter && chmod 0755 /usr/local/bin/goatcounter
args:
creates: /usr/local/bin/goatcounter
tags: goatcounter
- name: Check if GoatCounter admin user has been created
stat:
path: /var/lib/goatcounter/.admin_created
register: goatcounter_admin_marker
tags: goatcounter
- name: Create GoatCounter admin user
command: >
goatcounter db create site
-db sqlite+/var/lib/goatcounter/goatcounter.sqlite3
-createdb
-vhost stats.monotrope.au
-user.email {{ goatcounter_admin_email }}
-user.password {{ goatcounter_admin_password }}
become_user: goatcounter
when: not goatcounter_admin_marker.stat.exists
no_log: true
tags: goatcounter
- name: Mark GoatCounter admin user as created
file:
path: /var/lib/goatcounter/.admin_created
state: touch
owner: goatcounter
group: goatcounter
mode: '0600'
when: not goatcounter_admin_marker.stat.exists
tags: goatcounter
- name: Install GoatCounter systemd service
copy:
dest: /etc/systemd/system/goatcounter.service
owner: root
group: root
mode: '0644'
content: |
[Unit]
Description=GoatCounter analytics
After=network.target
[Service]
User=goatcounter
Group=goatcounter
ExecStart=/usr/local/bin/goatcounter serve \
-listen localhost:8081 \
-db sqlite+/var/lib/goatcounter/goatcounter.sqlite3 \
-tls none \
-domain stats.monotrope.au
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
notify: Restart GoatCounter
tags: goatcounter
- name: Enable and start GoatCounter
systemd:
name: goatcounter
enabled: true
state: started
daemon_reload: true
tags: goatcounter
# ── Deploy user ──────────────────────────────────────────────────────────
- name: Create deploy user
user:
name: "{{ deploy_user }}"
create_home: true
shell: /bin/bash
state: present
- name: Set up deploy user SSH directory
file:
path: "/home/{{ deploy_user }}/.ssh"
state: directory
owner: "{{ deploy_user }}"
group: "{{ deploy_user }}"
mode: '0700'
- name: Install deploy user SSH public key
ansible.posix.authorized_key:
user: "{{ deploy_user }}"
key: "{{ deploy_pubkey }}"
state: present
- name: Create site directory
file:
path: "{{ site_dir }}"
state: directory
owner: "{{ deploy_user }}"
group: www
mode: '0775'
handlers:
- name: Restart Caddy
systemd:
name: caddy
state: restarted
- name: Restart GoatCounter
systemd:
name: goatcounter
state: restarted
- name: Restart sshd
systemd:
name: ssh
state: restarted
- name: Restart fail2ban
systemd:
name: fail2ban
state: restarted