From b5860daf11ac353049cb1654b9414a129e5cfb96 Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 13 May 2026 00:13:57 +0300 Subject: Rework --- .gitignore | 1 + README.md | 4 +- ansible.cfg | 3 + .../__pycache__/jailexec.cpython-314.pyc | Bin 0 -> 15222 bytes connection_plugins/jailexec.py | 322 ++++++++++++++++++ filter_plugins/ipv4.py | 51 +++ group_vars/jails.yml | 5 + host_vars/pursotin.yml | 7 - host_vars/test_vm.yml | 12 - inventory | 3 - inventory/jails.py | 33 ++ inventory/prod/hosts.yml | 22 ++ inventory/prod/jails.py | 1 + inventory/test/hosts.yml | 34 ++ inventory/test/jails.py | 1 + playbook.yml | 109 ------ principles.md | 7 - reset-init-up.sh | 8 + roles/host/files/root_bashrc | 7 + roles/host/handlers/main.yml | 9 + roles/host/tasks/main.yml | 217 ++++++++++++ roles/host/tasks/userland.yml | 59 ++++ roles/host/templates/devfs.rules.j2 | 69 ++++ roles/host/templates/etc_crontab.j2 | 32 ++ roles/host/templates/etc_periodic.conf.j2 | 4 + roles/host/templates/etc_rc.conf.j2 | 50 +++ roles/host/templates/etc_resolv.conf.j2 | 2 + roles/host/templates/etc_ssh_sshd_config.j2 | 121 +++++++ roles/host/templates/jail.conf.j2 | 1 + roles/host/templates/usr_local_bin_logto.sh.j2 | 47 +++ .../usr_local_bin_pylogsentinel-batch-email.sh.j2 | 14 + .../templates/usr_local_etc_pylogsentinel.conf.j2 | 22 ++ .../usr_local_etc_rc.d_zpool_gpt_labels.j2 | 33 ++ roles/host/templates/usr_local_etc_smartd.conf.j2 | 4 + roles/host_prod/handlers/main.yml | 13 + roles/host_prod/tasks/main.yml | 153 +++++++++ roles/host_prod/templates/etc_aliases.j2 | 1 + roles/host_prod/templates/etc_dma_auth.conf.j2 | 1 + roles/host_prod/templates/etc_dma_dma.conf.j2 | 8 + roles/host_prod/templates/etc_fstab.j2 | 7 + roles/host_prod/templates/root_ssh_config.j2 | 4 + .../host_prod/templates/usr_local_bin_backup.sh.j2 | 365 +++++++++++++++++++++ roles/host_prod/templates/usr_local_bin_safepf.sh | 36 ++ roles/host_test/handlers/main.yml | 2 + roles/host_test/tasks/main.yml | 13 + roles/jail/handlers/main.yml | 5 + roles/jail/tasks/main.yml | 175 ++++++++++ roles/jail/templates/jail_conf.j2 | 56 ++++ roles/jails/01_ingress/defaults/main.yml | 92 ++++++ roles/jails/01_ingress/tasks/main.yml | 49 +++ roles/jails/01_ingress/templates/acme-dns-auth.py | 170 ++++++++++ roles/jails/01_ingress/templates/etc_crontab.j2 | 18 + roles/jails/01_ingress/templates/etc_pf.conf.j2 | 27 ++ .../templates/usr_local_bin_gen_goaccess.sh.j2 | 8 + .../templates/usr_local_bin_hetzner_ddns.sh.j2 | 130 ++++++++ .../templates/usr_local_etc_hetzner_auth.j2 | 1 + .../templates/usr_local_etc_nginx_nginx.conf.j2 | 123 +++++++ roles/jails/02_postgres/defaults/main.yml | 9 + roles/jails/02_postgres/tasks/main.yml | 2 + roles/jails/03_irc_thelounge/defaults/main.yml | 1 + roles/jails/03_irc_thelounge/tasks/main.yml | 2 + roles/jails/04_taulubot/defaults/main.yml | 1 + roles/jails/04_taulubot/tasks/main.yml | 2 + roles/jails/05_homepage/defaults/main.yml | 18 + roles/jails/05_homepage/tasks/main.yml | 2 + roles/jails/05_homepage/templates/etc_crontab.j2 | 9 + .../templates/usr_local_etc_nginx_nginx.conf.j2 | 55 ++++ roles/jails/06_hommabot/defaults/main.yml | 14 + roles/jails/06_hommabot/tasks/main.yml | 2 + roles/jails/06_hommabot/templates/etc_crontab.j2 | 10 + .../06_hommabot/templates/root_hommabot_deps.sh | 14 + .../06_hommabot/templates/root_hommabot_env.j2 | 4 + roles/jails/07_aggro/defaults/main.yml | 1 + roles/jails/07_aggro/tasks/main.yml | 2 + roles/jails/08_diddle/defaults/main.yml | 23 ++ roles/jails/08_diddle/tasks/main.yml | 2 + roles/jails/08_diddle/templates/root_clone.sh | 21 ++ roles/jails/08_diddle/templates/root_diddle_env.j2 | 9 + .../jails/08_diddle/templates/usr_local_bin_diddle | 17 + .../08_diddle/templates/usr_local_etc_rc.d_diddle | 43 +++ roles/jails/09_redis/defaults/main.yml | 1 + roles/jails/09_redis/tasks/main.yml | 2 + roles/jails/10_samba/defaults/main.yml | 5 + roles/jails/10_samba/tasks/main.yml | 2 + roles/jails/11_spliit/defaults/main.yml | 1 + roles/jails/11_spliit/tasks/main.yml | 2 + roles/jails/12_goaccess/defaults/main.yml | 5 + roles/jails/12_goaccess/tasks/main.yml | 2 + roles/jails/13_plex/defaults/main.yml | 5 + roles/jails/13_plex/tasks/main.yml | 2 + roles/jails/14_freshrss/defaults/main.yml | 1 + roles/jails/14_freshrss/tasks/main.yml | 2 + roles/jails/15_paste/defaults/main.yml | 1 + roles/jails/15_paste/tasks/main.yml | 2 + roles/jails/16_dl/defaults/main.yml | 11 + roles/jails/16_dl/tasks/main.yml | 2 + roles/jails/17_syncthing/defaults/main.yml | 11 + roles/jails/17_syncthing/tasks/main.yml | 2 + roles/jails/18_komga/defaults/main.yml | 5 + roles/jails/18_komga/tasks/main.yml | 2 + roles/jails/19_leolalla_fi/defaults/main.yml | 1 + roles/jails/19_leolalla_fi/tasks/main.yml | 2 + site.yml | 74 +++++ tasks/email.yml | 50 --- tasks/general.yml | 167 ---------- tasks/jail_diddle.yml | 63 ---- tasks/jail_homepage.yml | 52 --- tasks/jail_hommabot.yml | 62 ---- tasks/jail_ingress.yml | 211 ------------ tasks/jail_postgres.yml | 10 - tasks/jails.yml | 49 --- tasks/jails_dataset.yml | 61 ---- tasks/jails_single.yml | 27 -- tasks/network.yml | 48 --- tasks/pkg_jail_install.yml | 9 - tasks/zfs.yml | 48 --- templates/diddle/root_diddle_env.j2 | 9 - templates/diddle/usr_local_bin_diddle | 17 - templates/diddle/usr_local_etc_rc.d_diddle | 43 --- templates/etc_aliases.j2 | 1 - templates/etc_crontab.j2 | 32 -- templates/etc_devfs.rules.j2 | 91 ----- templates/etc_dma_auth.conf.j2 | 1 - templates/etc_dma_dma.conf.j2 | 8 - templates/etc_fstab.j2 | 7 - templates/etc_jail.conf.d_[jailname].conf.j2 | 43 --- templates/etc_jail.conf.j2 | 6 - templates/etc_periodic.conf.j2 | 4 - templates/etc_rc.conf.j2 | 48 --- templates/etc_resolv.conf.j2 | 2 - templates/etc_ssh_sshd_config.j2 | 121 ------- templates/homepage/etc_crontab.j2 | 9 - .../homepage/usr_local_etc_nginx_nginx.conf.j2 | 55 ---- templates/hommabot/etc_crontab.j2 | 10 - templates/hommabot/root_hommabot_deps.sh | 14 - templates/hommabot/root_hommabot_env.j2 | 4 - templates/ingress/acme-dns-auth.py | 170 ---------- templates/ingress/etc_crontab.j2 | 18 - templates/ingress/etc_pf.conf.j2 | 27 -- templates/ingress/nginx_snippet_ban.inc | 17 - templates/ingress/pf-ban-socket.py | 72 ---- templates/ingress/usr_local_bin_gen_goaccess.sh.j2 | 8 - templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 | 130 -------- templates/ingress/usr_local_etc_hetzner_auth.j2 | 1 - .../ingress/usr_local_etc_nginx_nginx.conf.j2 | 141 -------- templates/ingress/usr_local_etc_rc.d_pf_ban_socket | 43 --- templates/jail_confs/_default.j2 | 5 - templates/jail_confs/_epl_network.j2 | 18 - templates/jail_confs/dl.j2 | 6 - templates/jail_confs/ingress.j2 | 22 -- templates/jail_confs/komga.j2 | 4 - templates/jail_confs/plex.j2 | 4 - templates/jail_confs/postgres.j2 | 4 - templates/jail_confs/samba.j2 | 4 - templates/jail_confs/syncthing.j2 | 11 - templates/root_bashrc | 7 - templates/root_clone.sh | 21 -- templates/root_ssh_config.j2 | 4 - templates/usr_local_bin_backup.sh.j2 | 365 --------------------- templates/usr_local_bin_logto.sh.j2 | 47 --- .../usr_local_bin_pylogsentinel-batch-email.sh.j2 | 14 - templates/usr_local_bin_safepf.sh | 36 -- templates/usr_local_etc_pylogsentinel.conf.j2 | 22 -- templates/usr_local_etc_rc.d_zpool_gpt_labels.j2 | 33 -- templates/usr_local_etc_smartd.conf.j2 | 4 - vm.sh | 7 +- 166 files changed, 3040 insertions(+), 2784 deletions(-) create mode 100644 ansible.cfg create mode 100644 connection_plugins/__pycache__/jailexec.cpython-314.pyc create mode 100644 connection_plugins/jailexec.py create mode 100644 filter_plugins/ipv4.py create mode 100644 group_vars/jails.yml delete mode 100644 host_vars/pursotin.yml delete mode 100644 host_vars/test_vm.yml delete mode 100644 inventory create mode 100755 inventory/jails.py create mode 100644 inventory/prod/hosts.yml create mode 120000 inventory/prod/jails.py create mode 100644 inventory/test/hosts.yml create mode 120000 inventory/test/jails.py delete mode 100644 playbook.yml delete mode 100644 principles.md create mode 100755 reset-init-up.sh create mode 100644 roles/host/files/root_bashrc create mode 100644 roles/host/handlers/main.yml create mode 100644 roles/host/tasks/main.yml create mode 100644 roles/host/tasks/userland.yml create mode 100644 roles/host/templates/devfs.rules.j2 create mode 100644 roles/host/templates/etc_crontab.j2 create mode 100644 roles/host/templates/etc_periodic.conf.j2 create mode 100644 roles/host/templates/etc_rc.conf.j2 create mode 100644 roles/host/templates/etc_resolv.conf.j2 create mode 100644 roles/host/templates/etc_ssh_sshd_config.j2 create mode 100644 roles/host/templates/jail.conf.j2 create mode 100644 roles/host/templates/usr_local_bin_logto.sh.j2 create mode 100644 roles/host/templates/usr_local_bin_pylogsentinel-batch-email.sh.j2 create mode 100644 roles/host/templates/usr_local_etc_pylogsentinel.conf.j2 create mode 100644 roles/host/templates/usr_local_etc_rc.d_zpool_gpt_labels.j2 create mode 100644 roles/host/templates/usr_local_etc_smartd.conf.j2 create mode 100644 roles/host_prod/handlers/main.yml create mode 100644 roles/host_prod/tasks/main.yml create mode 100644 roles/host_prod/templates/etc_aliases.j2 create mode 100644 roles/host_prod/templates/etc_dma_auth.conf.j2 create mode 100644 roles/host_prod/templates/etc_dma_dma.conf.j2 create mode 100644 roles/host_prod/templates/etc_fstab.j2 create mode 100644 roles/host_prod/templates/root_ssh_config.j2 create mode 100644 roles/host_prod/templates/usr_local_bin_backup.sh.j2 create mode 100644 roles/host_prod/templates/usr_local_bin_safepf.sh create mode 100644 roles/host_test/handlers/main.yml create mode 100644 roles/host_test/tasks/main.yml create mode 100644 roles/jail/handlers/main.yml create mode 100644 roles/jail/tasks/main.yml create mode 100644 roles/jail/templates/jail_conf.j2 create mode 100644 roles/jails/01_ingress/defaults/main.yml create mode 100644 roles/jails/01_ingress/tasks/main.yml create mode 100755 roles/jails/01_ingress/templates/acme-dns-auth.py create mode 100644 roles/jails/01_ingress/templates/etc_crontab.j2 create mode 100644 roles/jails/01_ingress/templates/etc_pf.conf.j2 create mode 100644 roles/jails/01_ingress/templates/usr_local_bin_gen_goaccess.sh.j2 create mode 100644 roles/jails/01_ingress/templates/usr_local_bin_hetzner_ddns.sh.j2 create mode 100644 roles/jails/01_ingress/templates/usr_local_etc_hetzner_auth.j2 create mode 100644 roles/jails/01_ingress/templates/usr_local_etc_nginx_nginx.conf.j2 create mode 100644 roles/jails/02_postgres/defaults/main.yml create mode 100644 roles/jails/02_postgres/tasks/main.yml create mode 100644 roles/jails/03_irc_thelounge/defaults/main.yml create mode 100644 roles/jails/03_irc_thelounge/tasks/main.yml create mode 100644 roles/jails/04_taulubot/defaults/main.yml create mode 100644 roles/jails/04_taulubot/tasks/main.yml create mode 100644 roles/jails/05_homepage/defaults/main.yml create mode 100644 roles/jails/05_homepage/tasks/main.yml create mode 100644 roles/jails/05_homepage/templates/etc_crontab.j2 create mode 100644 roles/jails/05_homepage/templates/usr_local_etc_nginx_nginx.conf.j2 create mode 100644 roles/jails/06_hommabot/defaults/main.yml create mode 100644 roles/jails/06_hommabot/tasks/main.yml create mode 100644 roles/jails/06_hommabot/templates/etc_crontab.j2 create mode 100644 roles/jails/06_hommabot/templates/root_hommabot_deps.sh create mode 100644 roles/jails/06_hommabot/templates/root_hommabot_env.j2 create mode 100644 roles/jails/07_aggro/defaults/main.yml create mode 100644 roles/jails/07_aggro/tasks/main.yml create mode 100644 roles/jails/08_diddle/defaults/main.yml create mode 100644 roles/jails/08_diddle/tasks/main.yml create mode 100644 roles/jails/08_diddle/templates/root_clone.sh create mode 100644 roles/jails/08_diddle/templates/root_diddle_env.j2 create mode 100644 roles/jails/08_diddle/templates/usr_local_bin_diddle create mode 100644 roles/jails/08_diddle/templates/usr_local_etc_rc.d_diddle create mode 100644 roles/jails/09_redis/defaults/main.yml create mode 100644 roles/jails/09_redis/tasks/main.yml create mode 100644 roles/jails/10_samba/defaults/main.yml create mode 100644 roles/jails/10_samba/tasks/main.yml create mode 100644 roles/jails/11_spliit/defaults/main.yml create mode 100644 roles/jails/11_spliit/tasks/main.yml create mode 100644 roles/jails/12_goaccess/defaults/main.yml create mode 100644 roles/jails/12_goaccess/tasks/main.yml create mode 100644 roles/jails/13_plex/defaults/main.yml create mode 100644 roles/jails/13_plex/tasks/main.yml create mode 100644 roles/jails/14_freshrss/defaults/main.yml create mode 100644 roles/jails/14_freshrss/tasks/main.yml create mode 100644 roles/jails/15_paste/defaults/main.yml create mode 100644 roles/jails/15_paste/tasks/main.yml create mode 100644 roles/jails/16_dl/defaults/main.yml create mode 100644 roles/jails/16_dl/tasks/main.yml create mode 100644 roles/jails/17_syncthing/defaults/main.yml create mode 100644 roles/jails/17_syncthing/tasks/main.yml create mode 100644 roles/jails/18_komga/defaults/main.yml create mode 100644 roles/jails/18_komga/tasks/main.yml create mode 100644 roles/jails/19_leolalla_fi/defaults/main.yml create mode 100644 roles/jails/19_leolalla_fi/tasks/main.yml create mode 100644 site.yml delete mode 100644 tasks/email.yml delete mode 100644 tasks/general.yml delete mode 100644 tasks/jail_diddle.yml delete mode 100644 tasks/jail_homepage.yml delete mode 100644 tasks/jail_hommabot.yml delete mode 100644 tasks/jail_ingress.yml delete mode 100644 tasks/jail_postgres.yml delete mode 100644 tasks/jails.yml delete mode 100644 tasks/jails_dataset.yml delete mode 100644 tasks/jails_single.yml delete mode 100644 tasks/network.yml delete mode 100644 tasks/pkg_jail_install.yml delete mode 100644 tasks/zfs.yml delete mode 100644 templates/diddle/root_diddle_env.j2 delete mode 100644 templates/diddle/usr_local_bin_diddle delete mode 100644 templates/diddle/usr_local_etc_rc.d_diddle delete mode 100644 templates/etc_aliases.j2 delete mode 100644 templates/etc_crontab.j2 delete mode 100644 templates/etc_devfs.rules.j2 delete mode 100644 templates/etc_dma_auth.conf.j2 delete mode 100644 templates/etc_dma_dma.conf.j2 delete mode 100644 templates/etc_fstab.j2 delete mode 100644 templates/etc_jail.conf.d_[jailname].conf.j2 delete mode 100644 templates/etc_jail.conf.j2 delete mode 100644 templates/etc_periodic.conf.j2 delete mode 100644 templates/etc_rc.conf.j2 delete mode 100644 templates/etc_resolv.conf.j2 delete mode 100644 templates/etc_ssh_sshd_config.j2 delete mode 100644 templates/homepage/etc_crontab.j2 delete mode 100644 templates/homepage/usr_local_etc_nginx_nginx.conf.j2 delete mode 100644 templates/hommabot/etc_crontab.j2 delete mode 100644 templates/hommabot/root_hommabot_deps.sh delete mode 100644 templates/hommabot/root_hommabot_env.j2 delete mode 100755 templates/ingress/acme-dns-auth.py delete mode 100644 templates/ingress/etc_crontab.j2 delete mode 100644 templates/ingress/etc_pf.conf.j2 delete mode 100644 templates/ingress/nginx_snippet_ban.inc delete mode 100644 templates/ingress/pf-ban-socket.py delete mode 100644 templates/ingress/usr_local_bin_gen_goaccess.sh.j2 delete mode 100644 templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 delete mode 100644 templates/ingress/usr_local_etc_hetzner_auth.j2 delete mode 100644 templates/ingress/usr_local_etc_nginx_nginx.conf.j2 delete mode 100644 templates/ingress/usr_local_etc_rc.d_pf_ban_socket delete mode 100644 templates/jail_confs/_default.j2 delete mode 100644 templates/jail_confs/_epl_network.j2 delete mode 100644 templates/jail_confs/dl.j2 delete mode 100644 templates/jail_confs/ingress.j2 delete mode 100644 templates/jail_confs/komga.j2 delete mode 100644 templates/jail_confs/plex.j2 delete mode 100644 templates/jail_confs/postgres.j2 delete mode 100644 templates/jail_confs/samba.j2 delete mode 100644 templates/jail_confs/syncthing.j2 delete mode 100644 templates/root_bashrc delete mode 100644 templates/root_clone.sh delete mode 100644 templates/root_ssh_config.j2 delete mode 100644 templates/usr_local_bin_backup.sh.j2 delete mode 100644 templates/usr_local_bin_logto.sh.j2 delete mode 100644 templates/usr_local_bin_pylogsentinel-batch-email.sh.j2 delete mode 100644 templates/usr_local_bin_safepf.sh delete mode 100644 templates/usr_local_etc_pylogsentinel.conf.j2 delete mode 100644 templates/usr_local_etc_rc.d_zpool_gpt_labels.j2 delete mode 100644 templates/usr_local_etc_smartd.conf.j2 diff --git a/.gitignore b/.gitignore index ec7629e..b24fdbd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .envrc secrets.yml vm/ +__pycache__/ diff --git a/README.md b/README.md index 389e728..b6d906f 100644 --- a/README.md +++ b/README.md @@ -37,10 +37,10 @@ Use a `secrets.yml` file to provide secrets for Ansible to use. Required variabl Run the playbook or only some tagged sub-playbooks: ```shell -ansible-playbook -i inventory playbook.yml -t [tag1] [tag2] ... +ansible-playbook -i inventory.prod site.yml ``` -See `playbook.yml` for available tags. +See `site.yml` for available tags. ## Author diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..803d273 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +roles_path = roles +connection_plugins = connection_plugins diff --git a/connection_plugins/__pycache__/jailexec.cpython-314.pyc b/connection_plugins/__pycache__/jailexec.cpython-314.pyc new file mode 100644 index 0000000..c1fe98b Binary files /dev/null and b/connection_plugins/__pycache__/jailexec.cpython-314.pyc differ diff --git a/connection_plugins/jailexec.py b/connection_plugins/jailexec.py new file mode 100644 index 0000000..4f4fab3 --- /dev/null +++ b/connection_plugins/jailexec.py @@ -0,0 +1,322 @@ +#!/usr/bin/env python3 +# Copyright (c) 2025 Christian Hofstede-Kuhn +# SPDX-License-Identifier: BSD-2-Clause + +"""FreeBSD jail connection plugin for Ansible. + +Opens an SSH session to a FreeBSD jail host (inheriting Ansible's built-in +ssh connection plugin) and wraps every command with ``jexec`` so Ansible +operates *inside* the target jail without needing direct SSH access to it. +""" + +from __future__ import annotations + +import os +import posixpath +import re +import shlex + +import yaml +from ansible.errors import AnsibleConnectionFailure, AnsibleError +from ansible.plugins.connection import ssh as _ssh_module +from ansible.plugins.connection.ssh import Connection as SSHConnection +from ansible.utils.display import Display + +display = Display() + +# Static stub so ``ansible-doc -t connection jailexec`` can read the plugin +# (ansible-doc parses the source file as AST and only understands literal +# strings). The full option set is built below and assigned over the top via +# ``globals()`` -- the AST walker only inspects ``ast.Assign`` nodes with a +# simple Name target, so a plain function-call *expression* statement is +# invisible to it. At runtime, the plugin loader reads the merged version. +DOCUMENTATION = """ + name: jailexec + short_description: Execute tasks in FreeBSD jails via jexec over SSH + description: + - Opens an SSH session to a FreeBSD jail host and wraps every command + with jexec so Ansible runs inside the target jail without needing + direct SSH into the jail. + - Inherits all options from the built-in ssh connection plugin. + author: Christian Hofstede-Kuhn + version_added: "1.1.0" + options: + jail_name: + description: Jail name. Defaults to the inventory hostname. + type: str + vars: + - name: ansible_jail_name + jail_host: + description: Hostname or IP of the FreeBSD host that runs the jail. + type: str + required: true + vars: + - name: ansible_jail_host + jail_root: + description: + - Absolute on-host filesystem path of the jail, used as the + base for put_file and fetch_file. + - If unset, the plugin probes the host with + ``jls -j path`` on the first file transfer. + - Set this for nested or VNET jail setups where the probe + does not return the expected path. + type: str + version_added: "1.2.0" + vars: + - name: ansible_jail_root + jail_user: + description: User to run commands as inside the jail. + type: str + default: root + vars: + - name: ansible_jail_user + privilege_escalation: + description: Command used on the jail host to run jexec as root. + type: str + default: doas + choices: [doas, sudo, none] + vars: + - name: ansible_jail_privilege_escalation +""" + + +def _extend_with_ssh_options(doc): + """Merge SSH plugin options into our DOCUMENTATION at import time. + + Pulling options from the live SSH plugin (rather than freezing a copy) + keeps us in sync with whichever ansible-core version is installed; newer + ansible-core releases have added options (e.g. ``password_mechanism``) + that older snapshots didn't know about, and a frozen list would cause + ``get_option`` to return None and trigger type errors downstream. + """ + ssh_doc = yaml.safe_load(_ssh_module.DOCUMENTATION) or {} + our_doc = yaml.safe_load(doc) or {} + merged = dict(ssh_doc.get("options") or {}) + merged.update(our_doc.get("options") or {}) + our_doc["options"] = merged + return yaml.safe_dump(our_doc, sort_keys=False) + + +globals().update(DOCUMENTATION=_extend_with_ssh_options(DOCUMENTATION)) + + +MAX_JAIL_NAME_LENGTH = 255 +JAIL_NAME_RE = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9._-]*$") +PRIVESC_CHOICES = ("doas", "sudo", "none") +# /tmp is on the remote jail host, not the Ansible controller. File names are +# randomized via ``os.urandom`` in ``put_file``, which defeats predictable-name +# attacks. Bandit's B108 check is about local-tmp usage and does not apply. +STAGING_DIR = "/tmp" # nosec B108 +STAGING_PREFIX = "ansible-jailexec-" + + +def validate_jail_name(name): + """Reject empty, overlong, or shell-unsafe jail names.""" + if not name or not str(name).strip(): + raise AnsibleConnectionFailure("Jail name cannot be empty") + name = str(name).strip() + if len(name) > MAX_JAIL_NAME_LENGTH: + raise AnsibleConnectionFailure( + f"Jail name too long (max {MAX_JAIL_NAME_LENGTH}): {name!r}" + ) + if not JAIL_NAME_RE.match(name): + raise AnsibleConnectionFailure( + f"Invalid jail name {name!r}: must start with a letter, digit or " + "underscore and contain only letters, digits, dots, underscores " + "or hyphens." + ) + return name + + +def ensure_no_traversal(path): + """Reject paths containing a ``..`` component (path traversal).""" + if path and ".." in path.replace("\\", "/").split("/"): + raise AnsibleError(f"Path contains '..' traversal: {path}") + + +def validate_jail_root(path): + """Normalize and validate a user-provided jail-root override. + + Must be a non-empty absolute POSIX path without any ``..`` components. + """ + path = (path or "").strip() + if not path: + raise AnsibleConnectionFailure("ansible_jail_root cannot be empty") + if not path.startswith("/"): + raise AnsibleConnectionFailure( + f"ansible_jail_root must be an absolute path, got {path!r}" + ) + ensure_no_traversal(path) + return posixpath.normpath(path) + + +def _decode(data): + """Return ``data`` as a str. Bytes are decoded leniently; None becomes ''.""" + if data is None: + return "" + if isinstance(data, bytes): + return data.decode("utf-8", "replace") + return data + + +def _shelljoin(*argv): + """Shell-join a command + args safely for transport over SSH.""" + return " ".join(shlex.quote(str(a)) for a in argv) + + +class Connection(SSHConnection): + """SSH to a jail host, run commands inside the jail via jexec.""" + + transport = "jailexec" + has_pipelining = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._jail_root = None + + # ---- options --------------------------------------------------------- + + @property + def jail_name(self): + name = self.get_option("jail_name") or self._play_context.remote_addr + return validate_jail_name(name) + + @property + def jail_user(self): + # Normalize None / blank / whitespace-only to "root". + return (self.get_option("jail_user") or "").strip() or "root" + + @property + def privesc(self): + # ansible-core >= 2.20 rejects off-``choices`` values at ``set_option`` + # time; older releases defer the check, so validate here too. + value = self.get_option("privilege_escalation") + if value not in PRIVESC_CHOICES: + raise AnsibleConnectionFailure( + f"Invalid privilege_escalation {value!r}: " + f"must be one of {', '.join(PRIVESC_CHOICES)}" + ) + if value == "none": + return "" + return value + + # ---- connect / lifecycle -------------------------------------------- + + def _connect(self): + if self._connected: + return self + + jail_host = (self.get_option("jail_host") or "").strip() + if not jail_host: + raise AnsibleConnectionFailure( + f"ansible_jail_host is not set for jail {self.jail_name!r}" + ) + # Redirect the inherited SSH plugin at the jail *host* instead of the + # jail (inventory) name. This is the one hook we need -- everything + # else comes from the SSH base class. + self.set_option("host", jail_host) + super()._connect() + # SSH's _connect is a no-op on _connected, but ConnectionBase's + # exec_command/put_file/fetch_file are wrapped with @ensure_connect, + # which re-enters self._connect() whenever _connected is False. We + # flip it here so the jail-root probe issued on first file op (via + # super().exec_command) doesn't recurse into us. + self._connected = True + return self + + def close(self): + self._jail_root = None + super().close() + + # ---- jail metadata --------------------------------------------------- + + def _resolve_jail_root(self): + """Look up and cache the on-host filesystem path of the jail. + + If ``ansible_jail_root`` is set, that value is used verbatim and no + SSH probe happens. Otherwise the path is resolved via + ``jls -j path`` on the first file operation, then cached. + """ + if self._jail_root: + return self._jail_root + + override = self.get_option("jail_root") + if override: + self._jail_root = validate_jail_root(override) + display.vvv( + f"jailexec: jail {self.jail_name!r} root is {self._jail_root} " + "(from ansible_jail_root)", + host=self.jail_name, + ) + return self._jail_root + + name = self.jail_name + rc, stdout, stderr = super().exec_command( + _shelljoin(*(([self.privesc] if self.privesc else []) + ["jls", "-j", name, "path"])) + ) + if rc != 0: + msg = _decode(stderr).strip() or "jail not found or inaccessible" + raise AnsibleConnectionFailure(f"Cannot access jail {name!r}: {msg}") + lines = _decode(stdout).strip().splitlines() + root = lines[0].strip() if lines else "" + if not root: + raise AnsibleConnectionFailure( + f"Jail {name!r} returned no filesystem root (is it running?)" + ) + self._jail_root = root + display.vvv(f"jailexec: jail {name!r} root is {root}", host=name) + return root + + def _jail_path(self, path): + """Map a path inside the jail to its absolute path on the host.""" + ensure_no_traversal(path) + root = self._resolve_jail_root() + return posixpath.normpath(posixpath.join(root, path.lstrip("/"))) + + # ---- exec / transfer ------------------------------------------------- + + def exec_command(self, cmd, in_data=None, sudoable=True): + if not cmd or not str(cmd).strip(): + raise AnsibleError("Command cannot be empty") + + argv = [self.privesc, "jexec"] if self.privesc else ["jexec"] + if self.jail_user != "root": + argv += ["-u", self.jail_user] + argv += [self.jail_name, "/bin/sh", "-c", cmd] + wrapped = _shelljoin(*argv) + + display.vvv(f"jailexec: exec [{self.jail_name}]: {cmd}", host=self.jail_name) + return super().exec_command(wrapped, in_data=in_data, sudoable=sudoable) + + def put_file(self, in_path, out_path): + dest = self._jail_path(out_path) + dest_dir = posixpath.dirname(dest) + staged = posixpath.join(STAGING_DIR, f"{STAGING_PREFIX}{os.urandom(12).hex()}") + + display.vvv( + f"jailexec: put_file {in_path} -> jail:{out_path}", host=self.jail_name + ) + super().put_file(in_path, staged) + # Single round-trip: mkdir + move. Both go through privilege + # escalation because the destination lives inside the jail root, + # which is typically only writable by root on the host. + pe = shlex.quote(self.privesc) + " " if self.privesc else "" + move = ( + f"{pe}mkdir -p {shlex.quote(dest_dir)} && " + f"{pe}mv {shlex.quote(staged)} {shlex.quote(dest)}" + ) + rc, _, stderr = super().exec_command(move) + if rc != 0: + # Best-effort cleanup of the orphan staged file; ignore failures. + super().exec_command(f"rm -f {shlex.quote(staged)}") + raise AnsibleError( + f"put_file to jail:{out_path} failed: " + f"{_decode(stderr).strip() or 'unknown error'}" + ) + + def fetch_file(self, in_path, out_path): + src = self._jail_path(in_path) + display.vvv( + f"jailexec: fetch_file jail:{in_path} -> {out_path}", host=self.jail_name + ) + super().fetch_file(src, out_path) diff --git a/filter_plugins/ipv4.py b/filter_plugins/ipv4.py new file mode 100644 index 0000000..1c42e82 --- /dev/null +++ b/filter_plugins/ipv4.py @@ -0,0 +1,51 @@ +"""Custom Jinja2 filters for IPv4 address math.""" +import struct +import socket + + +def _ip_to_int(ip): + return struct.unpack("!I", socket.inet_aton(ip))[0] + + +def _int_to_ip(n): + return socket.inet_ntoa(struct.pack("!I", n)) + + +def ipv4_network(cidr): + """Return the network address of a CIDR. '10.0.20.2/24' -> '10.0.20.0'""" + ip, prefix = cidr.split("/") + mask = (0xFFFFFFFF << (32 - int(prefix))) & 0xFFFFFFFF + return _int_to_ip(_ip_to_int(ip) & mask) + + +def ipv4_nth(cidr, n): + """Return the nth host address in a network. '10.0.20.0/24' | ipv4_nth(101) -> '10.0.20.101'""" + network = ipv4_network(cidr) + return _int_to_ip(_ip_to_int(network) + int(n)) + + +def ipv4_nth_cidr(cidr, n): + """Return the nth host address with the original prefix. '10.0.20.0/24' | ipv4_nth_cidr(101) -> '10.0.20.101/24'""" + _, prefix = cidr.split("/") + return ipv4_nth(cidr, n) + "/" + prefix + + +def ipv4_host(cidr): + """Return just the host part of a CIDR. '10.0.20.2/24' -> '10.0.20.2'""" + return cidr.split("/")[0] + + +def ipv4_prefixlen(cidr): + """Return just the prefix length. '10.0.20.2/24' -> '24'""" + return cidr.split("/")[1] + + +class FilterModule(object): + def filters(self): + return { + "ipv4_network": ipv4_network, + "ipv4_nth": ipv4_nth, + "ipv4_nth_cidr": ipv4_nth_cidr, + "ipv4_host": ipv4_host, + "ipv4_prefixlen": ipv4_prefixlen, + } diff --git a/group_vars/jails.yml b/group_vars/jails.yml new file mode 100644 index 0000000..990469c --- /dev/null +++ b/group_vars/jails.yml @@ -0,0 +1,5 @@ +ansible_connection: jailexec +ansible_jail_name: "{{ inventory_hostname }}" +ansible_python_interpreter: /usr/local/bin/python3 +ansible_shell_executable: /bin/sh +ansible_jail_privilege_escalation: none diff --git a/host_vars/pursotin.yml b/host_vars/pursotin.yml deleted file mode 100644 index 0f7e173..0000000 --- a/host_vars/pursotin.yml +++ /dev/null @@ -1,7 +0,0 @@ -nic_lan: igc0 -nic_wan: igc1 -arch: amd64/amd64 -is_test_vm: false -jail_lan_prefix: "192.168.2" -jail_lan_prefixlen: "16" -jail_ip_offset: 0 diff --git a/host_vars/test_vm.yml b/host_vars/test_vm.yml deleted file mode 100644 index 821f137..0000000 --- a/host_vars/test_vm.yml +++ /dev/null @@ -1,12 +0,0 @@ -ansible_host: 10.0.20.2 -ansible_user: root -ansible_ssh_private_key_file: vm/id_ed25519 -ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" -nic_lan: vtnet0 -nic_wan: vtnet1 -arch: arm64/aarch64 -is_test_vm: true -dns_nameserver: "10.0.20.1" -jail_lan_prefix: "10.0.20" -jail_lan_prefixlen: "24" -jail_ip_offset: 100 diff --git a/inventory b/inventory deleted file mode 100644 index 05bdd3a..0000000 --- a/inventory +++ /dev/null @@ -1,3 +0,0 @@ -[hosts] -pursotin -test_vm diff --git a/inventory/jails.py b/inventory/jails.py new file mode 100755 index 0000000..0458dc0 --- /dev/null +++ b/inventory/jails.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 +"""Dynamic inventory script that discovers jails from roles/jails/.""" +import json +import os +import re + +def main(): + # Resolve to project root (one level up from inventory/) + script_dir = os.path.dirname(os.path.realpath(__file__)) + project_root = os.path.dirname(script_dir) + jails_dir = os.path.join(project_root, "roles", "jails") + + hosts = [] + if os.path.isdir(jails_dir): + for entry in sorted(os.listdir(jails_dir)): + path = os.path.join(jails_dir, entry) + if os.path.isdir(path) and re.match(r"^\d+_", entry): + name = re.sub(r"^\d+_", "", entry) + hosts.append(name) + + inventory = { + "jails": { + "hosts": hosts, + }, + "_meta": { + "hostvars": {}, + }, + } + + print(json.dumps(inventory)) + +if __name__ == "__main__": + main() diff --git a/inventory/prod/hosts.yml b/inventory/prod/hosts.yml new file mode 100644 index 0000000..eff5ecb --- /dev/null +++ b/inventory/prod/hosts.yml @@ -0,0 +1,22 @@ +all: + children: + servers: + hosts: + pursotin: + host_roles: + - host + - host_prod + nic_lan: igc0 + nic_wan: igc1 + arch: amd64/amd64 + lan_ipv4_gateway: "192.168.0.1" + lan_ipv4_cidr: "192.168.0.10/16" + is_prod: true + jails: + vars: + ansible_jail_host: pursotin + jail_delegate_host: pursotin + jail_lan_cidr: "192.168.2.0/16" + jail_lan_offset: 0 + ssl_enabled: true + is_prod: true diff --git a/inventory/prod/jails.py b/inventory/prod/jails.py new file mode 120000 index 0000000..d036389 --- /dev/null +++ b/inventory/prod/jails.py @@ -0,0 +1 @@ +../jails.py \ No newline at end of file diff --git a/inventory/test/hosts.yml b/inventory/test/hosts.yml new file mode 100644 index 0000000..fdf982a --- /dev/null +++ b/inventory/test/hosts.yml @@ -0,0 +1,34 @@ +all: + children: + servers: + hosts: + test_vm: + ansible_host: 10.0.20.2 + ansible_user: root + ansible_ssh_private_key_file: vm/id_ed25519 + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityAgent=none" + host_roles: + - host + - host_test + nic_lan: vtnet0 + nic_wan: vtnet0 + arch: arm64/aarch64 + dns_nameserver: "10.0.20.1" + lan_ipv4_gateway: "10.0.20.1" + lan_ipv4_cidr: "10.0.20.2/24" + is_prod: false + jails: + vars: + ansible_host: 10.0.20.2 + ansible_jail_host: 10.0.20.2 + ansible_user: root + ansible_ssh_private_key_file: vm/id_ed25519 + ansible_ssh_common_args: "-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityAgent=none" + jail_delegate_host: test_vm + jail_lan_cidr: "10.0.20.0/24" + jail_lan_offset: 100 + ssl_enabled: false + is_prod: false + ingress_wan_static: "10.0.20.3/24" + dns_nameserver: "10.0.20.1" + lan_ipv4_gateway: "10.0.20.1" diff --git a/inventory/test/jails.py b/inventory/test/jails.py new file mode 120000 index 0000000..d036389 --- /dev/null +++ b/inventory/test/jails.py @@ -0,0 +1 @@ +../jails.py \ No newline at end of file diff --git a/playbook.yml b/playbook.yml deleted file mode 100644 index 9269a44..0000000 --- a/playbook.yml +++ /dev/null @@ -1,109 +0,0 @@ ---- -- name: Configure my FreeBSD home server - hosts: hosts - - pre_tasks: - - name: Fail if secrets.yml wasn't loaded and smtp_user is missing - assert: - that: - - smtp_user != '' - fail_msg: > - Required variables are missing. Did you forget to create a secrets.yml file? - vars: - ansible_assert_no_log: true - - vars_files: - - secrets.yml - vars: - lan_ipv4_cidr: 192.168.0.10/16 - lan_ipv4_network: 192.168.0.0/16 - lan_ipv4_gateway: 192.168.0.1 - lan_search_domain: local.jan.systems - jail_userland_14_3: 14.3-RELEASE - jail_userland_15_0: 15.0-RELEASE - jails: - - { userland: "{{ jail_userland_14_3 }}", num: 1, name: ingress } - - { userland: "{{ jail_userland_14_3 }}", num: 2, name: postgres } - - { userland: "{{ jail_userland_14_3 }}", num: 3, name: irc_thelounge } - - { userland: "{{ jail_userland_14_3 }}", num: 4, name: taulubot } - - { userland: "{{ jail_userland_14_3 }}", num: 5, name: homepage } - - { userland: "{{ jail_userland_14_3 }}", num: 6, name: hommabot } - - { userland: "{{ jail_userland_14_3 }}", num: 7, name: aggro } - - { userland: "{{ jail_userland_14_3 }}", num: 8, name: diddle } - - { userland: "{{ jail_userland_15_0 }}", num: 9, name: redis } - - { userland: "{{ jail_userland_15_0 }}", num: 10, name: samba } - - { userland: "{{ jail_userland_14_3 }}", num: 11, name: spliit } - - { userland: "{{ jail_userland_15_0 }}", num: 12, name: goaccess } - - { userland: "{{ jail_userland_15_0 }}", num: 13, name: plex } - - { userland: "{{ jail_userland_14_3 }}", num: 14, name: freshrss } - - { userland: "{{ jail_userland_14_3 }}", num: 15, name: paste } - - { userland: "{{ jail_userland_15_0 }}", num: 16, name: dl } - - { userland: "{{ jail_userland_15_0 }}", num: 17, name: syncthing } - - { userland: "{{ jail_userland_15_0 }}", num: 18, name: komga } - - { userland: "{{ jail_userland_15_0 }}", num: 19, name: leolalla_fi } - ingress_ip: "{{ jail_lan_prefix }}.{{ 1 + jail_ip_offset }}" - ingress_routes: - - { host: jan.systems, jail: homepage } - - { host: jantuomi.fi, redirect: jan.systems } - - { host: aggro.jan.systems, jail: aggro } - - { host: diddle.jan.systems, jail: diddle } - - { host: spliit.jan.systems, jail: spliit } - - { host: freshrss.jan.systems, jail: freshrss } - - { host: irc.jan.systems, jail: irc_thelounge } - - { host: paste.jan.systems, jail: paste } - - { host: leolalla.fi, jail: leolalla_fi } - - { host: immich.jan.systems, ip: 192.168.3.3, port: 2283 } # vms aren't configured in ansible - - { host: plex.jan.systems, jail: plex, port: 32400 } - - { host: komga.jan.systems, jail: komga, port: 25600 } - cert_domains: - - "jan.systems" - - "*.jan.systems" - - "jantuomi.fi" - - "*.jantuomi.fi" - - "leolalla.fi" - - "*.leolalla.fi" - cert_name: "{{ cert_domains[0] }}" - contact_email: jan@jantuomi.fi - - tasks: - - name: Run general tasks - tags: [general] - import_tasks: tasks/general.yml - - - name: Run network tasks - tags: [network] - import_tasks: tasks/network.yml - - - name: Run email tasks - tags: [email] - import_tasks: tasks/email.yml - when: not is_test_vm - - - name: Run ZFS tasks - tags: [zfs] - import_tasks: tasks/zfs.yml - when: not is_test_vm - - - name: Run common jails tasks - tags: [jails] - import_tasks: tasks/jails.yml - - - name: Run ingress jail tasks - tags: [jail_ingress] - import_tasks: tasks/jail_ingress.yml - - - name: Run postgres jail tasks - tags: [jail_postgres] - import_tasks: tasks/jail_postgres.yml - - - name: Run diddle jail tasks - tags: [jail_diddle] - import_tasks: tasks/jail_diddle.yml - - - name: Run hommabot jail tasks - tags: [jail_hommabot] - import_tasks: tasks/jail_hommabot.yml - - - name: Run homepage jail tasks - tags: [jail_homepage] - import_tasks: tasks/jail_homepage.yml diff --git a/principles.md b/principles.md deleted file mode 100644 index 245846c..0000000 --- a/principles.md +++ /dev/null @@ -1,7 +0,0 @@ -# Migration principles - -1. Jail definitions should be localized, e.g. jails/ingress.yml. A jail definition should define zfs mounts, nullfs mounts, files to install, networking scripts (pre_start etc) to insert into jail.conf. -2. Jails should be defined as hosts in the inventory file. To reach them, ansible should use a ssh conn to the host and then `jexec $jailname`. -3. Each structure in the jail definition yml (e.g. `pkg: ["nginx"]`) should be handled by an ansible role that activates when a relevant structure is defined in the jail definition. -4. Both the host (pursotin) and the jails inside it should be defined in this project, so ansible should be configured to use correct hosts and correct connection methods depending on host. -5. The main playbooks should be very light and just dispatch. diff --git a/reset-init-up.sh b/reset-init-up.sh new file mode 100755 index 0000000..d16485c --- /dev/null +++ b/reset-init-up.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +set -euxo pipefail + +./vm.sh reset +./vm.sh init +./vm.sh up + diff --git a/roles/host/files/root_bashrc b/roles/host/files/root_bashrc new file mode 100644 index 0000000..f580db6 --- /dev/null +++ b/roles/host/files/root_bashrc @@ -0,0 +1,7 @@ +HISTCONTROL=ignoredups:erasedups # no duplicate entries +HISTSIZE=100000 # big big history +HISTFILESIZE=100000 # big big history +shopt -s histappend # append to history, don't overwrite it + +# Save and reload the history after each command finishes +PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND" diff --git a/roles/host/handlers/main.yml b/roles/host/handlers/main.yml new file mode 100644 index 0000000..5d0a990 --- /dev/null +++ b/roles/host/handlers/main.yml @@ -0,0 +1,9 @@ +- name: Restart sshd + service: + name: sshd + state: restarted + +- name: Restart cron + service: + name: cron + state: restarted diff --git a/roles/host/tasks/main.yml b/roles/host/tasks/main.yml new file mode 100644 index 0000000..ed9b476 --- /dev/null +++ b/roles/host/tasks/main.yml @@ -0,0 +1,217 @@ +- name: Disable resolvconf + copy: + content: "resolvconf=NO\n" + dest: /etc/resolvconf.conf + owner: root + group: wheel + mode: "0644" + +- name: Set up resolv.conf + template: + src: etc_resolv.conf.j2 + dest: /etc/resolv.conf + owner: root + group: wheel + mode: "0644" + +- name: Install packages + package: + name: "{{ item }}" + state: present + loop: + - rsync + - dma + - jq + - curl + - bash + - python + - py311-pip + - fastfetch + +- name: Set up sshd + template: + src: etc_ssh_sshd_config.j2 + dest: /etc/ssh/sshd_config + owner: root + group: wheel + mode: "0644" + notify: Restart sshd + +- name: Start sshd + service: + name: sshd + state: started + +- name: Deploy logto script + template: + src: usr_local_bin_logto.sh.j2 + dest: /usr/local/bin/logto + owner: root + group: wheel + mode: "0755" + +- name: Check if pylogsentinel is installed + shell: pip show pylogsentinel + register: pylogsentinel_check + failed_when: false + changed_when: false + +- name: Install pylogsentinel + shell: pip install pylogsentinel==0.3.0 --force --no-input + when: pylogsentinel_check.rc != 0 + +- name: Deploy pylogsentinel.conf + template: + src: usr_local_etc_pylogsentinel.conf.j2 + dest: /usr/local/etc/pylogsentinel.conf + owner: root + group: wheel + mode: "0644" + +- name: Deploy pylogsentinel-batch-email.sh + template: + src: usr_local_bin_pylogsentinel-batch-email.sh.j2 + dest: /usr/local/bin/pylogsentinel-batch-email.sh + owner: root + group: wheel + mode: "0755" + +- name: Start syslogd + service: + name: syslogd + state: started + +- name: Start auditd + service: + name: auditd + state: started + +- name: Set up periodic.conf + template: + src: etc_periodic.conf.j2 + dest: /etc/periodic.conf + owner: root + group: wheel + mode: "0644" + +- name: Set up crontab + template: + src: etc_crontab.j2 + dest: /etc/crontab + owner: root + group: wheel + mode: "0644" + notify: Restart cron + +- name: Install .bashrc + copy: + src: root_bashrc + dest: /root/.bashrc + owner: root + group: wheel + mode: "0644" + +# Jail infrastructure +- name: Discover jail directories + find: + paths: "{{ playbook_dir }}/roles/jails" + patterns: "main.yml" + recurse: true + delegate_to: localhost + register: _jail_specs + +- name: Load jail definitions + set_fact: + jail_defs: "{{ jail_defs | default([]) + [_content | combine({'num': _num, 'name': _name})] }}" + vars: + _content: "{{ lookup('file', item.path) | from_yaml }}" + _num: "{{ item.path | regex_replace('.*/jails/([^/]+)/.*', '\\1') | split('_') | first | int }}" + _name: "{{ item.path | regex_replace('.*/jails/([^/]+)/.*', '\\1') | regex_replace('^[0-9]+_', '') }}" + loop: "{{ _jail_specs.files | sort(attribute='path') }}" + loop_control: + label: "{{ item.path | regex_replace('.*/jails/([^/]+)/.*', '\\1') }}" + when: "'/defaults/' in item.path" + +- name: Collect unique userlands + set_fact: + jail_userlands: "{{ jail_defs | map(attribute='userland') | unique | list }}" + +- name: Create base ZFS datasets + community.general.zfs: + name: "{{ item.name }}" + state: present + extra_zfs_properties: + mountpoint: "{{ item.mountpoint | default(omit) }}" + loop: + - { name: "zroot/jails", mountpoint: "/usr/local/jails" } + - { name: "zroot/jails/media" } + - { name: "zroot/jails/templates" } + - { name: "zroot/jails/containers" } + - { name: "zroot/jails/volumes", mountpoint: "none" } + - { name: "zroot/jails/volumes/goaccess_www", mountpoint: "/usr/local/jails/volumes/goaccess_www" } + - { name: "zroot/jails/volumes/postgres_data", mountpoint: "/usr/local/jails/containers/postgres/var/db/postgres" } + - { name: "zroot/jails/volumes/irc_thelounge_logs", mountpoint: "/usr/local/jails/containers/irc_thelounge/root/.thelounge/logs" } + - { name: "zroot/jails/volumes/irc_thelounge_uploads", mountpoint: "/usr/local/jails/containers/irc_thelounge/root/.thelounge/uploads" } + - { name: "zroot/jails/volumes/komga_data", mountpoint: "/usr/local/jails/containers/komga/root/.komga" } + - { name: "zroot/storage", mountpoint: "/usr/local/jails/volumes/storage" } + loop_control: + label: "{{ item.name }}" + +- name: Create storage group + group: + name: storage + gid: 1001 + +- name: Create storage user + user: + name: storage + uid: 1001 + group: storage + home: /nonexistent + shell: /usr/sbin/nologin + create_home: false + +- name: Set storage volume permissions + file: + path: /usr/local/jails/volumes/storage + state: directory + owner: "1001" + group: "1001" + mode: "0777" + +- name: Create storage directories + file: + path: "/usr/local/jails/volumes/storage/{{ item.name }}" + state: directory + owner: "1001" + group: "1001" + mode: "{{ item.mode }}" + loop: + - { name: media, mode: "0777" } + - { name: docs, mode: "0775" } + - { name: downloads, mode: "0777" } + - { name: projects-ableton, mode: "0755" } + - { name: vault, mode: "0755" } + - { name: jan-systems-2025-content, mode: "0755" } + +- name: Set up userland templates + include_tasks: userland.yml + loop: "{{ jail_userlands }}" + loop_control: + loop_var: userland + +- name: Deploy /etc/jail.conf + template: + src: jail.conf.j2 + dest: /etc/jail.conf + owner: root + group: wheel + mode: "0644" + +- name: Deploy devfs.rules + template: + src: devfs.rules.j2 + dest: /etc/devfs.rules + owner: root + group: wheel + mode: "0644" diff --git a/roles/host/tasks/userland.yml b/roles/host/tasks/userland.yml new file mode 100644 index 0000000..59de0dd --- /dev/null +++ b/roles/host/tasks/userland.yml @@ -0,0 +1,59 @@ +- name: "Create template dataset for {{ userland }}" + community.general.zfs: + name: "zroot/jails/templates/{{ userland }}" + state: present + +- name: "Check if {{ userland }} snapshot exists" + shell: zfs list -t snapshot -o name | grep -Fxq "zroot/jails/templates/{{ userland }}@base" + failed_when: false + changed_when: false + register: userland_snap + +- name: "Set up {{ userland }} template" + when: userland_snap.rc != 0 + block: + - name: Download userland + get_url: + url: "https://download.freebsd.org/ftp/releases/{{ arch }}/{{ userland }}/base.txz" + dest: "/usr/local/jails/media/{{ userland }}-base.txz" + owner: root + group: wheel + mode: "0644" + + - name: Unarchive userland + shell: "chflags -R noschg /usr/local/jails/templates/{{ userland }} 2>/dev/null; tar -xzf /usr/local/jails/media/{{ userland }}-base.txz -C /usr/local/jails/templates/{{ userland }}" + + - name: Copy localtime + copy: + remote_src: true + src: /etc/localtime + dest: "/usr/local/jails/templates/{{ userland }}/etc/localtime" + + - name: Deploy resolv.conf + template: + src: etc_resolv.conf.j2 + dest: "/usr/local/jails/templates/{{ userland }}/etc/resolv.conf" + owner: root + group: wheel + mode: "0644" + + - name: Disable resolvconf + lineinfile: + path: "/usr/local/jails/templates/{{ userland }}/etc/resolvconf.conf" + line: "resolvconf=NO" + create: true + + - name: Update userland + shell: "freebsd-update -b /usr/local/jails/templates/{{ userland }}/ fetch install" + + - name: Disable syslogd + lineinfile: + path: "/usr/local/jails/templates/{{ userland }}/etc/rc.conf" + line: 'syslogd_enable="NO"' + create: true + + - name: Install packages in template + shell: "ASSUME_ALWAYS_YES=yes pkg -c /usr/local/jails/templates/{{ userland }} install -y python3 bash" + + - name: Create snapshot + shell: "zfs snapshot zroot/jails/templates/{{ userland }}@base" diff --git a/roles/host/templates/devfs.rules.j2 b/roles/host/templates/devfs.rules.j2 new file mode 100644 index 0000000..9fc0bd1 --- /dev/null +++ b/roles/host/templates/devfs.rules.j2 @@ -0,0 +1,69 @@ +[devfsrules_hide_all=1] +add hide + +[devfsrules_unhide_basic=2] +add path null unhide +add path zero unhide +add path crypto unhide +add path random unhide +add path urandom unhide + +[devfsrules_unhide_login=3] +add path 'ptyp*' unhide +add path 'ptyq*' unhide +add path 'ptyr*' unhide +add path 'ptys*' unhide +add path 'ptyP*' unhide +add path 'ptyQ*' unhide +add path 'ptyR*' unhide +add path 'ptyS*' unhide +add path 'ptyl*' unhide +add path 'ptym*' unhide +add path 'ptyn*' unhide +add path 'ptyo*' unhide +add path 'ptyL*' unhide +add path 'ptyM*' unhide +add path 'ptyN*' unhide +add path 'ptyO*' unhide +add path 'ttyp*' unhide +add path 'ttyq*' unhide +add path 'ttyr*' unhide +add path 'ttys*' unhide +add path 'ttyP*' unhide +add path 'ttyQ*' unhide +add path 'ttyR*' unhide +add path 'ttyS*' unhide +add path 'ttyl*' unhide +add path 'ttym*' unhide +add path 'ttyn*' unhide +add path 'ttyo*' unhide +add path 'ttyL*' unhide +add path 'ttyM*' unhide +add path 'ttyN*' unhide +add path 'ttyO*' unhide +add path ptmx unhide +add path pts unhide +add path 'pts/*' unhide +add path fd unhide +add path 'fd/*' unhide +add path stdin unhide +add path stdout unhide +add path stderr unhide +add path 'drm' unhide +add path 'drm/*' unhide +add path 'dri' unhide +add path 'dri/*' unhide + +[devfsrules_jail=4] +add include $devfsrules_hide_all +add include $devfsrules_unhide_basic +add include $devfsrules_unhide_login + +[devfsrules_jail_postgres=5] +add include $devfsrules_jail +add path 'bpf*' unhide + +[devfsrules_jail_ingress=6] +add include $devfsrules_jail +add path 'bpf*' unhide +add path 'pf*' unhide diff --git a/roles/host/templates/etc_crontab.j2 b/roles/host/templates/etc_crontab.j2 new file mode 100644 index 0000000..de1f3c6 --- /dev/null +++ b/roles/host/templates/etc_crontab.j2 @@ -0,0 +1,32 @@ +# /etc/crontab - root's crontab for FreeBSD +# +# +SHELL=/bin/sh +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin +# +#minute hour mday month wday who command +# +# Save some entropy so that /dev/random can re-seed on boot. +*/11 * * * * operator /usr/libexec/save-entropy +# +# Rotate log files every hour, if necessary. +0 * * * * root newsyslog +# +# Perform daily/weekly/monthly maintenance. +1 3 * * * root periodic daily +15 4 * * 6 root periodic weekly +30 5 1 * * root periodic monthly +# +# Adjust the time zone if the CMOS clock keeps local time, as opposed to +# UTC time. See adjkerntz(8) for details. +1,31 0-5 * * * root adjkerntz -a + +# Take snapshots every day at 3:05 AM +5 3 * * * root logto /var/log/backup /usr/local/bin/backup snapshot prune-local +# Send backup snapshots to remote every third day at 4:05 AM +5 4 */3 * * root logto /var/log/backup /usr/local/bin/backup send-to-remote prune-remote notify + +# Run pylogmonitor +*/10 * * * * root python -m pylogsentinel +# Run pylogsentinel batch job once a day +5 6 * * * root /usr/local/bin/pylogsentinel-batch-email.sh diff --git a/roles/host/templates/etc_periodic.conf.j2 b/roles/host/templates/etc_periodic.conf.j2 new file mode 100644 index 0000000..176b55d --- /dev/null +++ b/roles/host/templates/etc_periodic.conf.j2 @@ -0,0 +1,4 @@ +daily_status_smart_enable="YES" +daily_status_security_inline="YES" +daily_status_zfs_enable="YES" +weekly_certbot_enable="YES" diff --git a/roles/host/templates/etc_rc.conf.j2 b/roles/host/templates/etc_rc.conf.j2 new file mode 100644 index 0000000..7cdf65e --- /dev/null +++ b/roles/host/templates/etc_rc.conf.j2 @@ -0,0 +1,50 @@ +hostname="pursotin" +keymap="fi.kbd" +sshd_enable="YES" +ntpd_enable="YES" +ntpd_sync_on_start="YES" +powerd_enable="YES" +moused_nondefault_enable="NO" +dumpdev="AUTO" +zfs_enable="YES" +defaultrouter="{{ lan_ipv4_gateway }}" +cloned_interfaces="bridge0 bridge1" +{% if is_prod %} +ifconfig_{{ nic_lan }}_name="lan0" +ifconfig_{{ nic_wan }}_name="wan0" +ifconfig_bridge0_name="brlan0" +ifconfig_bridge1_name="brwan0" +ifconfig_lan0="inet {{ lan_ipv4_cidr }}" +ifconfig_lan0_ipv6="inet6 accept_rtadv" +ifconfig_wan0="up" +ifconfig_brlan0="addm lan0 up" +ifconfig_brwan0="addm wan0 up" +{% else %} +ifconfig_bridge0_name="brlan0" +ifconfig_bridge1_name="brwan0" +ifconfig_vtnet0="inet {{ lan_ipv4_cidr }}" +ifconfig_vtnet0_ipv6="inet6 accept_rtadv" +ifconfig_brlan0="addm vtnet0 up" +ifconfig_brwan0="up" +{% endif %} +zpool_gpt_labels_enable="YES" +smartd_enable="YES" +pf_enable="YES" +syslogd_enable="YES" +syslogd_flags="-s" +jail_enable="YES" +jail_parallel_start="YES" +jail_reverse_stop="YES" +clear_tmp_enable="YES" +cleanvar_enable="YES" +auditd_enable="YES" +devd_enable="YES" +{% if is_prod %} +vm_enable="YES" +vm_dir="zfs:zroot/vm" +vm_list="alpine0" +vm_delay="5" +kld_list="pf if_wg i915kms" +{% else %} +kld_list="pf" +{% endif %} diff --git a/roles/host/templates/etc_resolv.conf.j2 b/roles/host/templates/etc_resolv.conf.j2 new file mode 100644 index 0000000..b382ecf --- /dev/null +++ b/roles/host/templates/etc_resolv.conf.j2 @@ -0,0 +1,2 @@ +search {{ lan_search_domain }} +nameserver {{ dns_nameserver | default(lan_ipv4_gateway) }} diff --git a/roles/host/templates/etc_ssh_sshd_config.j2 b/roles/host/templates/etc_ssh_sshd_config.j2 new file mode 100644 index 0000000..06179ce --- /dev/null +++ b/roles/host/templates/etc_ssh_sshd_config.j2 @@ -0,0 +1,121 @@ +# $OpenBSD: sshd_config,v 1.104 2021/07/02 05:11:21 dtucker Exp $ + +# This is the sshd server system-wide configuration file. See +# sshd_config(5) for more information. + +# This sshd was compiled with PATH=/usr/bin:/bin:/usr/sbin:/sbin + +# The strategy used for options in the default sshd_config shipped with +# OpenSSH is to specify options with their default value where +# possible, but leave them commented. Uncommented options override the +# default value. + +# Note that some of FreeBSD's defaults differ from OpenBSD's, and +# FreeBSD has a few additional options. + +#Port 22 +#AddressFamily any +#ListenAddress 0.0.0.0 +#ListenAddress :: + +#HostKey /etc/ssh/ssh_host_rsa_key +#HostKey /etc/ssh/ssh_host_ecdsa_key +#HostKey /etc/ssh/ssh_host_ed25519_key + +# Ciphers and keying +#RekeyLimit default none + +# Logging +#SyslogFacility AUTH +#LogLevel INFO + +# Authentication: + +#LoginGraceTime 2m +PermitRootLogin prohibit-password +#StrictModes yes +#MaxAuthTries 6 +#MaxSessions 10 + +#PubkeyAuthentication yes + +# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 +# but this is overridden so installations will only check .ssh/authorized_keys +AuthorizedKeysFile .ssh/authorized_keys + +#AuthorizedPrincipalsFile none + +#AuthorizedKeysCommand none +#AuthorizedKeysCommandUser nobody + +# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts +#HostbasedAuthentication no +# Change to yes if you don't trust ~/.ssh/known_hosts for +# HostbasedAuthentication +#IgnoreUserKnownHosts no +# Don't read the user's ~/.rhosts and ~/.shosts files +#IgnoreRhosts yes + +# Change to yes to enable built-in password authentication. +# Note that passwords may also be accepted via KbdInteractiveAuthentication. +PasswordAuthentication no +#PermitEmptyPasswords no + +# Change to no to disable PAM authentication +#KbdInteractiveAuthentication yes + +# Kerberos options +#KerberosAuthentication no +#KerberosOrLocalPasswd yes +#KerberosTicketCleanup yes +#KerberosGetAFSToken no + +# GSSAPI options +#GSSAPIAuthentication no +#GSSAPICleanupCredentials yes + +# Set this to 'no' to disable PAM authentication, account processing, +# and session processing. If this is enabled, PAM authentication will +# be allowed through the KbdInteractiveAuthentication and +# PasswordAuthentication. Depending on your PAM configuration, +# PAM authentication via KbdInteractiveAuthentication may bypass +# the setting of "PermitRootLogin prohibit-password". +# If you just want the PAM account and session checks to run without +# PAM authentication, then enable this but set PasswordAuthentication +# and KbdInteractiveAuthentication to 'no'. +#UsePAM yes + +#AllowAgentForwarding yes +#AllowTcpForwarding yes +#GatewayPorts no +#X11Forwarding no +#X11DisplayOffset 10 +#X11UseLocalhost yes +#PermitTTY yes +#PrintMotd yes +#PrintLastLog yes +#TCPKeepAlive yes +#PermitUserEnvironment no +#Compression delayed +#ClientAliveInterval 0 +#ClientAliveCountMax 3 +#UseDNS yes +#PidFile /var/run/sshd.pid +#MaxStartups 10:30:100 +#PermitTunnel no +#ChrootDirectory none +#UseBlacklist no +#VersionAddendum FreeBSD-20250219 + +# no default banner path +#Banner none + +# override default of no subsystems +Subsystem sftp /usr/libexec/sftp-server + +# Example of overriding settings on a per-user basis +#Match User anoncvs +# X11Forwarding no +# AllowTcpForwarding no +# PermitTTY no +# ForceCommand cvs server diff --git a/roles/host/templates/jail.conf.j2 b/roles/host/templates/jail.conf.j2 new file mode 100644 index 0000000..81bfbad --- /dev/null +++ b/roles/host/templates/jail.conf.j2 @@ -0,0 +1 @@ +.include "/etc/jail.conf.d/*.conf"; diff --git a/roles/host/templates/usr_local_bin_logto.sh.j2 b/roles/host/templates/usr_local_bin_logto.sh.j2 new file mode 100644 index 0000000..9bb5aa1 --- /dev/null +++ b/roles/host/templates/usr_local_bin_logto.sh.j2 @@ -0,0 +1,47 @@ +#!/bin/sh + +set -ue + +# Wrapper for logging to file and prepending a timestamp. +# By default writes both stdout and stderr to the log file. + +usage() { + echo "Usage: logto [-s|-e] " + echo "Flags:" + echo " -s: Write only stdout to the log file." + echo " -e: Write only stderr to the log file." + echo "" + echo "Example usage:" + echo " logto /var/log/my.log run some command" + exit 1 +} + +mode="all" + +while getopts "se" opt; do + case $opt in + s) mode="stdout" ;; + e) mode="stderr" ;; + *) usage ;; + esac +done +shift $((OPTIND-1)) + +if [ $# -lt 2 ]; then + usage +fi + +log_file="$1" +shift + +if [ "$mode" = "stdout" ]; then + out=$(2>/dev/null $@) +elif [ "$mode" = "stderr" ]; then + out=$(2>&1 >/dev/null $@) +else + out=$(2>&1 $@) +fi + +if [ ! -z "$out" ]; then + echo "$(date +"%Y-%m-%dT%H:%M:%S%z")" "$out" >>"$log_file" +fi diff --git a/roles/host/templates/usr_local_bin_pylogsentinel-batch-email.sh.j2 b/roles/host/templates/usr_local_bin_pylogsentinel-batch-email.sh.j2 new file mode 100644 index 0000000..d28efd9 --- /dev/null +++ b/roles/host/templates/usr_local_bin_pylogsentinel-batch-email.sh.j2 @@ -0,0 +1,14 @@ +#!/bin/sh + +FILE=/tmp/pylogsentinel.daily + +if [ ! -f "$FILE" ]; then + echo "Nothing to do" + exit 0 +fi + +echo "Sending email" +cat "$FILE" | mail -s "pylogsentinel alert batch" root + +echo "Removing batch file" +rm "$FILE" diff --git a/roles/host/templates/usr_local_etc_pylogsentinel.conf.j2 b/roles/host/templates/usr_local_etc_pylogsentinel.conf.j2 new file mode 100644 index 0000000..cb80742 --- /dev/null +++ b/roles/host/templates/usr_local_etc_pylogsentinel.conf.j2 @@ -0,0 +1,22 @@ +[system] +state_dir = /var/run/pylogsentinel +max_block_size = 10M + +[logs.standard] +cmd = find / -type f -path '*/var/log/*' ! -name '*access.log' ! -name '*.bz2' ! -name 'dmesg*' ! -name 'syncthing*' + +[logs.access] +paths = /usr/local/jails/containers/ingress/var/log/nginx/access.log + +[action.default] +cmd = echo -e "---------\nMatched $RULE_ID in $FILE at line $LINE, context:\n\n$CONTEXT\n" >> /tmp/pylogsentinel.daily + +[rule.error] +description = Error-like conditions +pattern = /(error|fatal|exception|killed)/i +logs = standard + +[rule.access] +description = HTTP code >=500 in access log +pattern = /HTTP\/[0-9].[0-9]" 5[0-9][0-9]/ +logs = access diff --git a/roles/host/templates/usr_local_etc_rc.d_zpool_gpt_labels.j2 b/roles/host/templates/usr_local_etc_rc.d_zpool_gpt_labels.j2 new file mode 100644 index 0000000..36bc715 --- /dev/null +++ b/roles/host/templates/usr_local_etc_rc.d_zpool_gpt_labels.j2 @@ -0,0 +1,33 @@ +#!/bin/sh + +# PROVIDE: zpool_gpt_labels +# REQUIRE: zfs +# BEFORE: LOGIN +# KEYWORD: nojail + +. /etc/rc.subr + +name="zpool_gpt_labels" +rcvar="zpool_gpt_labels_enable" +start_cmd="zpool_gpt_labels_start" + +zpool_gpt_labels_start() { + logger "Setting zpool vdev paths to GPT labels" + local zpool_status="$(zpool status)" + for i in 0 1 2 3; do + local raw="nda${i}p4.eli" + local path="/dev/gpt/zfs${i}.eli" + case "$zpool_status" in + *"$raw"*) + zpool set "path=${path}" zroot "${raw}" + logger "Setting ${raw} -> ${path}" + ;; + *) + logger "Skipping ${raw}..." + ;; + esac + done +} + +load_rc_config $name +run_rc_command "$1" diff --git a/roles/host/templates/usr_local_etc_smartd.conf.j2 b/roles/host/templates/usr_local_etc_smartd.conf.j2 new file mode 100644 index 0000000..bdcbfd0 --- /dev/null +++ b/roles/host/templates/usr_local_etc_smartd.conf.j2 @@ -0,0 +1,4 @@ +/dev/nvme0 -a -d nvme +/dev/nvme1 -a -d nvme +/dev/nvme2 -a -d nvme +/dev/nvme3 -a -d nvme diff --git a/roles/host_prod/handlers/main.yml b/roles/host_prod/handlers/main.yml new file mode 100644 index 0000000..76ec23e --- /dev/null +++ b/roles/host_prod/handlers/main.yml @@ -0,0 +1,13 @@ +- name: Apply rc.conf + shell: service kld start; service netif restart && service routing restart + +- name: Mount filesystems + shell: mount -a + +- name: Restart smartd + service: + name: smartd + state: restarted + +- name: Run newaliases + shell: newaliases diff --git a/roles/host_prod/tasks/main.yml b/roles/host_prod/tasks/main.yml new file mode 100644 index 0000000..ec635ce --- /dev/null +++ b/roles/host_prod/tasks/main.yml @@ -0,0 +1,153 @@ +- name: Set up /etc/rc.conf + template: + src: "{{ playbook_dir }}/roles/host/templates/etc_rc.conf.j2" + dest: /etc/rc.conf + owner: root + group: wheel + mode: "0644" + vars: + is_prod: true + notify: Apply rc.conf + +- name: Flush handlers + meta: flush_handlers + +- name: Install hardware packages + package: + name: "{{ item }}" + state: present + loop: + - vm-bhyve + - drm-kmod + +- name: Set up fstab + template: + src: etc_fstab.j2 + dest: /etc/fstab + owner: root + group: wheel + mode: "0644" + notify: Mount filesystems + +- name: Set up bhyve + block: + - name: Create vm dataset + community.general.zfs: + name: zroot/vm + state: present + + - name: Check if vm-bhyve is initialized + stat: + path: /zroot/vm/.config + register: vm_init_check + + - name: Run vm init + shell: vm init + when: not vm_init_check.stat.exists + +- name: Copy backup SSH private key + copy: + src: "{{ backup_ssh_privkey_file }}" + dest: /root/.ssh/backup + owner: root + group: wheel + mode: "0600" + +- name: Copy backup SSH public key + copy: + src: "{{ backup_ssh_pubkey_file }}" + dest: /root/.ssh/backup.pub + owner: root + group: wheel + mode: "0644" + +- name: Copy SSH config + template: + src: root_ssh_config.j2 + dest: /root/.ssh/config + owner: root + group: wheel + mode: "0644" + +- name: Copy backup script + template: + src: usr_local_bin_backup.sh.j2 + dest: /usr/local/bin/backup + owner: root + group: wheel + mode: "0755" + +- name: Create dma config directory + file: + path: /etc/dma + state: directory + owner: root + group: wheel + mode: "0755" + +- name: Configure dma.conf + template: + src: etc_dma_dma.conf.j2 + dest: /etc/dma/dma.conf + owner: root + group: wheel + mode: "0644" + +- name: Configure dma auth.conf + template: + src: etc_dma_auth.conf.j2 + dest: /etc/dma/auth.conf + owner: root + group: mail + mode: "0640" + +- name: Configure mail aliases + template: + src: etc_aliases.j2 + dest: /etc/aliases + owner: root + group: wheel + mode: "0644" + notify: Run newaliases + +- name: Install smartmontools + package: + name: smartmontools + state: present + +- name: Deploy smartd.conf + template: + src: "{{ playbook_dir }}/roles/host/templates/usr_local_etc_smartd.conf.j2" + dest: /usr/local/etc/smartd.conf + owner: root + group: wheel + mode: "0644" + notify: Restart smartd + +- name: Start smartd + service: + name: smartd + state: started + +- name: Deploy zpool_gpt_labels rc.d script + template: + src: "{{ playbook_dir }}/roles/host/templates/usr_local_etc_rc.d_zpool_gpt_labels.j2" + dest: /usr/local/etc/rc.d/zpool_gpt_labels + owner: root + group: wheel + mode: "0755" + register: zpool_gpt_labels_script + +- name: Run zpool_gpt_labels + service: + name: zpool_gpt_labels + state: started + when: zpool_gpt_labels_script is changed + +- name: Deploy safepf script + copy: + src: "{{ playbook_dir }}/roles/host_prod/templates/usr_local_bin_safepf.sh" + dest: /usr/local/bin/safepf + owner: root + group: wheel + mode: "0755" diff --git a/roles/host_prod/templates/etc_aliases.j2 b/roles/host_prod/templates/etc_aliases.j2 new file mode 100644 index 0000000..4fe3e7f --- /dev/null +++ b/roles/host_prod/templates/etc_aliases.j2 @@ -0,0 +1 @@ +*: {{ dma_to_address }} diff --git a/roles/host_prod/templates/etc_dma_auth.conf.j2 b/roles/host_prod/templates/etc_dma_auth.conf.j2 new file mode 100644 index 0000000..a1000ea --- /dev/null +++ b/roles/host_prod/templates/etc_dma_auth.conf.j2 @@ -0,0 +1 @@ +{{ smtp_user }}|{{ smtp_host }}:{{ smtp_password }} diff --git a/roles/host_prod/templates/etc_dma_dma.conf.j2 b/roles/host_prod/templates/etc_dma_dma.conf.j2 new file mode 100644 index 0000000..dfaa04f --- /dev/null +++ b/roles/host_prod/templates/etc_dma_dma.conf.j2 @@ -0,0 +1,8 @@ +SMARTHOST {{ smtp_host }} +PORT {{ smtp_port }} +AUTHPATH /etc/dma/auth.conf +SECURETRANSFER +STARTTLS +MAILNAME {{ dma_mail_hostname }} +MASQUERADE {{ ansible_hostname }}@{{ dma_mail_hostname }} +#NULLCLIENT diff --git a/roles/host_prod/templates/etc_fstab.j2 b/roles/host_prod/templates/etc_fstab.j2 new file mode 100644 index 0000000..95d3fa0 --- /dev/null +++ b/roles/host_prod/templates/etc_fstab.j2 @@ -0,0 +1,7 @@ +# Device Mountpoint FStype Options Dump Pass# +/dev/gpt/efiboot0 /boot/efi msdosfs rw 2 2 +/dev/nda0p3.eli none swap sw 0 0 +/dev/nda1p3.eli none swap sw 0 0 +/dev/nda2p3.eli none swap sw 0 0 +/dev/nda3p3.eli none swap sw 0 0 +tmpfs /tmp tmpfs rw,mode=777,size=2g 0 0 diff --git a/roles/host_prod/templates/root_ssh_config.j2 b/roles/host_prod/templates/root_ssh_config.j2 new file mode 100644 index 0000000..96f78f2 --- /dev/null +++ b/roles/host_prod/templates/root_ssh_config.j2 @@ -0,0 +1,4 @@ +Host backup + HostName {{ backup_ssh_host }} + User {{ backup_ssh_user }} + IdentityFile /root/.ssh/backup diff --git a/roles/host_prod/templates/usr_local_bin_backup.sh.j2 b/roles/host_prod/templates/usr_local_bin_backup.sh.j2 new file mode 100644 index 0000000..f6a0e2c --- /dev/null +++ b/roles/host_prod/templates/usr_local_bin_backup.sh.j2 @@ -0,0 +1,365 @@ +{% raw %}#!/usr/bin/env bash +set -euo pipefail + +# ===== CONFIG (filled by Ansible) ===== +KEEP_LOCAL=30 +KEEP_REMOTE=10 +KEYFILE="/root/.ssh/backup" +HOST_DIR="backup" +DATASET="{% endraw %}{{ backup_zfs_dataset }}{% raw %}" +BACKUP_EXCLUDE_PROP="{% endraw %}{{ backup_zfs_exclude_property | default('com.pursotin:backup') }}{% raw %}" +USER="{% endraw %}{{ backup_ssh_user }}{% raw %}" +HOST="{% endraw %}{{ backup_ssh_host }}{% raw %}" +EMAIL_TO=root +# ===================================== + +# ----- Globals for notification ----- +STARTED_AT="$(date '+%Y-%m-%dT%H:%M:%S%z')" +FINISHED_AT="" +MESSAGE_LOG="" +BACKUP_NAME="" # e.g. zroot@2025-09-30-12-00-00-0300.enc +BACKUP_SIZE_BYTES="" # numeric bytes + +# ----- Helpers ----- +die() { echo "Error: $*" >&2; exit 1; } + +require_cmds() { + local cmds=("$@") + for c in "${cmds[@]}"; do command -v "$c" >/dev/null 2>&1 || die "Missing command: $c"; done +} + +log_note() { + # Echo to console and append to message buffer + local msg="$1" + echo "$msg" + MESSAGE_LOG+="$msg"$'\n' +} + +timestamp() { + # Replace '+' with '-' so timezone is filename-safe and lexicographically sortable within TZ. + date +%Y-%m-%d-%H-%M-%S%z | tr '+' '-' +} + +humanize_bytes() { + local bytes="$1" + if [[ "${bytes}" =~ ^[0-9]+$ ]]; then + awk -v b="${bytes}" ' + BEGIN { + unit_count = split("B KiB MiB GiB TiB PiB EiB", units, " ") + i = 1 + while (b >= 1024 && i < unit_count) { + b = b / 1024 + i++ + } + if (i == 1) { + printf "%.0f %s\n", b, units[i] + } else { + printf "%.2f %s\n", b, units[i] + } + } + ' + else + echo "unknown" + fi +} + +latest_snapshot_for_dataset() { + # Latest snapshot on the TOP dataset only (newest first). Returns e.g. zroot@2025-09-30-... + zfs list -t snapshot -o name -S creation "$DATASET" 2>/dev/null \ + | awk 'NR==2{print; exit}' +} + +list_top_snapshots_newest_first() { + # Only list snapshots on the top dataset (not children), newest first. + zfs list -t snapshot -o name -S creation "$DATASET" 2>/dev/null \ + | awk 'NR>1{print $1}' +} + +list_excluded_datasets() { + # List datasets (including inherited values) where backup property is explicitly false. + zfs get -r -H -o name,value "${BACKUP_EXCLUDE_PROP}" "${DATASET}" 2>/dev/null \ + | awk 'tolower($2)=="false"{print $1}' +} + +list_remote_backups_sorted() { + # We store files under ${HOST_DIR}/@.enc + echo "ls ${HOST_DIR}" \ + | sftp -q -i "${KEYFILE}" "${USER}@${HOST}" 2>/dev/null \ + | tail -n +2 | sort +} + +# Upload a local file to the storage box path "${HOST_DIR}/" +sftp_put() { + local local_file="$1" + local remote_name="$2" # just the filename, no directory + sftp -i "${KEYFILE}" "${USER}@${HOST}" </dev/null 2>&1; then + echo "Remote directory already exists: ${HOST_DIR}" + log_note "init-remote: verified remote directory '${HOST_DIR}'" + return 0 + fi + + echo "Creating remote directory: ${HOST_DIR}" + set +e + sftp -i "${KEYFILE}" "${USER}@${HOST}" </dev/null 2>&1; then + echo "Remote directory created: ${HOST_DIR}" + log_note "init-remote: created remote directory '${HOST_DIR}'" + else + die "Remote directory ${HOST_DIR} not found after creation" + fi +} + +cmd_snapshot() { + echo "" + echo "[snapshot]" + require_cmds zfs date tr + local ts snap + ts="$(timestamp)" + snap="${DATASET}@${ts}" + echo "Taking recursive snapshot \"${snap}\"" + (set -x; zfs snapshot -r "${snap}") + echo "Recursive snapshot \"${snap}\" created" + log_note "snapshot: created recursive snapshot '${snap}'" +} + +cmd_send_to_remote() { + echo "" + echo "[send-to-remote]" + require_cmds zfs age sftp mktemp stat awk sort + + local snap base target tmp size_bytes ds parent skip + local -a send_args excluded_raw excluded + snap="$(latest_snapshot_for_dataset)" + [[ -n "${snap}" ]] || die "No snapshot found to send. Run 'snapshot' first or ensure the dataset has snapshots." + + base="$(basename "${snap}")" # e.g., zroot@2025-09-30-12-00-00-0300 + target="${base}.enc" + tmp="$(mktemp -t backup_send.XXXXXX)" + trap 'rm -f "${tmp}"' EXIT + + send_args=(-Rvc) + mapfile -t excluded_raw < <(list_excluded_datasets | sort) + excluded=() + for ds in "${excluded_raw[@]}"; do + skip=0 + [[ -n "${ds}" ]] || continue + for parent in "${excluded[@]}"; do + if [[ "${ds}" == "${parent}" || "${ds}" == "${parent}/"* ]]; then + skip=1 + break + fi + done + [[ "${skip}" -eq 1 ]] || excluded+=("${ds}") + done + + if [[ "${#excluded[@]}" -gt 0 ]]; then + echo "Excluding datasets where ${BACKUP_EXCLUDE_PROP}=false:" + for ds in "${excluded[@]}"; do + if [[ "${ds}" == "${DATASET}" ]]; then + die "Top dataset ${DATASET} is excluded via ${BACKUP_EXCLUDE_PROP}=false; refusing to create an empty backup stream." + fi + echo " - ${ds}" + send_args+=(-X "${ds}") + done + log_note "send-to-remote: excluded ${#excluded[@]} dataset(s) using ${BACKUP_EXCLUDE_PROP}=false" + fi + + echo "Creating encrypted replication stream to temp file: ${tmp}" + # -R: recursive hierarchy, -v: progress to stderr, -c: send compressed (keeps on-disk compression) + (set -x; zfs send "${send_args[@]}" "${snap}" | age -e -i "${KEYFILE}" > "${tmp}") + + size_bytes="$(stat -f %z "${tmp}" 2>/dev/null || stat -c %s "${tmp}" 2>/dev/null || echo "unknown")" + echo "Local stream size: ${size_bytes} bytes" + + echo "Uploading via SFTP to ${HOST}:${HOST_DIR}/${target}" + if sftp_put "${tmp}" "${target}"; then + echo "Upload complete" + else + die "SFTP upload failed (does the remote directory '${HOST_DIR}' exist? Run 'init-remote')" + fi + + # Set globals for notify() + BACKUP_NAME="${target}" + BACKUP_SIZE_BYTES="${size_bytes}" + + log_note "send-to-remote: uploaded '${snap}' as '${target}' (${size_bytes} bytes) to ${HOST}:${HOST_DIR}" + rm -f "${tmp}" + trap - EXIT +} + +cmd_prune_remote() { + echo "" + echo "[prune-remote]" + require_cmds sftp awk sort wc + + echo "Fetching remote backup listing from sftp://${HOST}/${HOST_DIR}" + BACKUPS="$(list_remote_backups_sorted || true)" + mapfile -t BACKUP_ARR < <(printf "%s\n" "${BACKUPS}") + local count="${#BACKUP_ARR[@]}" + + if [[ "${count}" -le "${KEEP_REMOTE}" ]]; then + echo "Remote backups (${count}) <= KEEP_REMOTE (${KEEP_REMOTE}); nothing to prune." + log_note "prune-remote: kept ${count} (<= ${KEEP_REMOTE}); no deletions" + return 0 + fi + + echo "Pruning remote backups, keeping latest ${KEEP_REMOTE} (will delete $(("${count}" - "${KEEP_REMOTE}")))" + local to_delete_count=$((count - KEEP_REMOTE)) + local deleted=0 + for ((i=0; i /etc/pf.conf +pfctl -f /etc/pf.conf +service pf restart diff --git a/roles/host_test/handlers/main.yml b/roles/host_test/handlers/main.yml new file mode 100644 index 0000000..46fcbde --- /dev/null +++ b/roles/host_test/handlers/main.yml @@ -0,0 +1,2 @@ +- name: Apply rc.conf + shell: service kld start; service netif restart && service routing restart diff --git a/roles/host_test/tasks/main.yml b/roles/host_test/tasks/main.yml new file mode 100644 index 0000000..1246073 --- /dev/null +++ b/roles/host_test/tasks/main.yml @@ -0,0 +1,13 @@ +- name: Set up /etc/rc.conf + template: + src: "{{ playbook_dir }}/roles/host/templates/etc_rc.conf.j2" + dest: /etc/rc.conf + owner: root + group: wheel + mode: "0644" + vars: + is_prod: false + notify: Apply rc.conf + +- name: Flush handlers + meta: flush_handlers diff --git a/roles/jail/handlers/main.yml b/roles/jail/handlers/main.yml new file mode 100644 index 0000000..5375cb8 --- /dev/null +++ b/roles/jail/handlers/main.yml @@ -0,0 +1,5 @@ +- name: Restart jail services + service: + name: "{{ item }}" + state: restarted + loop: "{{ services | default([]) }}" diff --git a/roles/jail/tasks/main.yml b/roles/jail/tasks/main.yml new file mode 100644 index 0000000..be72e3b --- /dev/null +++ b/roles/jail/tasks/main.yml @@ -0,0 +1,175 @@ +# Host-side setup (runs on the jail host via SSH) +- name: "Check if {{ jail_name }} container exists" + shell: "zfs list -o name | grep -Fxq 'zroot/jails/containers/{{ jail_name }}'" + failed_when: false + changed_when: false + register: jail_exists + delegate_to: "{{ jail_delegate_host }}" + +- name: "Check {{ jail_name }} userland version" + shell: "zfs get -H -o value origin zroot/jails/containers/{{ jail_name }}" + register: jail_origin + changed_when: false + when: jail_exists.rc == 0 + delegate_to: "{{ jail_delegate_host }}" + +- name: "*** MIGRATION REQUIRED: {{ jail_name }} ***" + pause: + prompt: | + + ════════════════════════════════════════════════════════════════ + JAIL USERLAND MIGRATION: {{ jail_name }} + ════════════════════════════════════════════════════════════════ + Current: {{ jail_origin.stdout | trim }} + Target: zroot/jails/templates/{{ userland }}@base + + This will: + 1. Stop the jail + 2. Rename existing dataset to *.old. + 3. Clone fresh from {{ userland }} + ════════════════════════════════════════════════════════════════ + + Press Enter to continue or Ctrl+C to abort + when: + - jail_exists.rc == 0 + - "userland + '@base' not in jail_origin.stdout" + +- name: "Migrate {{ jail_name }} to {{ userland }}" + shell: | + service jail stop {{ jail_name }} || true + zfs rename zroot/jails/containers/{{ jail_name }} zroot/jails/containers/{{ jail_name }}.old.$(date +%s) + when: + - jail_exists.rc == 0 + - "userland + '@base' not in jail_origin.stdout" + delegate_to: "{{ jail_delegate_host }}" + +- name: "Clone {{ jail_name }} from template" + shell: "zfs clone zroot/jails/templates/{{ userland }}@base zroot/jails/containers/{{ jail_name }}" + when: jail_exists.rc != 0 or (jail_origin.stdout is defined and userland + '@base' not in jail_origin.stdout) + delegate_to: "{{ jail_delegate_host }}" + +- name: "Create directories for {{ jail_name }}" + file: + path: "/usr/local/jails/containers/{{ jail_name }}{{ item }}" + state: directory + owner: root + group: wheel + mode: "0755" + loop: "{{ dirs | default([]) }}" + delegate_to: "{{ jail_delegate_host }}" + +- name: "Create mount point sources for {{ jail_name }}" + file: + path: "{{ item.src }}" + state: directory + owner: root + group: wheel + mode: "0755" + loop: "{{ nullfs | default([]) }}" + loop_control: + label: "{{ item.src }}" + delegate_to: "{{ jail_delegate_host }}" + +- name: "Create mount point destinations for {{ jail_name }}" + file: + path: "/usr/local/jails/containers/{{ jail_name }}{{ item.dst }}" + state: directory + owner: root + group: wheel + mode: "0755" + loop: "{{ nullfs | default([]) }}" + loop_control: + label: "{{ item.dst }}" + delegate_to: "{{ jail_delegate_host }}" + +- name: "Deploy jail.conf.d/{{ jail_name }}.conf" + template: + src: jail_conf.j2 + dest: "/etc/jail.conf.d/{{ jail_name }}.conf" + owner: root + group: wheel + mode: "0644" + vars: + jail: + name: "{{ jail_name }}" + num: "{{ jail_num }}" + ip: "{{ jail_lan_cidr | ipv4_nth_cidr(jail_num | int + jail_lan_offset | int) }}" + devfs_ruleset: "{{ devfs_ruleset | default(4) }}" + options: "{{ jail_conf_options | default([]) }}" + default_route: "{{ not no_default_route | default(false) }}" + exec_prestart: "{{ exec_prestart | default([]) }}" + exec_start: "{{ exec_start | default([]) }}" + exec_poststart: "{{ exec_poststart | default([]) }}" + exec_prestop: "{{ exec_prestop | default([]) }}" + exec_stop: "{{ exec_stop | default([]) }}" + exec_poststop: "{{ exec_poststop | default([]) }}" + mounts: "{{ nullfs | default([]) }}" + delegate_to: "{{ jail_delegate_host }}" + +- name: "Start {{ jail_name }} jail" + shell: "service jail start {{ jail_name }}" + register: jail_start + failed_when: "jail_start.rc != 0 and 'already exists' not in jail_start.stdout" + changed_when: "'already exists' not in jail_start.stdout" + delegate_to: "{{ jail_delegate_host }}" + +# In-jail provisioning (runs inside the jail via jailexec) +- name: Install packages + shell: "pkg install -y {{ pkg | join(' ') }}" + environment: + ASSUME_ALWAYS_YES: "yes" + when: pkg is defined and pkg | length > 0 + register: pkg_result + changed_when: "'Number of packages to be installed' in pkg_result.stdout" + +- name: Create parent directories for files + file: + path: "{{ item.dest | dirname }}" + state: directory + owner: root + group: wheel + mode: "0755" + loop: "{{ files | default([]) }}" + loop_control: + label: "{{ item.dest | dirname }}" + when: files is defined + +- name: Deploy files + template: + src: "{{ jail_role_dir }}/templates/{{ item.src }}" + dest: "{{ item.dest }}" + owner: "{{ item.owner | default('root') }}" + group: "{{ item.group | default('wheel') }}" + mode: "{{ item.mode | default('0644') }}" + loop: "{{ files | default([]) }}" + loop_control: + label: "{{ item.dest }}" + when: files is defined + notify: Restart jail services + +- name: Enable services + community.general.sysrc: + name: "{{ item }}_enable" + value: "YES" + loop: "{{ services | default([]) }}" + +- name: Start services + service: + name: "{{ item }}" + state: started + loop: "{{ services | default([]) }}" + +- name: Set sysctl values + sysctl: + name: "{{ item.name }}" + value: "{{ item.value }}" + state: present + loop: "{{ sysctl | default([]) }}" + when: sysctl is defined + +- name: Set sysrc values + community.general.sysrc: + name: "{{ item.name }}" + value: "{{ item.value }}" + loop: "{{ sysrc | default([]) }}" + when: sysrc is defined diff --git a/roles/jail/templates/jail_conf.j2 b/roles/jail/templates/jail_conf.j2 new file mode 100644 index 0000000..2a0ea27 --- /dev/null +++ b/roles/jail/templates/jail_conf.j2 @@ -0,0 +1,56 @@ +{{ jail.name }} { + vnet; + persist; + exec.clean; + allow.raw_sockets; + mount.devfs; +{% for opt in jail.options %} + {{ opt }}; +{% endfor %} + + devfs_ruleset = {{ jail.devfs_ruleset }}; + host.hostname = "{{ jail.name }}"; + path = "/usr/local/jails/containers/${name}"; + + exec.start = "/bin/sh /etc/rc"; + exec.stop = "/bin/sh /etc/rc.shutdown"; + + # LAN epair + exec.prestart += "ifconfig epl{{ jail.num }}a destroy 2>/dev/null || true"; + exec.prestart += "ifconfig epair{{ jail.num }}000 create"; + exec.prestart += "ifconfig epair{{ jail.num }}000a name epl{{ jail.num }}a"; + exec.prestart += "ifconfig epair{{ jail.num }}000b name epl{{ jail.num }}b"; + exec.prestart += "ifconfig epl{{ jail.num }}b ether random"; + exec.prestart += "ifconfig brlan0 addm epl{{ jail.num }}a"; + exec.poststart += "ifconfig epl{{ jail.num }}b vnet ${name}"; + exec.poststart += "ifconfig epl{{ jail.num }}a up"; + exec.poststart += "jexec ${name} ifconfig epl{{ jail.num }}b up"; + exec.poststart += "jexec ${name} ifconfig epl{{ jail.num }}b {{ jail.ip }}"; + exec.poststart += "jexec ${name} route delete default || true"; +{% if jail.default_route %} + exec.poststart += "jexec ${name} route add default {{ ingress_ip }} || true"; +{% endif %} + exec.poststop += "ifconfig epl{{ jail.num }}a destroy 2>/dev/null || true"; +{% for cmd in jail.exec_prestart %} + exec.prestart += "{{ cmd }}"; +{% endfor %} +{% for cmd in jail.exec_start %} + exec.start += "{{ cmd }}"; +{% endfor %} +{% for cmd in jail.exec_poststart %} + exec.poststart += "{{ cmd }}"; +{% endfor %} +{% for cmd in jail.exec_prestop %} + exec.prestop += "{{ cmd }}"; +{% endfor %} +{% for cmd in jail.exec_stop %} + exec.stop += "{{ cmd }}"; +{% endfor %} +{% for cmd in jail.exec_poststop %} + exec.poststop += "{{ cmd }}"; +{% endfor %} +{% for mount in jail.mounts %} + exec.prestart += "mount -t nullfs {{ mount.src }} /usr/local/jails/containers/{{ jail.name }}{{ mount.dst }} || true"; + exec.poststop += "umount /usr/local/jails/containers/{{ jail.name }}{{ mount.dst }} || true"; +{% endfor %} +} diff --git a/roles/jails/01_ingress/defaults/main.yml b/roles/jails/01_ingress/defaults/main.yml new file mode 100644 index 0000000..f6d5aa4 --- /dev/null +++ b/roles/jails/01_ingress/defaults/main.yml @@ -0,0 +1,92 @@ +userland: "15.0-RELEASE" +devfs_ruleset: 6 +no_default_route: true + +ingress_routes: + - { host: jan.systems, jail: homepage } + - { host: jantuomi.fi, redirect: jan.systems } + - { host: aggro.jan.systems, jail: aggro } + - { host: diddle.jan.systems, jail: diddle } + - { host: spliit.jan.systems, jail: spliit } + - { host: freshrss.jan.systems, jail: freshrss } + - { host: irc.jan.systems, jail: irc_thelounge, presets: [websocket] } + - { host: paste.jan.systems, jail: paste } + - { host: leolalla.fi, jail: leolalla_fi } + - { host: immich.jan.systems, ip: 192.168.3.3, port: 2283 } + - { host: plex.jan.systems, jail: plex, port: 32400, presets: [streaming] } + - { host: komga.jan.systems, jail: komga, port: 25600 } + +cert_domains: + - "jan.systems" + - "*.jan.systems" + - "jantuomi.fi" + - "*.jantuomi.fi" + - "leolalla.fi" + - "*.leolalla.fi" + +cert_name: "{{ cert_domains[0] }}" +contact_email: jan@jantuomi.fi + +jail_conf_options: + - "allow.raw_sockets" + +nullfs: + - src: /usr/local/jails/volumes/goaccess_www + dst: /mnt/www_goaccess + +pkg: + - nginx + - py311-certbot + - py311-certbot-nginx + - goaccess + +files: + - src: etc_pf.conf.j2 + dest: /etc/pf.conf + - src: usr_local_etc_nginx_nginx.conf.j2 + dest: /usr/local/etc/nginx/nginx.conf + - src: acme-dns-auth.py + dest: /usr/local/bin/acme-dns-auth.py + mode: "0755" + - src: usr_local_bin_hetzner_ddns.sh.j2 + dest: /usr/local/bin/hetzner_ddns.sh + mode: "0755" + - src: usr_local_etc_hetzner_auth.j2 + dest: /usr/local/etc/hetzner_auth + mode: "0600" + - src: usr_local_bin_gen_goaccess.sh.j2 + dest: /usr/local/bin/gen_goaccess.sh + mode: "0755" + - src: etc_crontab.j2 + dest: /etc/crontab + +services: + - nginx + - pf + +sysctl: + - name: net.inet.ip.forwarding + value: "1" + +sysrc: + - name: gateway_enable + value: "YES" + +nginx_presets: + websocket: + - "proxy_http_version 1.1" + - 'proxy_set_header Connection "Upgrade"' + - "proxy_set_header Upgrade $http_upgrade" + - "proxy_read_timeout 1d" + - "proxy_send_timeout 1d" + - "proxy_buffering off" + - "proxy_request_buffering off" + - "client_max_body_size 100M" + streaming: + - "proxy_http_version 1.1" + - 'proxy_set_header Connection "Upgrade"' + - "proxy_set_header Upgrade $http_upgrade" + - "proxy_redirect off" + - "proxy_buffering off" + - "proxy_read_timeout 3600s" + - "proxy_send_timeout 3600s" diff --git a/roles/jails/01_ingress/tasks/main.yml b/roles/jails/01_ingress/tasks/main.yml new file mode 100644 index 0000000..5b56286 --- /dev/null +++ b/roles/jails/01_ingress/tasks/main.yml @@ -0,0 +1,49 @@ +- name: Set WAN hooks (test) + set_fact: + exec_prestart: + - "ifconfig epw{{ jail_num }}a destroy 2>/dev/null || true" + - "ifconfig epair{{ jail_num }}001 create" + - "ifconfig epair{{ jail_num }}001a name epw{{ jail_num }}a" + - "ifconfig epair{{ jail_num }}001b name epw{{ jail_num }}b" + - "ifconfig brlan0 addm epw{{ jail_num }}a" + exec_poststart: + - "ifconfig epw{{ jail_num }}b vnet {{ jail_name }}" + - "ifconfig epw{{ jail_num }}a up" + - "jexec {{ jail_name }} ifconfig epw{{ jail_num }}b up" + - "jexec {{ jail_name }} ifconfig epw{{ jail_num }}b inet {{ ingress_wan_static }}" + - "jexec {{ jail_name }} route add default {{ lan_ipv4_gateway }}" + exec_poststop: + - "ifconfig epw{{ jail_num }}a destroy 2>/dev/null || true" + when: not is_prod + +- name: Set WAN hooks (prod) + set_fact: + exec_prestart: + - "ifconfig epw{{ jail_num }}a destroy 2>/dev/null || true" + - "ifconfig epair{{ jail_num }}001 create" + - "ifconfig epair{{ jail_num }}001a name epw{{ jail_num }}a" + - "ifconfig epair{{ jail_num }}001b name epw{{ jail_num }}b" + - "ifconfig brwan0 addm epw{{ jail_num }}a" + exec_poststart: + - "ifconfig epw{{ jail_num }}b vnet {{ jail_name }}" + - "ifconfig epw{{ jail_num }}a up" + - "jexec {{ jail_name }} ifconfig epw{{ jail_num }}b up" + - "jexec {{ jail_name }} service dhclient restart epw{{ jail_num }}b" + - "jexec {{ jail_name }} route add 10.6.210.0/24 {{ lan_ipv4_gateway }} || true" + exec_poststop: + - "ifconfig epw{{ jail_num }}a destroy 2>/dev/null || true" + when: is_prod + +- import_role: + name: jail + +- name: Check if TLS certs exist + stat: + path: "/usr/local/etc/letsencrypt/live/{{ cert_name }}" + register: _certbot_certs + when: is_prod + +- name: Pause for manual certbot setup + pause: + prompt: "Run certbot manually in the ingress jail to obtain certs, then press Enter." + when: is_prod and not (_certbot_certs.stat.exists | default(true)) diff --git a/roles/jails/01_ingress/templates/acme-dns-auth.py b/roles/jails/01_ingress/templates/acme-dns-auth.py new file mode 100755 index 0000000..77928e6 --- /dev/null +++ b/roles/jails/01_ingress/templates/acme-dns-auth.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +# +# Source: https://github.com/joohoi/acme-dns-certbot-joohoi/blob/master/acme-dns-auth.py +# Some modifications to configuration values present. + + +import json +import os +import sys + +import requests + +### EDIT THESE: Configuration values ### + +# URL to acme-dns instance +ACMEDNS_URL = "https://auth.acme-dns.io" +# Path for acme-dns credential storage +STORAGE_PATH = "/usr/local/etc/letsencrypt/acmedns.json" +# Whitelist for address ranges to allow the updates from +# Example: ALLOW_FROM = ["192.168.10.0/24", "::1/128"] +ALLOW_FROM = [] +# Force re-registration. Overwrites the already existing acme-dns accounts. +FORCE_REGISTER = False + +### DO NOT EDIT BELOW THIS POINT ### +### HERE BE DRAGONS ### + +DOMAIN = os.environ["CERTBOT_DOMAIN"] +if DOMAIN.startswith("*."): + DOMAIN = DOMAIN[2:] +VALIDATION_DOMAIN = "_acme-challenge." + DOMAIN +VALIDATION_TOKEN = os.environ["CERTBOT_VALIDATION"] + + +class AcmeDnsClient(object): + """ + Handles the communication with ACME-DNS API + """ + + def __init__(self, acmedns_url): + self.acmedns_url = acmedns_url + + def register_account(self, allowfrom): + """Registers a new ACME-DNS account""" + + if allowfrom: + # Include whitelisted networks to the registration call + reg_data = {"allowfrom": allowfrom} + res = requests.post( + self.acmedns_url + "/register", data=json.dumps(reg_data) + ) + else: + res = requests.post(self.acmedns_url + "/register") + if res.status_code == 201: + # The request was successful + return res.json() + else: + # Encountered an error + msg = ( + "Encountered an error while trying to register a new acme-dns " + "account. HTTP status {}, Response body: {}" + ) + print(msg.format(res.status_code, res.text)) + sys.exit(1) + + def update_txt_record(self, account, txt): + """Updates the TXT challenge record to ACME-DNS subdomain.""" + update = {"subdomain": account["subdomain"], "txt": txt} + headers = { + "X-Api-User": account["username"], + "X-Api-Key": account["password"], + "Content-Type": "application/json", + } + res = requests.post( + self.acmedns_url + "/update", headers=headers, data=json.dumps(update) + ) + if res.status_code == 200: + # Successful update + return + else: + msg = ( + "Encountered an error while trying to update TXT record in " + "acme-dns. \n" + "------- Request headers:\n{}\n" + "------- Request body:\n{}\n" + "------- Response HTTP status: {}\n" + "------- Response body: {}" + ) + s_headers = json.dumps(headers, indent=2, sort_keys=True) + s_update = json.dumps(update, indent=2, sort_keys=True) + s_body = json.dumps(res.json(), indent=2, sort_keys=True) + print(msg.format(s_headers, s_update, res.status_code, s_body)) + sys.exit(1) + + +class Storage(object): + def __init__(self, storagepath): + self.storagepath = storagepath + self._data = self.load() + + def load(self): + """Reads the storage content from the disk to a dict structure""" + data = dict() + filedata = "" + try: + with open(self.storagepath, "r") as fh: + filedata = fh.read() + except IOError as e: + if os.path.isfile(self.storagepath): + # Only error out if file exists, but cannot be read + print("ERROR: Storage file exists but cannot be read") + sys.exit(1) + try: + data = json.loads(filedata) + except ValueError: + if len(filedata) > 0: + # Storage file is corrupted + print("ERROR: Storage JSON is corrupted") + sys.exit(1) + return data + + def save(self): + """Saves the storage content to disk""" + serialized = json.dumps(self._data) + try: + with os.fdopen( + os.open(self.storagepath, os.O_WRONLY | os.O_CREAT, 0o600), "w" + ) as fh: + fh.truncate() + fh.write(serialized) + except IOError as e: + print("ERROR: Could not write storage file.") + sys.exit(1) + + def put(self, key, value): + """Puts the configuration value to storage and sanitize it""" + # If wildcard domain, remove the wildcard part as this will use the + # same validation record name as the base domain + if key.startswith("*."): + key = key[2:] + self._data[key] = value + + def fetch(self, key): + """Gets configuration value from storage""" + try: + return self._data[key] + except KeyError: + return None + + +if __name__ == "__main__": + # Init + client = AcmeDnsClient(ACMEDNS_URL) + storage = Storage(STORAGE_PATH) + + # Check if an account already exists in storage + account = storage.fetch(DOMAIN) + if FORCE_REGISTER or not account: + # Create and save the new account + account = client.register_account(ALLOW_FROM) + storage.put(DOMAIN, account) + storage.save() + + # Display the notification for the user to update the main zone + msg = "Please add the following CNAME record to your main DNS zone:\n{}" + cname = "{} CNAME {}.".format(VALIDATION_DOMAIN, account["fulldomain"]) + print(msg.format(cname)) + + # Update the TXT record in acme-dns instance + client.update_txt_record(account, VALIDATION_TOKEN) diff --git a/roles/jails/01_ingress/templates/etc_crontab.j2 b/roles/jails/01_ingress/templates/etc_crontab.j2 new file mode 100644 index 0000000..6879766 --- /dev/null +++ b/roles/jails/01_ingress/templates/etc_crontab.j2 @@ -0,0 +1,18 @@ +# /etc/crontab - root's crontab for FreeBSD +# +# +SHELL=/bin/sh +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin +# +#minute hour mday month wday who command + +# Update LetsEncrypt certificates every day after 2:00 AM +0 2 * * * root certbot certonly -n -m {{ contact_email }} --agree-tos --manual --manual-auth-hook /usr/local/bin/acme-dns-auth.py --preferred-challenges dns --debug-challenges {% for domain in cert_domains %}-d '{{ domain }}' {% endfor %} || echo "ERROR: Failed to renew certs" >&2 + +# Update dynamic DNS (Hetzner) +{% for elem in hetzner_zone_record_ids %} +* * * * * root /usr/local/bin/hetzner_ddns.sh --zone '{{ elem.zone }}' --rr-id '{{ elem.rr_id }}' --iface-cmd 'ifconfig epw1b' >>/var/log/hetzner_ddns.log 2>&1 +{% endfor %} + +# Update goaccess report HTML +0 * * * * root /usr/local/bin/gen_goaccess.sh diff --git a/roles/jails/01_ingress/templates/etc_pf.conf.j2 b/roles/jails/01_ingress/templates/etc_pf.conf.j2 new file mode 100644 index 0000000..c0528e1 --- /dev/null +++ b/roles/jails/01_ingress/templates/etc_pf.conf.j2 @@ -0,0 +1,27 @@ +{% for jail in jails -%} +{% if jail.name == 'ingress' -%} +# Interfaces & nets +lan = "epl{{ jail.num }}b" +wan = "epw{{ jail.num }}b" +lan_net = "{{ lan_ipv4_network }}" + +table persist + +# Keep PF out of loopback, drop by default if you add blocks later +set skip on lo0 +set block-policy drop + +# NAT: translate LAN traffic to the WAN interface address +nat on $wan from $lan_net to any -> ($wan) + +# Block traffic from IPs in the blocked table +block in quick from to any + +# Allow all outbound traffic from the jail and LAN via both interfaces +# NAT will be applied automatically when source is in $lan_net and going out $wan +pass out on $wan all keep state +pass out on $lan all keep state + +pass in on $wan inet proto {tcp, udp} from any to any port {80, 443} keep state +{% endif %} +{% endfor %} diff --git a/roles/jails/01_ingress/templates/usr_local_bin_gen_goaccess.sh.j2 b/roles/jails/01_ingress/templates/usr_local_bin_gen_goaccess.sh.j2 new file mode 100644 index 0000000..2cfc93a --- /dev/null +++ b/roles/jails/01_ingress/templates/usr_local_bin_gen_goaccess.sh.j2 @@ -0,0 +1,8 @@ +#!/bin/sh + +TARGET=/mnt/www_goaccess/index.html + +cat /var/log/nginx/access.log | awk '$8=$1$8' | /usr/local/bin/goaccess --log-format=VCOMBINED -j "$(($(nproc) + 1))" --keep-last=30 -a -o $TARGET --restore --persist +chmod 644 $TARGET +chown www $TARGET +chgrp www $TARGET diff --git a/roles/jails/01_ingress/templates/usr_local_bin_hetzner_ddns.sh.j2 b/roles/jails/01_ingress/templates/usr_local_bin_hetzner_ddns.sh.j2 new file mode 100644 index 0000000..a2f4430 --- /dev/null +++ b/roles/jails/01_ingress/templates/usr_local_bin_hetzner_ddns.sh.j2 @@ -0,0 +1,130 @@ +#!/bin/sh +# +# Hetzner DNS record updater (one-shot for cron) +# Requirements: curl, awk + +set -eu + +# ---------------------------- Defaults --------------------------------------- +# Env-overridable: +: "${API_TOKEN_FILE:=/usr/local/etc/hetzner_auth}" +: "${API_BASE:=https://api.hetzner.cloud/v1}" +# ----------------------------------------------------------------------------- + +# Defaults +TTL=300 + +usage() { + cat <<'USAGE' >&2 +Usage: hetzner_ddns.sh [OPTIONS] + +Options (named): + --zone NAME_OR_ID Hetzner Zone name or ID (primary mode only) + --rr-id ID RRSet identifier in the form "rr-name/rr-type" (e.g., "host/A") + --iface-cmd CMD Command that prints interface info (for IP discovery) + --ttl TTL Time-to-live of the record (optional) + -h, --help Show this help + +Environment: + API_TOKEN_FILE Path to file containing ONLY the API token + (default: /usr/local/etc/hetzner_auth) + API_BASE Hetzner Cloud DNS API base URL + (default: https://api.hetzner.cloud/v1) + +Examples: + API_TOKEN_FILE=/secret/token \ + ./hetzner_ddns.sh \ + --zone example.com --rr-name host --rr-type A \ + --iface-cmd "ifconfig em0" +USAGE +} + +ts() { date +"%Y-%m-%dT%H:%M:%S%z"; } +fail() { echo "$(ts) ERROR: $*" >&2; exit 2; } +error() { echo "$(ts) ERROR: $*" >&2; } +warn() { echo "$(ts) WARN: $*" >&2; } +info() { echo "$(ts) INFO: $*"; } + +# ----------------------------- Arg parsing ----------------------------------- +# Accept both `--key value` and `--key=value` + +while [ $# -gt 0 ]; do + case "$1" in + --zone=*) ZONE=${1#*=} ;; + --zone) ZONE=$2; shift ;; + --rr-id=*) RR_ID=${1#*=} ;; + --rr-id) RR_ID=$2; shift ;; + --iface-cmd=*) IFACE_CMD=${1#*=} ;; + --iface-cmd) IFACE_CMD=$2; shift ;; + --ttl=*) TTL=${1#*=} ;; + --ttl) TTL=$2; shift ;; + -h|--help) usage; exit 0 ;; + --) shift; break ;; + -*) + error "Unknown option: $1" + usage + exit 2 + ;; + *) + error "Unexpected positional argument: $1" + usage + exit 2 + ;; + esac + shift +done + +# ----------------------------- Validation ------------------------------------ +[ -n "${ZONE:-}" ] || fail "Missing --zone" +[ -n "${RR_ID:-}" ] || fail "Missing --rr-id" +[ -n "${IFACE_CMD:-}" ] || fail "Missing --iface-cmd" + +# ------------------------------ Auth ----------------------------------------- +if [ ! -r "${API_TOKEN_FILE}" ]; then + fail "Token file missing or unreadable: ${API_TOKEN_FILE}" +fi +API_TOKEN="$(cat "${API_TOKEN_FILE}" | tr -d '[:space:]')" +[ -n "${API_TOKEN}" ] || fail "API token is empty" + +# ------------------------------ Helpers -------------------------------------- +get_ip() { + # Extract first IPv4 after 'inet ' (ignore inet6) + sh -c "${IFACE_CMD}" 2>/dev/null | awk '/(^|[[:space:]])inet[[:space:]]/ {print $2; exit}' +} + +# ------------------------------ Main ----------------------------------------- +IFACE_IP="$(get_ip || true)" +if [ -z "${IFACE_IP}" ]; then + warn "No IPv4 address found via '${IFACE_CMD}' (interface not ready?)" + exit 1 +fi + +BODY=$(printf '{"records":[{"value":"%s","comment":"Updated by hetzner_ddns.sh at %s","ttl":"%s"}]}' \ + "${IFACE_IP}" "$(ts)" "${TTL}") + +HTTP_CODE=$( + curl -sS -o /dev/null -w "%{http_code}" -X POST \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer ${API_TOKEN}" \ + --data "${BODY}" \ + "${API_BASE}/zones/${ZONE}/rrsets/${RR_ID}/actions/set_records" +) + +case "${HTTP_CODE}" in + 2*) + info "Updated zone ${ZONE} RRSet ${RR_ID} -> ${IFACE_IP}" + exit 0 + ;; + 4*) + error "Client error from API (HTTP ${HTTP_CODE}) - check zone/rr params/token/body" + exit 2 + ;; + 5*) + warn "Server error from API (HTTP ${HTTP_CODE})" + exit 1 + ;; + *) + warn "Unexpected HTTP status ${HTTP_CODE}" + exit 1 + ;; +esac diff --git a/roles/jails/01_ingress/templates/usr_local_etc_hetzner_auth.j2 b/roles/jails/01_ingress/templates/usr_local_etc_hetzner_auth.j2 new file mode 100644 index 0000000..129dccf --- /dev/null +++ b/roles/jails/01_ingress/templates/usr_local_etc_hetzner_auth.j2 @@ -0,0 +1 @@ +{{ hetzner_pat }} diff --git a/roles/jails/01_ingress/templates/usr_local_etc_nginx_nginx.conf.j2 b/roles/jails/01_ingress/templates/usr_local_etc_nginx_nginx.conf.j2 new file mode 100644 index 0000000..3f853c5 --- /dev/null +++ b/roles/jails/01_ingress/templates/usr_local_etc_nginx_nginx.conf.j2 @@ -0,0 +1,123 @@ +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + log_format vcombined '$host:$server_port ' + '$remote_addr - $remote_user [$time_local] ' + '"$request" $status $body_bytes_sent ' + '"$http_referer" "$http_user_agent"'; + + access_log /var/log/nginx/access.log vcombined; + + gzip on; + gzip_vary on; + gzip_min_length 512; + gzip_types + text/plain + text/css + application/json + application/javascript + application/xml + image/svg+xml; + + server { + listen 80 default_server; + server_name _; + + location / { + return 404; + } + } + + {% for route in ingress_routes -%} + server { + listen 80; + listen [::]:80; + server_name {{ route.host }}; + +{% if ssl_enabled %} + return 307 https://$host$request_uri; + } + + server { + server_name {{ route.host }}; + http2 on; + + listen 443 ssl; + listen [::]:443 ssl; + + # See https://ssl-config.mozilla.org/#server=nginx&version=1.28.0&config=intermediate&openssl=3.4.0&guideline=5.7 + add_header Strict-Transport-Security "max-age=63072000" always; + + # Common hardening headers + add_header X-Content-Type-Options nosniff always; + add_header X-Frame-Options DENY always; + add_header Referrer-Policy strict-origin-when-cross-origin always; + add_header Permissions-Policy interest-cohort=(); + + # Hide "Server: nginx/1.28.0" header + server_tokens off; + + ssl_certificate /usr/local/etc/letsencrypt/live/{{ cert_name }}/fullchain.pem; + ssl_certificate_key /usr/local/etc/letsencrypt/live/{{ cert_name }}/privkey.pem; + include /usr/local/etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem; +{% endif %} + + {% if route.jail is defined -%} + {% for jail in jails if jail.name == route.jail -%} + location / { + proxy_pass http://{{ jail_lan_cidr | ipv4_nth(jail.num + jail_lan_offset | int) }}{% if route.port is defined %}:{{ route.port }}{% endif %}; + 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; + {% if route.presets is defined -%} + {% set directives = [] -%} + {% for p in route.presets -%} + {% for d in nginx_presets[p] -%} + {% if d not in directives -%}{% set _ = directives.append(d) -%}{% endif -%} + {% endfor -%} + {% endfor -%} + {% for directive in directives -%} + {{ directive }}; + {% endfor -%} + {% endif %} + } + {% endfor %} + {% elif route.redirect is defined -%} + return 307 https://{{ route.redirect }}$request_uri; + {% elif route.ip is defined -%} + location / { + proxy_pass http://{{ route.ip }}:{{ route.port }}; + 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; + + # TODO: these shouldn't configured for all ip:port proxies but Immich needs them + proxy_http_version 1.1; + proxy_set_header Connection "upgrade"; + proxy_set_header Upgrade $http_upgrade; + + # by default nginx times out connections in one minute + proxy_read_timeout 1d; + proxy_send_timeout 1d; + proxy_buffering off; + proxy_request_buffering off; + + client_max_body_size 10G; + } + {% endif %} + } + {% endfor %} +} diff --git a/roles/jails/02_postgres/defaults/main.yml b/roles/jails/02_postgres/defaults/main.yml new file mode 100644 index 0000000..045ba4d --- /dev/null +++ b/roles/jails/02_postgres/defaults/main.yml @@ -0,0 +1,9 @@ +userland: "14.3-RELEASE" +devfs_ruleset: 5 + +jail_conf_options: + - "allow.raw_sockets" + - "allow.sysvipc" + +pkg: + - postgresql18-server diff --git a/roles/jails/02_postgres/tasks/main.yml b/roles/jails/02_postgres/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/02_postgres/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/03_irc_thelounge/defaults/main.yml b/roles/jails/03_irc_thelounge/defaults/main.yml new file mode 100644 index 0000000..697ea79 --- /dev/null +++ b/roles/jails/03_irc_thelounge/defaults/main.yml @@ -0,0 +1 @@ +userland: "14.3-RELEASE" diff --git a/roles/jails/03_irc_thelounge/tasks/main.yml b/roles/jails/03_irc_thelounge/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/03_irc_thelounge/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/04_taulubot/defaults/main.yml b/roles/jails/04_taulubot/defaults/main.yml new file mode 100644 index 0000000..697ea79 --- /dev/null +++ b/roles/jails/04_taulubot/defaults/main.yml @@ -0,0 +1 @@ +userland: "14.3-RELEASE" diff --git a/roles/jails/04_taulubot/tasks/main.yml b/roles/jails/04_taulubot/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/04_taulubot/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/05_homepage/defaults/main.yml b/roles/jails/05_homepage/defaults/main.yml new file mode 100644 index 0000000..d48c9fa --- /dev/null +++ b/roles/jails/05_homepage/defaults/main.yml @@ -0,0 +1,18 @@ +userland: "14.3-RELEASE" + +pkg: + - nginx + - rsync + - bash + +files: + - src: usr_local_etc_nginx_nginx.conf.j2 + dest: /usr/local/etc/nginx/nginx.conf + - src: etc_crontab.j2 + dest: /etc/crontab + +dirs: + - /var/www + +services: + - nginx diff --git a/roles/jails/05_homepage/tasks/main.yml b/roles/jails/05_homepage/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/05_homepage/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/05_homepage/templates/etc_crontab.j2 b/roles/jails/05_homepage/templates/etc_crontab.j2 new file mode 100644 index 0000000..8541f57 --- /dev/null +++ b/roles/jails/05_homepage/templates/etc_crontab.j2 @@ -0,0 +1,9 @@ +# /etc/crontab - root's crontab for FreeBSD +# +# +SHELL=/bin/sh +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin +# +#minute hour mday month wday who command + +# TODO: update linklog diff --git a/roles/jails/05_homepage/templates/usr_local_etc_nginx_nginx.conf.j2 b/roles/jails/05_homepage/templates/usr_local_etc_nginx_nginx.conf.j2 new file mode 100644 index 0000000..ee45405 --- /dev/null +++ b/roles/jails/05_homepage/templates/usr_local_etc_nginx_nginx.conf.j2 @@ -0,0 +1,55 @@ +worker_processes auto; + +events { + worker_connections 1024; +} + +http { + include mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 6; + gzip_min_length 256; + + # Compress sensible, text-based response types (HTML is covered by default) + gzip_types + text/plain + text/css + text/xml + text/javascript + application/javascript + application/json + application/ld+json + application/xml + application/rss+xml + application/atom+xml + image/svg+xml; + + server { + listen 80 default_server; + server_name _; + + root /var/www; + + index index.html; + + # Migration redirects from legacy site + location ~ ^/archive(?:/(.*))?$ { + return 307 /posts/$1$is_args$args; + } + location = /feed.xml { + # Serve atom.xml content at the legacy feed.xml URL (no redirect) + rewrite ^ /atom.xml break; + } + + location / { + try_files $uri $uri.html $uri/ =404; + } + } +} diff --git a/roles/jails/06_hommabot/defaults/main.yml b/roles/jails/06_hommabot/defaults/main.yml new file mode 100644 index 0000000..d12d40f --- /dev/null +++ b/roles/jails/06_hommabot/defaults/main.yml @@ -0,0 +1,14 @@ +userland: "14.3-RELEASE" + +pkg: + - npm + +files: + - src: root_hommabot_env.j2 + dest: /root/hommabot/.env + mode: "0600" + - src: root_hommabot_deps.sh + dest: /root/hommabot/deps.sh + mode: "0755" + - src: etc_crontab.j2 + dest: /etc/crontab diff --git a/roles/jails/06_hommabot/tasks/main.yml b/roles/jails/06_hommabot/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/06_hommabot/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/06_hommabot/templates/etc_crontab.j2 b/roles/jails/06_hommabot/templates/etc_crontab.j2 new file mode 100644 index 0000000..556d12a --- /dev/null +++ b/roles/jails/06_hommabot/templates/etc_crontab.j2 @@ -0,0 +1,10 @@ +# /etc/crontab - root's crontab for FreeBSD +# +# +SHELL=/bin/sh +PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin +# +#minute hour mday month wday who command + +# Run hommabot every Monday at 9:00 AM +0 9 * * 1 root /bin/sh -c "cd /root/hommabot && node index.js" diff --git a/roles/jails/06_hommabot/templates/root_hommabot_deps.sh b/roles/jails/06_hommabot/templates/root_hommabot_deps.sh new file mode 100644 index 0000000..25c0e56 --- /dev/null +++ b/roles/jails/06_hommabot/templates/root_hommabot_deps.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -eux +cd /root/hommabot +npm ci + +# We need to build the better-sqlite3.node file before running the app +if [ ! -f build/better_sqlite3.node ]; then ( + mkdir -p build + cd node_modules/better-sqlite3 + npm run build-release + cp build/Release/better_sqlite3.node ../../build/better_sqlite3.node +) +fi diff --git a/roles/jails/06_hommabot/templates/root_hommabot_env.j2 b/roles/jails/06_hommabot/templates/root_hommabot_env.j2 new file mode 100644 index 0000000..c66c666 --- /dev/null +++ b/roles/jails/06_hommabot/templates/root_hommabot_env.j2 @@ -0,0 +1,4 @@ +TELEGRAM_BOT_TOKEN="{{ hommabot_telegram_bot_token }}" +SHEETS_SPREADSHEET_ID="{{ hommabot_sheets_spreadsheet_id }}" +SHEETS_RANGE="{{ hommabot_sheets_range }}" +G_SA_JSON_B64="{{ hommabot_g_sa_json_b64 }}" diff --git a/roles/jails/07_aggro/defaults/main.yml b/roles/jails/07_aggro/defaults/main.yml new file mode 100644 index 0000000..697ea79 --- /dev/null +++ b/roles/jails/07_aggro/defaults/main.yml @@ -0,0 +1 @@ +userland: "14.3-RELEASE" diff --git a/roles/jails/07_aggro/tasks/main.yml b/roles/jails/07_aggro/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/07_aggro/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/08_diddle/defaults/main.yml b/roles/jails/08_diddle/defaults/main.yml new file mode 100644 index 0000000..9423bd6 --- /dev/null +++ b/roles/jails/08_diddle/defaults/main.yml @@ -0,0 +1,23 @@ +userland: "14.3-RELEASE" + +pkg: + - python311 + - py311-sqlite3 + - git + +files: + - src: root_clone.sh + dest: /root/clone.sh + mode: "0755" + - src: root_diddle_env.j2 + dest: /root/diddle/.env + mode: "0600" + - src: usr_local_bin_diddle + dest: /usr/local/bin/diddle + mode: "0755" + - src: usr_local_etc_rc.d_diddle + dest: /usr/local/etc/rc.d/diddle + mode: "0755" + +services: + - diddle diff --git a/roles/jails/08_diddle/tasks/main.yml b/roles/jails/08_diddle/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/08_diddle/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/08_diddle/templates/root_clone.sh b/roles/jails/08_diddle/templates/root_clone.sh new file mode 100644 index 0000000..7f92c5f --- /dev/null +++ b/roles/jails/08_diddle/templates/root_clone.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +REPO="$1" +TARGET="$2" + +set -eux + +# Exits with 100 if the directory was changed + +if [ ! -d "$TARGET" ]; then + git clone --depth=1 --branch main --single-branch "$REPO" "$TARGET" + exit 100 +fi + +cd "$TARGET" +before=$(git rev-parse HEAD) +git fetch --depth=1 --prune origin main +git reset --hard origin/main +after=$(git rev-parse HEAD) + +if [ "$before" != "$after" ]; then exit 100; fi diff --git a/roles/jails/08_diddle/templates/root_diddle_env.j2 b/roles/jails/08_diddle/templates/root_diddle_env.j2 new file mode 100644 index 0000000..ca7db78 --- /dev/null +++ b/roles/jails/08_diddle/templates/root_diddle_env.j2 @@ -0,0 +1,9 @@ +PYTHONUNBUFFERED="1" +BASE_URL="https://diddle.jan.systems" +DB_PATH="db.sqlite3" +EMAIL_HOST="smtp.postmarkapp.com" +EMAIL_PORT="587" +EMAIL_HOST_USER="{{ diddle_email_host_user }}" +EMAIL_HOST_PASSWORD="{{ diddle_email_host_password }}" +EMAIL_USE_TLS="true" +EMAIL_MESSAGE_FROM="diddle@jan.systems" diff --git a/roles/jails/08_diddle/templates/usr_local_bin_diddle b/roles/jails/08_diddle/templates/usr_local_bin_diddle new file mode 100644 index 0000000..d07cd5e --- /dev/null +++ b/roles/jails/08_diddle/templates/usr_local_bin_diddle @@ -0,0 +1,17 @@ +#!/bin/sh + +set -eux + +PATH=$PATH:/usr/local/bin + +cd /root/diddle + +if [ ! -f venv ]; then + python3.11 -m venv venv +fi + +PATH=$PATH:/root/diddle/venv/bin + +pip install -r requirements.txt +python apply_migrations.py +gunicorn --bind "0.0.0.0:80" -w 4 app:app diff --git a/roles/jails/08_diddle/templates/usr_local_etc_rc.d_diddle b/roles/jails/08_diddle/templates/usr_local_etc_rc.d_diddle new file mode 100644 index 0000000..01deaad --- /dev/null +++ b/roles/jails/08_diddle/templates/usr_local_etc_rc.d_diddle @@ -0,0 +1,43 @@ +#!/bin/sh +# +# PROVIDE: diddle +# REQUIRE: NETWORKING +# KEYWORD: shutdown +# +# Enable in /etc/rc.conf: +# diddle_enable="YES" +# +. /etc/rc.subr + +name="diddle" +rcvar=diddle_enable + +load_rc_config $name + +: ${diddle_command:=/usr/local/bin/diddle} +: ${diddle_log:=/var/log/${name}.log} + +start_cmd="${name}_start" +stop_cmd="${name}_stop" +status_cmd="${name}_status" + +extra_commands="status" + +diddle_start() { + /usr/local/bin/logto ${diddle_log} ${diddle_command} & +} + +diddle_status() { + if ps aux | grep diddle | grep -v grep | grep -v rc.d/diddle; then + echo "diddle is running" + else + echo "diddle is not running" + exit 1 + fi +} + +diddle_stop() { + ps aux | grep diddle | grep -v grep | grep -v rc.d/diddle | awk '{print $2}' | xargs kill -TERM +} + +run_rc_command "$1" diff --git a/roles/jails/09_redis/defaults/main.yml b/roles/jails/09_redis/defaults/main.yml new file mode 100644 index 0000000..5416267 --- /dev/null +++ b/roles/jails/09_redis/defaults/main.yml @@ -0,0 +1 @@ +userland: "15.0-RELEASE" diff --git a/roles/jails/09_redis/tasks/main.yml b/roles/jails/09_redis/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/09_redis/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/10_samba/defaults/main.yml b/roles/jails/10_samba/defaults/main.yml new file mode 100644 index 0000000..99c53e1 --- /dev/null +++ b/roles/jails/10_samba/defaults/main.yml @@ -0,0 +1,5 @@ +userland: "15.0-RELEASE" + +nullfs: + - src: /usr/local/jails/volumes/storage + dst: /mnt/storage diff --git a/roles/jails/10_samba/tasks/main.yml b/roles/jails/10_samba/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/10_samba/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/11_spliit/defaults/main.yml b/roles/jails/11_spliit/defaults/main.yml new file mode 100644 index 0000000..697ea79 --- /dev/null +++ b/roles/jails/11_spliit/defaults/main.yml @@ -0,0 +1 @@ +userland: "14.3-RELEASE" diff --git a/roles/jails/11_spliit/tasks/main.yml b/roles/jails/11_spliit/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/11_spliit/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/12_goaccess/defaults/main.yml b/roles/jails/12_goaccess/defaults/main.yml new file mode 100644 index 0000000..0fa6078 --- /dev/null +++ b/roles/jails/12_goaccess/defaults/main.yml @@ -0,0 +1,5 @@ +userland: "15.0-RELEASE" + +nullfs: + - src: /usr/local/jails/volumes/goaccess_www + dst: /var/www/goaccess diff --git a/roles/jails/12_goaccess/tasks/main.yml b/roles/jails/12_goaccess/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/12_goaccess/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/13_plex/defaults/main.yml b/roles/jails/13_plex/defaults/main.yml new file mode 100644 index 0000000..91ea619 --- /dev/null +++ b/roles/jails/13_plex/defaults/main.yml @@ -0,0 +1,5 @@ +userland: "15.0-RELEASE" + +nullfs: + - src: /usr/local/jails/volumes/storage/media + dst: /mnt/media diff --git a/roles/jails/13_plex/tasks/main.yml b/roles/jails/13_plex/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/13_plex/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/14_freshrss/defaults/main.yml b/roles/jails/14_freshrss/defaults/main.yml new file mode 100644 index 0000000..697ea79 --- /dev/null +++ b/roles/jails/14_freshrss/defaults/main.yml @@ -0,0 +1 @@ +userland: "14.3-RELEASE" diff --git a/roles/jails/14_freshrss/tasks/main.yml b/roles/jails/14_freshrss/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/14_freshrss/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/15_paste/defaults/main.yml b/roles/jails/15_paste/defaults/main.yml new file mode 100644 index 0000000..697ea79 --- /dev/null +++ b/roles/jails/15_paste/defaults/main.yml @@ -0,0 +1 @@ +userland: "14.3-RELEASE" diff --git a/roles/jails/15_paste/tasks/main.yml b/roles/jails/15_paste/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/15_paste/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/16_dl/defaults/main.yml b/roles/jails/16_dl/defaults/main.yml new file mode 100644 index 0000000..c296953 --- /dev/null +++ b/roles/jails/16_dl/defaults/main.yml @@ -0,0 +1,11 @@ +userland: "15.0-RELEASE" +devfs_ruleset: 4 +no_default_route: true + +jail_conf_options: + - "allow.raw_sockets" + - "allow.mlock" + +nullfs: + - src: /usr/local/jails/volumes/storage + dst: /mnt/storage diff --git a/roles/jails/16_dl/tasks/main.yml b/roles/jails/16_dl/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/16_dl/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/17_syncthing/defaults/main.yml b/roles/jails/17_syncthing/defaults/main.yml new file mode 100644 index 0000000..a2d7d8f --- /dev/null +++ b/roles/jails/17_syncthing/defaults/main.yml @@ -0,0 +1,11 @@ +userland: "15.0-RELEASE" + +nullfs: + - src: /usr/local/jails/volumes/storage/docs + dst: /mnt/docs + - src: /usr/local/jails/volumes/storage/vault + dst: /mnt/vault + - src: /usr/local/jails/volumes/storage/jan-systems-2025-content + dst: /mnt/jan-systems-2025-content + - src: /usr/local/jails/volumes/storage/projects-ableton + dst: /mnt/projects-ableton diff --git a/roles/jails/17_syncthing/tasks/main.yml b/roles/jails/17_syncthing/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/17_syncthing/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/18_komga/defaults/main.yml b/roles/jails/18_komga/defaults/main.yml new file mode 100644 index 0000000..d69a731 --- /dev/null +++ b/roles/jails/18_komga/defaults/main.yml @@ -0,0 +1,5 @@ +userland: "15.0-RELEASE" + +nullfs: + - src: /usr/local/jails/volumes/storage/media/manga + dst: /mnt/manga diff --git a/roles/jails/18_komga/tasks/main.yml b/roles/jails/18_komga/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/18_komga/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/roles/jails/19_leolalla_fi/defaults/main.yml b/roles/jails/19_leolalla_fi/defaults/main.yml new file mode 100644 index 0000000..5416267 --- /dev/null +++ b/roles/jails/19_leolalla_fi/defaults/main.yml @@ -0,0 +1 @@ +userland: "15.0-RELEASE" diff --git a/roles/jails/19_leolalla_fi/tasks/main.yml b/roles/jails/19_leolalla_fi/tasks/main.yml new file mode 100644 index 0000000..2abdaff --- /dev/null +++ b/roles/jails/19_leolalla_fi/tasks/main.yml @@ -0,0 +1,2 @@ +- import_role: + name: jail diff --git a/site.yml b/site.yml new file mode 100644 index 0000000..f55eef1 --- /dev/null +++ b/site.yml @@ -0,0 +1,74 @@ +--- +# Site playbook +# +# Usage: +# ansible-playbook -i inventory/test site.yml +# ansible-playbook -i inventory/test site.yml --limit test_vm +# ansible-playbook -i inventory/test site.yml --limit ingress +# ansible-playbook -i inventory/prod site.yml + +- name: Configure host + hosts: servers + tags: [host] + vars_files: + - secrets.yml + vars: + lan_ipv4_network: 192.168.0.0/16 + lan_search_domain: local.jan.systems + ingress_ip: "{{ jail_lan_cidr | ipv4_nth(1 + jail_lan_offset | int) }}" + pre_tasks: + - name: Verify secrets are loaded + assert: + that: smtp_user != '' + fail_msg: "Required variables missing. Did you forget to create secrets.yml?" + no_log: true + + tasks: + - name: Apply host roles + include_role: + name: "{{ item }}" + loop: "{{ host_roles }}" + +- name: Provision jails + hosts: jails + serial: 1 + gather_facts: false + vars_files: + - secrets.yml + vars: + lan_ipv4_network: 192.168.0.0/16 + lan_search_domain: local.jan.systems + ingress_ip: "{{ jail_lan_cidr | ipv4_nth(1 + jail_lan_offset | int) }}" + pre_tasks: + - name: Resolve jail role directory + set_fact: + jail_name: "{{ ansible_jail_name }}" + jail_num: "{{ _jail_dir | basename | split('_') | first | int }}" + jail_role_dir: "{{ _jail_dir }}" + vars: + _jail_dir: '{{ lookup(''pipe'', ''find '' + playbook_dir + ''/roles/jails -maxdepth 1 -type d -name "*_'' + ansible_jail_name + ''"'') }}' + + - name: Discover jail directories + find: + paths: "{{ playbook_dir }}/roles/jails" + patterns: "main.yml" + file_type: file + recurse: true + delegate_to: localhost + register: _jail_specs + + - name: Build jails list + set_fact: + jails: "{{ jails | default([]) + [{'num': _num, 'name': _name}] }}" + vars: + _num: "{{ item.path | regex_replace('.*/jails/([^/]+)/.*', '\\1') | split('_') | first | int }}" + _name: "{{ item.path | regex_replace('.*/jails/([^/]+)/.*', '\\1') | regex_replace('^[0-9]+_', '') }}" + loop: "{{ _jail_specs.files | sort(attribute='path') }}" + loop_control: + label: "{{ item.path | regex_replace('.*/jails/([^/]+)/.*', '\\1') }}" + when: "'/defaults/' in item.path" + + tasks: + - name: Apply jail role + include_role: + name: "{{ jail_role_dir }}" diff --git a/tasks/email.yml b/tasks/email.yml deleted file mode 100644 index 3585c3f..0000000 --- a/tasks/email.yml +++ /dev/null @@ -1,50 +0,0 @@ -- name: Create dma config directory - file: - path: /etc/dma - state: directory - owner: root - group: wheel - mode: "0755" - -- name: Configure dma.conf - template: - src: etc_dma_dma.conf.j2 - dest: /etc/dma/dma.conf - owner: root - group: wheel - mode: "0644" - register: etc_dma_dma_conf - -- name: Configure auth.conf - template: - src: etc_dma_auth.conf.j2 - dest: /etc/dma/auth.conf - owner: root - group: mail - mode: "0640" - register: etc_dma_auth_conf - -- name: Configure aliases - template: - src: etc_aliases.j2 - dest: /etc/aliases - owner: root - group: wheel - mode: "0644" - register: etc_aliases_conf - -- name: Run newaliases - shell: newaliases - when: etc_aliases_conf.changed - -- name: Test mail delivery - when: etc_dma_dma_conf.changed or etc_dma_auth_conf.changed or etc_aliases_conf.changed - block: - - name: Send email - shell: echo "DMA test message from Ansible" | mail -s "Test DMA" root - register: mail_result - changed_when: false - - - name: Show mail result - debug: - var: mail_result diff --git a/tasks/general.yml b/tasks/general.yml deleted file mode 100644 index 0ec9219..0000000 --- a/tasks/general.yml +++ /dev/null @@ -1,167 +0,0 @@ -- name: Install packages - package: - name: "{{ item }}" - state: present - loop: - - rsync - - dma - - jq - - curl - - bash - - python - - py311-pip - - fastfetch - -- name: Install amd64-specific packages - package: - name: "{{ item }}" - state: present - loop: - - vm-bhyve - - drm-kmod - when: not is_test_vm - -- name: Set up periodic.conf - template: - src: etc_periodic.conf.j2 - dest: /etc/periodic.conf - owner: root - group: wheel - mode: "0644" - -- name: Set up /etc/rc.conf - template: - src: etc_rc.conf.j2 - dest: /etc/rc.conf - owner: root - group: wheel - mode: "0644" - register: rc_conf - -- name: Set up fstab - template: - src: etc_fstab.j2 - dest: /etc/fstab - owner: root - group: wheel - mode: "0644" - register: fstab - when: not is_test_vm - -- name: Run mount -a - shell: mount -a - when: not is_test_vm and fstab.changed - -- name: Start auditd - service: - name: auditd - state: started - -- name: Check if pylogsentinel is installed - shell: pip show pylogsentinel - register: pylogsentinel_check - failed_when: false - changed_when: false - -- name: Install pylogsentinel - shell: pip install pylogsentinel==0.3.0 --force --no-input - when: pylogsentinel_check.rc != 0 - -- name: Install pylogsentinel.conf - template: - src: usr_local_etc_pylogsentinel.conf.j2 - dest: /usr/local/etc/pylogsentinel.conf - owner: root - group: wheel - mode: "0644" - -- name: Install pylogsentinel-batch-email.sh - template: - src: usr_local_bin_pylogsentinel-batch-email.sh.j2 - dest: /usr/local/bin/pylogsentinel-batch-email.sh - owner: root - group: wheel - mode: "0755" - -- name: Set up crontab - template: - src: etc_crontab.j2 - dest: /etc/crontab - owner: root - group: wheel - mode: "0644" - register: etc_crontab - -- name: Restart cron - service: - name: cron - state: restarted - when: etc_crontab.changed - -- name: Install logto - template: - src: usr_local_bin_logto.sh.j2 - dest: /usr/local/bin/logto - owner: root - group: wheel - mode: "0755" - -- name: Copy backup SSH private key - copy: - src: "{{ backup_ssh_privkey_file }}" - dest: /root/.ssh/backup - owner: root - group: wheel - mode: "0600" - when: not is_test_vm - -- name: Copy backup SSH public key - copy: - src: "{{ backup_ssh_pubkey_file }}" - dest: /root/.ssh/backup.pub - owner: root - group: wheel - mode: "0644" - when: not is_test_vm - -- name: Copy SSH config - template: - src: root_ssh_config.j2 - dest: /root/.ssh/config - owner: root - group: wheel - mode: "0644" - when: not is_test_vm - -- name: Copy backup script - template: - src: usr_local_bin_backup.sh.j2 - dest: /usr/local/bin/backup - owner: root - group: wheel - mode: "0755" - when: not is_test_vm - -- name: Check if zroot/vm exists - shell: zfs list zroot/vm - register: zroot_vm_check - failed_when: false - changed_when: false - when: not is_test_vm - -- name: Add vm dataset for bhyve and init - when: not is_test_vm and zroot_vm_check.rc != 0 - block: - - name: Create vm dataset - shell: zfs create zroot/vm - - - name: Run vm init - shell: vm init - -- name: Install .bashrc - copy: - src: templates/root_bashrc - dest: /root/.bashrc - owner: root - group: wheel - mode: "0644" diff --git a/tasks/jail_diddle.yml b/tasks/jail_diddle.yml deleted file mode 100644 index 73424f5..0000000 --- a/tasks/jail_diddle.yml +++ /dev/null @@ -1,63 +0,0 @@ -- name: Install packages inside jail - loop: - - { jail: diddle, package: python311 } - - { jail: diddle, package: py311-sqlite3 } - - { jail: diddle, package: git } - include_tasks: pkg_jail_install.yml - -- name: Install clone.sh - template: - src: root_clone.sh - dest: /usr/local/jails/containers/diddle/root/clone.sh - owner: root - group: wheel - mode: "0755" - -- name: Clone & update diddle - shell: jexec diddle sh /root/clone.sh https://github.com/jantuomi/diddle.git /root/diddle - register: diddle_git - changed_when: diddle_git.rc == 100 - failed_when: diddle_git.rc != 0 and diddle_git.rc != 100 - -- name: Install diddle env file - template: - src: diddle/root_diddle_env.j2 - dest: /usr/local/jails/containers/diddle/root/diddle/.env - owner: root - group: wheel - mode: "0600" - register: diddle_env - -- name: Install diddle startup script - template: - src: diddle/usr_local_bin_diddle - dest: /usr/local/jails/containers/diddle/usr/local/bin/diddle - owner: root - group: wheel - mode: "0755" - register: diddle_bin - -- name: Install diddle service - template: - src: diddle/usr_local_etc_rc.d_diddle - dest: /usr/local/jails/containers/diddle/usr/local/etc/rc.d/diddle - owner: root - group: wheel - mode: "0755" - register: diddle_rc - -- name: Check if diddle service is running - command: service -j diddle diddle status - changed_when: false - failed_when: false - register: diddle_status - -- name: Ensure diddle service is enabled - shell: service -j diddle diddle enable - changed_when: false - failed_when: false - -- name: (Re)start diddle service - shell: service -j diddle diddle restart - when: diddle_git.changed or diddle_bin.changed - or diddle_rc.changed or diddle_env.changed or diddle_status.rc != 0 diff --git a/tasks/jail_homepage.yml b/tasks/jail_homepage.yml deleted file mode 100644 index 6751ad5..0000000 --- a/tasks/jail_homepage.yml +++ /dev/null @@ -1,52 +0,0 @@ -- name: Install packages inside jail - loop: - - { jail: homepage, package: nginx } - - { jail: homepage, package: rsync } - - { jail: homepage, package: bash } - include_tasks: pkg_jail_install.yml - -- name: Create /var/www - file: - path: /usr/local/jails/containers/homepage/var/www - state: directory - owner: www - group: www - mode: "0755" - -# It's important to run this after generating certs, because nginx.conf refers to files -# generated by certbot. Certbot will fail validation if nginx.conf is configured too early. -- name: Configure nginx.conf - template: - src: homepage/usr_local_etc_nginx_nginx.conf.j2 - dest: /usr/local/jails/containers/homepage/usr/local/etc/nginx/nginx.conf - owner: root - group: wheel - mode: "0644" - register: homepage_nginx_conf - -- name: Check if nginx is enabled - shell: service -j homepage nginx status - changed_when: false - failed_when: false - register: homepage_nginx_enabled - -- name: Enable and start nginx - shell: | - service -j ingress nginx enable - service -j ingress nginx restart - when: homepage_nginx_enabled.rc != 0 or homepage_nginx_conf.changed - -- name: Set up crontab - template: - src: homepage/etc_crontab.j2 - dest: /usr/local/jails/containers/homepage/etc/crontab - owner: root - group: wheel - mode: "0644" - register: jail_homepage_etc_crontab - -- name: Restart cron - service: - name: cron - state: restarted - when: jail_homepage_etc_crontab.changed diff --git a/tasks/jail_hommabot.yml b/tasks/jail_hommabot.yml deleted file mode 100644 index b86d5d6..0000000 --- a/tasks/jail_hommabot.yml +++ /dev/null @@ -1,62 +0,0 @@ -- name: Install packages inside jail - loop: - - { jail: hommabot, package: npm } - include_tasks: pkg_jail_install.yml - -- name: Build hommabot on this machine - delegate_to: localhost - shell: | - cd ../hommabot2 - npm run build - -- name: Create hommabot directory - file: - path: /usr/local/jails/containers/hommabot/root/hommabot - state: directory - owner: root - group: wheel - mode: "0755" - -- name: Install index.js - copy: - src: "{{ item }}" - dest: /usr/local/jails/containers/hommabot/root/hommabot/ - owner: root - group: wheel - mode: "0644" - loop: - - "../hommabot2/build/index.js" - - "../hommabot2/package.json" - - "../hommabot2/package-lock.json" - -- name: Install hommabot env file - template: - src: hommabot/root_hommabot_env.j2 - dest: /usr/local/jails/containers/hommabot/root/hommabot/.env - owner: root - group: wheel - mode: "0600" - -- name: Install deps.sh - copy: - src: templates/hommabot/root_hommabot_deps.sh - dest: /usr/local/jails/containers/hommabot/root/hommabot/deps.sh - owner: root - group: wheel - mode: "0755" - -- name: Install dependencies - shell: jexec hommabot /root/hommabot/deps.sh - -- name: Set up crontab - template: - src: hommabot/etc_crontab.j2 - dest: /usr/local/jails/containers/hommabot/etc/crontab - owner: root - group: wheel - mode: "0644" - register: jail_hommabot_etc_crontab - -- name: Restart cron - shell: jexec hommabot service cron restart - when: jail_hommabot_etc_crontab.changed diff --git a/tasks/jail_ingress.yml b/tasks/jail_ingress.yml deleted file mode 100644 index 5945e42..0000000 --- a/tasks/jail_ingress.yml +++ /dev/null @@ -1,211 +0,0 @@ -- name: Ensure ingress goaccess mount points exist - file: - path: "{{ item }}" - state: directory - owner: root - group: wheel - mode: "0755" - loop: - - /usr/local/jails/containers/goaccess/var/www/goaccess - - /usr/local/jails/containers/ingress/mnt/www_goaccess - -- name: Start ingress jail - shell: service jail start ingress - register: ingress_jail_start - failed_when: false - changed_when: "'already running' not in ingress_jail_start.stderr" - -- name: Install packages inside jail - loop: - - { jail: ingress, package: nginx } - - { jail: ingress, package: py311-certbot } - - { jail: ingress, package: py311-certbot-nginx } - - { jail: ingress, package: goaccess } - include_tasks: pkg_jail_install.yml - -- name: Configure pf.conf - template: - src: ingress/etc_pf.conf.j2 - dest: /usr/local/jails/containers/ingress/etc/pf.conf - owner: root - group: wheel - mode: "0644" - register: ingress_pf_conf - -- name: Reload pf.conf - shell: jexec ingress pfctl -f /etc/pf.conf - when: ingress_pf_conf.changed - -- name: Check if gateway mode is enabled - shell: jexec ingress sysrc gateway_enable | grep -q "YES" - register: ingress_gateway_enabled - failed_when: false - changed_when: false - -- name: Enable gateway mode - shell: jexec ingress sysrc gateway_enable=YES - when: ingress_gateway_enabled.rc != 0 - -- name: Check if IP forwarding is enabled - shell: jexec ingress sysctl net.inet.ip.forwarding | grep -q "1" - register: ingress_ip_forwarding_enabled - failed_when: false - changed_when: false - -- name: Enable IP forwarding - shell: jexec ingress sysctl net.inet.ip.forwarding=1 - when: ingress_ip_forwarding_enabled.rc != 0 - -- name: Install pf-ban-socket.py - copy: - src: templates/ingress/pf-ban-socket.py - dest: /usr/local/jails/containers/ingress/usr/local/bin/pf-ban-socket.py - owner: root - group: wheel - mode: "0755" - register: ingress_pf_ban_socket_py - -- name: Install pf-ban-socket service - copy: - src: templates/ingress/usr_local_etc_rc.d_pf_ban_socket - dest: /usr/local/jails/containers/ingress/usr/local/etc/rc.d/pf_ban_socket - owner: root - group: wheel - mode: "0755" - register: ingress_pf_ban_socket_service - -- name: Check if pf enabled - shell: jexec ingress sysrc pf_enable | grep -q "YES" - register: ingress_pf_enabled - failed_when: false - changed_when: false - -- name: Enable pf - shell: jexec ingress sysrc pf_enable=YES - when: ingress_pf_enabled.rc != 0 - -- name: Check if pf is running - shell: service -j ingress pf status - register: ingress_pf_status - changed_when: ingress_pf_status.rc != 0 - -- name: Start pf - shell: service -j ingress pf start - when: ingress_pf_status.rc != 0 - -- name: Enable pf-ban-socket service - shell: | - service -j ingress pf_ban_socket enable - service -j ingress pf_ban_socket restart - when: ingress_pf_ban_socket_service.changed or ingress_pf_ban_socket_py.changed - -- name: Copy acme-dns-auth.py - copy: - src: templates/ingress/acme-dns-auth.py - dest: /usr/local/jails/containers/ingress/usr/local/bin/acme-dns-auth.py - owner: root - group: wheel - mode: "0755" - -- name: Check if LetsEncrypt certs are generated - shell: ls /usr/local/jails/containers/ingress/usr/local/etc/letsencrypt/live/{{ cert_name }} - register: cert_exists - failed_when: false - changed_when: false - -- name: Manually get certs with certbot and DNS challenge - pause: - prompt: | - /usr/local/etc/letsencrypt/live/{{ cert_name }} does not exist. This means that this is the first run of this playbook. - The {{ cert_name }} cert contains all of the @ and * certs for all domains. - Check the ingress crontab template and run the certbot command manually in the ingress jail. Remove the "-n" flag. - Add the requested DNS records manually into Hetzner DNS. Continue after this is done. - when: cert_exists.rc != 0 - -- name: Configure nginx.conf - template: - src: ingress/usr_local_etc_nginx_nginx.conf.j2 - dest: /usr/local/jails/containers/ingress/usr/local/etc/nginx/nginx.conf - owner: root - group: wheel - mode: "0644" - register: nginx_conf - -- name: Create nginx snippets directory - file: - path: "/usr/local/jails/containers/ingress/usr/local/etc/nginx/snippets" - state: directory - owner: root - group: wheel - mode: "0755" - -- name: Include ban.inc - template: - src: ingress/nginx_snippet_ban.inc - dest: /usr/local/jails/containers/ingress/usr/local/etc/nginx/snippets/ban.inc - owner: root - group: wheel - mode: "0644" - -- name: Create static directories - loop: "{{ ingress_routes | selectattr('static', 'defined') | map(attribute='static') | unique | list }}" - file: - path: "/usr/local/jails/containers/ingress{{ item }}" - state: directory - recurse: yes - owner: 80 - group: 80 - mode: "0755" - -- name: Check if nginx is enabled - shell: service -j ingress nginx status - changed_when: false - failed_when: false - register: ingress_nginx_enabled - -- name: Enable and start nginx - shell: | - service -j ingress nginx enable - service -j ingress nginx restart - when: ingress_nginx_enabled.rc != 0 or nginx_conf.changed - -- name: Install hetzner_ddns.sh - template: - src: ingress/usr_local_bin_hetzner_ddns.sh.j2 - dest: /usr/local/jails/containers/ingress/usr/local/bin/hetzner_ddns.sh - owner: root - group: wheel - mode: "0755" - -- name: Set up hetzner_auth - template: - src: ingress/usr_local_etc_hetzner_auth.j2 - dest: /usr/local/jails/containers/ingress/usr/local/etc/hetzner_auth - owner: root - group: wheel - mode: "0600" - -- name: Set up gen_goaccess.sh - template: - src: ingress/usr_local_bin_gen_goaccess.sh.j2 - dest: /usr/local/jails/containers/ingress/usr/local/bin/gen_goaccess.sh - owner: root - group: wheel - mode: "0755" - -- name: Set up crontab - template: - src: ingress/etc_crontab.j2 - dest: /usr/local/jails/containers/ingress/etc/crontab - owner: root - group: wheel - mode: "0644" - vars: - tls_hosts: "{{ ingress_routes | map(attribute='host') | unique }}" - register: jail_ingress_etc_crontab - -- name: Restart cron - service: - name: cron - state: restarted - when: jail_ingress_etc_crontab.changed diff --git a/tasks/jail_postgres.yml b/tasks/jail_postgres.yml deleted file mode 100644 index 42cdacf..0000000 --- a/tasks/jail_postgres.yml +++ /dev/null @@ -1,10 +0,0 @@ -- name: Install packages inside jail - loop: - - { jail: postgres, package: postgresql18-server } - include_tasks: pkg_jail_install.yml - -- name: Enable postgresql service inside jail - shell: | - service -j postgres postgresql enable - changed_when: false -# Run initdb and start manually, not managed by Ansible diff --git a/tasks/jails.yml b/tasks/jails.yml deleted file mode 100644 index 58d22d8..0000000 --- a/tasks/jails.yml +++ /dev/null @@ -1,49 +0,0 @@ -- name: Ensure jails directory exists - file: - path: /usr/local/jails - state: directory - owner: root - group: wheel - mode: "0755" - -- name: Create ZFS datasets - loop: - - { name: "zroot/jails", mountpoint: "/usr/local/jails" } - - { name: "zroot/jails/media" } - - { name: "zroot/jails/templates" } - - { - name: "zroot/jails/templates/{{ jail_userland_14_3 }}", - userland: "{{ jail_userland_14_3 }}", - } - - { - name: "zroot/jails/templates/{{ jail_userland_15_0 }}", - userland: "{{ jail_userland_15_0 }}", - } - - { name: "zroot/jails/containers" } - loop_control: - loop_var: dataset - include_tasks: jails_dataset.yml - -- name: Configure jails - block: - - name: Configure jail.conf - template: - src: etc_jail.conf.j2 - dest: /etc/jail.conf - owner: root - group: wheel - mode: "0644" - - - name: Configure devfs.rules - template: - src: etc_devfs.rules.j2 - dest: /etc/devfs.rules - owner: root - group: wheel - mode: "0644" - - - name: Configure individual jails - loop: "{{ jails }}" - loop_control: - loop_var: jail - include_tasks: jails_single.yml diff --git a/tasks/jails_dataset.yml b/tasks/jails_dataset.yml deleted file mode 100644 index 9e2748c..0000000 --- a/tasks/jails_dataset.yml +++ /dev/null @@ -1,61 +0,0 @@ -- name: "Check if dataset {{ dataset.name }} exists" - shell: zfs list -o name | grep -Fxq "{{ dataset.name }}" - changed_when: false - failed_when: false - register: check_dataset_exists - -- name: "Create ZFS dataset {{ dataset.name }}" - shell: | - {% if dataset.mountpoint is defined %} - zfs create -o "mountpoint={{ dataset.mountpoint }}" -p "{{ dataset.name }}" - {% else %} - zfs create -p "{{ dataset.name }}" - {% endif %} - when: check_dataset_exists.rc != 0 - -- name: Check if userland snapshot already exists - shell: zfs list -t snapshot -o name | grep -Fxq "{{ dataset.name }}@base" - failed_when: false - changed_when: false - register: zfs_userland_check - when: dataset.userland is defined - -- name: Set up userland - when: dataset.userland is defined and zfs_userland_check.rc != 0 - block: - - name: Download userland - get_url: - url: https://download.freebsd.org/ftp/releases/{{ arch }}/{{ dataset.userland }}/base.txz - dest: /usr/local/jails/media/{{ dataset.userland }}-base.txz - owner: root - group: wheel - mode: "0644" - - - name: Unarchive userland - shell: tar -xzf /usr/local/jails/media/{{ dataset.userland }}-base.txz -C /usr/local/jails/templates/{{ dataset.userland }} - - - name: Copy localtime to jail userland - copy: - remote_src: true - src: /etc/localtime - dest: /usr/local/jails/templates/{{ dataset.userland }}/etc/localtime - - - name: Copy resolv.conf to jail userland - template: - src: etc_resolv.conf.j2 - dest: /usr/local/jails/templates/{{ dataset.userland }}/etc/resolv.conf - owner: root - group: wheel - mode: "0644" - - - name: Disable resolvconf in the template - shell: echo 'resolvconf=NO' >> /usr/local/jails/templates/{{ dataset.userland }}/etc/resolvconf.conf - - - name: Update userland to latest patch level - shell: freebsd-update -b /usr/local/jails/templates/{{ dataset.userland }}/ fetch install - - - name: Disable syslogd in the template - shell: echo 'syslogd_enable="NO"' >> /usr/local/jails/templates/{{ dataset.userland }}/etc/rc.conf - - - name: Create userland ZFS snapshot - shell: zfs snapshot zroot/jails/templates/{{ dataset.userland }}@base diff --git a/tasks/jails_single.yml b/tasks/jails_single.yml deleted file mode 100644 index 7c5dd74..0000000 --- a/tasks/jails_single.yml +++ /dev/null @@ -1,27 +0,0 @@ -- name: "Check if jail directory for {{ jail.name }} exists" - shell: zfs list -o name | grep -Fxq "zroot/jails/containers/{{ jail.name }}" - failed_when: false - changed_when: false - register: check_jail_directory - -- name: "ZFS clone snapshot to jail directory for {{ jail.name }}" - shell: zfs clone zroot/jails/templates/{{ jail.userland }}@base "zroot/jails/containers/{{ jail.name }}" - when: check_jail_directory.rc != 0 - -- name: Configure jail.conf.d/{{ jail.name }}.conf - template: - src: etc_jail.conf.d_[jailname].conf.j2 - dest: /etc/jail.conf.d/{{ jail.name }}.conf - owner: root - group: wheel - mode: "0644" -# -#- name: "Check if jail {{ jail.name }} is running" -# shell: jls -j "{{ jail.name }}" -# failed_when: false -# changed_when: false -# register: check_jail_active -# -#- name: "(Re)start jail {{ jail.name }}" -# shell: service jail restart "{{ jail.name }}" -# when: check_jail_active.rc != 0 diff --git a/tasks/network.yml b/tasks/network.yml deleted file mode 100644 index 86cbf46..0000000 --- a/tasks/network.yml +++ /dev/null @@ -1,48 +0,0 @@ -- name: Set up resolv.conf - template: - src: etc_resolv.conf.j2 - dest: /etc/resolv.conf - owner: root - group: wheel - mode: "0644" - -# TODO: this doesn't work without rc_conf having been run -#- name: Restart networking if interface configuration changed -# shell: service netif restart && service routing restart -# when: rc_conf.changed or resolv_conf.changed - -- name: Apply network configuration - shell: service netif restart && service routing restart - when: rc_conf.changed - -- name: Set up sshd - template: - src: etc_ssh_sshd_config.j2 - dest: /etc/ssh/sshd_config - owner: root - group: wheel - mode: "0644" - register: etc_sshd_config - -- name: Start sshd - service: - name: sshd - state: started - register: started_sshd - -- name: Restart sshd - service: - name: sshd - state: restarted - when: not started_sshd.changed and etc_sshd_config.changed - -- name: Start syslogd - service: - name: syslogd - state: started - register: started_syslogd -#- name: Restart syslogd -# service: -# name: syslogd -# state: restarted -# when: not started_syslogd.changed and rc_conf.changed diff --git a/tasks/pkg_jail_install.yml b/tasks/pkg_jail_install.yml deleted file mode 100644 index 0315a03..0000000 --- a/tasks/pkg_jail_install.yml +++ /dev/null @@ -1,9 +0,0 @@ -- name: Check if {{ item.package }} is installed - shell: pkg -j {{ item.jail }} info {{ item.package }} - register: pkg_jail_installed - changed_when: false - failed_when: false - -- name: Install {{ item.package }} - shell: pkg -j {{ item.jail }} install -y {{ item.package }} - when: pkg_jail_installed.rc != 0 diff --git a/tasks/zfs.yml b/tasks/zfs.yml deleted file mode 100644 index 59495dc..0000000 --- a/tasks/zfs.yml +++ /dev/null @@ -1,48 +0,0 @@ -- name: Ensure /usr/local/etc/rc.d/ exists - file: - path: /usr/local/etc/rc.d/ - state: directory - owner: root - group: wheel - mode: "0755" - -- name: Add zpool_gpt_labels rc.d service - template: - src: usr_local_etc_rc.d_zpool_gpt_labels.j2 - dest: /usr/local/etc/rc.d/zpool_gpt_labels - owner: root - group: wheel - mode: "0755" - register: rc_zpool_gpt_labels - -- name: Run zpool_gpt_labels - service: - name: zpool_gpt_labels - state: started - when: rc_zpool_gpt_labels.changed - -- name: Install smartd - package: - name: smartmontools - state: present - -- name: Configure smartd.conf - template: - src: usr_local_etc_smartd.conf.j2 - dest: /usr/local/etc/smartd.conf - owner: root - group: wheel - mode: "0644" - register: smartd_conf - -- name: Start smartd - service: - name: smartd - state: started - register: started_smartd - -- name: Restart smartd - service: - name: smartd - state: restarted - when: not started_smartd.changed and smartd_conf.changed diff --git a/templates/diddle/root_diddle_env.j2 b/templates/diddle/root_diddle_env.j2 deleted file mode 100644 index ca7db78..0000000 --- a/templates/diddle/root_diddle_env.j2 +++ /dev/null @@ -1,9 +0,0 @@ -PYTHONUNBUFFERED="1" -BASE_URL="https://diddle.jan.systems" -DB_PATH="db.sqlite3" -EMAIL_HOST="smtp.postmarkapp.com" -EMAIL_PORT="587" -EMAIL_HOST_USER="{{ diddle_email_host_user }}" -EMAIL_HOST_PASSWORD="{{ diddle_email_host_password }}" -EMAIL_USE_TLS="true" -EMAIL_MESSAGE_FROM="diddle@jan.systems" diff --git a/templates/diddle/usr_local_bin_diddle b/templates/diddle/usr_local_bin_diddle deleted file mode 100644 index d07cd5e..0000000 --- a/templates/diddle/usr_local_bin_diddle +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh - -set -eux - -PATH=$PATH:/usr/local/bin - -cd /root/diddle - -if [ ! -f venv ]; then - python3.11 -m venv venv -fi - -PATH=$PATH:/root/diddle/venv/bin - -pip install -r requirements.txt -python apply_migrations.py -gunicorn --bind "0.0.0.0:80" -w 4 app:app diff --git a/templates/diddle/usr_local_etc_rc.d_diddle b/templates/diddle/usr_local_etc_rc.d_diddle deleted file mode 100644 index 01deaad..0000000 --- a/templates/diddle/usr_local_etc_rc.d_diddle +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/sh -# -# PROVIDE: diddle -# REQUIRE: NETWORKING -# KEYWORD: shutdown -# -# Enable in /etc/rc.conf: -# diddle_enable="YES" -# -. /etc/rc.subr - -name="diddle" -rcvar=diddle_enable - -load_rc_config $name - -: ${diddle_command:=/usr/local/bin/diddle} -: ${diddle_log:=/var/log/${name}.log} - -start_cmd="${name}_start" -stop_cmd="${name}_stop" -status_cmd="${name}_status" - -extra_commands="status" - -diddle_start() { - /usr/local/bin/logto ${diddle_log} ${diddle_command} & -} - -diddle_status() { - if ps aux | grep diddle | grep -v grep | grep -v rc.d/diddle; then - echo "diddle is running" - else - echo "diddle is not running" - exit 1 - fi -} - -diddle_stop() { - ps aux | grep diddle | grep -v grep | grep -v rc.d/diddle | awk '{print $2}' | xargs kill -TERM -} - -run_rc_command "$1" diff --git a/templates/etc_aliases.j2 b/templates/etc_aliases.j2 deleted file mode 100644 index 4fe3e7f..0000000 --- a/templates/etc_aliases.j2 +++ /dev/null @@ -1 +0,0 @@ -*: {{ dma_to_address }} diff --git a/templates/etc_crontab.j2 b/templates/etc_crontab.j2 deleted file mode 100644 index de1f3c6..0000000 --- a/templates/etc_crontab.j2 +++ /dev/null @@ -1,32 +0,0 @@ -# /etc/crontab - root's crontab for FreeBSD -# -# -SHELL=/bin/sh -PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin -# -#minute hour mday month wday who command -# -# Save some entropy so that /dev/random can re-seed on boot. -*/11 * * * * operator /usr/libexec/save-entropy -# -# Rotate log files every hour, if necessary. -0 * * * * root newsyslog -# -# Perform daily/weekly/monthly maintenance. -1 3 * * * root periodic daily -15 4 * * 6 root periodic weekly -30 5 1 * * root periodic monthly -# -# Adjust the time zone if the CMOS clock keeps local time, as opposed to -# UTC time. See adjkerntz(8) for details. -1,31 0-5 * * * root adjkerntz -a - -# Take snapshots every day at 3:05 AM -5 3 * * * root logto /var/log/backup /usr/local/bin/backup snapshot prune-local -# Send backup snapshots to remote every third day at 4:05 AM -5 4 */3 * * root logto /var/log/backup /usr/local/bin/backup send-to-remote prune-remote notify - -# Run pylogmonitor -*/10 * * * * root python -m pylogsentinel -# Run pylogsentinel batch job once a day -5 6 * * * root /usr/local/bin/pylogsentinel-batch-email.sh diff --git a/templates/etc_devfs.rules.j2 b/templates/etc_devfs.rules.j2 deleted file mode 100644 index 531b767..0000000 --- a/templates/etc_devfs.rules.j2 +++ /dev/null @@ -1,91 +0,0 @@ -# "as-is" to the devfs(8) command with the exception that -# any references to other rulesets will be expanded first. These -# references must include a dollar sign '$' in front of the -# name to be expanded properly. -# -# - -# Very basic and secure ruleset: Hide everything. -# Used as a basis for other rules. -# -[devfsrules_hide_all=1] -add hide - -# Basic devices typically necessary. -# Requires: devfsrules_hide_all -# -[devfsrules_unhide_basic=2] -add path null unhide -add path zero unhide -add path crypto unhide -add path random unhide -add path urandom unhide - -# Devices typically needed to support logged-in users. -# Requires: devfsrules_hide_all -# -[devfsrules_unhide_login=3] -add path 'ptyp*' unhide -add path 'ptyq*' unhide -add path 'ptyr*' unhide -add path 'ptys*' unhide -add path 'ptyP*' unhide -add path 'ptyQ*' unhide -add path 'ptyR*' unhide -add path 'ptyS*' unhide -add path 'ptyl*' unhide -add path 'ptym*' unhide -add path 'ptyn*' unhide -add path 'ptyo*' unhide -add path 'ptyL*' unhide -add path 'ptyM*' unhide -add path 'ptyN*' unhide -add path 'ptyO*' unhide -add path 'ttyp*' unhide -add path 'ttyq*' unhide -add path 'ttyr*' unhide -add path 'ttys*' unhide -add path 'ttyP*' unhide -add path 'ttyQ*' unhide -add path 'ttyR*' unhide -add path 'ttyS*' unhide -add path 'ttyl*' unhide -add path 'ttym*' unhide -add path 'ttyn*' unhide -add path 'ttyo*' unhide -add path 'ttyL*' unhide -add path 'ttyM*' unhide -add path 'ttyN*' unhide -add path 'ttyO*' unhide -add path ptmx unhide -add path pts unhide -add path 'pts/*' unhide -add path fd unhide -add path 'fd/*' unhide -add path stdin unhide -add path stdout unhide -add path stderr unhide -add path 'drm' unhide -add path 'drm/*' unhide -add path 'dri' unhide -add path 'dri/*' unhide - -[devfsrules_jail=4] -add include $devfsrules_hide_all -add include $devfsrules_unhide_basic -add include $devfsrules_unhide_login - -[devfsrules_jail_postgres=5] -add include $devfsrules_hide_all -add include $devfsrules_unhide_basic -add include $devfsrules_unhide_login -add include $devfsrules_jail -add path 'bpf*' unhide - -[devfsrules_jail_ingress=5] -add include $devfsrules_hide_all -add include $devfsrules_unhide_basic -add include $devfsrules_unhide_login -add include $devfsrules_jail -add path 'bpf*' unhide -add path 'pf*' unhide diff --git a/templates/etc_dma_auth.conf.j2 b/templates/etc_dma_auth.conf.j2 deleted file mode 100644 index a1000ea..0000000 --- a/templates/etc_dma_auth.conf.j2 +++ /dev/null @@ -1 +0,0 @@ -{{ smtp_user }}|{{ smtp_host }}:{{ smtp_password }} diff --git a/templates/etc_dma_dma.conf.j2 b/templates/etc_dma_dma.conf.j2 deleted file mode 100644 index dfaa04f..0000000 --- a/templates/etc_dma_dma.conf.j2 +++ /dev/null @@ -1,8 +0,0 @@ -SMARTHOST {{ smtp_host }} -PORT {{ smtp_port }} -AUTHPATH /etc/dma/auth.conf -SECURETRANSFER -STARTTLS -MAILNAME {{ dma_mail_hostname }} -MASQUERADE {{ ansible_hostname }}@{{ dma_mail_hostname }} -#NULLCLIENT diff --git a/templates/etc_fstab.j2 b/templates/etc_fstab.j2 deleted file mode 100644 index 95d3fa0..0000000 --- a/templates/etc_fstab.j2 +++ /dev/null @@ -1,7 +0,0 @@ -# Device Mountpoint FStype Options Dump Pass# -/dev/gpt/efiboot0 /boot/efi msdosfs rw 2 2 -/dev/nda0p3.eli none swap sw 0 0 -/dev/nda1p3.eli none swap sw 0 0 -/dev/nda2p3.eli none swap sw 0 0 -/dev/nda3p3.eli none swap sw 0 0 -tmpfs /tmp tmpfs rw,mode=777,size=2g 0 0 diff --git a/templates/etc_jail.conf.d_[jailname].conf.j2 b/templates/etc_jail.conf.d_[jailname].conf.j2 deleted file mode 100644 index df10f47..0000000 --- a/templates/etc_jail.conf.d_[jailname].conf.j2 +++ /dev/null @@ -1,43 +0,0 @@ -# eplXa is host end (local network bridge), eplXb is jail end. -# The corresponding pubnet interface is epwX, but that's not created for all jails. -{{ jail.name }} { - # STARTUP/LOGGING/VNET - vnet; - persist; - exec.clean; - - exec.prestart = ""; - exec.start = "/bin/sh /etc/rc"; - exec.poststart = ""; - exec.prestop = ""; - exec.stop = "/bin/sh /etc/rc.shutdown"; - exec.poststop = ""; - - exec.consolelog = "/var/log/jail_console_${name}.log"; - - # PERMISSIONS - allow.raw_sockets; - exec.clean; - mount.devfs; - - # HOSTNAME/PATH - host.hostname = "${name}"; - path = "/usr/local/jails/containers/${name}"; - - # JAIL-SPECIFIC CONFIGURATION -{% set t = lookup( - 'ansible.builtin.first_found', - { - 'files': [ - 'jail_confs/' ~ jail.name ~ '.j2', - 'jail_confs/_default.j2', - ], - 'paths': [ playbook_dir ~ '/templates' ] - }, - errors='ignore' -) %} - -{% if t %} -{{ lookup('ansible.builtin.template', t) | indent(2, true) }} -{% endif %} -} diff --git a/templates/etc_jail.conf.j2 b/templates/etc_jail.conf.j2 deleted file mode 100644 index 7190816..0000000 --- a/templates/etc_jail.conf.j2 +++ /dev/null @@ -1,6 +0,0 @@ -# Include configurations from standard locations. -.include "/etc/jail.conf.d/*.conf"; -.include "/etc/jail.*.conf"; -.include "/usr/local/etc/jail[.]conf"; -.include "/usr/local/etc/jail.conf.d/*.conf"; -.include "/usr/local/etc/jail.*.conf"; diff --git a/templates/etc_periodic.conf.j2 b/templates/etc_periodic.conf.j2 deleted file mode 100644 index 176b55d..0000000 --- a/templates/etc_periodic.conf.j2 +++ /dev/null @@ -1,4 +0,0 @@ -daily_status_smart_enable="YES" -daily_status_security_inline="YES" -daily_status_zfs_enable="YES" -weekly_certbot_enable="YES" diff --git a/templates/etc_rc.conf.j2 b/templates/etc_rc.conf.j2 deleted file mode 100644 index 5380056..0000000 --- a/templates/etc_rc.conf.j2 +++ /dev/null @@ -1,48 +0,0 @@ -hostname="pursotin" -keymap="fi.kbd" -sshd_enable="YES" -ntpd_enable="YES" -ntpd_sync_on_start="YES" -powerd_enable="YES" -moused_nondefault_enable="NO" -# Set dumpdev to "AUTO" to enable crash dumps, "NO" to disable -dumpdev="AUTO" -zfs_enable="YES" -{% if is_test_vm %} -defaultrouter="10.0.20.1" -{% else %} -defaultrouter="{{ lan_ipv4_gateway }}" -{% endif %} -cloned_interfaces="bridge0 bridge1" -ifconfig_{{ nic_lan }}_name="lan0" -ifconfig_{{ nic_wan }}_name="wan0" -ifconfig_bridge0_name="brlan0" -ifconfig_bridge1_name="brwan0" -{% if is_test_vm %} -ifconfig_lan0="inet 10.0.20.2/24" -{% else %} -ifconfig_lan0="inet {{ lan_ipv4_cidr }}" -{% endif %} -ifconfig_lan0_ipv6="inet6 accept_rtadv" -ifconfig_wan0="up" -ifconfig_brlan0="addm lan0 up" -ifconfig_brwan0="addm wan0 up" -zpool_gpt_labels_enable="YES" -smartd_enable="YES" -pf_enable="YES" -syslogd_enable="YES" -syslogd_flags="-s" -jail_enable="YES" -jail_parallel_start="YES" -jail_reverse_stop="YES" -clear_tmp_enable="YES" -cleanvar_enable="YES" -auditd_enable="YES" -devd_enable="YES" -{% if not is_test_vm %} -vm_enable="YES" -vm_dir="zfs:zroot/vm" -vm_list="alpine0" -vm_delay="5" -kld_list="if_wg i915kms" -{% endif %} diff --git a/templates/etc_resolv.conf.j2 b/templates/etc_resolv.conf.j2 deleted file mode 100644 index b382ecf..0000000 --- a/templates/etc_resolv.conf.j2 +++ /dev/null @@ -1,2 +0,0 @@ -search {{ lan_search_domain }} -nameserver {{ dns_nameserver | default(lan_ipv4_gateway) }} diff --git a/templates/etc_ssh_sshd_config.j2 b/templates/etc_ssh_sshd_config.j2 deleted file mode 100644 index 06179ce..0000000 --- a/templates/etc_ssh_sshd_config.j2 +++ /dev/null @@ -1,121 +0,0 @@ -# $OpenBSD: sshd_config,v 1.104 2021/07/02 05:11:21 dtucker Exp $ - -# This is the sshd server system-wide configuration file. See -# sshd_config(5) for more information. - -# This sshd was compiled with PATH=/usr/bin:/bin:/usr/sbin:/sbin - -# The strategy used for options in the default sshd_config shipped with -# OpenSSH is to specify options with their default value where -# possible, but leave them commented. Uncommented options override the -# default value. - -# Note that some of FreeBSD's defaults differ from OpenBSD's, and -# FreeBSD has a few additional options. - -#Port 22 -#AddressFamily any -#ListenAddress 0.0.0.0 -#ListenAddress :: - -#HostKey /etc/ssh/ssh_host_rsa_key -#HostKey /etc/ssh/ssh_host_ecdsa_key -#HostKey /etc/ssh/ssh_host_ed25519_key - -# Ciphers and keying -#RekeyLimit default none - -# Logging -#SyslogFacility AUTH -#LogLevel INFO - -# Authentication: - -#LoginGraceTime 2m -PermitRootLogin prohibit-password -#StrictModes yes -#MaxAuthTries 6 -#MaxSessions 10 - -#PubkeyAuthentication yes - -# The default is to check both .ssh/authorized_keys and .ssh/authorized_keys2 -# but this is overridden so installations will only check .ssh/authorized_keys -AuthorizedKeysFile .ssh/authorized_keys - -#AuthorizedPrincipalsFile none - -#AuthorizedKeysCommand none -#AuthorizedKeysCommandUser nobody - -# For this to work you will also need host keys in /etc/ssh/ssh_known_hosts -#HostbasedAuthentication no -# Change to yes if you don't trust ~/.ssh/known_hosts for -# HostbasedAuthentication -#IgnoreUserKnownHosts no -# Don't read the user's ~/.rhosts and ~/.shosts files -#IgnoreRhosts yes - -# Change to yes to enable built-in password authentication. -# Note that passwords may also be accepted via KbdInteractiveAuthentication. -PasswordAuthentication no -#PermitEmptyPasswords no - -# Change to no to disable PAM authentication -#KbdInteractiveAuthentication yes - -# Kerberos options -#KerberosAuthentication no -#KerberosOrLocalPasswd yes -#KerberosTicketCleanup yes -#KerberosGetAFSToken no - -# GSSAPI options -#GSSAPIAuthentication no -#GSSAPICleanupCredentials yes - -# Set this to 'no' to disable PAM authentication, account processing, -# and session processing. If this is enabled, PAM authentication will -# be allowed through the KbdInteractiveAuthentication and -# PasswordAuthentication. Depending on your PAM configuration, -# PAM authentication via KbdInteractiveAuthentication may bypass -# the setting of "PermitRootLogin prohibit-password". -# If you just want the PAM account and session checks to run without -# PAM authentication, then enable this but set PasswordAuthentication -# and KbdInteractiveAuthentication to 'no'. -#UsePAM yes - -#AllowAgentForwarding yes -#AllowTcpForwarding yes -#GatewayPorts no -#X11Forwarding no -#X11DisplayOffset 10 -#X11UseLocalhost yes -#PermitTTY yes -#PrintMotd yes -#PrintLastLog yes -#TCPKeepAlive yes -#PermitUserEnvironment no -#Compression delayed -#ClientAliveInterval 0 -#ClientAliveCountMax 3 -#UseDNS yes -#PidFile /var/run/sshd.pid -#MaxStartups 10:30:100 -#PermitTunnel no -#ChrootDirectory none -#UseBlacklist no -#VersionAddendum FreeBSD-20250219 - -# no default banner path -#Banner none - -# override default of no subsystems -Subsystem sftp /usr/libexec/sftp-server - -# Example of overriding settings on a per-user basis -#Match User anoncvs -# X11Forwarding no -# AllowTcpForwarding no -# PermitTTY no -# ForceCommand cvs server diff --git a/templates/homepage/etc_crontab.j2 b/templates/homepage/etc_crontab.j2 deleted file mode 100644 index 8541f57..0000000 --- a/templates/homepage/etc_crontab.j2 +++ /dev/null @@ -1,9 +0,0 @@ -# /etc/crontab - root's crontab for FreeBSD -# -# -SHELL=/bin/sh -PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin -# -#minute hour mday month wday who command - -# TODO: update linklog diff --git a/templates/homepage/usr_local_etc_nginx_nginx.conf.j2 b/templates/homepage/usr_local_etc_nginx_nginx.conf.j2 deleted file mode 100644 index ee45405..0000000 --- a/templates/homepage/usr_local_etc_nginx_nginx.conf.j2 +++ /dev/null @@ -1,55 +0,0 @@ -worker_processes auto; - -events { - worker_connections 1024; -} - -http { - include mime.types; - default_type application/octet-stream; - - sendfile on; - keepalive_timeout 65; - - gzip on; - gzip_vary on; - gzip_proxied any; - gzip_comp_level 6; - gzip_min_length 256; - - # Compress sensible, text-based response types (HTML is covered by default) - gzip_types - text/plain - text/css - text/xml - text/javascript - application/javascript - application/json - application/ld+json - application/xml - application/rss+xml - application/atom+xml - image/svg+xml; - - server { - listen 80 default_server; - server_name _; - - root /var/www; - - index index.html; - - # Migration redirects from legacy site - location ~ ^/archive(?:/(.*))?$ { - return 307 /posts/$1$is_args$args; - } - location = /feed.xml { - # Serve atom.xml content at the legacy feed.xml URL (no redirect) - rewrite ^ /atom.xml break; - } - - location / { - try_files $uri $uri.html $uri/ =404; - } - } -} diff --git a/templates/hommabot/etc_crontab.j2 b/templates/hommabot/etc_crontab.j2 deleted file mode 100644 index 556d12a..0000000 --- a/templates/hommabot/etc_crontab.j2 +++ /dev/null @@ -1,10 +0,0 @@ -# /etc/crontab - root's crontab for FreeBSD -# -# -SHELL=/bin/sh -PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin -# -#minute hour mday month wday who command - -# Run hommabot every Monday at 9:00 AM -0 9 * * 1 root /bin/sh -c "cd /root/hommabot && node index.js" diff --git a/templates/hommabot/root_hommabot_deps.sh b/templates/hommabot/root_hommabot_deps.sh deleted file mode 100644 index 25c0e56..0000000 --- a/templates/hommabot/root_hommabot_deps.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -set -eux -cd /root/hommabot -npm ci - -# We need to build the better-sqlite3.node file before running the app -if [ ! -f build/better_sqlite3.node ]; then ( - mkdir -p build - cd node_modules/better-sqlite3 - npm run build-release - cp build/Release/better_sqlite3.node ../../build/better_sqlite3.node -) -fi diff --git a/templates/hommabot/root_hommabot_env.j2 b/templates/hommabot/root_hommabot_env.j2 deleted file mode 100644 index c66c666..0000000 --- a/templates/hommabot/root_hommabot_env.j2 +++ /dev/null @@ -1,4 +0,0 @@ -TELEGRAM_BOT_TOKEN="{{ hommabot_telegram_bot_token }}" -SHEETS_SPREADSHEET_ID="{{ hommabot_sheets_spreadsheet_id }}" -SHEETS_RANGE="{{ hommabot_sheets_range }}" -G_SA_JSON_B64="{{ hommabot_g_sa_json_b64 }}" diff --git a/templates/ingress/acme-dns-auth.py b/templates/ingress/acme-dns-auth.py deleted file mode 100755 index 77928e6..0000000 --- a/templates/ingress/acme-dns-auth.py +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env python3 -# -# Source: https://github.com/joohoi/acme-dns-certbot-joohoi/blob/master/acme-dns-auth.py -# Some modifications to configuration values present. - - -import json -import os -import sys - -import requests - -### EDIT THESE: Configuration values ### - -# URL to acme-dns instance -ACMEDNS_URL = "https://auth.acme-dns.io" -# Path for acme-dns credential storage -STORAGE_PATH = "/usr/local/etc/letsencrypt/acmedns.json" -# Whitelist for address ranges to allow the updates from -# Example: ALLOW_FROM = ["192.168.10.0/24", "::1/128"] -ALLOW_FROM = [] -# Force re-registration. Overwrites the already existing acme-dns accounts. -FORCE_REGISTER = False - -### DO NOT EDIT BELOW THIS POINT ### -### HERE BE DRAGONS ### - -DOMAIN = os.environ["CERTBOT_DOMAIN"] -if DOMAIN.startswith("*."): - DOMAIN = DOMAIN[2:] -VALIDATION_DOMAIN = "_acme-challenge." + DOMAIN -VALIDATION_TOKEN = os.environ["CERTBOT_VALIDATION"] - - -class AcmeDnsClient(object): - """ - Handles the communication with ACME-DNS API - """ - - def __init__(self, acmedns_url): - self.acmedns_url = acmedns_url - - def register_account(self, allowfrom): - """Registers a new ACME-DNS account""" - - if allowfrom: - # Include whitelisted networks to the registration call - reg_data = {"allowfrom": allowfrom} - res = requests.post( - self.acmedns_url + "/register", data=json.dumps(reg_data) - ) - else: - res = requests.post(self.acmedns_url + "/register") - if res.status_code == 201: - # The request was successful - return res.json() - else: - # Encountered an error - msg = ( - "Encountered an error while trying to register a new acme-dns " - "account. HTTP status {}, Response body: {}" - ) - print(msg.format(res.status_code, res.text)) - sys.exit(1) - - def update_txt_record(self, account, txt): - """Updates the TXT challenge record to ACME-DNS subdomain.""" - update = {"subdomain": account["subdomain"], "txt": txt} - headers = { - "X-Api-User": account["username"], - "X-Api-Key": account["password"], - "Content-Type": "application/json", - } - res = requests.post( - self.acmedns_url + "/update", headers=headers, data=json.dumps(update) - ) - if res.status_code == 200: - # Successful update - return - else: - msg = ( - "Encountered an error while trying to update TXT record in " - "acme-dns. \n" - "------- Request headers:\n{}\n" - "------- Request body:\n{}\n" - "------- Response HTTP status: {}\n" - "------- Response body: {}" - ) - s_headers = json.dumps(headers, indent=2, sort_keys=True) - s_update = json.dumps(update, indent=2, sort_keys=True) - s_body = json.dumps(res.json(), indent=2, sort_keys=True) - print(msg.format(s_headers, s_update, res.status_code, s_body)) - sys.exit(1) - - -class Storage(object): - def __init__(self, storagepath): - self.storagepath = storagepath - self._data = self.load() - - def load(self): - """Reads the storage content from the disk to a dict structure""" - data = dict() - filedata = "" - try: - with open(self.storagepath, "r") as fh: - filedata = fh.read() - except IOError as e: - if os.path.isfile(self.storagepath): - # Only error out if file exists, but cannot be read - print("ERROR: Storage file exists but cannot be read") - sys.exit(1) - try: - data = json.loads(filedata) - except ValueError: - if len(filedata) > 0: - # Storage file is corrupted - print("ERROR: Storage JSON is corrupted") - sys.exit(1) - return data - - def save(self): - """Saves the storage content to disk""" - serialized = json.dumps(self._data) - try: - with os.fdopen( - os.open(self.storagepath, os.O_WRONLY | os.O_CREAT, 0o600), "w" - ) as fh: - fh.truncate() - fh.write(serialized) - except IOError as e: - print("ERROR: Could not write storage file.") - sys.exit(1) - - def put(self, key, value): - """Puts the configuration value to storage and sanitize it""" - # If wildcard domain, remove the wildcard part as this will use the - # same validation record name as the base domain - if key.startswith("*."): - key = key[2:] - self._data[key] = value - - def fetch(self, key): - """Gets configuration value from storage""" - try: - return self._data[key] - except KeyError: - return None - - -if __name__ == "__main__": - # Init - client = AcmeDnsClient(ACMEDNS_URL) - storage = Storage(STORAGE_PATH) - - # Check if an account already exists in storage - account = storage.fetch(DOMAIN) - if FORCE_REGISTER or not account: - # Create and save the new account - account = client.register_account(ALLOW_FROM) - storage.put(DOMAIN, account) - storage.save() - - # Display the notification for the user to update the main zone - msg = "Please add the following CNAME record to your main DNS zone:\n{}" - cname = "{} CNAME {}.".format(VALIDATION_DOMAIN, account["fulldomain"]) - print(msg.format(cname)) - - # Update the TXT record in acme-dns instance - client.update_txt_record(account, VALIDATION_TOKEN) diff --git a/templates/ingress/etc_crontab.j2 b/templates/ingress/etc_crontab.j2 deleted file mode 100644 index 6879766..0000000 --- a/templates/ingress/etc_crontab.j2 +++ /dev/null @@ -1,18 +0,0 @@ -# /etc/crontab - root's crontab for FreeBSD -# -# -SHELL=/bin/sh -PATH=/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:/usr/local/bin -# -#minute hour mday month wday who command - -# Update LetsEncrypt certificates every day after 2:00 AM -0 2 * * * root certbot certonly -n -m {{ contact_email }} --agree-tos --manual --manual-auth-hook /usr/local/bin/acme-dns-auth.py --preferred-challenges dns --debug-challenges {% for domain in cert_domains %}-d '{{ domain }}' {% endfor %} || echo "ERROR: Failed to renew certs" >&2 - -# Update dynamic DNS (Hetzner) -{% for elem in hetzner_zone_record_ids %} -* * * * * root /usr/local/bin/hetzner_ddns.sh --zone '{{ elem.zone }}' --rr-id '{{ elem.rr_id }}' --iface-cmd 'ifconfig epw1b' >>/var/log/hetzner_ddns.log 2>&1 -{% endfor %} - -# Update goaccess report HTML -0 * * * * root /usr/local/bin/gen_goaccess.sh diff --git a/templates/ingress/etc_pf.conf.j2 b/templates/ingress/etc_pf.conf.j2 deleted file mode 100644 index c0528e1..0000000 --- a/templates/ingress/etc_pf.conf.j2 +++ /dev/null @@ -1,27 +0,0 @@ -{% for jail in jails -%} -{% if jail.name == 'ingress' -%} -# Interfaces & nets -lan = "epl{{ jail.num }}b" -wan = "epw{{ jail.num }}b" -lan_net = "{{ lan_ipv4_network }}" - -table persist - -# Keep PF out of loopback, drop by default if you add blocks later -set skip on lo0 -set block-policy drop - -# NAT: translate LAN traffic to the WAN interface address -nat on $wan from $lan_net to any -> ($wan) - -# Block traffic from IPs in the blocked table -block in quick from to any - -# Allow all outbound traffic from the jail and LAN via both interfaces -# NAT will be applied automatically when source is in $lan_net and going out $wan -pass out on $wan all keep state -pass out on $lan all keep state - -pass in on $wan inet proto {tcp, udp} from any to any port {80, 443} keep state -{% endif %} -{% endfor %} diff --git a/templates/ingress/nginx_snippet_ban.inc b/templates/ingress/nginx_snippet_ban.inc deleted file mode 100644 index 029b965..0000000 --- a/templates/ingress/nginx_snippet_ban.inc +++ /dev/null @@ -1,17 +0,0 @@ -# Trap route -location ^~ /wp-admin/ { - proxy_set_header X-IP $remote_addr; - proxy_method POST; - proxy_pass http://unix:/var/run/pfban/ban.sock:/ban; - - proxy_connect_timeout 50ms; - proxy_send_timeout 50ms; - proxy_read_timeout 50ms; - - # If the socket isn't up yet, still return something - error_page 500 502 503 504 = @ban_fallback; -} - -location @ban_fallback { - return 204; -} diff --git a/templates/ingress/pf-ban-socket.py b/templates/ingress/pf-ban-socket.py deleted file mode 100644 index cbcd80e..0000000 --- a/templates/ingress/pf-ban-socket.py +++ /dev/null @@ -1,72 +0,0 @@ -#!/usr/bin/env python3 -import http.server -import os -import re -import socketserver -import subprocess - -SOCK_PATH = "/var/run/pfban/ban.sock" -PF_TABLE = "blocked" - -# Simple, conservative filter to avoid junk / injection -IP_RE = re.compile(r"^[0-9A-Fa-f:.]{3,}$") - - -def ensure_socket_dir(path: str) -> None: - os.makedirs(os.path.dirname(path), mode=0o755, exist_ok=True) - - -class BanHandler(http.server.BaseHTTPRequestHandler): - # Silence default logging - def log_message(self, format, *args): - return - - def do_POST(self): - if self.path != "/ban": - self.send_response(404) - self.end_headers() - return - - ip = self.headers.get("X-IP", "").strip() - - if ip and IP_RE.match(ip): - subprocess.run( - ["/sbin/pfctl", "-t", PF_TABLE, "-T", "add", ip], - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - check=False, - ) - - self.send_response(204) - self.end_headers() - - def do_GET(self): - self.send_response(405) - self.end_headers() - - -class ThreadingUnixHTTPServer( - socketserver.ThreadingMixIn, - socketserver.UnixStreamServer, -): - daemon_threads = True - - -def main() -> None: - ensure_socket_dir(SOCK_PATH) - - # Remove stale socket if present - try: - os.unlink(SOCK_PATH) - except FileNotFoundError: - pass - - with ThreadingUnixHTTPServer(SOCK_PATH, BanHandler) as httpd: - # Allow nginx workers (www) to connect - os.chmod(SOCK_PATH, 0o660) - - httpd.serve_forever() - - -if __name__ == "__main__": - main() diff --git a/templates/ingress/usr_local_bin_gen_goaccess.sh.j2 b/templates/ingress/usr_local_bin_gen_goaccess.sh.j2 deleted file mode 100644 index 2cfc93a..0000000 --- a/templates/ingress/usr_local_bin_gen_goaccess.sh.j2 +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -TARGET=/mnt/www_goaccess/index.html - -cat /var/log/nginx/access.log | awk '$8=$1$8' | /usr/local/bin/goaccess --log-format=VCOMBINED -j "$(($(nproc) + 1))" --keep-last=30 -a -o $TARGET --restore --persist -chmod 644 $TARGET -chown www $TARGET -chgrp www $TARGET diff --git a/templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 b/templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 deleted file mode 100644 index a2f4430..0000000 --- a/templates/ingress/usr_local_bin_hetzner_ddns.sh.j2 +++ /dev/null @@ -1,130 +0,0 @@ -#!/bin/sh -# -# Hetzner DNS record updater (one-shot for cron) -# Requirements: curl, awk - -set -eu - -# ---------------------------- Defaults --------------------------------------- -# Env-overridable: -: "${API_TOKEN_FILE:=/usr/local/etc/hetzner_auth}" -: "${API_BASE:=https://api.hetzner.cloud/v1}" -# ----------------------------------------------------------------------------- - -# Defaults -TTL=300 - -usage() { - cat <<'USAGE' >&2 -Usage: hetzner_ddns.sh [OPTIONS] - -Options (named): - --zone NAME_OR_ID Hetzner Zone name or ID (primary mode only) - --rr-id ID RRSet identifier in the form "rr-name/rr-type" (e.g., "host/A") - --iface-cmd CMD Command that prints interface info (for IP discovery) - --ttl TTL Time-to-live of the record (optional) - -h, --help Show this help - -Environment: - API_TOKEN_FILE Path to file containing ONLY the API token - (default: /usr/local/etc/hetzner_auth) - API_BASE Hetzner Cloud DNS API base URL - (default: https://api.hetzner.cloud/v1) - -Examples: - API_TOKEN_FILE=/secret/token \ - ./hetzner_ddns.sh \ - --zone example.com --rr-name host --rr-type A \ - --iface-cmd "ifconfig em0" -USAGE -} - -ts() { date +"%Y-%m-%dT%H:%M:%S%z"; } -fail() { echo "$(ts) ERROR: $*" >&2; exit 2; } -error() { echo "$(ts) ERROR: $*" >&2; } -warn() { echo "$(ts) WARN: $*" >&2; } -info() { echo "$(ts) INFO: $*"; } - -# ----------------------------- Arg parsing ----------------------------------- -# Accept both `--key value` and `--key=value` - -while [ $# -gt 0 ]; do - case "$1" in - --zone=*) ZONE=${1#*=} ;; - --zone) ZONE=$2; shift ;; - --rr-id=*) RR_ID=${1#*=} ;; - --rr-id) RR_ID=$2; shift ;; - --iface-cmd=*) IFACE_CMD=${1#*=} ;; - --iface-cmd) IFACE_CMD=$2; shift ;; - --ttl=*) TTL=${1#*=} ;; - --ttl) TTL=$2; shift ;; - -h|--help) usage; exit 0 ;; - --) shift; break ;; - -*) - error "Unknown option: $1" - usage - exit 2 - ;; - *) - error "Unexpected positional argument: $1" - usage - exit 2 - ;; - esac - shift -done - -# ----------------------------- Validation ------------------------------------ -[ -n "${ZONE:-}" ] || fail "Missing --zone" -[ -n "${RR_ID:-}" ] || fail "Missing --rr-id" -[ -n "${IFACE_CMD:-}" ] || fail "Missing --iface-cmd" - -# ------------------------------ Auth ----------------------------------------- -if [ ! -r "${API_TOKEN_FILE}" ]; then - fail "Token file missing or unreadable: ${API_TOKEN_FILE}" -fi -API_TOKEN="$(cat "${API_TOKEN_FILE}" | tr -d '[:space:]')" -[ -n "${API_TOKEN}" ] || fail "API token is empty" - -# ------------------------------ Helpers -------------------------------------- -get_ip() { - # Extract first IPv4 after 'inet ' (ignore inet6) - sh -c "${IFACE_CMD}" 2>/dev/null | awk '/(^|[[:space:]])inet[[:space:]]/ {print $2; exit}' -} - -# ------------------------------ Main ----------------------------------------- -IFACE_IP="$(get_ip || true)" -if [ -z "${IFACE_IP}" ]; then - warn "No IPv4 address found via '${IFACE_CMD}' (interface not ready?)" - exit 1 -fi - -BODY=$(printf '{"records":[{"value":"%s","comment":"Updated by hetzner_ddns.sh at %s","ttl":"%s"}]}' \ - "${IFACE_IP}" "$(ts)" "${TTL}") - -HTTP_CODE=$( - curl -sS -o /dev/null -w "%{http_code}" -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer ${API_TOKEN}" \ - --data "${BODY}" \ - "${API_BASE}/zones/${ZONE}/rrsets/${RR_ID}/actions/set_records" -) - -case "${HTTP_CODE}" in - 2*) - info "Updated zone ${ZONE} RRSet ${RR_ID} -> ${IFACE_IP}" - exit 0 - ;; - 4*) - error "Client error from API (HTTP ${HTTP_CODE}) - check zone/rr params/token/body" - exit 2 - ;; - 5*) - warn "Server error from API (HTTP ${HTTP_CODE})" - exit 1 - ;; - *) - warn "Unexpected HTTP status ${HTTP_CODE}" - exit 1 - ;; -esac diff --git a/templates/ingress/usr_local_etc_hetzner_auth.j2 b/templates/ingress/usr_local_etc_hetzner_auth.j2 deleted file mode 100644 index 129dccf..0000000 --- a/templates/ingress/usr_local_etc_hetzner_auth.j2 +++ /dev/null @@ -1 +0,0 @@ -{{ hetzner_pat }} diff --git a/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 b/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 deleted file mode 100644 index 16299ef..0000000 --- a/templates/ingress/usr_local_etc_nginx_nginx.conf.j2 +++ /dev/null @@ -1,141 +0,0 @@ -worker_processes auto; - -events { - worker_connections 1024; -} - -http { - include mime.types; - default_type application/octet-stream; - - sendfile on; - keepalive_timeout 65; - - log_format vcombined '$host:$server_port ' - '$remote_addr - $remote_user [$time_local] ' - '"$request" $status $body_bytes_sent ' - '"$http_referer" "$http_user_agent"'; - - access_log /var/log/nginx/access.log vcombined; - - gzip on; - gzip_vary on; - gzip_min_length 512; - gzip_types - text/plain - text/css - application/json - application/javascript - application/xml - image/svg+xml; - - server { - listen 80 default_server; - server_name _; - - include /usr/local/etc/nginx/snippets/ban.inc; - - location / { - return 404; - } - } - - {% for route in ingress_routes -%} - server { - listen 80; - listen [::]:80; - server_name {{ route.host }}; - - include /usr/local/etc/nginx/snippets/ban.inc; - - return 307 https://$host$request_uri; - } - - server { - server_name {{ route.host }}; - http2 on; - - include /usr/local/etc/nginx/snippets/ban.inc; - - listen 443 ssl; - listen [::]:443 ssl; - - # See https://ssl-config.mozilla.org/#server=nginx&version=1.28.0&config=intermediate&openssl=3.4.0&guideline=5.7 - add_header Strict-Transport-Security "max-age=63072000" always; - - # Common hardening headers - add_header X-Content-Type-Options nosniff always; - add_header X-Frame-Options DENY always; - add_header Referrer-Policy strict-origin-when-cross-origin always; - add_header Permissions-Policy interest-cohort=(); - - # Hide "Server: nginx/1.28.0" header - server_tokens off; - - ssl_certificate /usr/local/etc/letsencrypt/live/{{ cert_name }}/fullchain.pem; - ssl_certificate_key /usr/local/etc/letsencrypt/live/{{ cert_name }}/privkey.pem; - include /usr/local/etc/letsencrypt/options-ssl-nginx.conf; - ssl_dhparam /usr/local/etc/letsencrypt/ssl-dhparams.pem; - - {% if route.jail is defined -%} - {% for jail in jails if jail.name == route.jail -%} - location / { - proxy_pass http://192.168.2.{{ jail.num }}{% if route.port is defined %}:{{ route.port }}{% endif %}; - 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; - {% if jail.name == 'irc_thelounge' -%} - proxy_http_version 1.1; - proxy_set_header Connection "Upgrade"; - proxy_set_header Upgrade $http_upgrade; - - # by default nginx times out connections in one minute - proxy_read_timeout 1d; - proxy_send_timeout 1d; - proxy_buffering off; - proxy_request_buffering off; - - client_max_body_size 100M; - {% elif jail.name == 'plex' -%} - proxy_http_version 1.1; - proxy_set_header Connection "Upgrade"; - proxy_set_header Upgrade $http_upgrade; - - # Streaming-friendly behavior - proxy_redirect off; - proxy_buffering off; - - # Long streams / slow clients - proxy_read_timeout 3600s; - proxy_send_timeout 3600s; - {% endif %} - } - {% endfor %} - {% elif route.redirect is defined -%} - return 307 https://{{ route.redirect }}$request_uri; - {% elif route.ip is defined -%} - location / { - proxy_pass http://{{ route.ip }}:{{ route.port }}; - 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; - - # TODO: these shouldn't configured for all ip:port proxies but Immich needs them - proxy_http_version 1.1; - proxy_set_header Connection "upgrade"; - proxy_set_header Upgrade $http_upgrade; - - # by default nginx times out connections in one minute - proxy_read_timeout 1d; - proxy_send_timeout 1d; - proxy_buffering off; - proxy_request_buffering off; - - client_max_body_size 10G; - } - {% endif %} - } - {% endfor %} -} diff --git a/templates/ingress/usr_local_etc_rc.d_pf_ban_socket b/templates/ingress/usr_local_etc_rc.d_pf_ban_socket deleted file mode 100644 index d21bc2d..0000000 --- a/templates/ingress/usr_local_etc_rc.d_pf_ban_socket +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/sh -# -# PROVIDE: pf_ban_socket -# REQUIRE: NETWORKING pf -# KEYWORD: shutdown -# -# Enable in /etc/rc.conf: -# pf_ban_socket_enable="YES" -# -. /etc/rc.subr - -name="pf_ban_socket" -rcvar=pf_ban_socket_enable - -load_rc_config $name - -: ${pf_ban_socket_command:=/usr/local/bin/pf-ban-socket.py} -: ${pf_ban_socket_log:=/var/log/${name}.log} - -start_cmd="${name}_start" -stop_cmd="${name}_stop" -status_cmd="${name}_status" - -extra_commands="status" - -pf_ban_socket_start() { - /usr/local/bin/logto ${pf_ban_socket_log} ${pf_ban_socket_command} & -} - -pf_ban_socket_status() { - if pgrep -f "pf-ban-socket.py"; then - echo "pf_ban_socket is running" - else - echo "pf_ban_socket is not running" - exit 1 - fi -} - -pf_ban_socket_stop() { - pkill "pf-ban-socket.py" -} - -run_rc_command "$1" diff --git a/templates/jail_confs/_default.j2 b/templates/jail_confs/_default.j2 deleted file mode 100644 index 3a4e492..0000000 --- a/templates/jail_confs/_default.j2 +++ /dev/null @@ -1,5 +0,0 @@ -devfs_ruleset = 4; - -{% include 'jail_confs/_epl_network.j2' %} - -exec.poststart += "jexec ${name} route add default {{ ingress_ip }} || echo 'Failed to add default route'"; diff --git a/templates/jail_confs/_epl_network.j2 b/templates/jail_confs/_epl_network.j2 deleted file mode 100644 index fa3744a..0000000 --- a/templates/jail_confs/_epl_network.j2 +++ /dev/null @@ -1,18 +0,0 @@ -## _epl_network begin -exec.prestart += "ifconfig epair{{ jail.num }}000 create || echo 'Failed to create epair{{ jail.num }}000'"; -exec.prestart += "ifconfig epair{{ jail.num }}000a name epl{{ jail.num }}a"; -exec.prestart += "ifconfig epair{{ jail.num }}000b name epl{{ jail.num }}b"; -exec.prestart += "ifconfig epl{{ jail.num }}b ether random"; -exec.prestart += "ifconfig brlan0 addm epl{{ jail.num }}a"; - -exec.poststart += "ifconfig epl{{ jail.num }}b vnet ${name}"; -exec.poststart += "jexec ${name} ifconfig epl{{ jail.num }}b up"; -exec.poststart += "ifconfig epl{{ jail.num }}a up"; -exec.poststart += "jexec ${name} ifconfig epl{{ jail.num }}b {{ jail_lan_prefix }}.{{ jail.num + jail_ip_offset }}/{{ jail_lan_prefixlen }}"; -exec.poststart += "jexec ${name} route delete default || echo 'No default route to delete'"; - -{% if not is_test_vm %} -exec.poststart += "jexec ${name} route add 10.6.210.0/24 {{ lan_ipv4_gateway }} || echo 'Failed to add Wireguard return route'"; -{% endif %} -exec.poststop += "ifconfig epl{{ jail.num }}a destroy"; -## _epl_network end diff --git a/templates/jail_confs/dl.j2 b/templates/jail_confs/dl.j2 deleted file mode 100644 index 8d98ef4..0000000 --- a/templates/jail_confs/dl.j2 +++ /dev/null @@ -1,6 +0,0 @@ -{% include 'jail_confs/_epl_network.j2' %} -devfs_ruleset = 4; -allow.mlock; - -exec.prestart += "mount -t nullfs /usr/local/jails/volumes/storage /usr/local/jails/containers/dl/mnt/storage || echo 'Failed to mount'"; -exec.poststop += "umount /usr/local/jails/containers/dl/mnt/storage || echo 'Failed to umount'"; diff --git a/templates/jail_confs/ingress.j2 b/templates/jail_confs/ingress.j2 deleted file mode 100644 index 5964d06..0000000 --- a/templates/jail_confs/ingress.j2 +++ /dev/null @@ -1,22 +0,0 @@ -devfs_ruleset = 6; - -{% include 'jail_confs/_epl_network.j2' %} - -exec.prestart += "ifconfig epair{{ jail.num }}001 create || echo 'Failed to create epair{{ jail.num }}001'"; -exec.prestart += "ifconfig epair{{ jail.num }}001a name epw1a"; -exec.prestart += "ifconfig epair{{ jail.num }}001b name epw1b"; -exec.prestart += "ifconfig brwan0 addm epw{{ jail.num }}a"; - -exec.poststart += "ifconfig epw{{ jail.num }}b vnet ${name}"; -exec.poststart += "jexec ${name} ifconfig epw{{ jail.num }}b up"; -exec.poststart += "ifconfig epw{{ jail.num }}a up"; -{% if is_test_vm %} -exec.poststart += "jexec ${name} ifconfig epw{{ jail.num }}b inet 10.0.20.3/24"; -exec.poststart += "jexec ${name} route add default 10.0.20.1"; -{% else %} -exec.poststart += "jexec ${name} service dhclient restart epw{{ jail.num }}b"; -{% endif %} - -exec.prestart += "mount -t nullfs /usr/local/jails/containers/goaccess/var/www/goaccess /usr/local/jails/containers/ingress/mnt/www_goaccess || echo 'Failed to mount'"; -exec.poststop += "umount /usr/local/jails/containers/ingress/mnt/www_goaccess || echo 'Failed to umount'"; -exec.poststop += "ifconfig epw{{ jail.num }}a destroy"; diff --git a/templates/jail_confs/komga.j2 b/templates/jail_confs/komga.j2 deleted file mode 100644 index 2567fa2..0000000 --- a/templates/jail_confs/komga.j2 +++ /dev/null @@ -1,4 +0,0 @@ -{% include 'jail_confs/_default.j2' %} - -exec.prestart += "mount -t nullfs /usr/local/jails/volumes/storage/media/manga /usr/local/jails/containers/komga/mnt/manga || echo 'Failed to mount'"; -exec.poststop += "umount /usr/local/jails/containers/komga/mnt/manga || echo 'Failed to umount'"; diff --git a/templates/jail_confs/plex.j2 b/templates/jail_confs/plex.j2 deleted file mode 100644 index 2446a14..0000000 --- a/templates/jail_confs/plex.j2 +++ /dev/null @@ -1,4 +0,0 @@ -{% include 'jail_confs/_default.j2' %} - -exec.prestart += "mount -t nullfs /usr/local/jails/volumes/storage/media /usr/local/jails/containers/plex/mnt/media || echo 'Failed to mount'"; -exec.poststop += "umount /usr/local/jails/containers/plex/mnt/media || echo 'Failed to umount'"; diff --git a/templates/jail_confs/postgres.j2 b/templates/jail_confs/postgres.j2 deleted file mode 100644 index bb3d1f1..0000000 --- a/templates/jail_confs/postgres.j2 +++ /dev/null @@ -1,4 +0,0 @@ -{% include 'jail_confs/_epl_network.j2' %} -devfs_ruleset = 5; - -allow.sysvipc; diff --git a/templates/jail_confs/samba.j2 b/templates/jail_confs/samba.j2 deleted file mode 100644 index 24835fa..0000000 --- a/templates/jail_confs/samba.j2 +++ /dev/null @@ -1,4 +0,0 @@ -{% include 'jail_confs/_default.j2' %} - -exec.prestart += "mount -t nullfs /usr/local/jails/volumes/storage /usr/local/jails/containers/samba/mnt/storage || echo 'Failed to mount'"; -exec.poststop += "umount /usr/local/jails/containers/samba/mnt/storage || echo 'Failed to umount'"; diff --git a/templates/jail_confs/syncthing.j2 b/templates/jail_confs/syncthing.j2 deleted file mode 100644 index 980d47c..0000000 --- a/templates/jail_confs/syncthing.j2 +++ /dev/null @@ -1,11 +0,0 @@ -{% include 'jail_confs/_default.j2' %} - -exec.prestart += "mount -t nullfs /usr/local/jails/volumes/storage/docs /usr/local/jails/containers/syncthing/mnt/docs || echo 'Failed to mount'"; -exec.prestart += "mount -t nullfs /usr/local/jails/volumes/storage/vault /usr/local/jails/containers/syncthing/mnt/vault || echo 'Failed to mount'"; -exec.prestart += "mount -t nullfs /usr/local/jails/volumes/storage/jan-systems-2025-content /usr/local/jails/containers/syncthing/mnt/jan-systems-2025-content || echo 'Failed to mount'"; -exec.prestart += "mount -t nullfs /usr/local/jails/volumes/storage/projects-ableton /usr/local/jails/containers/syncthing/mnt/projects-ableton || echo 'Failed to mount'"; - -exec.poststop += "umount /usr/local/jails/containers/syncthing/mnt/projects-ableton || echo 'Failed to umount'"; -exec.poststop += "umount /usr/local/jails/containers/syncthing/mnt/jan-systems-2025-content || echo 'Failed to umount'"; -exec.poststop += "umount /usr/local/jails/containers/syncthing/mnt/vault || echo 'Failed to umount'"; -exec.poststop += "umount /usr/local/jails/containers/syncthing/mnt/docs || echo 'Failed to umount'"; diff --git a/templates/root_bashrc b/templates/root_bashrc deleted file mode 100644 index f580db6..0000000 --- a/templates/root_bashrc +++ /dev/null @@ -1,7 +0,0 @@ -HISTCONTROL=ignoredups:erasedups # no duplicate entries -HISTSIZE=100000 # big big history -HISTFILESIZE=100000 # big big history -shopt -s histappend # append to history, don't overwrite it - -# Save and reload the history after each command finishes -PROMPT_COMMAND="history -a; history -c; history -r; $PROMPT_COMMAND" diff --git a/templates/root_clone.sh b/templates/root_clone.sh deleted file mode 100644 index 7f92c5f..0000000 --- a/templates/root_clone.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -REPO="$1" -TARGET="$2" - -set -eux - -# Exits with 100 if the directory was changed - -if [ ! -d "$TARGET" ]; then - git clone --depth=1 --branch main --single-branch "$REPO" "$TARGET" - exit 100 -fi - -cd "$TARGET" -before=$(git rev-parse HEAD) -git fetch --depth=1 --prune origin main -git reset --hard origin/main -after=$(git rev-parse HEAD) - -if [ "$before" != "$after" ]; then exit 100; fi diff --git a/templates/root_ssh_config.j2 b/templates/root_ssh_config.j2 deleted file mode 100644 index 96f78f2..0000000 --- a/templates/root_ssh_config.j2 +++ /dev/null @@ -1,4 +0,0 @@ -Host backup - HostName {{ backup_ssh_host }} - User {{ backup_ssh_user }} - IdentityFile /root/.ssh/backup diff --git a/templates/usr_local_bin_backup.sh.j2 b/templates/usr_local_bin_backup.sh.j2 deleted file mode 100644 index f6a0e2c..0000000 --- a/templates/usr_local_bin_backup.sh.j2 +++ /dev/null @@ -1,365 +0,0 @@ -{% raw %}#!/usr/bin/env bash -set -euo pipefail - -# ===== CONFIG (filled by Ansible) ===== -KEEP_LOCAL=30 -KEEP_REMOTE=10 -KEYFILE="/root/.ssh/backup" -HOST_DIR="backup" -DATASET="{% endraw %}{{ backup_zfs_dataset }}{% raw %}" -BACKUP_EXCLUDE_PROP="{% endraw %}{{ backup_zfs_exclude_property | default('com.pursotin:backup') }}{% raw %}" -USER="{% endraw %}{{ backup_ssh_user }}{% raw %}" -HOST="{% endraw %}{{ backup_ssh_host }}{% raw %}" -EMAIL_TO=root -# ===================================== - -# ----- Globals for notification ----- -STARTED_AT="$(date '+%Y-%m-%dT%H:%M:%S%z')" -FINISHED_AT="" -MESSAGE_LOG="" -BACKUP_NAME="" # e.g. zroot@2025-09-30-12-00-00-0300.enc -BACKUP_SIZE_BYTES="" # numeric bytes - -# ----- Helpers ----- -die() { echo "Error: $*" >&2; exit 1; } - -require_cmds() { - local cmds=("$@") - for c in "${cmds[@]}"; do command -v "$c" >/dev/null 2>&1 || die "Missing command: $c"; done -} - -log_note() { - # Echo to console and append to message buffer - local msg="$1" - echo "$msg" - MESSAGE_LOG+="$msg"$'\n' -} - -timestamp() { - # Replace '+' with '-' so timezone is filename-safe and lexicographically sortable within TZ. - date +%Y-%m-%d-%H-%M-%S%z | tr '+' '-' -} - -humanize_bytes() { - local bytes="$1" - if [[ "${bytes}" =~ ^[0-9]+$ ]]; then - awk -v b="${bytes}" ' - BEGIN { - unit_count = split("B KiB MiB GiB TiB PiB EiB", units, " ") - i = 1 - while (b >= 1024 && i < unit_count) { - b = b / 1024 - i++ - } - if (i == 1) { - printf "%.0f %s\n", b, units[i] - } else { - printf "%.2f %s\n", b, units[i] - } - } - ' - else - echo "unknown" - fi -} - -latest_snapshot_for_dataset() { - # Latest snapshot on the TOP dataset only (newest first). Returns e.g. zroot@2025-09-30-... - zfs list -t snapshot -o name -S creation "$DATASET" 2>/dev/null \ - | awk 'NR==2{print; exit}' -} - -list_top_snapshots_newest_first() { - # Only list snapshots on the top dataset (not children), newest first. - zfs list -t snapshot -o name -S creation "$DATASET" 2>/dev/null \ - | awk 'NR>1{print $1}' -} - -list_excluded_datasets() { - # List datasets (including inherited values) where backup property is explicitly false. - zfs get -r -H -o name,value "${BACKUP_EXCLUDE_PROP}" "${DATASET}" 2>/dev/null \ - | awk 'tolower($2)=="false"{print $1}' -} - -list_remote_backups_sorted() { - # We store files under ${HOST_DIR}/@.enc - echo "ls ${HOST_DIR}" \ - | sftp -q -i "${KEYFILE}" "${USER}@${HOST}" 2>/dev/null \ - | tail -n +2 | sort -} - -# Upload a local file to the storage box path "${HOST_DIR}/" -sftp_put() { - local local_file="$1" - local remote_name="$2" # just the filename, no directory - sftp -i "${KEYFILE}" "${USER}@${HOST}" </dev/null 2>&1; then - echo "Remote directory already exists: ${HOST_DIR}" - log_note "init-remote: verified remote directory '${HOST_DIR}'" - return 0 - fi - - echo "Creating remote directory: ${HOST_DIR}" - set +e - sftp -i "${KEYFILE}" "${USER}@${HOST}" </dev/null 2>&1; then - echo "Remote directory created: ${HOST_DIR}" - log_note "init-remote: created remote directory '${HOST_DIR}'" - else - die "Remote directory ${HOST_DIR} not found after creation" - fi -} - -cmd_snapshot() { - echo "" - echo "[snapshot]" - require_cmds zfs date tr - local ts snap - ts="$(timestamp)" - snap="${DATASET}@${ts}" - echo "Taking recursive snapshot \"${snap}\"" - (set -x; zfs snapshot -r "${snap}") - echo "Recursive snapshot \"${snap}\" created" - log_note "snapshot: created recursive snapshot '${snap}'" -} - -cmd_send_to_remote() { - echo "" - echo "[send-to-remote]" - require_cmds zfs age sftp mktemp stat awk sort - - local snap base target tmp size_bytes ds parent skip - local -a send_args excluded_raw excluded - snap="$(latest_snapshot_for_dataset)" - [[ -n "${snap}" ]] || die "No snapshot found to send. Run 'snapshot' first or ensure the dataset has snapshots." - - base="$(basename "${snap}")" # e.g., zroot@2025-09-30-12-00-00-0300 - target="${base}.enc" - tmp="$(mktemp -t backup_send.XXXXXX)" - trap 'rm -f "${tmp}"' EXIT - - send_args=(-Rvc) - mapfile -t excluded_raw < <(list_excluded_datasets | sort) - excluded=() - for ds in "${excluded_raw[@]}"; do - skip=0 - [[ -n "${ds}" ]] || continue - for parent in "${excluded[@]}"; do - if [[ "${ds}" == "${parent}" || "${ds}" == "${parent}/"* ]]; then - skip=1 - break - fi - done - [[ "${skip}" -eq 1 ]] || excluded+=("${ds}") - done - - if [[ "${#excluded[@]}" -gt 0 ]]; then - echo "Excluding datasets where ${BACKUP_EXCLUDE_PROP}=false:" - for ds in "${excluded[@]}"; do - if [[ "${ds}" == "${DATASET}" ]]; then - die "Top dataset ${DATASET} is excluded via ${BACKUP_EXCLUDE_PROP}=false; refusing to create an empty backup stream." - fi - echo " - ${ds}" - send_args+=(-X "${ds}") - done - log_note "send-to-remote: excluded ${#excluded[@]} dataset(s) using ${BACKUP_EXCLUDE_PROP}=false" - fi - - echo "Creating encrypted replication stream to temp file: ${tmp}" - # -R: recursive hierarchy, -v: progress to stderr, -c: send compressed (keeps on-disk compression) - (set -x; zfs send "${send_args[@]}" "${snap}" | age -e -i "${KEYFILE}" > "${tmp}") - - size_bytes="$(stat -f %z "${tmp}" 2>/dev/null || stat -c %s "${tmp}" 2>/dev/null || echo "unknown")" - echo "Local stream size: ${size_bytes} bytes" - - echo "Uploading via SFTP to ${HOST}:${HOST_DIR}/${target}" - if sftp_put "${tmp}" "${target}"; then - echo "Upload complete" - else - die "SFTP upload failed (does the remote directory '${HOST_DIR}' exist? Run 'init-remote')" - fi - - # Set globals for notify() - BACKUP_NAME="${target}" - BACKUP_SIZE_BYTES="${size_bytes}" - - log_note "send-to-remote: uploaded '${snap}' as '${target}' (${size_bytes} bytes) to ${HOST}:${HOST_DIR}" - rm -f "${tmp}" - trap - EXIT -} - -cmd_prune_remote() { - echo "" - echo "[prune-remote]" - require_cmds sftp awk sort wc - - echo "Fetching remote backup listing from sftp://${HOST}/${HOST_DIR}" - BACKUPS="$(list_remote_backups_sorted || true)" - mapfile -t BACKUP_ARR < <(printf "%s\n" "${BACKUPS}") - local count="${#BACKUP_ARR[@]}" - - if [[ "${count}" -le "${KEEP_REMOTE}" ]]; then - echo "Remote backups (${count}) <= KEEP_REMOTE (${KEEP_REMOTE}); nothing to prune." - log_note "prune-remote: kept ${count} (<= ${KEEP_REMOTE}); no deletions" - return 0 - fi - - echo "Pruning remote backups, keeping latest ${KEEP_REMOTE} (will delete $(("${count}" - "${KEEP_REMOTE}")))" - local to_delete_count=$((count - KEEP_REMOTE)) - local deleted=0 - for ((i=0; i " - echo "Flags:" - echo " -s: Write only stdout to the log file." - echo " -e: Write only stderr to the log file." - echo "" - echo "Example usage:" - echo " logto /var/log/my.log run some command" - exit 1 -} - -mode="all" - -while getopts "se" opt; do - case $opt in - s) mode="stdout" ;; - e) mode="stderr" ;; - *) usage ;; - esac -done -shift $((OPTIND-1)) - -if [ $# -lt 2 ]; then - usage -fi - -log_file="$1" -shift - -if [ "$mode" = "stdout" ]; then - out=$(2>/dev/null $@) -elif [ "$mode" = "stderr" ]; then - out=$(2>&1 >/dev/null $@) -else - out=$(2>&1 $@) -fi - -if [ ! -z "$out" ]; then - echo "$(date +"%Y-%m-%dT%H:%M:%S%z")" "$out" >>"$log_file" -fi diff --git a/templates/usr_local_bin_pylogsentinel-batch-email.sh.j2 b/templates/usr_local_bin_pylogsentinel-batch-email.sh.j2 deleted file mode 100644 index d28efd9..0000000 --- a/templates/usr_local_bin_pylogsentinel-batch-email.sh.j2 +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/sh - -FILE=/tmp/pylogsentinel.daily - -if [ ! -f "$FILE" ]; then - echo "Nothing to do" - exit 0 -fi - -echo "Sending email" -cat "$FILE" | mail -s "pylogsentinel alert batch" root - -echo "Removing batch file" -rm "$FILE" diff --git a/templates/usr_local_bin_safepf.sh b/templates/usr_local_bin_safepf.sh deleted file mode 100644 index 1b6efee..0000000 --- a/templates/usr_local_bin_safepf.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/sh - -set -eu - -cmd="pfctl -f /etc/pf.conf" -read -p "Run this command (y/n)? $cmd " answer -if [ "$answer" = "y" ]; then - (set -x; $cmd) -else - exit 0 -fi - -cmd="service pf restart" -read -p "Run this command (y/n)? $cmd " answer -if [ "$answer" = "y" ]; then - (set -x; $cmd) -else - exit 0 -fi - -timeout=60 -echo "Running safety timeout ($timeout seconds). Press CTRL-C if everything is working." -while [ $timeout -gt 0 ] -do - sleep 1 - timeout=$((timeout - 1)) - echo -n "." -done - -echo "Timeout reached. Enabling empty pf rules" - -set -x -mv /etc/pf.conf /etc/pf.conf.locked_out -echo "" > /etc/pf.conf -pfctl -f /etc/pf.conf -service pf restart diff --git a/templates/usr_local_etc_pylogsentinel.conf.j2 b/templates/usr_local_etc_pylogsentinel.conf.j2 deleted file mode 100644 index cb80742..0000000 --- a/templates/usr_local_etc_pylogsentinel.conf.j2 +++ /dev/null @@ -1,22 +0,0 @@ -[system] -state_dir = /var/run/pylogsentinel -max_block_size = 10M - -[logs.standard] -cmd = find / -type f -path '*/var/log/*' ! -name '*access.log' ! -name '*.bz2' ! -name 'dmesg*' ! -name 'syncthing*' - -[logs.access] -paths = /usr/local/jails/containers/ingress/var/log/nginx/access.log - -[action.default] -cmd = echo -e "---------\nMatched $RULE_ID in $FILE at line $LINE, context:\n\n$CONTEXT\n" >> /tmp/pylogsentinel.daily - -[rule.error] -description = Error-like conditions -pattern = /(error|fatal|exception|killed)/i -logs = standard - -[rule.access] -description = HTTP code >=500 in access log -pattern = /HTTP\/[0-9].[0-9]" 5[0-9][0-9]/ -logs = access diff --git a/templates/usr_local_etc_rc.d_zpool_gpt_labels.j2 b/templates/usr_local_etc_rc.d_zpool_gpt_labels.j2 deleted file mode 100644 index 36bc715..0000000 --- a/templates/usr_local_etc_rc.d_zpool_gpt_labels.j2 +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh - -# PROVIDE: zpool_gpt_labels -# REQUIRE: zfs -# BEFORE: LOGIN -# KEYWORD: nojail - -. /etc/rc.subr - -name="zpool_gpt_labels" -rcvar="zpool_gpt_labels_enable" -start_cmd="zpool_gpt_labels_start" - -zpool_gpt_labels_start() { - logger "Setting zpool vdev paths to GPT labels" - local zpool_status="$(zpool status)" - for i in 0 1 2 3; do - local raw="nda${i}p4.eli" - local path="/dev/gpt/zfs${i}.eli" - case "$zpool_status" in - *"$raw"*) - zpool set "path=${path}" zroot "${raw}" - logger "Setting ${raw} -> ${path}" - ;; - *) - logger "Skipping ${raw}..." - ;; - esac - done -} - -load_rc_config $name -run_rc_command "$1" diff --git a/templates/usr_local_etc_smartd.conf.j2 b/templates/usr_local_etc_smartd.conf.j2 deleted file mode 100644 index bdcbfd0..0000000 --- a/templates/usr_local_etc_smartd.conf.j2 +++ /dev/null @@ -1,4 +0,0 @@ -/dev/nvme0 -a -d nvme -/dev/nvme1 -a -d nvme -/dev/nvme2 -a -d nvme -/dev/nvme3 -a -d nvme diff --git a/vm.sh b/vm.sh index 337a16d..93e204d 100755 --- a/vm.sh +++ b/vm.sh @@ -43,7 +43,7 @@ SEED_ISO="${VM_DIR}/seed.iso" SSH_KEY="${VM_DIR}/id_ed25519" SSH_PORT_LAN=22 -VM_RAM=2048 +VM_RAM=4096 VM_CPUS=2 DISK_SIZE=20G @@ -100,6 +100,7 @@ runcmd: - cp /usr/share/zoneinfo/Europe/Helsinki /etc/localtime - sysrc ifconfig_vtnet0="inet 10.0.20.2/24" - sysrc defaultrouter="10.0.20.1" + - echo 'vfs.zfs.arc.max=1073741824' >> /boot/loader.conf - ifconfig vtnet0 inet 10.0.20.2/24 - route add default 10.0.20.1 - sed -i '' 's/^#PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config @@ -151,8 +152,6 @@ cmd_up() { -cdrom "$SEED_ISO" \ -netdev vmnet-shared,id=lan,start-address=10.0.20.1,end-address=10.0.20.254,subnet-mask=255.255.255.0 \ -device virtio-net-pci,netdev=lan \ - -netdev vmnet-shared,id=wan,start-address=10.0.20.1,end-address=10.0.20.254,subnet-mask=255.255.255.0 \ - -device virtio-net-pci,netdev=wan \ -serial unix:"$SERIAL_SOCK",server,nowait \ -monitor unix:"$MONITOR_SOCK",server,nowait \ -pidfile "$PID_FILE" \ @@ -207,7 +206,7 @@ cmd_reset() { } cmd_ssh() { - exec ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ + exec env SSH_AUTH_SOCK= ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityAgent=none \ -i "$SSH_KEY" root@10.0.20.2 "$@" } -- cgit v1.3