From 40ac02c67eb089afd762c7abd981881fcb6cbc0c Mon Sep 17 00:00:00 2001 From: Hoang Nguyen Date: Fri, 16 Jun 2023 00:00:00 +0700 Subject: [PATCH] snapshot: add new role with 'snapper' option btrbk will be next \^*^/ Also: - detect the root filesystem in play with `ansible_mounts` instead of specifying it manually. - dnscrypt: hardcode some privacy settings --- .ansible-lint | 3 ++ TODO.md | 13 ++++--- group_vars/all.yml | 42 +++++++++++++++++++--- requirements/accepted_variables.yml | 2 +- roles/dns/templates/dnscrypt-proxy.j2 | 8 ++--- roles/fstab/tasks/main.yml | 1 - roles/ntpd/tasks/busybox.yml | 2 +- roles/snapshot/handlers/main.yml | 7 ++++ roles/snapshot/tasks/btrbk.yml | 1 + roles/snapshot/tasks/main.yml | 3 ++ roles/snapshot/tasks/sanoid.yml | 1 + roles/snapshot/tasks/snapper.yml | 26 ++++++++++++++ roles/snapshot/tasks/zrepl.yml | 1 + roles/snapshot/templates/btrbk.conf.j2 | 0 roles/snapshot/templates/snapper.j2 | 41 ++++++++++++++++++++++ setup.yml | 48 +++++++++++++------------- 16 files changed, 158 insertions(+), 41 deletions(-) create mode 100644 roles/snapshot/handlers/main.yml create mode 100644 roles/snapshot/tasks/btrbk.yml create mode 100644 roles/snapshot/tasks/main.yml create mode 100644 roles/snapshot/tasks/sanoid.yml create mode 100644 roles/snapshot/tasks/snapper.yml create mode 100644 roles/snapshot/tasks/zrepl.yml create mode 100644 roles/snapshot/templates/btrbk.conf.j2 create mode 100644 roles/snapshot/templates/snapper.j2 diff --git a/.ansible-lint b/.ansible-lint index 0e07d67..3070ad3 100644 --- a/.ansible-lint +++ b/.ansible-lint @@ -5,3 +5,6 @@ skip_list: - package-latest - fqcn[action-core] - name[casing] + +warn_list: + - var-naming[no-role-prefix] diff --git a/TODO.md b/TODO.md index bacacd0..24cacbe 100644 --- a/TODO.md +++ b/TODO.md @@ -1,13 +1,16 @@ -# TODO +# Todo list -Stuff that are planned to be changed. +Stuff that are planned to be added/changed. ## Configuration - [ ] /etc/security/access.conf - [ ] Filesystem snapshot: - - [ ] snapper / btrbk (rootfs=btrfs) - - [ ] sanoid (rootfs=zfs) + - [ ] btrbk (rootfs=btrfs) + - [ ] sanoid / zrepl (rootfs=zfs) +- [ ] Filesystem backup (I don't have spare hard drives -_- so not supported for now): + - [ ] Local incremental backups (to spare disk) + - [ ] Remote backups - [ ] incron - [ ] bees - [ ] kea as another option for dhcp client @@ -17,7 +20,7 @@ Stuff that are planned to be changed. ## Cosmetic -- [ ] Packer + Terraform / Pulumi (zfs + btrfs VMs) for testing the playbook +- [ ] Packer + Terraform / Pulumi (zfs + btrfs VMs) for testing the playbook (need implemented first) ## Just in case I forget diff --git a/group_vars/all.yml b/group_vars/all.yml index b4cf170..f69a876 100644 --- a/group_vars/all.yml +++ b/group_vars/all.yml @@ -29,8 +29,6 @@ dns_resolver: dnscrypt-proxy repository: https://ftp.udx.icscoe.jp/Linux/alpine -rootfs: btrfs - username: follie # Don't specify "seat" or "polkitd" group here @@ -61,9 +59,6 @@ dnscrypt: - quad9-dnscrypt-ip4-filter-pri - cloudflare-security - cloudflare-security-ipv6 - ephemeral_keys: true - tls_disable_session_tickets: true - tls_cipher_suite: [52392, 49199] bootstrap_resolvers: [9.9.9.9:53, 1.1.1.1:53] netprobe_address: 1.1.1.1:53 local_doh: @@ -139,6 +134,43 @@ earlyoom: # auditd by default rotates its logfile when reaching file size limit auditd_logrotate_daily: false +# Configuration for filesystem snapshot tools ───────────────────────────────── +# NOTE: these are examples to take note of available options + +snapper: + - name: home + subvolume: /home + pre_post_cleanup: + enabled: true + number_cleanup: + enabled: false + timeline: + cleanup_enabled: true + min_age: 1800 + hourly: 8 + daily: 4 + weekly: 2 + monthly: 0 + yearly: 0 + - name: root + subvolume: / + pre_post_cleanup: + enabled: true + min_age: 900 + number_cleanup: + enabled: true + min_age: 1800 + limit: 10-30 + limit_important: 10 + timeline: + cleanup_enabled: false + +btrbk: + +sanoid: + +zrepl: + # Secrets encrypted with ansible-vault ──────────────────────────────────────── password: '{{ vault_password }}' diff --git a/requirements/accepted_variables.yml b/requirements/accepted_variables.yml index 6e05a44..7b6e483 100644 --- a/requirements/accepted_variables.yml +++ b/requirements/accepted_variables.yml @@ -6,7 +6,7 @@ initramfs_generator: snapshot_tool: '{{ ["sanoid", "zrepl"] if rootfs == "zfs" else ["snapper", "btrbk"] if rootfs == "btrfs" - else ["lvm"] }}' + else ["none"] }}' # NOTE: Keep this in sync with `shell_mappings` in roles/user/defaults/main.yml usershell: diff --git a/roles/dns/templates/dnscrypt-proxy.j2 b/roles/dns/templates/dnscrypt-proxy.j2 index 0a85840..83b09e5 100644 --- a/roles/dns/templates/dnscrypt-proxy.j2 +++ b/roles/dns/templates/dnscrypt-proxy.j2 @@ -171,12 +171,12 @@ cert_refresh_delay = 240 ## This may improve privacy but can also have a significant impact on CPU usage ## Only enable if you don't have a lot of network load -dnscrypt_ephemeral_keys = {{ dnscrypt.ephemeral_keys | lower }} +dnscrypt_ephemeral_keys = true ## DoH: Disable TLS session tickets - increases privacy but also latency -tls_disable_session_tickets = {{ dnscrypt.tls_disable_session_tickets | lower }} +tls_disable_session_tickets = true ## DoH: Use a specific cipher suite instead of the server preference @@ -194,7 +194,7 @@ tls_disable_session_tickets = {{ dnscrypt.tls_disable_session_tickets | lower }} ## Keep tls_cipher_suite empty if you have issues fetching sources or ## connecting to some DoH servers. Google and Cloudflare are fine with it. -tls_cipher_suite = {{ dnscrypt.tls_cipher_suite }} +tls_cipher_suite = [52392, 49199] ## Bootstrap resolvers @@ -497,7 +497,7 @@ cert_key_file = '{{ ansible_hostname }}.pem' [blocked_names] -{% if dnscrypt.adblock %} +{% if (dnscrypt.adblock | bool) %} ## Path to the file of blocking rules (absolute, or relative to the same directory as the config file) blocked_names_file = '/etc/dnscrypt-proxy/blocked-names.txt' diff --git a/roles/fstab/tasks/main.yml b/roles/fstab/tasks/main.yml index 05be460..f96a930 100644 --- a/roles/fstab/tasks/main.yml +++ b/roles/fstab/tasks/main.yml @@ -7,7 +7,6 @@ fstype: tmpfs opts: rw,nosuid,nodev,size=4G,mode=1777 state: present - when: rootfs != 'zfs' # /run is mounted with exec by default - name: fstab | Harden mount options for /run diff --git a/roles/ntpd/tasks/busybox.yml b/roles/ntpd/tasks/busybox.yml index 81da4ab..10dc8ea 100644 --- a/roles/ntpd/tasks/busybox.yml +++ b/roles/ntpd/tasks/busybox.yml @@ -1,6 +1,6 @@ --- - name: ntpd | Adjust ntpd service configuration - copy: + copy: # noqa: jinja[spacing] content: > NTPD_OPTS="-N {%- for pool in ntp_opts.pools %} -p {{ pool }}{% endfor %} diff --git a/roles/snapshot/handlers/main.yml b/roles/snapshot/handlers/main.yml new file mode 100644 index 0000000..5f0aae1 --- /dev/null +++ b/roles/snapshot/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: Create .snapshots subvolumes manually + debug: + msg: > + Please create .snapshots/ directories and corresponding mounted subvolumes + under {{ snapper | map(attribute='subvolume') | join(', ') }} targets + manually. diff --git a/roles/snapshot/tasks/btrbk.yml b/roles/snapshot/tasks/btrbk.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/snapshot/tasks/btrbk.yml @@ -0,0 +1 @@ +--- diff --git a/roles/snapshot/tasks/main.yml b/roles/snapshot/tasks/main.yml new file mode 100644 index 0000000..c8015f7 --- /dev/null +++ b/roles/snapshot/tasks/main.yml @@ -0,0 +1,3 @@ +--- +- name: snapshot | Setup {{ snapshot_tool }} + include_tasks: '{{ snapshot_tool }}.yml' diff --git a/roles/snapshot/tasks/sanoid.yml b/roles/snapshot/tasks/sanoid.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/snapshot/tasks/sanoid.yml @@ -0,0 +1 @@ +--- diff --git a/roles/snapshot/tasks/snapper.yml b/roles/snapshot/tasks/snapper.yml new file mode 100644 index 0000000..d174ff9 --- /dev/null +++ b/roles/snapshot/tasks/snapper.yml @@ -0,0 +1,26 @@ +--- +- name: snapper | Install snapper package + community.general.apk: + name: snapper + state: present + +- name: snapper | Install config for each target + template: + src: snapper.j2 + dest: /etc/snapper/configs/{{ item.name }} + mode: '600' + owner: root + group: root + loop: '{{ snapper }}' + +- name: snapper | Install main snapper config + copy: + content: | + # List of snapper configurations. + SNAPPER_CONFIGS="{{ snapper | map(attribute='name') | join(' ') }}" + dest: /etc/snapper/snapper + mode: '644' + owner: root + group: root + notify: + - Create .snapshots subvolumes manually diff --git a/roles/snapshot/tasks/zrepl.yml b/roles/snapshot/tasks/zrepl.yml new file mode 100644 index 0000000..ed97d53 --- /dev/null +++ b/roles/snapshot/tasks/zrepl.yml @@ -0,0 +1 @@ +--- diff --git a/roles/snapshot/templates/btrbk.conf.j2 b/roles/snapshot/templates/btrbk.conf.j2 new file mode 100644 index 0000000..e69de29 diff --git a/roles/snapshot/templates/snapper.j2 b/roles/snapshot/templates/snapper.j2 new file mode 100644 index 0000000..04702c7 --- /dev/null +++ b/roles/snapshot/templates/snapper.j2 @@ -0,0 +1,41 @@ +SUBVOLUME="{{ item.subvolume }}" + +FSTYPE="btrfs" + +# No QGROUP value (yet?) since it's currently troublesome +QGROUP="" + +# these are the default settings +SPACE_LIMIT="0.5" +FREE_LIMIT="0.2" + +# Allow a list of users/groups to operate on this config's snapshots +ALLOW_USERS="" +ALLOW_GROUPS="" + +SYNC_ACL="no" + +BACKGROUND_COMPARISON="yes" + +# Only "none" or "gzip" (I wish it supported zstd ^-^) +COMPRESSION="gzip" + +# Cleaup snapshots based on the number created +NUMBER_CLEANUP="{{ item.number_cleanup.enabled | ternary('yes', 'no') }}" +NUMBER_MIN_AGE="{{ item.number_cleanup.min_age is defined | ternary(item.number_cleanup.min_age, 1800) }}" +NUMBER_LIMIT="{{ item.number_cleanup.limit is defined | ternary(item.number_cleanup.limit, 50) }}" +NUMBER_LIMIT_IMPORTANT="{{ item.number_cleanup.limit_important is defined | ternary(item.number_cleanup.limit_important, 10) }}" + +# Configure hourly snapshots +TIMELINE_CREATE="yes" +TIMELINE_CLEANUP="{{ item.timeline.cleanup_enabled | ternary('yes', 'no') }}" +TIMELINE_MIN_AGE="{{ item.timeline.min_age is defined | ternary(item.timeline.min_age, 1800) }}" +TIMELINE_LIMIT_HOURLY="{{ item.timeline.hourly is defined | ternary(item.timeline.hourly, 10) }}" +TIMELINE_LIMIT_DAILY="{{ item.timeline.daily is defined | ternary(item.timeline.daily, 10) }}" +TIMELINE_LIMIT_WEEKLY="{{ item.timeline.weekly is defined | ternary(item.timeline.weekly, 0) }}" +TIMELINE_LIMIT_MONTHLY="{{ item.timeline.monthly is defined | ternary(item.timeline.monthly, 10) }}" +TIMELINE_LIMIT_YEARLY="{{ item.timeline.yearly is defined | ternary(item.timeline.yearly, 10) }}" + +# Pre-post snapshot pairs +EMPTY_PRE_POST_CLEANUP="{{ item.pre_post_cleanup.enabled | ternary('yes', 'no') }}" +EMPTY_PRE_POST_MIN_AGE="{{ item.pre_post_cleanup.min_age is defined | ternary(item.pre_post_cleanup.min_age, 1800) }}" diff --git a/setup.yml b/setup.yml index 5084a4d..b64d185 100644 --- a/setup.yml +++ b/setup.yml @@ -1,33 +1,30 @@ --- -- name: Gathering facts - hosts: all - gather_facts: true - tags: always - -- name: Sanity checks - hosts: all - tags: always - tasks: - - name: Check user ID - fail: - msg: This playbook should only be run as 'root' - when: ansible_real_user_id != 0 - - name: Import list of accepted values for defined variables - include_vars: - name: accepted_values - file: ./requirements/accepted_variables.yml - - name: Check defined values of top-level variables - fail: - msg: 'Variable `{{ item }}` needs to be 1 of {{ accepted_values[item] }}' - when: not vars[item] in accepted_values[item] - loop: '{{ accepted_values | flatten }}' - - name: Setup the system hosts: all - # Hard-coded variables that shouldn't be configured + gather_facts: true vars: + # Determine the fstype of root filesystem + # PERF: a shorter version but requires `py3-jmespath`: '{{ ansible_mounts | json_query("[?mount == `/`].fstype") | first }}' + rootfs: '{{ ansible_mounts | selectattr("mount", "equalto", "/") | map(attribute="fstype") | first }}' # elogind needs polkit to function use_polkit: '{{ (seat_manager == "elogind") | ternary("True", polkit) }}' + pre_tasks: + - name: Sanity checks + tags: always + block: + - name: Check user ID + fail: + msg: This playbook should be run as 'root' + when: ansible_real_user_id != 0 + - name: Import list of accepted values for custom variables + include_vars: + name: accepted_values + file: ./requirements/accepted_variables.yml + - name: Check defined values of top-level variables + fail: + msg: 'Variable `{{ item }}` needs to be 1 of {{ accepted_values[item] }}' + when: not vars[item] in accepted_values[item] + loop: '{{ accepted_values | flatten }}' roles: - role: essential tags: essential @@ -61,6 +58,9 @@ tags: usbguard - role: zram tags: zram + - role: snapshot + tags: snapshot + when: snapshot_tool != 'none' - role: earlyoom tags: earlyoom - role: user