diff --git a/.gitignore b/.gitignore index bcfcaf4..541237f 100644 --- a/.gitignore +++ b/.gitignore @@ -4,5 +4,6 @@ registry/ .vault-password *.private *.secret +playbooks/wireguard-ingress/clients/ mail.txt containers/opentogethertube \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fde23ef..58517cd 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -170,6 +170,36 @@ "panel": "dedicated" } }, + { + "label": "Deploy: vaultwarden", + "type": "shell", + "command": "make deploy-vaultwarden", + "group": "build", + "presentation": { + "reveal": "always", + "panel": "dedicated" + } + }, + { + "label": "Deploy: hetzner", + "type": "shell", + "command": "make deploy-hetzner", + "group": "build", + "presentation": { + "reveal": "always", + "panel": "dedicated" + } + }, + { + "label": "Deploy: ingress-hardening", + "type": "shell", + "command": "make deploy-ingress-hardening", + "group": "build", + "presentation": { + "reveal": "always", + "panel": "dedicated" + } + }, { "label": "Build & Push: ott", "type": "shell", diff --git a/makefile b/makefile index 5fdda84..a3dbc14 100644 --- a/makefile +++ b/makefile @@ -8,4 +8,4 @@ push-%: build-% podman push $(REGISTRY)/bee/$*:latest deploy-%: - ansible-playbook -i $(INVENTORY) playbooks/$*/$*.yml --vault-password-file .vault-password \ No newline at end of file + ansible-playbook -i $(INVENTORY) playbooks/$*/$*.yml --vault-password-file .vault-password $(ARGS) \ No newline at end of file diff --git a/playbooks/backup/backup.yml b/playbooks/backup/backup.yml index ff17140..098948e 100644 --- a/playbooks/backup/backup.yml +++ b/playbooks/backup/backup.yml @@ -12,7 +12,9 @@ tasks: - name: Install rsync ansible.builtin.apt: - name: rsync + name: + - rsync + - sqlite3 state: present - name: Create backup directory @@ -33,6 +35,12 @@ dest: /opt/backup/backupWeekly.sh mode: '0700' + - name: Copy restore test + ansible.builtin.copy: + src: restoreTest.sh + dest: /opt/backup/restoreTest.sh + mode: '0700' + - name: Copy passphrase ansible.builtin.template: src: passphrase.txt.j2 @@ -61,3 +69,12 @@ hour: '5' weekday: '0' job: /opt/backup/backupWeekly.sh >> /var/log/beepi-backup.log 2>&1 + + - name: Schedule weekly restore test + ansible.builtin.cron: + name: beepi restore test + user: root + minute: '0' + hour: '6' + weekday: '0' + job: /opt/backup/restoreTest.sh >> /var/log/beepi-backup.log 2>&1 diff --git a/playbooks/backup/backupDaily.sh b/playbooks/backup/backupDaily.sh index 3d84bac..eeee364 100644 --- a/playbooks/backup/backupDaily.sh +++ b/playbooks/backup/backupDaily.sh @@ -28,6 +28,7 @@ restore() { systemctl --user --machine gitea@.host start gitea-db.service gitea-server.service || true systemctl --user --machine immich@.host start immich-db.service immich-redis.service immich-server.service || true + systemctl --user --machine vaultwarden@.host start vaultwarden.service || true rm -f /home/immich/systemd-immich-*.tar } @@ -69,4 +70,17 @@ systemctl --user --machine immich@.host start immich-db.service immich-redis.ser mkdir -p /opt/backup/beeserver/immich/ mv /home/immich/systemd-immich-*.tar /opt/backup/beeserver/immich/ -tar -czf - /opt/backup/beeserver | gpg --batch --passphrase-file /opt/backup/passphrase.txt -c -o /home/beeshare/raid/serverBackup/beeserver_daily_${now}.tar.gz.gpg +systemctl --user --machine vaultwarden@.host stop vaultwarden.service +rsync -aH /home/vaultwarden/data /opt/backup/beeserver/vaultwarden +systemctl --user --machine vaultwarden@.host start vaultwarden.service + +archive=/home/beeshare/raid/serverBackup/beeserver_daily_${now}.tar.gz.gpg + +tar -czf - /opt/backup/beeserver | gpg --batch --passphrase-file /opt/backup/passphrase.txt -c -o ${archive} + +if ! gpg --batch --passphrase-file /opt/backup/passphrase.txt -d ${archive} | tar -tzf - >/dev/null +then + echo "[${now}] verify failed, discarding ${archive}" + rm -f ${archive} + exit 1 +fi diff --git a/playbooks/backup/backupWeekly.sh b/playbooks/backup/backupWeekly.sh index 21f5950..20c7c62 100644 --- a/playbooks/backup/backupWeekly.sh +++ b/playbooks/backup/backupWeekly.sh @@ -4,12 +4,18 @@ set -euo pipefail now=$(date +"%Y%m%d%H%M") -tar -czf - /opt/backup/beeserver | gpg --batch --passphrase-file /opt/backup/passphrase.txt -c -o /home/beeshare/raid/serverBackup/beeserver_weekly_${now}.tar.gz.gpg +archive=/home/beeshare/raid/serverBackup/beeserver_weekly_${now}.tar.gz.gpg -if [ -s /home/beeshare/raid/serverBackup/beeserver_weekly_${now}.tar.gz.gpg ] +tar -czf - /opt/backup/beeserver | gpg --batch --passphrase-file /opt/backup/passphrase.txt -c -o ${archive} + +if ! gpg --batch --passphrase-file /opt/backup/passphrase.txt -d ${archive} | tar -tzf - >/dev/null then - rm -rf /opt/backup/beeserver + echo "[${now}] verify failed, discarding ${archive} and keeping staging" + rm -f ${archive} + exit 1 fi +rm -rf /opt/backup/beeserver + find /home/beeshare/raid/serverBackup -maxdepth 1 -name 'beeserver_weekly_*.tar.gz.gpg' -mtime +30 -delete find /home/beeshare/raid/serverBackup -maxdepth 1 -name 'beeserver_daily_*.tar.gz.gpg' -mtime +7 -delete \ No newline at end of file diff --git a/playbooks/backup/restoreTest.sh b/playbooks/backup/restoreTest.sh new file mode 100644 index 0000000..69195b5 --- /dev/null +++ b/playbooks/backup/restoreTest.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +set -euo pipefail + +now=$(date +"%Y%m%d%H%M") +backup_dir=/home/beeshare/raid/serverBackup +image=docker.io/vaultwarden/server:1.37.1 +container=vaultwarden-restoretest +port=18222 +work=$(mktemp -d) + +cleanup() { + podman rm -f ${container} >/dev/null 2>&1 || true + rm -rf "${work}" +} + +trap cleanup EXIT + +latest=$(ls -1t ${backup_dir}/beeserver_*.tar.gz.gpg | head -1) +echo "[${now}] restore test using ${latest}" + +gpg --batch --passphrase-file /opt/backup/passphrase.txt -d "${latest}" \ + | tar -xzf - -C "${work}" opt/backup/beeserver/vaultwarden + +data="${work}/opt/backup/beeserver/vaultwarden/data" + +[[ -s "${data}/db.sqlite3" ]] +[[ -s "${data}/rsa_key.pem" ]] +echo " db and rsa_key present" + +integrity=$(sqlite3 "${data}/db.sqlite3" "PRAGMA integrity_check;") +[[ "${integrity}" == "ok" ]] +echo " integrity_check ok" + +users=$(sqlite3 "${data}/db.sqlite3" "select count(*) from users;") +[[ "${users}" -ge 1 ]] +echo " users ${users}" + +email=$(sqlite3 "${data}/db.sqlite3" "select email from users limit 1;") + +podman run -d --rm --name ${container} -p 127.0.0.1:${port}:80 -v "${data}":/data ${image} >/dev/null + +for _ in $(seq 60) +do + if curl -sf http://127.0.0.1:${port}/api/config >/dev/null 2>&1 + then + break + fi + sleep 1 +done + +config=$(curl -sf http://127.0.0.1:${port}/api/config) +[[ "${config}" == *'"version"'* ]] +echo " api/config ok" + +prelogin=$(curl -sf -X POST http://127.0.0.1:${port}/identity/accounts/prelogin \ + -H 'Content-Type: application/json' \ + -d "{\"email\":\"${email}\"}") +[[ "${prelogin}" == *[Kk]df* ]] +echo " prelogin ok for ${email}" + +echo "[${now}] restore test PASSED" diff --git a/playbooks/gitea/gitea.yml b/playbooks/gitea/gitea.yml index 37ee124..ccb1e48 100644 --- a/playbooks/gitea/gitea.yml +++ b/playbooks/gitea/gitea.yml @@ -97,14 +97,14 @@ DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ user_info.uid }}/bus" ansible.builtin.systemd: name: gitea-server.service - state: started + state: restarted scope: user - name: Debug ansible.builtin.debug: msg: - - "Service status: systemctl --user --machine {{ user_info.name }}@.host status gitea-server" + - "Service status: sudo systemctl --user --machine {{ user_info.name }}@.host status gitea-server" - "Read logs: sudo journalctl _UID={{ user_info.uid }} -n 200 --no-pager" - - "Shell: machinectl shell {{ user_info.name }}@.host /bin/bash" + - "Shell: sudo machinectl shell {{ user_info.name }}@.host /bin/bash" tags: - always diff --git a/playbooks/hetzner/hetzner.yml b/playbooks/hetzner/hetzner.yml new file mode 100644 index 0000000..09ea6c0 --- /dev/null +++ b/playbooks/hetzner/hetzner.yml @@ -0,0 +1,97 @@ +- name: Hetzner + hosts: beepi + become: true + vars: + user: hetzner + user_home: /home/hetzner + + hetzner_raid_mount: /home/beeshare/raid + hetzner_port: 23 + + hetzner_paths: + - src: /home/beeshare/raid/serverBackup + dest: ./beepi + - src: /home/beeshare/raid/foundry/backup + dest: ./foundry + + # Fill in after creating the storage box sub-account, then re-run + hetzner_host: "u651287.your-storagebox.de" + hetzner_user: "u651287" + + tasks: + - name: Install rsync + ansible.builtin.apt: + name: rsync + state: present + + - name: Create user + ansible.builtin.user: + name: "{{ user }}" + home: "{{ user_home }}" + shell: /usr/sbin/nologin + create_home: true + + - name: Create ssh directory + ansible.builtin.file: + path: "{{ user_home }}/.ssh" + state: directory + owner: "{{ user }}" + group: "{{ user }}" + mode: '0700' + + - name: Generate ssh key + ansible.builtin.command: + cmd: "ssh-keygen -t ed25519 -N '' -C beepi-hetzner -f {{ user_home }}/.ssh/id_ed25519" + creates: "{{ user_home }}/.ssh/id_ed25519" + + - name: Set key ownership + ansible.builtin.file: + path: "{{ user_home }}/.ssh/{{ item.name }}" + owner: "{{ user }}" + group: "{{ user }}" + mode: "{{ item.mode }}" + loop: + - name: id_ed25519 + mode: '0600' + - name: id_ed25519.pub + mode: '0644' + + - name: Scan storage box host key + ansible.builtin.command: + cmd: "ssh-keyscan -p {{ hetzner_port }} {{ hetzner_host }}" + register: hetzner_hostkey + changed_when: false + when: hetzner_host | length > 0 + + - name: Pin known hosts + ansible.builtin.copy: + dest: "{{ user_home }}/.ssh/known_hosts" + content: "{{ hetzner_hostkey.stdout }}\n" + owner: "{{ user }}" + group: "{{ user }}" + mode: '0600' + when: hetzner_host | length > 0 + + - name: Copy sync script + ansible.builtin.template: + src: hetznerSync.sh.j2 + dest: /opt/backup/hetznerSync.sh + mode: '0700' + when: hetzner_host | length > 0 + + - name: Schedule daily sync + ansible.builtin.cron: + name: beepi hetzner sync + user: root + minute: '0' + hour: '7' + job: /opt/backup/hetznerSync.sh >> /var/log/beepi-backup.log 2>&1 + when: hetzner_host | length > 0 + + - name: Debug + ansible.builtin.debug: + msg: + - "Test manually: sudo /opt/backup/hetznerSync.sh" + - "Read logs: sudo tail -50 /var/log/beepi-backup.log" + tags: + - always diff --git a/playbooks/hetzner/hetznerSync.sh.j2 b/playbooks/hetzner/hetznerSync.sh.j2 new file mode 100644 index 0000000..a2870f8 --- /dev/null +++ b/playbooks/hetzner/hetznerSync.sh.j2 @@ -0,0 +1,34 @@ +#!/bin/bash + +set -euo pipefail + +now=$(date +"%Y%m%d%H%M") + +if ! mountpoint -q {{ hetzner_raid_mount }} +then + echo "[${now}] {{ hetzner_raid_mount }} not mounted, refusing to sync" + exit 1 +fi + +sync_path() { + local src=$1 + local dest=$2 + + if [ -z "$(ls -A ${src} 2>/dev/null)" ] + then + echo "[${now}] ${src} empty, refusing to sync" + exit 1 + fi + + echo "[${now}] syncing ${src} to ${dest}" + sudo -u {{ user }} rsync -a --delete --partial \ + -e "ssh -p {{ hetzner_port }} -i {{ user_home }}/.ssh/id_ed25519 -o UserKnownHostsFile={{ user_home }}/.ssh/known_hosts -o StrictHostKeyChecking=yes" \ + ${src}/ \ + {{ hetzner_user }}@{{ hetzner_host }}:${dest}/ +} + +echo "[${now}] offsite sync start" +{% for path in hetzner_paths %} +sync_path {{ path.src }} {{ path.dest }} +{% endfor %} +echo "[${now}] offsite sync done" diff --git a/playbooks/immich/immich-server.container.j2 b/playbooks/immich/immich-server.container.j2 index d03fd00..40a5cb7 100644 --- a/playbooks/immich/immich-server.container.j2 +++ b/playbooks/immich/immich-server.container.j2 @@ -5,7 +5,7 @@ After=immich-db.service immich-redis.service [Container] ContainerName=immich-server -Image=ghcr.io/immich-app/immich-server:release +Image=ghcr.io/immich-app/immich-server:v3.1.0 Network=immich.network PublishPort=127.0.0.1:2283:2283 Volume=immich-upload.volume:/usr/src/app/upload diff --git a/playbooks/immich/immich.yml b/playbooks/immich/immich.yml index ed7e710..89d0f79 100644 --- a/playbooks/immich/immich.yml +++ b/playbooks/immich/immich.yml @@ -107,12 +107,12 @@ DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ immich_user_info.uid }}/bus" ansible.builtin.systemd: name: immich-server.service - state: started + state: restarted scope: user - name: Debug ansible.builtin.debug: msg: - - "Service status: systemctl --user --machine {{ immich_user_info.name }}@.host status immich-server" + - "Service status: sudo systemctl --user --machine {{ immich_user_info.name }}@.host status immich-server" - "Read logs: sudo journalctl _UID={{ immich_user_info.uid }} -n 200 --no-pager" - - "Shell: machinectl shell {{ immich_user_info.name }}@.host /bin/bash" + - "Shell: sudo machinectl shell {{ immich_user_info.name }}@.host /bin/bash" diff --git a/playbooks/ingress-hardening/ingress-hardening.yml b/playbooks/ingress-hardening/ingress-hardening.yml new file mode 100644 index 0000000..43467e6 --- /dev/null +++ b/playbooks/ingress-hardening/ingress-hardening.yml @@ -0,0 +1,96 @@ +- name: Ingress-hardening + hosts: animeistrash + become: true + tasks: + - name: Check for an authorized key + become: false + ansible.builtin.stat: + path: "~/.ssh/authorized_keys" + register: authorized_keys + + - name: Refuse to lock out + ansible.builtin.assert: + that: + - authorized_keys.stat.exists + - authorized_keys.stat.size > 0 + fail_msg: "No authorized_keys for the connecting user, refusing to disable password auth" + + - name: Install packages + ansible.builtin.apt: + name: + - unattended-upgrades + - needrestart + state: present + + - name: Harden sshd + ansible.builtin.copy: + dest: /etc/ssh/sshd_config.d/00-hardening.conf + content: | + PermitRootLogin prohibit-password + PasswordAuthentication no + KbdInteractiveAuthentication no + PubkeyAuthentication yes + PermitEmptyPasswords no + MaxAuthTries 3 + LoginGraceTime 30 + X11Forwarding no + AllowAgentForwarding no + owner: root + group: root + mode: '0644' + validate: /usr/sbin/sshd -t -f %s + notify: Reload sshd + + - name: Enable unattended upgrades + ansible.builtin.copy: + dest: /etc/apt/apt.conf.d/20auto-upgrades + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + APT::Periodic::AutocleanInterval "7"; + owner: root + group: root + mode: '0644' + + - name: Configure unattended upgrades + ansible.builtin.copy: + dest: /etc/apt/apt.conf.d/52unattended-upgrades-local + content: | + Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; + Unattended-Upgrade::Remove-Unused-Dependencies "true"; + Unattended-Upgrade::Automatic-Reboot "false"; + owner: root + group: root + mode: '0644' + + - name: Restart services after library upgrades + ansible.builtin.copy: + dest: /etc/needrestart/conf.d/99-auto.conf + content: "$nrconf{restart} = 'a';\n" + owner: root + group: root + mode: '0644' + + - name: Enable apt timers + ansible.builtin.service: + name: "{{ item }}" + state: started + enabled: true + loop: + - apt-daily.timer + - apt-daily-upgrade.timer + + - name: Debug + ansible.builtin.debug: + msg: + - "Effective sshd config: sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|maxauthtries'" + - "Dry-run upgrades: sudo unattended-upgrade --dry-run --debug" + - "Read logs: sudo tail -50 /var/log/unattended-upgrades/unattended-upgrades.log" + tags: + - always + + handlers: + - name: Reload sshd + ansible.builtin.service: + name: ssh + state: reloaded diff --git a/playbooks/letsencrypt/letsencrypt.yml b/playbooks/letsencrypt/letsencrypt.yml index 33364ba..7248e44 100644 --- a/playbooks/letsencrypt/letsencrypt.yml +++ b/playbooks/letsencrypt/letsencrypt.yml @@ -1,12 +1,30 @@ - name: Letsencrypt hosts: beepi become: true + vars: + njalla_api_token: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 65666463616462633533663835383164353566326437333336616266333864333465613732346633 + 6534633837373937393563633966343738316562653930610a346461613236653266303237306663 + 66663234313832373939346539386633316465373861663862646537303662313564363038303164 + 3865666132613130380a623737663837343438333837626338346565653735613435313164373663 + 33316231366334623461303263386136656234613133373361353262346366636133396134663838 + 3733353465643235653337306434396366623935303862633737 + tasks: - name: Install Certbot ansible.builtin.apt: - name: python3-certbot-nginx + name: + - python3-certbot-nginx + - python3-pip state: present + - name: Install Njalla DNS plugin + ansible.builtin.pip: + name: certbot-dns-njalla + state: present + extra_args: --break-system-packages + - name: Request root certificates changed_when: true ansible.builtin.command: "certbot certonly --nginx -m secretbumblebee@proton.me --agree-tos -n --domains {{ item }}" @@ -19,6 +37,22 @@ - gallery.secretbee.buzz - ott.secretbee.buzz + - name: Copy njalla credentials + ansible.builtin.copy: + dest: /etc/letsencrypt/njalla.ini + content: "dns_njalla_token={{ njalla_api_token }}\n" + owner: root + group: root + mode: '0600' + + - name: Request vault certificate + changed_when: true + ansible.builtin.command: >- + certbot certonly -a dns-njalla + --dns-njalla-credentials /etc/letsencrypt/njalla.ini + -m secretbumblebee@proton.me --agree-tos -n + --domains vault.secretbee.buzz + - name: Change permission on live ansible.builtin.file: path: /etc/letsencrypt/live/ diff --git a/playbooks/monitoring/docker-compose.yml.j2 b/playbooks/monitoring/docker-compose.yml.j2 index 9d6b084..3a19a33 100644 --- a/playbooks/monitoring/docker-compose.yml.j2 +++ b/playbooks/monitoring/docker-compose.yml.j2 @@ -71,7 +71,7 @@ services: restart: always podman-exporter: - image: quay.io/navidys/prometheus-podman-exporter:1.21.2 + image: quay.io/navidys/prometheus-podman-exporter:v1.21.2 container_name: podman-exporter pull_policy: always restart: always diff --git a/playbooks/nginx/nginx.yml b/playbooks/nginx/nginx.yml index 7990abb..4ec5c82 100644 --- a/playbooks/nginx/nginx.yml +++ b/playbooks/nginx/nginx.yml @@ -28,6 +28,14 @@ group: root mode: '0644' + - name: Copy ssl.conf + ansible.builtin.copy: + src: ssl.conf + dest: /etc/nginx/ssl.conf + owner: root + group: root + mode: '0644' + - name: Copy html content ansible.builtin.copy: src: www/ @@ -36,7 +44,17 @@ group: www-data mode: '0644' - - name: Restart nginx + - name: Validate config + ansible.builtin.command: nginx -t + changed_when: false + + - name: Start nginx ansible.builtin.service: name: nginx - state: restarted + state: started + enabled: true + + - name: Reload nginx + ansible.builtin.service: + name: nginx + state: reloaded diff --git a/playbooks/nginx/sites-enabled/00-default b/playbooks/nginx/sites-enabled/00-default new file mode 100644 index 0000000..b374b4f --- /dev/null +++ b/playbooks/nginx/sites-enabled/00-default @@ -0,0 +1,6 @@ +server { + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + include /etc/nginx/ssl.conf; + ssl_reject_handshake on; +} diff --git a/playbooks/nginx/sites-enabled/gallery.secretbee.buzz b/playbooks/nginx/sites-enabled/gallery.secretbee.buzz index 9a51e85..f164a54 100644 --- a/playbooks/nginx/sites-enabled/gallery.secretbee.buzz +++ b/playbooks/nginx/sites-enabled/gallery.secretbee.buzz @@ -15,8 +15,7 @@ server { ssl_certificate /etc/letsencrypt/live/gallery.secretbee.buzz/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/gallery.secretbee.buzz/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; + include /etc/nginx/ssl.conf; # allow large file uploads client_max_body_size 50000M; diff --git a/playbooks/nginx/sites-enabled/git.secretbee.buzz b/playbooks/nginx/sites-enabled/git.secretbee.buzz index 41444a5..3102910 100644 --- a/playbooks/nginx/sites-enabled/git.secretbee.buzz +++ b/playbooks/nginx/sites-enabled/git.secretbee.buzz @@ -17,8 +17,7 @@ server { ssl_certificate /etc/letsencrypt/live/git.secretbee.buzz/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/git.secretbee.buzz/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; + include /etc/nginx/ssl.conf; location / { client_max_body_size 512M; diff --git a/playbooks/nginx/sites-enabled/grafana.secretbee.buzz b/playbooks/nginx/sites-enabled/grafana.secretbee.buzz index 10447aa..e2c07ab 100644 --- a/playbooks/nginx/sites-enabled/grafana.secretbee.buzz +++ b/playbooks/nginx/sites-enabled/grafana.secretbee.buzz @@ -26,8 +26,7 @@ server { ssl_certificate /etc/letsencrypt/live/grafana.secretbee.buzz/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/grafana.secretbee.buzz/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; + include /etc/nginx/ssl.conf; location / { proxy_set_header Host $host; diff --git a/playbooks/nginx/sites-enabled/lounge.secretbee.buzz b/playbooks/nginx/sites-enabled/lounge.secretbee.buzz index 4b75436..5a116b9 100644 --- a/playbooks/nginx/sites-enabled/lounge.secretbee.buzz +++ b/playbooks/nginx/sites-enabled/lounge.secretbee.buzz @@ -17,8 +17,7 @@ server { ssl_certificate /etc/letsencrypt/live/lounge.secretbee.buzz/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/lounge.secretbee.buzz/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; + include /etc/nginx/ssl.conf; location / { proxy_pass http://127.0.0.1:9000/; diff --git a/playbooks/nginx/sites-enabled/matrix.secretbee.buzz b/playbooks/nginx/sites-enabled/matrix.secretbee.buzz index b87b112..de7046c 100644 --- a/playbooks/nginx/sites-enabled/matrix.secretbee.buzz +++ b/playbooks/nginx/sites-enabled/matrix.secretbee.buzz @@ -7,6 +7,7 @@ server { ssl_certificate /etc/letsencrypt/live/matrix.secretbee.buzz/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/matrix.secretbee.buzz/privkey.pem; + include /etc/nginx/ssl.conf; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header X-Content-Type-Options nosniff; diff --git a/playbooks/nginx/sites-enabled/ott.secretbee.buzz b/playbooks/nginx/sites-enabled/ott.secretbee.buzz index cf72ce5..c819694 100644 --- a/playbooks/nginx/sites-enabled/ott.secretbee.buzz +++ b/playbooks/nginx/sites-enabled/ott.secretbee.buzz @@ -9,6 +9,7 @@ server { ssl_certificate /etc/letsencrypt/live/ott.secretbee.buzz/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/ott.secretbee.buzz/privkey.pem; + include /etc/nginx/ssl.conf; #deny all; location / { diff --git a/playbooks/nginx/sites-enabled/secretbee.buzz b/playbooks/nginx/sites-enabled/secretbee.buzz index 82ff70d..ca1d08c 100644 --- a/playbooks/nginx/sites-enabled/secretbee.buzz +++ b/playbooks/nginx/sites-enabled/secretbee.buzz @@ -28,8 +28,7 @@ server { ssl_certificate /etc/letsencrypt/live/secretbee.buzz/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/secretbee.buzz/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; + include /etc/nginx/ssl.conf; location /.well-known/matrix/server { default_type application/json; diff --git a/playbooks/nginx/sites-enabled/vault.secretbee.buzz b/playbooks/nginx/sites-enabled/vault.secretbee.buzz new file mode 100644 index 0000000..3787368 --- /dev/null +++ b/playbooks/nginx/sites-enabled/vault.secretbee.buzz @@ -0,0 +1,29 @@ +server { + listen 443 ssl http2; + server_name vault.secretbee.buzz; + + ssl_certificate /etc/letsencrypt/live/vault.secretbee.buzz/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/vault.secretbee.buzz/privkey.pem; + include /etc/nginx/ssl.conf; + + allow 10.10.1.0/24; + allow 192.168.178.0/24; + deny all; + + client_max_body_size 128M; + + location / { + proxy_pass http://localhost:8222; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /admin { + return 404; + } +} diff --git a/playbooks/nginx/ssl.conf b/playbooks/nginx/ssl.conf new file mode 100644 index 0000000..4af5bbd --- /dev/null +++ b/playbooks/nginx/ssl.conf @@ -0,0 +1,3 @@ +ssl_protocols TLSv1.2 TLSv1.3; +ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384; +ssl_prefer_server_ciphers off; diff --git a/playbooks/ott/ott.yml b/playbooks/ott/ott.yml index e839697..8efe073 100644 --- a/playbooks/ott/ott.yml +++ b/playbooks/ott/ott.yml @@ -144,14 +144,14 @@ DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ user_info.uid }}/bus" ansible.builtin.systemd: name: ott-server.service - state: started + state: restarted scope: user - name: Debug ansible.builtin.debug: msg: - - "Service status: systemctl --user --machine {{ user_info.name }}@.host status ott-server" + - "Service status: sudo systemctl --user --machine {{ user_info.name }}@.host status ott-server" - "Read logs: sudo journalctl _UID={{ user_info.uid }} -n 200 --no-pager" - - "Shell: machinectl shell {{ user_info.name }}@.host /bin/bash" + - "Shell: sudo machinectl shell {{ user_info.name }}@.host /bin/bash" tags: - always diff --git a/playbooks/vaultwarden/vaultwarden.container.j2 b/playbooks/vaultwarden/vaultwarden.container.j2 new file mode 100644 index 0000000..5c08439 --- /dev/null +++ b/playbooks/vaultwarden/vaultwarden.container.j2 @@ -0,0 +1,18 @@ +[Unit] +Description=Vaultwarden + +[Container] +ContainerName=vaultwarden +Image=docker.io/vaultwarden/server:1.37.1 +PublishPort=127.0.0.1:8222:80 +Volume={{ user_home }}/data:/data +Environment=DOMAIN=https://{{ vaultwarden_domain }} +Environment=SIGNUPS_ALLOWED={{ vaultwarden_signups_allowed | string | lower }} +Environment=INVITATIONS_ALLOWED=false +Environment=ROCKET_PORT=80 + +[Service] +Restart=on-failure + +[Install] +WantedBy=default.target diff --git a/playbooks/vaultwarden/vaultwarden.yml b/playbooks/vaultwarden/vaultwarden.yml new file mode 100644 index 0000000..e26ba49 --- /dev/null +++ b/playbooks/vaultwarden/vaultwarden.yml @@ -0,0 +1,101 @@ +- name: Vaultwarden + hosts: beepi + become: true + vars: + recovery_2fa: !vault | + $ANSIBLE_VAULT;1.1;AES256 + 33656334663834356232623236316238396630656530363332353566363433356133313932613230 + 3063613030363566376635363830336466333237633063310a666236333335363566343237646135 + 65656466363833373231653565306563333036373664316535366339393531363434386336303563 + 6636396665316634320a643966313338633564363830663263666461303635623164376265633631 + 39633162323130663533343734303538636433323632316131373531376438633432323433303234 + 3162626630373264356338356638323364656563666635393636 + + user: vaultwarden + user_home: /home/vaultwarden + vaultwarden_domain: vault.secretbee.buzz + + # Bootstrap = -e vaultwarden_signups_allowed=true + vaultwarden_signups_allowed: false + + tasks: + - name: Install dependencies + ansible.builtin.apt: + name: "{{ item }}" + state: present + loop: + - podman + - systemd-container + - passt + + - name: Create user + ansible.builtin.user: + name: "{{ user }}" + home: "{{ user_home }}" + shell: /usr/sbin/nologin + create_home: true + register: user_info + + - name: Enable linger + ansible.builtin.command: + cmd: "loginctl enable-linger {{ user }}" + creates: "/var/lib/systemd/linger/{{ user }}" + + - name: Create quadlet directory + ansible.builtin.file: + path: "{{ user_home }}/.config/containers/systemd" + state: directory + owner: "{{ user }}" + group: "{{ user }}" + mode: '0755' + + - name: Create data directory + ansible.builtin.file: + path: "{{ user_home }}/data" + state: directory + owner: "{{ user }}" + group: "{{ user }}" + mode: '0700' + + - name: Deploy server quadlet + ansible.builtin.template: + src: vaultwarden.container.j2 + dest: "{{ user_home }}/.config/containers/systemd/vaultwarden.container" + owner: "{{ user }}" + group: "{{ user }}" + mode: '0600' + + - name: Reload user systemd + become: true + become_user: "{{ user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ user_info.uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ user_info.uid }}/bus" + ansible.builtin.systemd: + daemon_reload: true + scope: user + + - name: Start user manager + ansible.builtin.systemd: + name: "user@{{ user_info.uid }}.service" + state: started + + - name: Start vaultwarden + become: true + become_user: "{{ user }}" + environment: + XDG_RUNTIME_DIR: "/run/user/{{ user_info.uid }}" + DBUS_SESSION_BUS_ADDRESS: "unix:path=/run/user/{{ user_info.uid }}/bus" + ansible.builtin.systemd: + name: vaultwarden.service + state: restarted + scope: user + + - name: Debug + ansible.builtin.debug: + msg: + - "Service status: sudo systemctl --user --machine {{ user_info.name }}@.host status vaultwarden" + - "Read logs: sudo journalctl _UID={{ user_info.uid }} -n 200 --no-pager" + - "Shell: sudo machinectl shell {{ user_info.name }}@.host /bin/bash" + tags: + - always diff --git a/playbooks/wireguard-ingress/client.conf.j2 b/playbooks/wireguard-ingress/client.conf.j2 new file mode 100644 index 0000000..ab9dbff --- /dev/null +++ b/playbooks/wireguard-ingress/client.conf.j2 @@ -0,0 +1,10 @@ +[Interface] +PrivateKey = {{ lookup('file', wireguard_client_dir ~ '/' ~ item.item.name ~ '.private') }} +Address = {{ item.item.ip }}/32 + +[Peer] +PublicKey = {{ wireguard_server_public_key }} +PresharedKey = {{ lookup('file', wireguard_client_dir ~ '/' ~ item.item.name ~ '.psk.private') }} +Endpoint = {{ wireguard_ingress_endpoint }}:{{ wireguard_ingress_port }} +AllowedIPs = {{ item.item.allowed_ips | default(wireguard_client_allowed_ips) }} +PersistentKeepalive = 25 diff --git a/playbooks/wireguard-ingress/wg0.conf.j2 b/playbooks/wireguard-ingress/wg0.conf.j2 index 45e0f29..60dfd03 100644 --- a/playbooks/wireguard-ingress/wg0.conf.j2 +++ b/playbooks/wireguard-ingress/wg0.conf.j2 @@ -1,36 +1,26 @@ [Interface] PrivateKey = {{ wireguard_server_private_key }} -Address = 10.10.1.1/24 -ListenPort = 41194 +Address = {{ wireguard_ingress_address }} +ListenPort = {{ wireguard_ingress_port }} PostUp = sysctl -w net.ipv4.ip_forward=1 -PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 25 -j DNAT --to-destination 10.10.1.2:25 -PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 25 -j DNAT --to-destination 10.10.1.2:25 - -PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 993 -j DNAT --to-destination 10.10.1.2:993 -PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 993 -j DNAT --to-destination 10.10.1.2:993 - -PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 587 -j DNAT --to-destination 10.10.1.2:587 -PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 587 -j DNAT --to-destination 10.10.1.2:587 - -PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 465 -j DNAT --to-destination 10.10.1.2:465 -PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 465 -j DNAT --to-destination 10.10.1.2:465 - -PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 10.10.1.2:80 -PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 80 -j DNAT --to-destination 10.10.1.2:80 - -PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 443 -j DNAT --to-destination 10.10.1.2:443 -PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 443 -j DNAT --to-destination 10.10.1.2:443 - -PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 8448 -j DNAT --to-destination 10.10.1.2:8448 -PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 8448 -j DNAT --to-destination 10.10.1.2:8448 - -PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 4444 -j DNAT --to-destination 10.10.1.2:4444 -PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport 4444 -j DNAT --to-destination 10.10.1.2:4444 +{% for port in wireguard_dnat_ports %} +PreUp = iptables -t nat -A PREROUTING -i eth0 -p tcp --dport {{ port }} -j DNAT --to-destination {{ wireguard_beepi_ip }}:{{ port }} +PostDown = iptables -t nat -D PREROUTING -i eth0 -p tcp --dport {{ port }} -j DNAT --to-destination {{ wireguard_beepi_ip }}:{{ port }} +{% endfor %} #PreUp = iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE #PostDown = iptables -t nat -D POSTROUTING -o wg0 -j MASQUERADE - + +# ----- site-to-site: beepi ----- [Peer] -PublicKey = oI69uWkbkwtpuIYxDNGmzg4nRjY1BIZFVFjZwguIvwE= -AllowedIPs = 10.10.1.2/32 \ No newline at end of file +PublicKey = {{ wireguard_beepi_public_key }} +AllowedIPs = {{ wireguard_beepi_ip }}/32 + +{% for peer in client_public_keys.results %} +# ----- road-warrior: {{ peer.item.name }} ----- +[Peer] +PublicKey = {{ peer.stdout }} +PresharedKey = {{ lookup('file', wireguard_client_dir ~ '/' ~ peer.item.name ~ '.psk.private') }} +AllowedIPs = {{ peer.item.ip }}/32 +{% endfor %} diff --git a/playbooks/wireguard-ingress/wireguard-ingress.yml b/playbooks/wireguard-ingress/wireguard-ingress.yml index 8e1e5b9..eb5be6a 100644 --- a/playbooks/wireguard-ingress/wireguard-ingress.yml +++ b/playbooks/wireguard-ingress/wireguard-ingress.yml @@ -10,23 +10,163 @@ 3239666637326337650a636433616264386162326535306461326233323961643664373134363634 35633238393933386231373535353635646462383763613362363761363935363965383836366431 3766343166643632306162343761613030653631353361373936 + + wireguard_ingress_address: 10.10.1.1/24 + wireguard_ingress_port: 41194 + wireguard_ingress_endpoint: 192.3.44.243 + wireguard_server_public_key: r0XaxIE3AVud9Z42UPZF3mwoh2R0B0M1EUDY9cvuyDA= + + wireguard_beepi_ip: 10.10.1.2 + wireguard_beepi_public_key: oI69uWkbkwtpuIYxDNGmzg4nRjY1BIZFVFjZwguIvwE= + + wireguard_dnat_ports: + - 25 # mail + - 80 + - 443 + - 465 # mail + - 587 # mail + - 993 # mail + - 4444 # backup ssh + - 8448 # matrix federation + + wireguard_clients: + - name: phone + ip: 10.10.1.3 + + wireguard_client_allowed_ips: 10.10.1.2/32 + + wireguard_client_dir: "{{ playbook_dir }}/clients" + + # Changes to Address, ListenPort or wireguard_dnat_ports = -e wireguard_full_restart=true + wireguard_full_restart: false + tasks: - name: Install wireguard ansible.builtin.apt: name: wireguard state: present - - name: Down wg0 - changed_when: true - failed_when: false - ansible.builtin.command: wg-quick down wg0 + - name: Create local client key directory + ansible.builtin.file: + path: "{{ wireguard_client_dir }}" + state: directory + mode: '0700' + delegate_to: localhost + become: false + + - name: Generate client private keys + ansible.builtin.shell: + cmd: "umask 077 && wg genkey > {{ wireguard_client_dir }}/{{ item.name }}.private" + creates: "{{ wireguard_client_dir }}/{{ item.name }}.private" + loop: "{{ wireguard_clients }}" + loop_control: + label: "{{ item.name }}" + delegate_to: localhost + become: false + + - name: Generate client preshared keys + ansible.builtin.shell: + cmd: "umask 077 && wg genpsk > {{ wireguard_client_dir }}/{{ item.name }}.psk.private" + creates: "{{ wireguard_client_dir }}/{{ item.name }}.psk.private" + loop: "{{ wireguard_clients }}" + loop_control: + label: "{{ item.name }}" + delegate_to: localhost + become: false + + - name: Derive client public keys + ansible.builtin.command: + cmd: "wg pubkey" + stdin: "{{ lookup('file', wireguard_client_dir ~ '/' ~ item.name ~ '.private') }}" + loop: "{{ wireguard_clients }}" + loop_control: + label: "{{ item.name }}" + register: client_public_keys + changed_when: false + delegate_to: localhost + become: false - name: Copy wg0.conf ansible.builtin.template: src: wg0.conf.j2 dest: /etc/wireguard/wg0.conf mode: '0600' + register: wg0_config - - name: Up wg0 + - name: Check whether wg0 is up + ansible.builtin.command: + cmd: wg show wg0 + register: wg0_state + failed_when: false + changed_when: false + + - name: Sync peers without dropping the tunnel + ansible.builtin.shell: + cmd: "wg syncconf wg0 <(wg-quick strip wg0)" + executable: /bin/bash changed_when: true - ansible.builtin.command: wg-quick up wg0 + when: + - wg0_config.changed + - wg0_state.rc == 0 + - not wireguard_full_restart | bool + + - name: Bounce wg0 + ansible.builtin.shell: + cmd: "wg-quick down wg0 || true; wg-quick up wg0" + changed_when: true + when: wg0_state.rc != 0 or wireguard_full_restart | bool + + - name: Enable wg-quick@wg0 on boot + ansible.builtin.service: + name: wg-quick@wg0 + enabled: true + + - name: Render client configs + ansible.builtin.template: + src: client.conf.j2 + dest: "{{ wireguard_client_dir }}/{{ item.item.name }}.conf" + mode: '0600' + loop: "{{ client_public_keys.results }}" + loop_control: + label: "{{ item.item.name }}" + delegate_to: localhost + become: false + + - name: Check for qrencode + ansible.builtin.command: + cmd: "which qrencode" + register: qrencode_check + failed_when: false + changed_when: false + delegate_to: localhost + become: false + + - name: Render QR codes for mobile enrolment + ansible.builtin.command: + cmd: "qrencode -t ansiutf8 -r {{ wireguard_client_dir }}/{{ item.item.name }}.conf" + loop: "{{ client_public_keys.results }}" + loop_control: + label: "{{ item.item.name }}" + register: client_qr + changed_when: false + when: qrencode_check.rc == 0 + delegate_to: localhost + become: false + + - name: Show QR codes + ansible.builtin.debug: + msg: "{{ (item.stdout | default('')).split('\n') }}" + loop: "{{ client_qr.results | default([]) }}" + loop_control: + label: "{{ item.item.item.name | default('') }}" + when: qrencode_check.rc == 0 + + - name: Show peer status + ansible.builtin.command: + cmd: wg show wg0 + register: wg0_final + changed_when: false + + - name: Report + ansible.builtin.debug: + msg: "{{ wg0_final.stdout_lines }}"