From c482d75d5f84eaf315c10581299b49cf505e4a87 Mon Sep 17 00:00:00 2001 From: drew Date: Thu, 30 Apr 2026 02:14:51 -0400 Subject: [PATCH] Initial commit --- .codex | 0 .gitignore | 48 +++++ bootstrap.yml | 24 +++ inventory/group_vars/all.example.yml | 91 ++++++++ inventory/group_vars/bots.example.yml | 25 +++ inventory/group_vars/controller.example.yml | 21 ++ inventory/group_vars/media.example.yml | 2 + inventory/group_vars/nas.example.yml | 11 + inventory/group_vars/prod.example.yml | 2 + inventory/group_vars/test.example.yml | 2 + inventory/group_vars/workstation.example.yml | 2 + inventory/hosts.example.yml | 73 +++++++ playbook.yml | 46 +++++ roles/adguard/meta/main.yml | 4 + roles/adguard/tasks/firewall.yml | 11 + roles/adguard/tasks/main.yml | 58 ++++++ roles/adguard/templates/AdGuardHome.yaml.j2 | 194 ++++++++++++++++++ roles/adguard/templates/adguard.container.j2 | 22 ++ roles/base_os/defaults/main.yml | 8 + roles/base_os/tasks/main.yml | 10 + roles/base_os/tasks/time_sync.yml | 29 +++ roles/caddy/meta/main.yml | 4 + roles/caddy/tasks/main.yml | 49 +++++ roles/caddy/templates/Caddyfile.j2 | 35 ++++ roles/caddy/templates/caddy.container.j2 | 24 +++ roles/container_runtime/defaults/main.yml | 5 + roles/container_runtime/tasks/main.yml | 64 ++++++ .../templates/homelab.network.j2 | 2 + roles/firewall_base/tasks/main.yml | 25 +++ roles/nfs_client/defaults/main.yml | 5 + roles/nfs_client/tasks/main.yml | 73 +++++++ roles/nfs_server/defaults/main.yml | 4 + roles/nfs_server/tasks/main.yml | 29 +++ roles/selinux_containers/tasks/labels.yml | 12 ++ roles/selinux_containers/tasks/main.yml | 19 ++ roles/selinux_containers/tasks/storage.yml | 17 ++ roles/selinux_containers/tasks/vpn.yml | 12 ++ roles/servarr/defaults/main.yml | 9 + roles/servarr/meta/main.yml | 5 + roles/servarr/tasks/firewall.yml | 11 + roles/servarr/tasks/main.yml | 66 ++++++ roles/servarr/templates/bazarr.container.j2 | 18 ++ .../templates/flaresolverr.container.j2 | 15 ++ roles/servarr/templates/prowlarr.container.j2 | 17 ++ .../templates/qbittorrent.container.j2 | 23 +++ roles/servarr/templates/radarr.container.j2 | 20 ++ roles/servarr/templates/sonarr.container.j2 | 20 ++ roles/storage_client/defaults/main.yml | 7 + roles/storage_client/tasks/main.yml | 43 ++++ roles/vpn/tasks/main.yml | 82 ++++++++ roles/vpn/templates/gluetun.container.j2 | 44 ++++ roles/vpn_guard/tasks/main.yml | 40 ++++ 52 files changed, 1482 insertions(+) create mode 100644 .codex create mode 100644 .gitignore create mode 100644 bootstrap.yml create mode 100644 inventory/group_vars/all.example.yml create mode 100644 inventory/group_vars/bots.example.yml create mode 100644 inventory/group_vars/controller.example.yml create mode 100644 inventory/group_vars/media.example.yml create mode 100644 inventory/group_vars/nas.example.yml create mode 100644 inventory/group_vars/prod.example.yml create mode 100644 inventory/group_vars/test.example.yml create mode 100644 inventory/group_vars/workstation.example.yml create mode 100644 inventory/hosts.example.yml create mode 100644 playbook.yml create mode 100644 roles/adguard/meta/main.yml create mode 100644 roles/adguard/tasks/firewall.yml create mode 100644 roles/adguard/tasks/main.yml create mode 100644 roles/adguard/templates/AdGuardHome.yaml.j2 create mode 100644 roles/adguard/templates/adguard.container.j2 create mode 100644 roles/base_os/defaults/main.yml create mode 100644 roles/base_os/tasks/main.yml create mode 100644 roles/base_os/tasks/time_sync.yml create mode 100644 roles/caddy/meta/main.yml create mode 100644 roles/caddy/tasks/main.yml create mode 100644 roles/caddy/templates/Caddyfile.j2 create mode 100644 roles/caddy/templates/caddy.container.j2 create mode 100644 roles/container_runtime/defaults/main.yml create mode 100644 roles/container_runtime/tasks/main.yml create mode 100644 roles/container_runtime/templates/homelab.network.j2 create mode 100644 roles/firewall_base/tasks/main.yml create mode 100644 roles/nfs_client/defaults/main.yml create mode 100644 roles/nfs_client/tasks/main.yml create mode 100644 roles/nfs_server/defaults/main.yml create mode 100644 roles/nfs_server/tasks/main.yml create mode 100644 roles/selinux_containers/tasks/labels.yml create mode 100644 roles/selinux_containers/tasks/main.yml create mode 100644 roles/selinux_containers/tasks/storage.yml create mode 100644 roles/selinux_containers/tasks/vpn.yml create mode 100644 roles/servarr/defaults/main.yml create mode 100644 roles/servarr/meta/main.yml create mode 100644 roles/servarr/tasks/firewall.yml create mode 100644 roles/servarr/tasks/main.yml create mode 100644 roles/servarr/templates/bazarr.container.j2 create mode 100644 roles/servarr/templates/flaresolverr.container.j2 create mode 100644 roles/servarr/templates/prowlarr.container.j2 create mode 100644 roles/servarr/templates/qbittorrent.container.j2 create mode 100644 roles/servarr/templates/radarr.container.j2 create mode 100644 roles/servarr/templates/sonarr.container.j2 create mode 100644 roles/storage_client/defaults/main.yml create mode 100644 roles/storage_client/tasks/main.yml create mode 100644 roles/vpn/tasks/main.yml create mode 100644 roles/vpn/templates/gluetun.container.j2 create mode 100644 roles/vpn_guard/tasks/main.yml diff --git a/.codex b/.codex new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e978717 --- /dev/null +++ b/.gitignore @@ -0,0 +1,48 @@ +# Ansible variable files (sensitive / environment-specific) +group_vars/ +host_vars/ +commands/ + +# Inventory files (often environment-specific and not meant for sharing) +inventory/* +inventory/group_vars/* +inventories/* + +# Keep sanitized examples +!inventory/ +!inventory/hosts.example.yml +!inventory/group_vars/ +!inventory/group_vars/*.example.yml + +# Codex/local assistant config +.codex/ + +# Ansible Vault / secrets +*.vault +*.secret +*.secrets +secrets.yml +vault.yml +**/vault.yml +**/secrets.yml + +# Local environment files +.env +.env.* +*.local + +# Optional: Ansible retry files +*.retry + +# Python/cache +__pycache__/ +.pytest_cache/ + +# OS/editor noise +.DS_Store +Thumbs.db +.vscode/ +.idea/ + +# Logs +*.log \ No newline at end of file diff --git a/bootstrap.yml b/bootstrap.yml new file mode 100644 index 0000000..847d6bc --- /dev/null +++ b/bootstrap.yml @@ -0,0 +1,24 @@ +- name: Bootstrap SSH access + hosts: all + become: true + + tasks: + - name: Ensure .ssh directory exists + file: + path: "/home/{{ bootstrap_user }}/.ssh" + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + mode: "0700" + + - name: Install authorized key + authorized_key: + user: "{{ bootstrap_user }}" + state: present + key: "{{ lookup('file', '~/.ssh/id_ed25519.pub') }}" + + - name: Allow passwordless sudo + copy: + dest: /etc/sudoers.d/{{ bootstrap_user }} + content: "{{ bootstrap_user }} ALL=(ALL) NOPASSWD:ALL\n" + mode: "0440" diff --git a/inventory/group_vars/all.example.yml b/inventory/group_vars/all.example.yml new file mode 100644 index 0000000..d46effd --- /dev/null +++ b/inventory/group_vars/all.example.yml @@ -0,0 +1,91 @@ +--- +# Global settings shared by all inventory groups. + +timezone: America/New_York +container_user: ansible +container_group: ansible +container_uid: 1000 +container_runtime_dir: "/run/user/{{ container_uid }}" + +bootstrap_user: "{{ container_user }}" +deployment_directory: "/home/{{ container_user }}/sbots1" + +# Stack directories +stack_root: "/home/{{ container_user }}/lab" + +adguard_dir: "{{ stack_root }}/adguard" +prowlarr_dir: "{{ stack_root }}/prowlarr" +qbittorrent_dir: "{{ stack_root }}/qbittorrent" +radarr_dir: "{{ stack_root }}/radarr" +sonarr_dir: "{{ stack_root }}/sonarr" +bazarr_dir: "{{ stack_root }}/bazarr" +caddy_dir: "{{ stack_root }}/caddy" +nas01_dir: /mnt/nas01 +downloads_root: "{{ nas01_dir }}" + +# Container paths +container_config_dir: "/home/{{ container_user }}/.config/containers/systemd" + +adguard_base_directories: + - "{{ stack_root }}" + - "{{ adguard_dir }}/work/data" + - "{{ adguard_dir }}/conf" + +servarr_base_directories: + - "{{ stack_root }}" + - "{{ prowlarr_dir }}/config" + - "{{ qbittorrent_dir }}/config" + - "{{ radarr_dir }}/config" + - "{{ sonarr_dir }}/config" + - "{{ bazarr_dir }}/config" + +caddy_base_directories: + - "{{ stack_root }}" + - "{{ caddy_dir }}/data" + - "{{ caddy_dir }}/config/caddy" + - "{{ container_config_dir }}" + +storage_tree: + - data + - data/media/movies + - data/media/tv + - data/media/music + - data/torrents/movies + - data/torrents/tv + - data/torrents/music + - data/torrents/incomplete + - data/torrents/complete + +arr_suite: + - qbittorrent + - prowlarr + - radarr + - sonarr + - bazarr + - flaresolverr + +base_firewall_rules: + - service: ssh + +adguard_firewall_rules: + - port: 53/tcp + - port: 53/udp + - port: 80/tcp + - port: 443/tcp + - port: 3000/tcp + +nfs_exports: + - path: "{{ nas01_dir }}" + clients: + - "192.168.1.11(rw,sync,no_subtree_check,fsid=0)" + - "192.168.1.12(rw,sync,no_subtree_check,fsid=0)" + - "192.168.1.14(rw,sync,no_subtree_check,fsid=0)" + +adguard_admin_user: ansible +adguard_admin_password_hash: "replace-with-adguard-bcrypt-hash" + +nfs_server_name: 192.168.1.10 +nfs_export_location: / +nfs_export_path: "{{ nas01_dir }}" +nfs_export_type: nfs4 +nfs_export_options: rw,sync,no_subtree_check,fsid=0 diff --git a/inventory/group_vars/bots.example.yml b/inventory/group_vars/bots.example.yml new file mode 100644 index 0000000..d38b29c --- /dev/null +++ b/inventory/group_vars/bots.example.yml @@ -0,0 +1,25 @@ +--- +# Gluetun SELinux access +selinux_allow_gluetun: true + +# Servarr stack +servarr_stack: + - src: qbittorrent.container.j2 + dest: qbittorrent.container + - src: prowlarr.container.j2 + dest: prowlarr.container + - src: radarr.container.j2 + dest: radarr.container + - src: sonarr.container.j2 + dest: sonarr.container + - src: bazarr.container.j2 + dest: bazarr.container + - src: flaresolverr.container.j2 + dest: flaresolverr.container + +# Gluetun setup +vpn_provider: mullvad +vpn_type: wireguard +vpn_countries: "Netherlands,USA,Canada" +vpn_private_key: "replace-with-wireguard-private-key" +vpn_addresses: "10.0.0.2/32" diff --git a/inventory/group_vars/controller.example.yml b/inventory/group_vars/controller.example.yml new file mode 100644 index 0000000..6ffc8df --- /dev/null +++ b/inventory/group_vars/controller.example.yml @@ -0,0 +1,21 @@ +--- +# Caddy +adguard_domain: "adguard.example.{{ caddy_node }}" +adguard_upstream: adguard:80 + +qbittorrent_domain: "qbittorrent.example.{{ caddy_node }}" +qbittorrent_upstream: host.containers.internal:8080 + +prowlarr_domain: "prowlarr.example.{{ caddy_node }}" +prowlarr_upstream: host.containers.internal:9696 + +radarr_domain: "radarr.example.{{ caddy_node }}" +radarr_upstream: host.containers.internal:7878 + +sonarr_domain: "sonarr.example.{{ caddy_node }}" +sonarr_upstream: host.containers.internal:8989 + +bazarr_domain: "bazarr.example.{{ caddy_node }}" +bazarr_upstream: host.containers.internal:6767 + +caddy_email: "admin@example.{{ caddy_node }}" diff --git a/inventory/group_vars/media.example.yml b/inventory/group_vars/media.example.yml new file mode 100644 index 0000000..0f8b963 --- /dev/null +++ b/inventory/group_vars/media.example.yml @@ -0,0 +1,2 @@ +--- +# Media host variables go here when the media role needs host-specific settings. diff --git a/inventory/group_vars/nas.example.yml b/inventory/group_vars/nas.example.yml new file mode 100644 index 0000000..13350bf --- /dev/null +++ b/inventory/group_vars/nas.example.yml @@ -0,0 +1,11 @@ +--- +# MergerFS +drive_a_path: /mnt/disk1 +drive_b_path: /mnt/disk2 + +mergerfs_path: /mnt/nas01 +mergerfs_opts: "defaults,allow_other,use_ino,category.create=mfs" + +storage_backends: + - "{{ drive_a_path }}" + - "{{ drive_b_path }}" diff --git a/inventory/group_vars/prod.example.yml b/inventory/group_vars/prod.example.yml new file mode 100644 index 0000000..0338118 --- /dev/null +++ b/inventory/group_vars/prod.example.yml @@ -0,0 +1,2 @@ +--- +caddy_node: com diff --git a/inventory/group_vars/test.example.yml b/inventory/group_vars/test.example.yml new file mode 100644 index 0000000..44985e7 --- /dev/null +++ b/inventory/group_vars/test.example.yml @@ -0,0 +1,2 @@ +--- +caddy_node: test diff --git a/inventory/group_vars/workstation.example.yml b/inventory/group_vars/workstation.example.yml new file mode 100644 index 0000000..4faece1 --- /dev/null +++ b/inventory/group_vars/workstation.example.yml @@ -0,0 +1,2 @@ +--- +# Workstation host variables go here when workstation-specific settings are needed. diff --git a/inventory/hosts.example.yml b/inventory/hosts.example.yml new file mode 100644 index 0000000..0738f3c --- /dev/null +++ b/inventory/hosts.example.yml @@ -0,0 +1,73 @@ +all: + children: + + test: + hosts: + nas-test: + ansible_host: 192.168.122.10 + ansible_user: ansible + env: test + media-test: + ansible_host: 192.168.122.11 + ansible_user: ansible + env: test + controller-test: + ansible_host: 192.168.122.12 + ansible_user: ansible + env: test + bots-test: + ansible_host: 192.168.122.13 + ansible_user: ansible + env: test + workstation-test: + ansible_host: 192.168.122.14 + ansible_user: ansible + env: test + + prod: + hosts: + nas01: + ansible_host: 192.168.1.10 + ansible_user: ansible + env: prod + media1: + ansible_host: 192.168.1.11 + ansible_user: ansible + env: prod + bots1: + ansible_host: 192.168.1.12 + ansible_user: ansible + env: prod + controller1: + ansible_host: 192.168.1.13 + ansible_user: ansible + env: prod + workstation1: + ansible_host: 192.168.1.14 + ansible_user: ansible + env: prod + + nas: + hosts: + nas-test: + nas01: + + media: + hosts: + media-test: + media1: + + controller: + hosts: + controller-test: + controller1: + + bots: + hosts: + bots-test: + bots1: + + workstation: + hosts: + workstation-test: + workstation1: diff --git a/playbook.yml b/playbook.yml new file mode 100644 index 0000000..bc2150b --- /dev/null +++ b/playbook.yml @@ -0,0 +1,46 @@ +- name: Storage + hosts: nas + become: true + roles: + - base_os + - firewall_base + - container_runtime + - storage_client + - nfs_server + +- name: Jellyfin + hosts: media + become: true + roles: + - base_os + - firewall_base + - container_runtime + +- name: Bots + hosts: bots + become: true + roles: + - base_os + - firewall_base + - container_runtime + - nfs_client + - servarr + +- name: DNS + hosts: controller + become: true + roles: + - base_os + - firewall_base + - container_runtime + - adguard + - caddy + +- name: Workstation Setup + hosts: workstation + become: true + roles: + - base_os + - firewall_base + - container_runtime + - selinux_containers \ No newline at end of file diff --git a/roles/adguard/meta/main.yml b/roles/adguard/meta/main.yml new file mode 100644 index 0000000..cc7e2ed --- /dev/null +++ b/roles/adguard/meta/main.yml @@ -0,0 +1,4 @@ +--- +# adguard/meta/main.yml +dependencies: + - role: selinux_containers \ No newline at end of file diff --git a/roles/adguard/tasks/firewall.yml b/roles/adguard/tasks/firewall.yml new file mode 100644 index 0000000..096b116 --- /dev/null +++ b/roles/adguard/tasks/firewall.yml @@ -0,0 +1,11 @@ +--- +#adguard/tasks/firewall.yml +- name: Open required firewall rules + become: true + ansible.posix.firewalld: + port: "{{ item.port | default(omit) }}" + service: "{{ item.service | default(omit) }}" + permanent: true + state: enabled + immediate: true + loop: "{{ adguard_firewall_rules }}" \ No newline at end of file diff --git a/roles/adguard/tasks/main.yml b/roles/adguard/tasks/main.yml new file mode 100644 index 0000000..bccde31 --- /dev/null +++ b/roles/adguard/tasks/main.yml @@ -0,0 +1,58 @@ +--- +#adguard/tasks/main.yml +- import_tasks: firewall.yml + +- name: Create stack and config directories + file: + path: "{{ item }}" + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + mode: "0755" + recurse: yes + loop: "{{ adguard_base_directories }}" + +- name: Directory SELinux requirement + ansible.builtin.set_fact: + selinux_container_paths: "{{ adguard_base_directories }}" + +- import_role: + name: selinux_containers + tasks_from: labels + +- name: Ensure container app config directories are owned by container UID + become: true + file: + path: "{{ adguard_dir }}/conf" + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + recurse: true + +- name: Deploy AdGuard configuration template + template: + src: AdGuardHome.yaml.j2 + dest: "{{ stack_root }}/adguard/conf/AdGuardHome.yaml" + mode: '0600' + +- name: Deploy AdGuard Quadlet + template: + src: adguard.container.j2 + dest: "{{ container_config_dir }}/adguard.container" + +- name: Force systemd reload (blocking) + become: true + become_user: "{{ container_user }}" + command: systemctl --user daemon-reload + +- name: Wait for quadlet generation + pause: + seconds: 1 + +- name: Start and enable AdGuard service + become: true + become_user: "{{ container_user }}" + systemd: + name: adguard.service + scope: user + state: started \ No newline at end of file diff --git a/roles/adguard/templates/AdGuardHome.yaml.j2 b/roles/adguard/templates/AdGuardHome.yaml.j2 new file mode 100644 index 0000000..1ed8911 --- /dev/null +++ b/roles/adguard/templates/AdGuardHome.yaml.j2 @@ -0,0 +1,194 @@ +http: + pprof: + port: 6060 + enabled: false + doh: + routes: + - GET /dns-query + - POST /dns-query + - GET /dns-query/{ClientID} + - POST /dns-query/{ClientID} + insecure_enabled: false + address: 0.0.0.0:80 + session_ttl: 720h +users: + - name: "{{ adguard_admin_user }}" + password: "{{ adguard_admin_password_hash }}" +auth_attempts: 5 +block_auth_min: 15 +http_proxy: "" +language: "" +theme: auto +dns: + bind_hosts: + - 0.0.0.0 + port: 53 + anonymize_client_ip: false + ratelimit: 20 + ratelimit_subnet_len_ipv4: 24 + ratelimit_subnet_len_ipv6: 56 + ratelimit_whitelist: [] + refuse_any: true + upstream_dns: + - https://dns10.quad9.net/dns-query + upstream_dns_file: "" + bootstrap_dns: + - 9.9.9.10 + - 149.112.112.10 + - 2620:fe::10 + - 2620:fe::fe:10 + fallback_dns: [] + upstream_mode: load_balance + fastest_timeout: 1s + allowed_clients: [] + disallowed_clients: [] + blocked_hosts: + - version.bind + - id.server + - hostname.bind + trusted_proxies: + - 127.0.0.0/8 + - ::1/128 + cache_enabled: true + cache_size: 4194304 + cache_ttl_min: 0 + cache_ttl_max: 0 + cache_optimistic: false + cache_optimistic_answer_ttl: 30s + cache_optimistic_max_age: 12h + bogus_nxdomain: [] + aaaa_disabled: false + enable_dnssec: false + edns_client_subnet: + custom_ip: "" + enabled: false + use_custom: false + max_goroutines: 300 + handle_ddr: true + ipset: [] + ipset_file: "" + bootstrap_prefer_ipv6: false + upstream_timeout: 10s + private_networks: [] + use_private_ptr_resolvers: true + local_ptr_upstreams: [] + use_dns64: false + dns64_prefixes: [] + serve_http3: false + use_http3_upstreams: false + serve_plain_dns: true + hostsfile_enabled: true + pending_requests: + enabled: true +tls: + enabled: false + server_name: "" + force_https: false + port_https: 443 + port_dns_over_tls: 853 + port_dns_over_quic: 853 + port_dnscrypt: 0 + dnscrypt_config_file: "" + certificate_chain: "" + private_key: "" + certificate_path: "" + private_key_path: "" + strict_sni_check: false +querylog: + dir_path: "" + ignored: [] + interval: 2160h + size_memory: 1000 + enabled: true + ignored_enabled: false + file_enabled: true +statistics: + dir_path: "" + ignored: [] + interval: 24h + enabled: true + ignored_enabled: false +filters: + - enabled: true + url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_1.txt + name: AdGuard DNS filter + id: 1 + - enabled: false + url: https://adguardteam.github.io/HostlistsRegistry/assets/filter_2.txt + name: AdAway Default Blocklist + id: 2 +whitelist_filters: [] +user_rules: [] +dhcp: + enabled: false + interface_name: "" + local_domain_name: lan + dhcpv4: + gateway_ip: "" + subnet_mask: "" + range_start: "" + range_end: "" + lease_duration: 86400 + icmp_timeout_msec: 1000 + options: [] + dhcpv6: + range_start: "" + lease_duration: 86400 + ra_slaac_only: false + ra_allow_slaac: false +filtering: + blocking_ipv4: "" + blocking_ipv6: "" + blocked_services: + schedule: + time_zone: UTC + ids: [] + protection_disabled_until: null + safe_search: + enabled: false + bing: true + duckduckgo: true + ecosia: true + google: true + pixabay: true + yandex: true + youtube: true + blocking_mode: default + parental_block_host: family-block.dns.adguard.com + safebrowsing_block_host: standard-block.dns.adguard.com + rewrites: [] + safe_fs_patterns: + - /opt/adguardhome/work/userfilters/* + safebrowsing_cache_size: 1048576 + safesearch_cache_size: 1048576 + parental_cache_size: 1048576 + cache_time: 30 + filters_update_interval: 24 + blocked_response_ttl: 10 + filtering_enabled: true + rewrites_enabled: true + parental_enabled: false + safebrowsing_enabled: false + protection_enabled: true +clients: + runtime_sources: + whois: true + arp: true + rdns: true + dhcp: true + hosts: true + persistent: [] +log: + enabled: true + file: "" + max_backups: 0 + max_size: 100 + max_age: 3 + compress: false + local_time: false + verbose: false +os: + group: "" + user: "" + rlimit_nofile: 0 +schema_version: 34 diff --git a/roles/adguard/templates/adguard.container.j2 b/roles/adguard/templates/adguard.container.j2 new file mode 100644 index 0000000..c90dbd1 --- /dev/null +++ b/roles/adguard/templates/adguard.container.j2 @@ -0,0 +1,22 @@ +[Unit] +Description=AdGuard Home DNS +Wants=network-online.target +After=network-online.target homelab-network.service +Requires=homelab-network.service + +[Container] +Image=docker.io/adguard/adguardhome:latest +ContainerName=adguard +Network=homelab:alias=adguard + +Volume={{ adguard_dir }}/work:/opt/adguardhome/work +Volume={{ adguard_dir }}/conf:/opt/adguardhome/conf + +PublishPort=53:53/tcp +PublishPort=53:53/udp + +[Service] +Restart=always + +[Install] +WantedBy=default.target diff --git a/roles/base_os/defaults/main.yml b/roles/base_os/defaults/main.yml new file mode 100644 index 0000000..b266f7a --- /dev/null +++ b/roles/base_os/defaults/main.yml @@ -0,0 +1,8 @@ +--- +#base_os/defaults/main.yml +base_os_install_packages: + - slirp4netns + - fuse-overlayfs + - policycoreutils-python-utils + - nano + - tree \ No newline at end of file diff --git a/roles/base_os/tasks/main.yml b/roles/base_os/tasks/main.yml new file mode 100644 index 0000000..384e199 --- /dev/null +++ b/roles/base_os/tasks/main.yml @@ -0,0 +1,10 @@ +--- +#base_os/tasks/main.yml +- import_tasks: time_sync.yml + +- name: Install required base packages + become: true + dnf: + name: "{{ item }}" + state: present + loop: "{{ base_os_install_packages }}" \ No newline at end of file diff --git a/roles/base_os/tasks/time_sync.yml b/roles/base_os/tasks/time_sync.yml new file mode 100644 index 0000000..a47e6a1 --- /dev/null +++ b/roles/base_os/tasks/time_sync.yml @@ -0,0 +1,29 @@ +--- +#base_os/tasks/time_sync.yml +- name: Chrony time sync (dev only) + when: env == "test" + block: + + - name: Ensure chronyd is running + become: true + ansible.builtin.service: + name: chronyd + state: started + enabled: true + + - name: Wait for chrony to have reachable sources + become: true + command: chronyc activity + register: chrony_activity + retries: 20 + delay: 2 + until: "'sources online' in chrony_activity.stdout and '0 sources online' not in chrony_activity.stdout" + + - name: Force time step correction + become: true + command: chronyc -a makestep + + - name: Verify system time is reasonable + command: date + register: date_check + failed_when: "'2026-04-13' in date_check.stdout" \ No newline at end of file diff --git a/roles/caddy/meta/main.yml b/roles/caddy/meta/main.yml new file mode 100644 index 0000000..4b5b6cc --- /dev/null +++ b/roles/caddy/meta/main.yml @@ -0,0 +1,4 @@ +--- +# caddy/meta/main.yml +dependencies: + - role: selinux_containers \ No newline at end of file diff --git a/roles/caddy/tasks/main.yml b/roles/caddy/tasks/main.yml new file mode 100644 index 0000000..8e1aaa3 --- /dev/null +++ b/roles/caddy/tasks/main.yml @@ -0,0 +1,49 @@ +--- +#caddy/tasks/main.yml +- name: Create stack and config directories + file: + path: "{{ item }}" + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + mode: "0755" + recurse: yes + loop: "{{ caddy_base_directories }}" + +- name: Base SELinux requirement + ansible.builtin.set_fact: + selinux_container_paths: "{{ caddy_base_directories }}" + +- import_role: + name: selinux_containers + tasks_from: labels + +- name: Ensure Caddyfile is deployed + template: + src: Caddyfile.j2 + dest: "{{ caddy_dir }}/Caddyfile" + owner: "{{ container_user }}" + group: "{{ container_group }}" + mode: "0644" + +- name: Deploy quadlet + template: + src: caddy.container.j2 + dest: "{{ container_config_dir }}/caddy.container" + +- name: Force systemd reload (blocking) + become: true + become_user: "{{ container_user }}" + command: systemctl --user daemon-reload + +- name: Wait for quadlet generation + pause: + seconds: 1 + +- name: Start service + become: true + become_user: "{{ container_user }}" + systemd: + name: caddy.service + scope: user + state: started \ No newline at end of file diff --git a/roles/caddy/templates/Caddyfile.j2 b/roles/caddy/templates/Caddyfile.j2 new file mode 100644 index 0000000..fa83f3d --- /dev/null +++ b/roles/caddy/templates/Caddyfile.j2 @@ -0,0 +1,35 @@ +# Adguard +{{ adguard_domain }} { + tls internal + reverse_proxy {{ adguard_upstream }} +} + +# QBittorrent +{{ qbittorrent_domain }} { + tls internal + reverse_proxy {{ qbittorrent_upstream }} +} + +# Prowlarr +{{ prowlarr_domain }} { + tls internal + reverse_proxy {{ prowlarr_upstream }} +} + +# Radarr +{{ radarr_domain }} { + tls internal + reverse_proxy {{ radarr_upstream }} +} + +# Sonarr +{{ sonarr_domain }} { + tls internal + reverse_proxy {{ sonarr_upstream }} +} + +# Bazarr +{{ bazarr_domain }} { + tls internal + reverse_proxy {{ bazarr_upstream }} +} \ No newline at end of file diff --git a/roles/caddy/templates/caddy.container.j2 b/roles/caddy/templates/caddy.container.j2 new file mode 100644 index 0000000..b252ea3 --- /dev/null +++ b/roles/caddy/templates/caddy.container.j2 @@ -0,0 +1,24 @@ +[Unit] +Description=Caddy Reverse Proxy +Wants=network-online.target +After=network-online.target +Requires=homelab-network.service +After=homelab-network.service + +[Container] +Image=docker.io/caddy:latest +ContainerName=caddy +Network=homelab + +Volume={{ caddy_dir }}/Caddyfile:/etc/caddy/Caddyfile +Volume={{ caddy_dir }}/data:/data +Volume={{ caddy_dir }}/config:/config + +PublishPort=80:80/tcp +PublishPort=443:443/tcp + +[Service] +Restart=always + +[Install] +WantedBy=default.target diff --git a/roles/container_runtime/defaults/main.yml b/roles/container_runtime/defaults/main.yml new file mode 100644 index 0000000..7697025 --- /dev/null +++ b/roles/container_runtime/defaults/main.yml @@ -0,0 +1,5 @@ +--- +#container_runtime/defaults/main.yml +base_runtime_install_packages: + - podman + - podman-docker \ No newline at end of file diff --git a/roles/container_runtime/tasks/main.yml b/roles/container_runtime/tasks/main.yml new file mode 100644 index 0000000..edbbc54 --- /dev/null +++ b/roles/container_runtime/tasks/main.yml @@ -0,0 +1,64 @@ +#container_runtime/tasks/main.yml +- name: Install required base packages + become: true + dnf: + name: "{{ item }}" + state: present + loop: "{{ base_runtime_install_packages }}" + +- name: Enable lingering for rootless containers + become: true + command: "loginctl enable-linger {{ container_user }}" + args: + creates: "/var/lib/systemd/linger/{{ container_user }}" + +- name: Allow rootless to bind to low ports + become: true + sysctl: + name: net.ipv4.ip_unprivileged_port_start + value: '53' + state: present + +- name: Create stack directories + file: + path: "{{ item }}" + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + mode: "0755" + recurse: yes + loop: + - "{{ stack_root }}" + - "{{ container_config_dir }}" + +- name: Configure SELinux container policies + ansible.builtin.import_tasks: ../selinux_containers/tasks/main.yml + +- name: Deploy Podman Network Quadlet + become: true + template: + src: homelab.network.j2 + dest: "{{ container_config_dir }}/homelab.network" + mode: "0644" + owner: "{{ container_user }}" + group: "{{ container_group }}" + +#- name: Force systemd reload (blocking) +# become: true +# become_user: "{{ container_user }}" +# command: systemctl --user daemon-reload + +- name: Force systemd reload (blocking) + become: true + become_user: "{{ container_user }}" + environment: + XDG_RUNTIME_DIR: "{{ container_runtime_dir }}" + command: systemctl --user daemon-reload + +- name: Start homelab network + become: true + become_user: "{{ container_user }}" + systemd: + name: homelab-network.service + scope: user + state: started \ No newline at end of file diff --git a/roles/container_runtime/templates/homelab.network.j2 b/roles/container_runtime/templates/homelab.network.j2 new file mode 100644 index 0000000..069bc26 --- /dev/null +++ b/roles/container_runtime/templates/homelab.network.j2 @@ -0,0 +1,2 @@ +[Network] +NetworkName=homelab diff --git a/roles/firewall_base/tasks/main.yml b/roles/firewall_base/tasks/main.yml new file mode 100644 index 0000000..18015bb --- /dev/null +++ b/roles/firewall_base/tasks/main.yml @@ -0,0 +1,25 @@ +--- +#firewall_base/tasks/main.yml +- name: Install required base packages + become: true + dnf: + name: + - firewalld + state: present + +- name: Enable firewalld + become: true + systemd: + name: firewalld + enabled: true + state: started + +- name: Open required firewall rules + become: true + ansible.posix.firewalld: + port: "{{ item.port | default(omit) }}" + service: "{{ item.service | default(omit) }}" + permanent: true + state: enabled + immediate: true + loop: "{{ base_firewall_rules }}" \ No newline at end of file diff --git a/roles/nfs_client/defaults/main.yml b/roles/nfs_client/defaults/main.yml new file mode 100644 index 0000000..6684ec8 --- /dev/null +++ b/roles/nfs_client/defaults/main.yml @@ -0,0 +1,5 @@ +nfs_server: "{{ nfs_server_name }}" +nfs_export: "{{ nfs_export_location }}" +nfs_mount_point: "{{ nfs_export_path }}" +nfs_fstype: "{{ nfs_export_type }}" +nfs_options: "defaults,_netdev,nofail,x-systemd.automount,x-systemd.requires=network-online.target,x-systemd.after=network-online.target,x-systemd.idle-timeout=60" \ No newline at end of file diff --git a/roles/nfs_client/tasks/main.yml b/roles/nfs_client/tasks/main.yml new file mode 100644 index 0000000..c746fee --- /dev/null +++ b/roles/nfs_client/tasks/main.yml @@ -0,0 +1,73 @@ +--- +#nfs_client/tasks/main.yml +- name: Create dummy NAS root for test environment + become: true + file: + path: "{{ nfs_mount_point }}" + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + mode: "0755" + when: env == "test" + +- name: Create dummy NAS storage tree for test environment + become: true + file: + path: "{{ nfs_mount_point }}/{{ item }}" + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + mode: "0775" + loop: "{{ storage_tree }}" + when: env == "test" + +- name: Set SELinux context for dummy NAS storage in test environment + become: true + community.general.sefcontext: + target: "{{ nfs_mount_point }}(/.*)?" + setype: container_file_t + state: present + when: env == "test" + +- name: Apply SELinux context for dummy NAS storage in test environment + become: true + command: restorecon -Rv "{{ nfs_mount_point }}" + changed_when: false + when: env == "test" + +- name: Install required NFS client packages + become: true + dnf: + name: nfs-utils + state: present + when: env != "test" + +- name: Check whether NFS mount point is already mounted + become: true + command: findmnt --mountpoint "{{ nfs_mount_point }}" + register: nfs_mount_check + changed_when: false + failed_when: false + when: env != "test" + +- name: Create NFS mount point + become: true + file: + path: "{{ nfs_mount_point }}" + state: directory + owner: root + group: root + mode: "0755" + when: + - env != "test" + - nfs_mount_check.rc != 0 + +- name: Configure NFS mount + become: true + ansible.posix.mount: + path: "{{ nfs_mount_point }}" + src: "{{ nfs_server }}:{{ nfs_export }}" + fstype: "{{ nfs_fstype }}" + opts: "{{ nfs_options }}" + state: mounted + when: env != "test" \ No newline at end of file diff --git a/roles/nfs_server/defaults/main.yml b/roles/nfs_server/defaults/main.yml new file mode 100644 index 0000000..96078ea --- /dev/null +++ b/roles/nfs_server/defaults/main.yml @@ -0,0 +1,4 @@ +#nfw_server/defaults/main.yml +nfs_packages: + - nfs-utils + - nfs-server \ No newline at end of file diff --git a/roles/nfs_server/tasks/main.yml b/roles/nfs_server/tasks/main.yml new file mode 100644 index 0000000..102438b --- /dev/null +++ b/roles/nfs_server/tasks/main.yml @@ -0,0 +1,29 @@ +--- +#nfs_server/tasks/main.yml +- name: Install NFS utilities + become: true + dnf: + name: nfs-utils + state: present + loop: "{{ nfs_packages }}" + +- name: Build NFS exports entries + become: true + lineinfile: + path: /etc/exports + line: "{{ item.path }} {{ item.clients | join(' ') }}" + create: true + state: present + loop: "{{ nfs_exports }}" + +- name: Export NFS shares + become: true + command: exportfs -ra + changed_when: false + +- name: Enable and start NFS server + become: true + systemd: + name: nfs-server + enabled: true + state: started \ No newline at end of file diff --git a/roles/selinux_containers/tasks/labels.yml b/roles/selinux_containers/tasks/labels.yml new file mode 100644 index 0000000..5965f2f --- /dev/null +++ b/roles/selinux_containers/tasks/labels.yml @@ -0,0 +1,12 @@ +--- +# selinux_containers/tasks/labels.yml +- name: Ensure SELinux context for container config paths + become: true + community.general.sefcontext: + target: "{{ item }}(/.*)?" + setype: container_file_t + state: present + loop: "{{ selinux_container_paths | default([]) }}" + when: + - selinux_container_paths is defined + - selinux_container_paths | length > 0 \ No newline at end of file diff --git a/roles/selinux_containers/tasks/main.yml b/roles/selinux_containers/tasks/main.yml new file mode 100644 index 0000000..1d8b75f --- /dev/null +++ b/roles/selinux_containers/tasks/main.yml @@ -0,0 +1,19 @@ +--- +#selinux_containers/tasks/main.yml +# ----------------------------- +# Set SELinux +# ----------------------------- +- name: Set SELinux context for stack + become: true + community.general.sefcontext: + target: "{{ stack_root }}(/.*)?" + setype: container_file_t + state: present + +# ----------------------------- +# APPLY SELinux +# ----------------------------- +- name: Apply SELinux context (stack) + become: true + command: restorecon -Rv "{{ stack_root }}" + changed_when: false \ No newline at end of file diff --git a/roles/selinux_containers/tasks/storage.yml b/roles/selinux_containers/tasks/storage.yml new file mode 100644 index 0000000..97136fb --- /dev/null +++ b/roles/selinux_containers/tasks/storage.yml @@ -0,0 +1,17 @@ +--- +# selinux_containers/tasks/storage.yml +- name: Set SELinux context for storage mounts + become: true + community.general.sefcontext: + target: "{{ item }}(/.*)?" + setype: container_file_t + state: present + loop: "{{ storage_backends }}" + when: "'nas' in group_names" + +- name: Apply SELinux context (storage) + become: true + command: restorecon -R -F -v "{{ item }}" + loop: "{{ storage_backends }}" + changed_when: false + when: "'nas' in group_names" \ No newline at end of file diff --git a/roles/selinux_containers/tasks/vpn.yml b/roles/selinux_containers/tasks/vpn.yml new file mode 100644 index 0000000..4aab5a9 --- /dev/null +++ b/roles/selinux_containers/tasks/vpn.yml @@ -0,0 +1,12 @@ +--- +# selinux_containers/tasks/vpn.yml +# ----------------------------- +# SELinux BOOLEANS +# ----------------------------- +- name: Allow containers to use devices (needed for Gluetun / /dev/net/tun) + become: true + ansible.posix.seboolean: + name: container_use_devices + state: true + persistent: true + when: selinux_allow_gluetun | default(false) \ No newline at end of file diff --git a/roles/servarr/defaults/main.yml b/roles/servarr/defaults/main.yml new file mode 100644 index 0000000..07f8daf --- /dev/null +++ b/roles/servarr/defaults/main.yml @@ -0,0 +1,9 @@ +--- +#servarr/defaults/main.yml +servarr_firewall_rules: + - port: 8080/tcp + - port: 6767/tcp + - port: 7878/tcp + - port: 8191/tcp + - port: 8989/tcp + - port: 9696/tcp \ No newline at end of file diff --git a/roles/servarr/meta/main.yml b/roles/servarr/meta/main.yml new file mode 100644 index 0000000..d7468a9 --- /dev/null +++ b/roles/servarr/meta/main.yml @@ -0,0 +1,5 @@ +--- +# servarr/meta/main.yml +dependencies: + - role: selinux_containers + - role: vpn \ No newline at end of file diff --git a/roles/servarr/tasks/firewall.yml b/roles/servarr/tasks/firewall.yml new file mode 100644 index 0000000..cbce3e6 --- /dev/null +++ b/roles/servarr/tasks/firewall.yml @@ -0,0 +1,11 @@ +--- +#servarr/tasks/firewall.yml +- name: Open required firewall rules + become: true + ansible.posix.firewalld: + port: "{{ item.port | default(omit) }}" + service: "{{ item.service | default(omit) }}" + permanent: true + state: enabled + immediate: true + loop: "{{ servarr_firewall_rules }}" \ No newline at end of file diff --git a/roles/servarr/tasks/main.yml b/roles/servarr/tasks/main.yml new file mode 100644 index 0000000..a211cf7 --- /dev/null +++ b/roles/servarr/tasks/main.yml @@ -0,0 +1,66 @@ +#servarr/tasks/main.yml +- import_tasks: firewall.yml + +- name: Create stack and config directories + file: + path: "{{ item }}" + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + mode: "0755" + recurse: yes + loop: "{{ servarr_base_directories }}" + +- name: Directory SELinux requirement + ansible.builtin.set_fact: + selinux_container_paths: "{{ servarr_base_directories }}" + +- import_role: + name: selinux_containers + tasks_from: labels + +- name: Ensure systemd directory exists for rootless user + file: + path: "{{ container_config_dir }}" + state: directory + mode: '0755' + owner: "{{ container_user }}" + group: "{{ container_group }}" + +- name: Deploy Quadlet files + template: + src: "{{ item.src }}" + dest: "{{ container_config_dir }}/{{ item.dest }}" + loop: "{{ servarr_stack }}" + register: quadlets_deployed + +- name: Ensure app config directories are writable + become: true + file: + path: "{{ item }}" + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + recurse: true + loop: + - "{{ radarr_dir }}/config" + - "{{ sonarr_dir }}/config" + - "{{ qbittorrent_dir }}/config" + - "{{ prowlarr_dir }}/config" + - "{{ bazarr_dir }}/config" + +#- name: Force systemd reload (blocking) +# become: true +# become_user: "{{ container_user }}" +# command: systemctl --user daemon-reload + +- name: Force systemd reload (blocking) + become: true + become_user: "{{ container_user }}" + environment: + XDG_RUNTIME_DIR: "{{ container_runtime_dir }}" + command: systemctl --user daemon-reload + +- name: Validate VPN and start arr stack + ansible.builtin.import_role: + name: vpn_guard \ No newline at end of file diff --git a/roles/servarr/templates/bazarr.container.j2 b/roles/servarr/templates/bazarr.container.j2 new file mode 100644 index 0000000..d37ea89 --- /dev/null +++ b/roles/servarr/templates/bazarr.container.j2 @@ -0,0 +1,18 @@ +[Unit] +Description=Bazarr Subtitle Manager +Requires=gluetun.service +After=gluetun.service + +[Container] +ContainerName=bazarr +Image=lscr.io/linuxserver/bazarr:latest + +Environment=PUID=0 +Environment=PGID=0 +Environment=TZ=America/New_York +Network=container:gluetun +Volume={{ bazarr_dir }}/config:/config +Volume={{ downloads_root }}:/data + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/roles/servarr/templates/flaresolverr.container.j2 b/roles/servarr/templates/flaresolverr.container.j2 new file mode 100644 index 0000000..05cd7ad --- /dev/null +++ b/roles/servarr/templates/flaresolverr.container.j2 @@ -0,0 +1,15 @@ +[Unit] +Description=Flaresolverr Cloudflare Solver +Requires=gluetun.service +After=gluetun.service + +[Container] +ContainerName=flaresolverr +Image=ghcr.io/flaresolverr/flaresolverr:latest + +Environment=TZ=America/New_York +Environment=LOG_LEVEL=info +Network=container:gluetun + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/roles/servarr/templates/prowlarr.container.j2 b/roles/servarr/templates/prowlarr.container.j2 new file mode 100644 index 0000000..d65449e --- /dev/null +++ b/roles/servarr/templates/prowlarr.container.j2 @@ -0,0 +1,17 @@ +[Unit] +Description=Prowlarr Indexer Manager +Requires=gluetun.service +After=gluetun.service + +[Container] +ContainerName=prowlarr +Image=lscr.io/linuxserver/prowlarr:latest + +Environment=PUID=0 +Environment=PGID=0 +Environment=TZ=America/New_York +Network=container:gluetun +Volume={{ prowlarr_dir }}/config:/config + +[Install] +WantedBy=default.target diff --git a/roles/servarr/templates/qbittorrent.container.j2 b/roles/servarr/templates/qbittorrent.container.j2 new file mode 100644 index 0000000..0498a51 --- /dev/null +++ b/roles/servarr/templates/qbittorrent.container.j2 @@ -0,0 +1,23 @@ +[Unit] +Description=qBittorrent Downloader +Requires=gluetun.service +After=gluetun.service +StartLimitIntervalSec=0 + +[Container] +Image=lscr.io/linuxserver/qbittorrent:latest +ContainerName=qbittorrent + +Environment=PUID=0 +Environment=PGID=0 +Environment=TZ={{ timezone }} +Environment=WEBUI_PORT=8080 +Environment=WEBUI_ADDRESS=0.0.0.0 + +Network=container:gluetun + +Volume={{ qbittorrent_dir }}/config:/config +Volume={{ downloads_root }}:/data + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/roles/servarr/templates/radarr.container.j2 b/roles/servarr/templates/radarr.container.j2 new file mode 100644 index 0000000..6c983ee --- /dev/null +++ b/roles/servarr/templates/radarr.container.j2 @@ -0,0 +1,20 @@ +[Unit] +Description=Radarr Movie Manager +Requires=gluetun.service +After=gluetun.service + +[Container] +ContainerName=radarr +Image=lscr.io/linuxserver/radarr:latest + +Environment=PUID=0 +Environment=PGID=0 +Environment=TZ=America/New_York + +Network=container:gluetun + +Volume={{ radarr_dir }}/config:/config +Volume={{ downloads_root }}:/data + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/roles/servarr/templates/sonarr.container.j2 b/roles/servarr/templates/sonarr.container.j2 new file mode 100644 index 0000000..5ba734b --- /dev/null +++ b/roles/servarr/templates/sonarr.container.j2 @@ -0,0 +1,20 @@ +[Unit] +Description=Sonarr TV Manager +Requires=gluetun.service +After=gluetun.service + +[Container] +ContainerName=sonarr +Image=lscr.io/linuxserver/sonarr:latest + +Environment=PUID=0 +Environment=PGID=0 +Environment=TZ=America/New_York + +Network=container:gluetun + +Volume={{ sonarr_dir }}/config:/config +Volume={{ downloads_root }}:/data + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/roles/storage_client/defaults/main.yml b/roles/storage_client/defaults/main.yml new file mode 100644 index 0000000..4874fcb --- /dev/null +++ b/roles/storage_client/defaults/main.yml @@ -0,0 +1,7 @@ +--- +#storage_client/defaults/main.yml +base_storage_install_packages: + - epel-release + - fuse + - fuse3 + - policycoreutils-python-utils \ No newline at end of file diff --git a/roles/storage_client/tasks/main.yml b/roles/storage_client/tasks/main.yml new file mode 100644 index 0000000..efd3614 --- /dev/null +++ b/roles/storage_client/tasks/main.yml @@ -0,0 +1,43 @@ +--- +#storage_client/tasks/main.yml +- name: Install required packages + become: true + dnf: + name: "{{ item }}" + state: present + loop: "{{ base_storage_install_packages }}" + +- name: Install mergerfs repo package + become: true + dnf: + name: https://github.com/trapexit/mergerfs/releases/download/2.41.1/mergerfs-2.41.1-1.el10.x86_64.rpm + disable_gpg_check: true + state: present + +- name: Ensure source data directories exist + become: true + file: + path: "{{ item }}" + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + mode: "0755" + loop: "{{ storage_backends }}" + +- name: Create mergerfs mountpoint + become: true + file: + path: /mnt/nas01 + state: directory + owner: "{{ container_user }}" + group: "{{ container_group }}" + mode: "0755" + +- name: Mount mergerfs pool + become: true + ansible.posix.mount: + path: "{{ mergerfs_path }}" + src: "{{ drive_a_path }}/data:{{ drive_b_path }}/data" + fstype: fuse.mergerfs + opts: defaults,allow_other,use_ino,category.create=mfs + state: mounted \ No newline at end of file diff --git a/roles/vpn/tasks/main.yml b/roles/vpn/tasks/main.yml new file mode 100644 index 0000000..1406cd1 --- /dev/null +++ b/roles/vpn/tasks/main.yml @@ -0,0 +1,82 @@ +--- +#vpn/tasks/main.yml +- name: Install kernel extra modules for Gluetun firewall + become: true + dnf: + name: kernel-modules-extra + state: present + register: kernel_modules_extra_install + +- name: Check whether xt_conntrack is available for running kernel + become: true + command: modinfo xt_conntrack + register: xt_conntrack_modinfo + changed_when: false + failed_when: false + +- name: Reboot if kernel modules were installed but running kernel cannot find xt_conntrack + become: true + reboot: + msg: "Rebooting to load kernel matching installed kernel-modules-extra for Gluetun firewall" + reboot_timeout: 600 + when: + - xt_conntrack_modinfo.rc != 0 + +- name: Load xt_conntrack for Gluetun firewall + become: true + community.general.modprobe: + name: xt_conntrack + state: present + +- name: Persist xt_conntrack module + become: true + copy: + dest: /etc/modules-load.d/gluetun.conf + mode: "0644" + content: | + xt_conntrack + +- name: Configure SELinux for Gluetun device access + ansible.builtin.import_role: + name: selinux_containers + tasks_from: vpn + +- name: Deploy Quadlet files + template: + src: "gluetun.container.j2" + dest: "{{ container_config_dir }}/gluetun.container" + +- name: Force systemd reload (blocking) + become: true + become_user: "{{ container_user }}" + environment: + XDG_RUNTIME_DIR: "{{ container_runtime_dir }}" + command: systemctl --user daemon-reload + +- name: Wait for quadlet generation + pause: + seconds: 1 + +- name: Start vpn + become: true + become_user: "{{ container_user }}" + environment: + XDG_RUNTIME_DIR: "{{ container_runtime_dir }}" + systemd: + name: gluetun.service + scope: user + state: started + +- name: Wait for Gluetun container to exist + become: true + become_user: "{{ container_user }}" + command: podman container exists gluetun + register: gluetun_exists + retries: 20 + delay: 2 + until: gluetun_exists.rc == 0 + changed_when: false + +- name: Wait for Gluetun to stabilize + pause: + seconds: 5 \ No newline at end of file diff --git a/roles/vpn/templates/gluetun.container.j2 b/roles/vpn/templates/gluetun.container.j2 new file mode 100644 index 0000000..3cf61dd --- /dev/null +++ b/roles/vpn/templates/gluetun.container.j2 @@ -0,0 +1,44 @@ +[Unit] +Description=Gluetun VPN Client +Requires=homelab-network.service +After=network-online.target homelab-network.service +Wants=network-online.target + +[Container] +Image=docker.io/qmcgaw/gluetun:latest +ContainerName=gluetun +UserNS=keep-id + +AddCapability=NET_ADMIN +AddCapability=NET_RAW +AddDevice=/dev/net/tun:/dev/net/tun + +# Port Mappings: +# 6767 Bazarr +# 7878 Radarr +# 8080 qBittorrent +# 8191 Flaresolverr +# 8989 Sonarr +# 9696 Prowlarr + +PublishPort=6767:6767 +PublishPort=7878:7878 +PublishPort=8080:8080 +PublishPort=8191:8191 +PublishPort=8989:8989 +PublishPort=9696:9696 + +Environment=VPN_SERVICE_PROVIDER=mullvad +Environment=VPN_TYPE=wireguard +Environment=WIREGUARD_PRIVATE_KEY={{ vpn_private_key }} +Environment=WIREGUARD_ADDRESSES={{ vpn_addresses }} + +Environment=FIREWALL_ENABLED=on +#Environment=FIREWALL_ENABLED_DISABLING_IT_SHOOTS_YOU_IN_YOUR_FOOT=off +Environment=DNS_ENABLED=false +Environment=FIREWALL_INPUT_PORTS=6767,7878,8080,8191,8989,9696 + +Network=homelab:alias=gluetun + +[Install] +WantedBy=default.target \ No newline at end of file diff --git a/roles/vpn_guard/tasks/main.yml b/roles/vpn_guard/tasks/main.yml new file mode 100644 index 0000000..568f3ea --- /dev/null +++ b/roles/vpn_guard/tasks/main.yml @@ -0,0 +1,40 @@ +--- +#vpn_guard/tasks/main.yml +- name: Get host public IP + command: curl -s https://ipinfo.io/ip + register: host_ip + changed_when: false + +- name: Get VPN public IP (via Gluetun) + become: true + become_user: "{{ container_user }}" + command: podman exec gluetun wget -qO- https://ipinfo.io/ip + register: vpn_ip + changed_when: false + +- name: Fail if VPN is not active (kill switch check) + fail: + msg: "VPN is NOT active (host={{ host_ip.stdout }} vpn={{ vpn_ip.stdout }}). Aborting arr stack start." + when: host_ip.stdout == vpn_ip.stdout + +#- name: Start arr stack only after VPN validation +# become: true +# become_user: "{{ container_user }}" +# systemd: +# name: "{{ item }}.service" +# enabled: yes +# state: started +# scope: user +# loop: "{{ arr_suite }}" + +- name: Start arr stack only after VPN validation + become: true + become_user: "{{ container_user }}" + environment: + XDG_RUNTIME_DIR: "{{ container_runtime_dir }}" + systemd: + name: "{{ item }}.service" + enabled: yes + state: started + scope: user + loop: "{{ arr_suite }}" \ No newline at end of file