716 lines
21 KiB
YAML
716 lines
21 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') }}"
|
|
hermes_openrouter_api_key: "{{ lookup('env', 'HERMES_OPENROUTER_API_KEY') }}"
|
|
hermes_telegram_bot_token: "{{ lookup('env', 'HERMES_TELEGRAM_BOT_TOKEN') }}"
|
|
hermes_telegram_allowed_users: "{{ lookup('env', 'HERMES_TELEGRAM_ALLOWED_USERS') }}"
|
|
hermes_miniflux_api_key: "{{ lookup('env', 'HERMES_MINIFLUX_API_KEY') }}"
|
|
wg_client_pubkey: "{{ lookup('env', 'WG_CLIENT_PUBKEY') }}"
|
|
|
|
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
|
|
- unattended-upgrades
|
|
state: present
|
|
|
|
- name: Configure unattended-upgrades
|
|
copy:
|
|
dest: /etc/apt/apt.conf.d/50unattended-upgrades
|
|
owner: root
|
|
group: root
|
|
mode: '0644'
|
|
content: |
|
|
Unattended-Upgrade::Allowed-Origins {
|
|
"${distro_id}:${distro_codename}-security";
|
|
"${distro_id}ESMApps:${distro_codename}-apps-security";
|
|
"${distro_id}ESM:${distro_codename}-infra-security";
|
|
};
|
|
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
|
|
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
|
Unattended-Upgrade::Automatic-Reboot "false";
|
|
|
|
- name: Enable automatic updates
|
|
copy:
|
|
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
|
owner: root
|
|
group: root
|
|
mode: '0644'
|
|
content: |
|
|
APT::Periodic::Update-Package-Lists "1";
|
|
APT::Periodic::Unattended-Upgrade "1";
|
|
|
|
# ── 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
|
|
- calibre
|
|
|
|
- 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
|
|
|
|
# ── WireGuard ───────────────────────────────────────────────────────────
|
|
|
|
- name: Install WireGuard
|
|
apt:
|
|
name: wireguard
|
|
state: present
|
|
tags: wireguard
|
|
|
|
- name: Generate WireGuard server private key
|
|
shell: wg genkey > /etc/wireguard/server_privatekey && chmod 600 /etc/wireguard/server_privatekey
|
|
args:
|
|
creates: /etc/wireguard/server_privatekey
|
|
tags: wireguard
|
|
|
|
- name: Generate WireGuard server public key
|
|
shell: cat /etc/wireguard/server_privatekey | wg pubkey > /etc/wireguard/server_publickey
|
|
args:
|
|
creates: /etc/wireguard/server_publickey
|
|
tags: wireguard
|
|
|
|
- name: Read server private key
|
|
slurp:
|
|
src: /etc/wireguard/server_privatekey
|
|
register: wg_server_privkey
|
|
tags: wireguard
|
|
|
|
- name: Read server public key
|
|
slurp:
|
|
src: /etc/wireguard/server_publickey
|
|
register: wg_server_pubkey
|
|
tags: wireguard
|
|
|
|
- name: Write WireGuard config
|
|
copy:
|
|
dest: /etc/wireguard/wg0.conf
|
|
owner: root
|
|
group: root
|
|
mode: '0600'
|
|
content: |
|
|
[Interface]
|
|
PrivateKey = {{ wg_server_privkey.content | b64decode | trim }}
|
|
Address = 10.100.0.1/24
|
|
ListenPort = 51820
|
|
|
|
[Peer]
|
|
PublicKey = {{ wg_client_pubkey }}
|
|
AllowedIPs = 10.100.0.2/32
|
|
notify: Restart WireGuard
|
|
tags: wireguard
|
|
|
|
- name: Allow WireGuard UDP port
|
|
ufw:
|
|
rule: allow
|
|
port: '51820'
|
|
proto: udp
|
|
tags: wireguard
|
|
|
|
- name: Enable and start WireGuard
|
|
systemd:
|
|
name: wg-quick@wg0
|
|
enabled: true
|
|
state: started
|
|
tags: wireguard
|
|
|
|
- name: Display server public key
|
|
debug:
|
|
msg: "WireGuard server public key: {{ wg_server_pubkey.content | b64decode | trim }}"
|
|
tags: wireguard
|
|
|
|
# ── 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
|
|
|
|
- name: Create shared Docker network
|
|
command: docker network create monotrope
|
|
register: docker_net
|
|
changed_when: docker_net.rc == 0
|
|
failed_when: docker_net.rc != 0 and 'already exists' not in docker_net.stderr
|
|
tags:
|
|
- miniflux
|
|
- hermes
|
|
|
|
# ── 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
|
|
|
|
# ── Hermes Agent ────────────────────────────────────────────────────────
|
|
|
|
- name: Create Hermes directory
|
|
file:
|
|
path: /opt/hermes
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: '0750'
|
|
tags: hermes
|
|
|
|
- name: Copy Hermes docker-compose.yml
|
|
copy:
|
|
src: ../hermes/docker-compose.yml
|
|
dest: /opt/hermes/docker-compose.yml
|
|
owner: root
|
|
group: root
|
|
mode: '0640'
|
|
tags: hermes
|
|
|
|
- name: Stage Hermes config.yaml
|
|
copy:
|
|
src: ../hermes/config.yaml
|
|
dest: /opt/hermes/config.yaml
|
|
owner: root
|
|
group: root
|
|
mode: '0640'
|
|
tags: hermes
|
|
|
|
- name: Copy config.yaml into Hermes volume
|
|
command: docker cp /opt/hermes/config.yaml hermes:/opt/data/config.yaml
|
|
notify: Restart Hermes
|
|
tags: hermes
|
|
|
|
- name: Copy Hermes plugins
|
|
copy:
|
|
src: ../hermes/plugins/
|
|
dest: /opt/hermes/plugins/
|
|
owner: root
|
|
group: root
|
|
mode: '0640'
|
|
directory_mode: '0750'
|
|
notify: Restart Hermes
|
|
tags: hermes
|
|
|
|
- name: Write Miniflux plugin config
|
|
copy:
|
|
dest: /opt/hermes/plugins/miniflux/config.json
|
|
owner: root
|
|
group: root
|
|
mode: '0600'
|
|
content: |
|
|
{
|
|
"base_url": "http://miniflux:8080",
|
|
"api_key": "{{ hermes_miniflux_api_key }}"
|
|
}
|
|
no_log: true
|
|
notify: Restart Hermes
|
|
tags: hermes
|
|
|
|
- name: Write Hermes .env
|
|
copy:
|
|
dest: /opt/hermes/.env
|
|
owner: root
|
|
group: root
|
|
mode: '0600'
|
|
content: |
|
|
OPENROUTER_API_KEY={{ hermes_openrouter_api_key }}
|
|
TELEGRAM_BOT_TOKEN={{ hermes_telegram_bot_token }}
|
|
TELEGRAM_ALLOWED_USERS={{ hermes_telegram_allowed_users }}
|
|
no_log: true
|
|
tags: hermes
|
|
|
|
- name: Pull and start Hermes
|
|
command: docker compose up -d --pull always
|
|
args:
|
|
chdir: /opt/hermes
|
|
tags: hermes
|
|
|
|
|
|
# ── Calibre (kobodl + calibre-web) ────────────────────────────────────
|
|
|
|
- name: Create Calibre directory
|
|
file:
|
|
path: /opt/calibre
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: '0750'
|
|
tags: calibre
|
|
|
|
- name: Copy Calibre docker-compose.yml
|
|
copy:
|
|
src: ../calibre/docker-compose.yml
|
|
dest: /opt/calibre/docker-compose.yml
|
|
owner: root
|
|
group: root
|
|
mode: '0640'
|
|
tags: calibre
|
|
|
|
- name: Pull and start Calibre services
|
|
command: docker compose up -d --pull always
|
|
args:
|
|
chdir: /opt/calibre
|
|
tags: calibre
|
|
|
|
- name: Fix downloads volume ownership
|
|
command: >
|
|
docker compose exec -T kobodl
|
|
chown 1000:1000 /downloads
|
|
args:
|
|
chdir: /opt/calibre
|
|
tags: calibre
|
|
|
|
- name: Check if Calibre library exists
|
|
command: >
|
|
docker compose exec -T calibre-web
|
|
test -f /library/metadata.db
|
|
args:
|
|
chdir: /opt/calibre
|
|
register: calibre_db_check
|
|
changed_when: false
|
|
failed_when: false
|
|
tags: calibre
|
|
|
|
- name: Initialise Calibre library
|
|
command: >
|
|
docker compose exec -T --user abc calibre-web
|
|
calibredb add --empty --with-library /library/
|
|
args:
|
|
chdir: /opt/calibre
|
|
when: calibre_db_check.rc != 0
|
|
tags: calibre
|
|
|
|
- name: Install calibre-sync script
|
|
copy:
|
|
dest: /opt/calibre/sync.sh
|
|
owner: root
|
|
group: root
|
|
mode: '0755'
|
|
content: |
|
|
#!/bin/bash
|
|
set -euo pipefail
|
|
cd /opt/calibre
|
|
|
|
# Download all books from Kobo
|
|
docker compose exec -T kobodl kobodl --config /home/config/kobodl.json book get --get-all --output-dir /downloads
|
|
|
|
# Import any new EPUBs into Calibre library
|
|
# Files are kept in /downloads so kobodl can skip them next run
|
|
docker compose exec -T --user abc calibre-web sh -c '
|
|
for f in /downloads/*.epub; do
|
|
[ -f "$f" ] || continue
|
|
calibredb add "$f" --with-library /library/ || true
|
|
done
|
|
'
|
|
tags: calibre
|
|
|
|
# ── 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'
|
|
checksum: "sha256:98d221cb9c8ef2bf76d8daa9cca647839f8d8b0bb5bc7400ff9337c5da834511"
|
|
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
|
|
|
|
- name: Restart Hermes
|
|
command: docker compose restart
|
|
args:
|
|
chdir: /opt/hermes
|
|
|
|
- name: Restart WireGuard
|
|
systemd:
|
|
name: wg-quick@wg0
|
|
state: restarted
|