diff options
| author | Jan Tuomi <jan@jantuomi.fi> | 2026-05-13 00:13:57 +0300 |
|---|---|---|
| committer | Jan Tuomi <jan@jantuomi.fi> | 2026-05-16 18:42:27 +0300 |
| commit | b5860daf11ac353049cb1654b9414a129e5cfb96 (patch) | |
| tree | 87ed89711e4f0e85ace0a97fa123199152c67302 /roles | |
| parent | 4715a28fdcd87440400d17154bfa361d99db29cc (diff) | |
Rework
Diffstat (limited to 'roles')
84 files changed, 2480 insertions, 0 deletions
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] <log_file> <command>" + 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}/<dataset>@<timestamp>.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}/<remote_name>" +sftp_put() { + local local_file="$1" + local remote_name="$2" # just the filename, no directory + sftp -i "${KEYFILE}" "${USER}@${HOST}" <<EOF +put ${local_file} ${HOST_DIR}/${remote_name} +EOF +} + +# ----- Subcommands ----- +cmd_init_remote() { + echo "" + echo "[init-remote]" + require_cmds sftp + + echo "Checking for remote directory: ${HOST_DIR}" + if echo "ls ${HOST_DIR}" | 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}" <<EOF +mkdir ${HOST_DIR} +EOF + rc=$? + set -e + if [[ $rc -ne 0 ]]; then + die "Failed to create remote directory ${HOST_DIR}" + fi + + # Re-check + if echo "ls ${HOST_DIR}" | sftp -q -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<to_delete_count; i++)); do + b="${BACKUP_ARR[$i]}" + [[ -n "${b}" ]] || continue + echo "Deleting remote: ${b}" + set +e + echo "rm ${b}" | sftp -i "${KEYFILE}" "${USER}@${HOST}" + rc=$? + set -e + if [[ $rc -ne 0 ]]; then + die "Failed to delete remote file: ${b}" + fi + deleted=$((deleted+1)) + done + echo "Remote prune done. Deleted: ${deleted}" + log_note "prune-remote: deleted ${deleted}, kept ${KEEP_REMOTE}" +} + +cmd_prune_local() { + echo "" + echo "[prune-local]" + require_cmds zfs awk + + echo "Pruning local snapshots on dataset: ${DATASET}; keeping latest ${KEEP_LOCAL}" + mapfile -t snaps < <(list_top_snapshots_newest_first) + local total="${#snaps[@]}" + if [[ "${total}" -le "${KEEP_LOCAL}" ]]; then + echo "Local snapshots (${total}) <= KEEP_LOCAL (${KEEP_LOCAL}); nothing to prune." + log_note "prune-local: kept ${total} (<= ${KEEP_LOCAL}); no deletions" + return 0 + fi + + local deleted=0 + # List is newest-first; skip first KEEP_LOCAL and destroy the rest RECURSIVELY across the tree + for ((i=KEEP_LOCAL; i<total; i++)); do + s="${snaps[$i]}" # e.g., zroot@2025-09-30-... + echo "Destroying recursive snapshot: ${s}" + (set -x; zfs destroy -r "${s}") + deleted=$((deleted+1)) + done + echo "Local prune done. Deleted: ${deleted}" + log_note "prune-local: deleted ${deleted}, kept ${KEEP_LOCAL}" +} + +cmd_notify() { + echo "" + echo "[notify]" + require_cmds mail date awk + local backup_size_human + FINISHED_AT="$(date '+%Y-%m-%dT%H:%M:%S%z')" + + # Derive snapshot name if not set yet (best-effort) + if [[ -z "${BACKUP_NAME}" ]]; then + # Try to infer from latest snapshot + latest="$(latest_snapshot_for_dataset || true)" + if [[ -n "${latest}" ]]; then + BACKUP_NAME="$(basename "${latest}").enc" + else + BACKUP_NAME="unknown" + fi + fi + + backup_size_human="$(humanize_bytes "${BACKUP_SIZE_BYTES:-unknown}")" + + # Compose subject and body + local subject="Backup completed: ${BACKUP_NAME}" + local body="" + body+="Backup run summary"$'\n' + body+="Started: ${STARTED_AT}"$'\n' + body+="Finished: ${FINISHED_AT}"$'\n' + body+="Dataset: ${DATASET}"$'\n' + body+="Remote: ${USER}@${HOST}:${HOST_DIR}"$'\n' + body+="Name: ${BACKUP_NAME}"$'\n' + body+="Size: ${backup_size_human} (${BACKUP_SIZE_BYTES:-unknown} bytes)"$'\n' + body+=$'\n' + body+="Steps:"$'\n' + body+="${MESSAGE_LOG:-<no steps recorded>}"$'\n' + + echo "Sending notification email..." + printf "%s\n" "$body" | mail -s "$subject" "${EMAIL_TO}" + log_note "notify: email sent to ${EMAIL_TO}" +} + +usage() { + cat <<EOF +Usage: $(basename "$0") [subcommand [subcommand ...]] + +Subcommands (executed in order): + init-remote Create the remote backup directory (\$HOST_DIR). Succeeds if it already exists. + snapshot Create a new **recursive** ZFS snapshot for \$DATASET + send-to-remote Create encrypted replication stream to a temp file, then upload via SFTP + prune-remote Keep latest \$KEEP_REMOTE backups on remote (default ${KEEP_REMOTE}) + prune-local Keep latest \$KEEP_LOCAL local snapshots on the top dataset (default ${KEEP_LOCAL}) + notify Send notification email (includes steps, timestamps, backup name & size) + +Notes: + - Run 'init-remote' once before the first upload, or anytime after changing \$HOST_DIR. + - Local pruning destroys older snapshots **recursively** to maintain consistency across descendants. + - Set \${BACKUP_EXCLUDE_PROP}=false on a dataset to exclude it (and inherited descendants) from send-to-remote. + Example: zfs set com.pursotin:backup=false zroot/some/heavy-dataset + +Examples: + $(basename "$0") init-remote snapshot send-to-remote prune-remote prune-local notify +EOF +} + +# ----- Main ----- +main() { + if [[ $# -eq 0 ]]; then + usage + exit 1 + fi + + # sanity + [[ -n "${DATASET}" ]] || die "DATASET not set" + [[ -n "${USER}" && -n "${HOST}" ]] || die "USER/HOST not set" + [[ -r "${KEYFILE}" ]] || die "KEYFILE not readable: ${KEYFILE}" + + while [[ $# -gt 0 ]]; do + case "$1" in + init-remote) cmd_init_remote ;; + snapshot) cmd_snapshot ;; + send-to-remote) cmd_send_to_remote ;; + prune-remote) cmd_prune_remote ;; + prune-local) cmd_prune_local ;; + notify) cmd_notify ;; + -h|--help|help) usage; exit 0 ;; + *) die "Unknown subcommand: $1" ;; + esac + shift + done + + echo "Done." +} + +main "$@" +{% endraw %} diff --git a/roles/host_prod/templates/usr_local_bin_safepf.sh b/roles/host_prod/templates/usr_local_bin_safepf.sh new file mode 100644 index 0000000..1b6efee --- /dev/null +++ b/roles/host_prod/templates/usr_local_bin_safepf.sh @@ -0,0 +1,36 @@ +#!/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/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.<timestamp> + 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 <blocked> 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 <blocked> 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 |
