From b9be45dc71176b24887b5941678301d5cc445e65 Mon Sep 17 00:00:00 2001 From: Pluto Date: Sun, 1 Feb 2026 23:14:46 -0600 Subject: [PATCH] Initial Ansible automation for hobbs.farm infrastructure Complete IaC covering: - 14 roles: common, docker, traefik, vaultwarden, monitoring, media, matrix, home_assistant, frigate, adguard, gitea, semaphore, openclaw, security, backup - Inventory for all hosts (Peggy, Bart, Yoda, NAS, Firewalla) - Stack playbooks: core-infrastructure, monitoring, media, communication, automation, smarthome, security-hardening - Maintenance playbooks: update-containers, backup, health-check - Production Docker Compose templates with Traefik labels - Security hardening (sysctl, auditd, UFW, fail2ban) - Automated backups with retention policy --- .gitignore | 12 ++ README.md | 81 +++++++++++- ansible.cfg | 25 ++++ inventory/group_vars/all.yml | 45 +++++++ inventory/group_vars/docker_hosts.yml | 9 ++ inventory/group_vars/monitoring.yml | 7 ++ inventory/host_vars/bart.yml | 14 +++ inventory/host_vars/firewalla.yml | 6 + inventory/host_vars/peggy.yml | 37 ++++++ inventory/hosts.yml | 26 ++++ playbooks/automation.yml | 12 ++ playbooks/communication.yml | 8 ++ playbooks/core-infrastructure.yml | 14 +++ playbooks/maintenance/backup.yml | 42 +++++++ playbooks/maintenance/health-check.yml | 87 +++++++++++++ playbooks/maintenance/update-containers.yml | 47 +++++++ playbooks/media.yml | 8 ++ playbooks/monitoring.yml | 8 ++ playbooks/security-hardening.yml | 8 ++ playbooks/site.yml | 71 +++++++++++ playbooks/smarthome.yml | 8 ++ requirements.yml | 14 +++ roles/adguard/defaults/main.yml | 5 + roles/adguard/tasks/main.yml | 29 +++++ roles/adguard/templates/docker-compose.yml.j2 | 24 ++++ roles/backup/defaults/main.yml | 9 ++ roles/backup/tasks/main.yml | 43 +++++++ roles/backup/templates/backup.sh.j2 | 42 +++++++ roles/common/defaults/main.yml | 24 ++++ roles/common/handlers/main.yml | 5 + roles/common/tasks/main.yml | 98 +++++++++++++++ roles/common/templates/sshd_config.j2 | 26 ++++ roles/docker/defaults/main.yml | 13 ++ roles/docker/handlers/main.yml | 5 + roles/docker/tasks/main.yml | 76 ++++++++++++ roles/frigate/defaults/main.yml | 6 + roles/frigate/tasks/main.yml | 28 +++++ roles/frigate/templates/docker-compose.yml.j2 | 30 +++++ roles/gitea/defaults/main.yml | 8 ++ roles/gitea/tasks/main.yml | 28 +++++ roles/gitea/templates/docker-compose.yml.j2 | 50 ++++++++ roles/home_assistant/defaults/main.yml | 12 ++ roles/home_assistant/tasks/main.yml | 32 +++++ .../templates/docker-compose.yml.j2 | 52 ++++++++ roles/matrix/defaults/main.yml | 7 ++ roles/matrix/tasks/main.yml | 27 ++++ roles/matrix/templates/docker-compose.yml.j2 | 22 ++++ roles/media/defaults/main.yml | 27 ++++ roles/media/tasks/main.yml | 32 +++++ roles/media/templates/docker-compose.yml.j2 | 116 ++++++++++++++++++ roles/monitoring/defaults/main.yml | 18 +++ roles/monitoring/handlers/main.yml | 6 + roles/monitoring/tasks/main.yml | 61 +++++++++ .../monitoring/templates/alertmanager.yml.j2 | 29 +++++ .../templates/docker-compose.yml.j2 | 86 +++++++++++++ roles/monitoring/templates/prometheus.yml.j2 | 37 ++++++ roles/openclaw/defaults/main.yml | 5 + roles/openclaw/tasks/main.yml | 28 +++++ .../openclaw/templates/docker-compose.yml.j2 | 22 ++++ roles/security/defaults/main.yml | 5 + roles/security/handlers/main.yml | 9 ++ roles/security/tasks/main.yml | 73 +++++++++++ roles/security/templates/audit.rules.j2 | 24 ++++ roles/security/templates/sysctl.conf.j2 | 24 ++++ roles/semaphore/defaults/main.yml | 6 + roles/semaphore/tasks/main.yml | 28 +++++ .../semaphore/templates/docker-compose.yml.j2 | 26 ++++ roles/traefik/defaults/main.yml | 10 ++ roles/traefik/handlers/main.yml | 6 + roles/traefik/tasks/main.yml | 41 +++++++ roles/traefik/templates/docker-compose.yml.j2 | 46 +++++++ roles/vaultwarden/defaults/main.yml | 7 ++ roles/vaultwarden/tasks/main.yml | 26 ++++ .../templates/docker-compose.yml.j2 | 24 ++++ 74 files changed, 2110 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 ansible.cfg create mode 100644 inventory/group_vars/all.yml create mode 100644 inventory/group_vars/docker_hosts.yml create mode 100644 inventory/group_vars/monitoring.yml create mode 100644 inventory/host_vars/bart.yml create mode 100644 inventory/host_vars/firewalla.yml create mode 100644 inventory/host_vars/peggy.yml create mode 100644 inventory/hosts.yml create mode 100644 playbooks/automation.yml create mode 100644 playbooks/communication.yml create mode 100644 playbooks/core-infrastructure.yml create mode 100644 playbooks/maintenance/backup.yml create mode 100644 playbooks/maintenance/health-check.yml create mode 100644 playbooks/maintenance/update-containers.yml create mode 100644 playbooks/media.yml create mode 100644 playbooks/monitoring.yml create mode 100644 playbooks/security-hardening.yml create mode 100644 playbooks/site.yml create mode 100644 playbooks/smarthome.yml create mode 100644 requirements.yml create mode 100644 roles/adguard/defaults/main.yml create mode 100644 roles/adguard/tasks/main.yml create mode 100644 roles/adguard/templates/docker-compose.yml.j2 create mode 100644 roles/backup/defaults/main.yml create mode 100644 roles/backup/tasks/main.yml create mode 100644 roles/backup/templates/backup.sh.j2 create mode 100644 roles/common/defaults/main.yml create mode 100644 roles/common/handlers/main.yml create mode 100644 roles/common/tasks/main.yml create mode 100644 roles/common/templates/sshd_config.j2 create mode 100644 roles/docker/defaults/main.yml create mode 100644 roles/docker/handlers/main.yml create mode 100644 roles/docker/tasks/main.yml create mode 100644 roles/frigate/defaults/main.yml create mode 100644 roles/frigate/tasks/main.yml create mode 100644 roles/frigate/templates/docker-compose.yml.j2 create mode 100644 roles/gitea/defaults/main.yml create mode 100644 roles/gitea/tasks/main.yml create mode 100644 roles/gitea/templates/docker-compose.yml.j2 create mode 100644 roles/home_assistant/defaults/main.yml create mode 100644 roles/home_assistant/tasks/main.yml create mode 100644 roles/home_assistant/templates/docker-compose.yml.j2 create mode 100644 roles/matrix/defaults/main.yml create mode 100644 roles/matrix/tasks/main.yml create mode 100644 roles/matrix/templates/docker-compose.yml.j2 create mode 100644 roles/media/defaults/main.yml create mode 100644 roles/media/tasks/main.yml create mode 100644 roles/media/templates/docker-compose.yml.j2 create mode 100644 roles/monitoring/defaults/main.yml create mode 100644 roles/monitoring/handlers/main.yml create mode 100644 roles/monitoring/tasks/main.yml create mode 100644 roles/monitoring/templates/alertmanager.yml.j2 create mode 100644 roles/monitoring/templates/docker-compose.yml.j2 create mode 100644 roles/monitoring/templates/prometheus.yml.j2 create mode 100644 roles/openclaw/defaults/main.yml create mode 100644 roles/openclaw/tasks/main.yml create mode 100644 roles/openclaw/templates/docker-compose.yml.j2 create mode 100644 roles/security/defaults/main.yml create mode 100644 roles/security/handlers/main.yml create mode 100644 roles/security/tasks/main.yml create mode 100644 roles/security/templates/audit.rules.j2 create mode 100644 roles/security/templates/sysctl.conf.j2 create mode 100644 roles/semaphore/defaults/main.yml create mode 100644 roles/semaphore/tasks/main.yml create mode 100644 roles/semaphore/templates/docker-compose.yml.j2 create mode 100644 roles/traefik/defaults/main.yml create mode 100644 roles/traefik/handlers/main.yml create mode 100644 roles/traefik/tasks/main.yml create mode 100644 roles/traefik/templates/docker-compose.yml.j2 create mode 100644 roles/vaultwarden/defaults/main.yml create mode 100644 roles/vaultwarden/tasks/main.yml create mode 100644 roles/vaultwarden/templates/docker-compose.yml.j2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2526ad5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +*.retry +.vault_pass +vault_password_file +*.pyc +__pycache__/ +.fact_cache/ +*.log +*.bak +*.swp +*~ +.env +secrets/ diff --git a/README.md b/README.md index 1d422b7..6d1da60 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,80 @@ -# ansible-homelab +# Ansible Homelab - hobbs.farm -Ansible automation for hobbs.farm infrastructure \ No newline at end of file +Infrastructure-as-Code for the hobbs.farm homelab environment. + +## Architecture + +| Host | IP | Role | OS | +|------|-----|------|-----| +| Peggy | 192.168.86.45 | Primary Docker host (48+ containers) | Ubuntu 24.04 | +| Bart | 192.168.86.181 | Frigate NVR | Ubuntu | +| Yoda | 192.168.86.31 | BlueIris NVR | Windows | +| NAS | 192.168.86.30 | 16TB storage (SMB/NFS) | Synology | +| Firewalla | 192.168.86.1 | Gateway / Firewall | Firewalla | + +## Quick Start + +```bash +# Install Galaxy requirements +ansible-galaxy install -r requirements.yml + +# Test connectivity +ansible all -m ping + +# Full deployment +ansible-playbook playbooks/site.yml + +# Deploy specific stack +ansible-playbook playbooks/core-infrastructure.yml +ansible-playbook playbooks/monitoring.yml +ansible-playbook playbooks/media.yml + +# Maintenance +ansible-playbook playbooks/maintenance/health-check.yml +ansible-playbook playbooks/maintenance/update-containers.yml +ansible-playbook playbooks/maintenance/backup.yml +``` + +## Tags + +All playbooks support selective execution via tags: + +```bash +ansible-playbook playbooks/site.yml --tags traefik +ansible-playbook playbooks/site.yml --tags monitoring +ansible-playbook playbooks/site.yml --tags media +``` + +Available tags: `common`, `docker`, `traefik`, `vaultwarden`, `monitoring`, `grafana`, `prometheus`, `alertmanager`, `media`, `jellyfin`, `sonarr`, `radarr`, `matrix`, `homeassistant`, `esphome`, `frigate`, `adguard`, `gitea`, `semaphore`, `openclaw`, `security`, `backup` + +## Vault + +Sensitive variables are referenced via `vault_` prefix and should be stored in an encrypted vault file: + +```bash +ansible-vault create inventory/group_vars/vault.yml +ansible-vault edit inventory/group_vars/vault.yml +``` + +Required vault variables: +- `vault_acme_email` - Let's Encrypt email +- `vault_cloudflare_api_token` - Cloudflare DNS API token +- `vault_grafana_admin_password` - Grafana admin password +- `vault_vaultwarden_admin_token` - Vaultwarden admin token +- `vault_matrix_shared_secret` - Matrix Synapse registration secret +- `vault_smtp_host` - SMTP relay host +- `vault_slack_webhook` - Slack/Discord webhook for alerts +- `vault_gitea_db_password` - Gitea PostgreSQL password +- `vault_semaphore_db_password` - Semaphore DB password +- `vault_nas_smb_password` - NAS SMB credentials + +## Directory Structure + +``` +├── ansible.cfg # Ansible configuration +├── requirements.yml # Galaxy dependencies +├── inventory/ # Hosts and variables +├── playbooks/ # Orchestration playbooks +│ └── maintenance/ # Maintenance tasks +└── roles/ # Reusable roles +``` diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..e0251d9 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,25 @@ +[defaults] +inventory = inventory/hosts.yml +roles_path = roles +remote_user = glyptic +private_key_file = ~/.ssh/id_docker_server +host_key_checking = False +retry_files_enabled = False +stdout_callback = yaml +callbacks_enabled = timer, profile_tasks +forks = 10 +timeout = 30 +gathering = smart +fact_caching = jsonfile +fact_caching_connection = .fact_cache +fact_caching_timeout = 3600 + +[privilege_escalation] +become = True +become_method = sudo +become_user = root +become_ask_pass = False + +[ssh_connection] +pipelining = True +ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o StrictHostKeyChecking=no diff --git a/inventory/group_vars/all.yml b/inventory/group_vars/all.yml new file mode 100644 index 0000000..4be5e50 --- /dev/null +++ b/inventory/group_vars/all.yml @@ -0,0 +1,45 @@ +--- +# Global variables for hobbs.farm +domain: hobbs.farm +timezone: America/Chicago +dns_server: 192.168.86.45 +gateway: 192.168.86.1 +nas_ip: 192.168.86.30 +nas_share: "//192.168.86.30/data" + +# SSH +ansible_user: glyptic +ansible_ssh_private_key_file: ~/.ssh/id_docker_server +ansible_python_interpreter: /usr/bin/python3 + +# Docker network +docker_network_name: homelab_network + +# Traefik +traefik_acme_email: "{{ vault_acme_email }}" +cloudflare_api_token: "{{ vault_cloudflare_api_token }}" + +# NAS mounts +nas_media_path: /mnt/nas/media +nas_backup_path: /mnt/nas/backups +nas_docker_path: /mnt/nas/docker + +# Common packages +common_packages: + - htop + - iotop + - curl + - wget + - git + - jq + - unzip + - net-tools + - ncdu + - tmux + - rsync + - lsof + +# NTP +ntp_servers: + - 0.us.pool.ntp.org + - 1.us.pool.ntp.org diff --git a/inventory/group_vars/docker_hosts.yml b/inventory/group_vars/docker_hosts.yml new file mode 100644 index 0000000..6d219ff --- /dev/null +++ b/inventory/group_vars/docker_hosts.yml @@ -0,0 +1,9 @@ +--- +docker_compose_base: /data/docker-compose +docker_data_dir: /data +docker_log_driver: json-file +docker_log_max_size: 10m +docker_log_max_file: "3" +docker_install_compose_plugin: true +docker_users: + - glyptic diff --git a/inventory/group_vars/monitoring.yml b/inventory/group_vars/monitoring.yml new file mode 100644 index 0000000..fc7c26b --- /dev/null +++ b/inventory/group_vars/monitoring.yml @@ -0,0 +1,7 @@ +--- +prometheus_retention: 30d +prometheus_scrape_interval: 15s +grafana_admin_password: "{{ vault_grafana_admin_password }}" +alertmanager_smtp_host: "{{ vault_smtp_host }}" +alertmanager_smtp_from: "alerts@hobbs.farm" +alertmanager_slack_webhook: "{{ vault_slack_webhook | default('') }}" diff --git a/inventory/host_vars/bart.yml b/inventory/host_vars/bart.yml new file mode 100644 index 0000000..9daac06 --- /dev/null +++ b/inventory/host_vars/bart.yml @@ -0,0 +1,14 @@ +--- +ansible_host: 192.168.86.181 +hostname: bart +hardware_description: "Intel i5-4570T, 16GB RAM" +os: "Ubuntu" + +# Frigate-specific +frigate_config_dir: /opt/frigate/config +frigate_media_dir: /media/frigate +coral_device: /dev/bus/usb + +backup_sources: + - /opt/frigate/config +backup_destination: /mnt/nas/backups/bart diff --git a/inventory/host_vars/firewalla.yml b/inventory/host_vars/firewalla.yml new file mode 100644 index 0000000..395f1ad --- /dev/null +++ b/inventory/host_vars/firewalla.yml @@ -0,0 +1,6 @@ +--- +ansible_host: 192.168.86.1 +hostname: firewalla +device_type: firewall +ansible_connection: local +note: "Firewalla managed via its own interface. Inventory entry for documentation and monitoring targets." diff --git a/inventory/host_vars/peggy.yml b/inventory/host_vars/peggy.yml new file mode 100644 index 0000000..fcfda42 --- /dev/null +++ b/inventory/host_vars/peggy.yml @@ -0,0 +1,37 @@ +--- +ansible_host: 192.168.86.45 +hostname: peggy +hardware_description: "Intel i7-6700T, 8GB RAM" +os: "Ubuntu 24.04" + +docker_compose_stacks: + - core-infrastructure + - infrastructure + - monitoring + - media + - automation + - communication + - smarthome + +# NAS mounts for Peggy +nfs_mounts: + - src: "192.168.86.30:/volume1/media" + dest: /mnt/nas/media + fstype: nfs + opts: "rw,hard,intr,rsize=8192,wsize=8192" + - src: "192.168.86.30:/volume1/backups" + dest: /mnt/nas/backups + fstype: nfs + opts: "rw,hard,intr" + +# Backup paths +backup_sources: + - /data/docker-compose + - /data/traefik + - /data/vaultwarden + - /data/grafana + - /data/prometheus + - /data/homeassistant + - /data/gitea + +backup_destination: /mnt/nas/backups/peggy diff --git a/inventory/hosts.yml b/inventory/hosts.yml new file mode 100644 index 0000000..2df05d5 --- /dev/null +++ b/inventory/hosts.yml @@ -0,0 +1,26 @@ +--- +all: + children: + docker_hosts: + hosts: + peggy: + ansible_host: 192.168.86.45 + bart: + ansible_host: 192.168.86.181 + nvr: + hosts: + bart: + yoda: + ansible_host: 192.168.86.31 + ansible_connection: local + ansible_shell_type: cmd + network: + hosts: + firewalla: + ansible_host: 192.168.86.1 + ansible_connection: local + storage: + hosts: + nas: + ansible_host: 192.168.86.30 + ansible_connection: local diff --git a/playbooks/automation.yml b/playbooks/automation.yml new file mode 100644 index 0000000..c972c83 --- /dev/null +++ b/playbooks/automation.yml @@ -0,0 +1,12 @@ +--- +- name: Deploy automation stack + hosts: peggy + roles: + - role: docker + tags: [docker] + - role: gitea + tags: [automation, gitea] + - role: semaphore + tags: [automation, semaphore] + - role: openclaw + tags: [automation, openclaw] diff --git a/playbooks/communication.yml b/playbooks/communication.yml new file mode 100644 index 0000000..c619b40 --- /dev/null +++ b/playbooks/communication.yml @@ -0,0 +1,8 @@ +--- +- name: Deploy communication stack + hosts: peggy + roles: + - role: docker + tags: [docker] + - role: matrix + tags: [communication, matrix] diff --git a/playbooks/core-infrastructure.yml b/playbooks/core-infrastructure.yml new file mode 100644 index 0000000..ec95114 --- /dev/null +++ b/playbooks/core-infrastructure.yml @@ -0,0 +1,14 @@ +--- +- name: Deploy core infrastructure + hosts: peggy + roles: + - role: common + tags: [common] + - role: docker + tags: [docker] + - role: traefik + tags: [traefik] + - role: vaultwarden + tags: [vaultwarden] + - role: adguard + tags: [adguard] diff --git a/playbooks/maintenance/backup.yml b/playbooks/maintenance/backup.yml new file mode 100644 index 0000000..c50ea6d --- /dev/null +++ b/playbooks/maintenance/backup.yml @@ -0,0 +1,42 @@ +--- +- name: Run backups + hosts: docker_hosts + become: true + tasks: + - name: Ensure backup destination exists + ansible.builtin.file: + path: "{{ backup_destination }}" + state: directory + mode: "0750" + tags: [backup] + + - name: Run backup script + ansible.builtin.command: + cmd: /usr/local/bin/homelab-backup.sh + register: backup_result + changed_when: backup_result.rc == 0 + tags: [backup] + + - name: Display backup result + ansible.builtin.debug: + msg: "{{ backup_result.stdout_lines }}" + tags: [backup] + + - name: Clean up backups older than 30 days + ansible.builtin.find: + paths: "{{ backup_destination }}" + age: 30d + recurse: false + file_type: file + patterns: "*.tar.gz" + register: old_backups + tags: [backup, cleanup] + + - name: Remove old backups + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + loop: "{{ old_backups.files }}" + loop_control: + label: "{{ item.path | basename }}" + tags: [backup, cleanup] diff --git a/playbooks/maintenance/health-check.yml b/playbooks/maintenance/health-check.yml new file mode 100644 index 0000000..5b4a073 --- /dev/null +++ b/playbooks/maintenance/health-check.yml @@ -0,0 +1,87 @@ +--- +- name: Health check all services + hosts: docker_hosts + become: true + tasks: + - name: Check Docker daemon is running + ansible.builtin.systemd: + name: docker + state: started + check_mode: true + register: docker_status + tags: [health] + + - name: Get running containers + community.docker.docker_host_info: + containers: true + register: docker_info + tags: [health] + + - name: Display container count + ansible.builtin.debug: + msg: "{{ inventory_hostname }}: {{ docker_info.containers | length }} containers running" + tags: [health] + + - name: Check for unhealthy containers + ansible.builtin.shell: | + docker ps --filter health=unhealthy --format '{{ '{{' }}.Names{{ '}}' }}' + register: unhealthy + changed_when: false + tags: [health] + + - name: Report unhealthy containers + ansible.builtin.debug: + msg: "WARNING: Unhealthy containers on {{ inventory_hostname }}: {{ unhealthy.stdout_lines }}" + when: unhealthy.stdout | length > 0 + tags: [health] + + - name: Check disk usage + ansible.builtin.shell: | + df -h / /data | tail -n +2 | awk '{print $1, $5, $6}' + register: disk_usage + changed_when: false + tags: [health] + + - name: Display disk usage + ansible.builtin.debug: + msg: "{{ disk_usage.stdout_lines }}" + tags: [health] + + - name: Check memory usage + ansible.builtin.shell: | + free -h | grep Mem | awk '{print "Total:", $2, "Used:", $3, "Free:", $4}' + register: mem_usage + changed_when: false + tags: [health] + + - name: Display memory usage + ansible.builtin.debug: + msg: "{{ mem_usage.stdout }}" + tags: [health] + + - name: Check key services via HTTP + ansible.builtin.uri: + url: "https://{{ item }}.{{ domain }}" + validate_certs: false + status_code: [200, 301, 302, 303, 401, 403] + timeout: 10 + loop: + - traefik + - vault + - grafana + - jellyfin + - gitea + - ha + register: http_checks + ignore_errors: true + tags: [health] + when: inventory_hostname == 'peggy' + + - name: Report HTTP check results + ansible.builtin.debug: + msg: "{{ item.item }}.{{ domain }}: {{ item.status | default('FAILED') }}" + loop: "{{ http_checks.results | default([]) }}" + loop_control: + label: "{{ item.item | default('unknown') }}" + when: inventory_hostname == 'peggy' + tags: [health] diff --git a/playbooks/maintenance/update-containers.yml b/playbooks/maintenance/update-containers.yml new file mode 100644 index 0000000..44cdb25 --- /dev/null +++ b/playbooks/maintenance/update-containers.yml @@ -0,0 +1,47 @@ +--- +- name: Update Docker containers + hosts: docker_hosts + become: true + vars: + update_stack: all + tasks: + - name: Get list of compose directories + ansible.builtin.find: + paths: "{{ docker_compose_base }}" + file_type: directory + recurse: false + register: compose_dirs + tags: [update] + + - name: Pull latest images for each stack + community.docker.docker_compose_v2_pull: + project_src: "{{ item.path }}" + loop: "{{ compose_dirs.files }}" + loop_control: + label: "{{ item.path | basename }}" + when: update_stack == 'all' or (item.path | basename) == update_stack + tags: [update] + + - name: Recreate containers with new images + community.docker.docker_compose_v2: + project_src: "{{ item.path }}" + state: present + recreate: auto + loop: "{{ compose_dirs.files }}" + loop_control: + label: "{{ item.path | basename }}" + when: update_stack == 'all' or (item.path | basename) == update_stack + tags: [update] + + - name: Prune unused Docker images + community.docker.docker_prune: + images: true + images_filters: + dangling: false + tags: [cleanup] + + - name: Prune unused Docker volumes + community.docker.docker_prune: + volumes: true + tags: [cleanup] + when: prune_volumes | default(false) | bool diff --git a/playbooks/media.yml b/playbooks/media.yml new file mode 100644 index 0000000..8b43005 --- /dev/null +++ b/playbooks/media.yml @@ -0,0 +1,8 @@ +--- +- name: Deploy media stack + hosts: peggy + roles: + - role: docker + tags: [docker] + - role: media + tags: [media, jellyfin, sonarr, radarr] diff --git a/playbooks/monitoring.yml b/playbooks/monitoring.yml new file mode 100644 index 0000000..dda2357 --- /dev/null +++ b/playbooks/monitoring.yml @@ -0,0 +1,8 @@ +--- +- name: Deploy monitoring stack + hosts: peggy + roles: + - role: docker + tags: [docker] + - role: monitoring + tags: [monitoring, prometheus, grafana, alertmanager] diff --git a/playbooks/security-hardening.yml b/playbooks/security-hardening.yml new file mode 100644 index 0000000..cb3412b --- /dev/null +++ b/playbooks/security-hardening.yml @@ -0,0 +1,8 @@ +--- +- name: Apply security hardening + hosts: docker_hosts + roles: + - role: common + tags: [common] + - role: security + tags: [security] diff --git a/playbooks/site.yml b/playbooks/site.yml new file mode 100644 index 0000000..8a4a052 --- /dev/null +++ b/playbooks/site.yml @@ -0,0 +1,71 @@ +--- +# Full site deployment for hobbs.farm +- name: Apply common configuration + hosts: docker_hosts + tags: [common] + roles: + - common + +- name: Configure Docker + hosts: docker_hosts + tags: [docker] + roles: + - docker + +- name: Deploy core infrastructure + hosts: peggy + tags: [core] + roles: + - traefik + - vaultwarden + - adguard + +- name: Deploy monitoring stack + hosts: peggy + tags: [monitoring] + roles: + - monitoring + +- name: Deploy media stack + hosts: peggy + tags: [media] + roles: + - media + +- name: Deploy communication stack + hosts: peggy + tags: [communication] + roles: + - matrix + +- name: Deploy smart home stack + hosts: peggy + tags: [smarthome] + roles: + - home_assistant + +- name: Deploy automation stack + hosts: peggy + tags: [automation] + roles: + - gitea + - semaphore + - openclaw + +- name: Deploy Frigate NVR + hosts: bart + tags: [frigate] + roles: + - frigate + +- name: Apply security hardening + hosts: docker_hosts + tags: [security] + roles: + - security + +- name: Configure backups + hosts: docker_hosts + tags: [backup] + roles: + - backup diff --git a/playbooks/smarthome.yml b/playbooks/smarthome.yml new file mode 100644 index 0000000..e23b8a1 --- /dev/null +++ b/playbooks/smarthome.yml @@ -0,0 +1,8 @@ +--- +- name: Deploy smart home stack + hosts: peggy + roles: + - role: docker + tags: [docker] + - role: home_assistant + tags: [smarthome, homeassistant, esphome] diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..4cdd830 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,14 @@ +--- +collections: + - name: community.docker + version: ">=3.4.0" + - name: community.general + version: ">=7.0.0" + - name: ansible.posix + version: ">=1.5.0" + +roles: + - name: geerlingguy.docker + version: "6.1.0" + - name: geerlingguy.pip + version: "2.2.0" diff --git a/roles/adguard/defaults/main.yml b/roles/adguard/defaults/main.yml new file mode 100644 index 0000000..84b552b --- /dev/null +++ b/roles/adguard/defaults/main.yml @@ -0,0 +1,5 @@ +--- +adguard_compose_dir: "{{ docker_compose_base }}/core-infrastructure" +adguard_data_dir: /data/adguard +adguard_image: "adguard/adguardhome:latest" +adguard_host: "adguard.{{ domain }}" diff --git a/roles/adguard/tasks/main.yml b/roles/adguard/tasks/main.yml new file mode 100644 index 0000000..ff88f1b --- /dev/null +++ b/roles/adguard/tasks/main.yml @@ -0,0 +1,29 @@ +--- +- name: Create AdGuard directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + loop: + - "{{ adguard_data_dir }}/work" + - "{{ adguard_data_dir }}/conf" + tags: [adguard] + +- name: Deploy AdGuard compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ adguard_compose_dir }}/docker-compose.adguard.yml" + owner: glyptic + group: docker + mode: "0644" + tags: [adguard] + +- name: Start AdGuard + community.docker.docker_compose_v2: + project_src: "{{ adguard_compose_dir }}" + files: + - docker-compose.adguard.yml + state: present + tags: [adguard] diff --git a/roles/adguard/templates/docker-compose.yml.j2 b/roles/adguard/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..08f3843 --- /dev/null +++ b/roles/adguard/templates/docker-compose.yml.j2 @@ -0,0 +1,24 @@ +services: + adguard: + image: {{ adguard_image }} + container_name: adguard + restart: unless-stopped + ports: + - "53:53/tcp" + - "53:53/udp" + - "8053:8053/tcp" + volumes: + - {{ adguard_data_dir }}/work:/opt/adguardhome/work + - {{ adguard_data_dir }}/conf:/opt/adguardhome/conf + labels: + traefik.enable: "true" + traefik.http.routers.adguard.rule: "Host(`{{ adguard_host }}`)" + traefik.http.routers.adguard.entrypoints: websecure + traefik.http.routers.adguard.tls.certresolver: cloudflare + traefik.http.services.adguard.loadbalancer.server.port: "3000" + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true diff --git a/roles/backup/defaults/main.yml b/roles/backup/defaults/main.yml new file mode 100644 index 0000000..38e4676 --- /dev/null +++ b/roles/backup/defaults/main.yml @@ -0,0 +1,9 @@ +--- +backup_script_path: /usr/local/bin/homelab-backup.sh +backup_schedule: "0 2 * * *" +backup_retention_days: 30 +backup_sources: + - /data/docker-compose +backup_destination: /mnt/nas/backups/{{ inventory_hostname }} +backup_pre_commands: [] +backup_post_commands: [] diff --git a/roles/backup/tasks/main.yml b/roles/backup/tasks/main.yml new file mode 100644 index 0000000..5263042 --- /dev/null +++ b/roles/backup/tasks/main.yml @@ -0,0 +1,43 @@ +--- +- name: Ensure backup destination directory exists + ansible.builtin.file: + path: "{{ backup_destination }}" + state: directory + owner: glyptic + group: glyptic + mode: "0750" + tags: [backup] + +- name: Deploy backup script + ansible.builtin.template: + src: backup.sh.j2 + dest: "{{ backup_script_path }}" + owner: root + group: root + mode: "0750" + tags: [backup] + +- name: Configure backup cron job + ansible.builtin.cron: + name: "homelab-backup" + minute: "0" + hour: "2" + job: "{{ backup_script_path }} >> /var/log/homelab-backup.log 2>&1" + user: root + tags: [backup] + +- name: Configure backup log rotation + ansible.builtin.copy: + content: | + /var/log/homelab-backup.log { + weekly + rotate 4 + compress + missingok + notifempty + } + dest: /etc/logrotate.d/homelab-backup + owner: root + group: root + mode: "0644" + tags: [backup] diff --git a/roles/backup/templates/backup.sh.j2 b/roles/backup/templates/backup.sh.j2 new file mode 100644 index 0000000..efb9bd3 --- /dev/null +++ b/roles/backup/templates/backup.sh.j2 @@ -0,0 +1,42 @@ +#!/bin/bash +# Homelab Backup Script - Managed by Ansible +# Host: {{ inventory_hostname }} + +set -euo pipefail + +BACKUP_DIR="{{ backup_destination }}" +DATE=$(date +%Y%m%d_%H%M%S) +HOSTNAME="{{ inventory_hostname }}" +BACKUP_FILE="${BACKUP_DIR}/${HOSTNAME}_${DATE}.tar.gz" +RETENTION_DAYS="{{ backup_retention_days }}" + +echo "=== Backup started at $(date) ===" +echo "Destination: ${BACKUP_FILE}" + +# Pre-backup commands +{% for cmd in backup_pre_commands %} +echo "Running pre-backup: {{ cmd }}" +{{ cmd }} +{% endfor %} + +# Create backup +tar czf "${BACKUP_FILE}" \ +{% for source in backup_sources %} + {{ source }} \ +{% endfor %} + 2>/dev/null || true + +BACKUP_SIZE=$(du -sh "${BACKUP_FILE}" | cut -f1) +echo "Backup created: ${BACKUP_FILE} (${BACKUP_SIZE})" + +# Post-backup commands +{% for cmd in backup_post_commands %} +echo "Running post-backup: {{ cmd }}" +{{ cmd }} +{% endfor %} + +# Cleanup old backups +echo "Removing backups older than ${RETENTION_DAYS} days..." +find "${BACKUP_DIR}" -name "${HOSTNAME}_*.tar.gz" -mtime +${RETENTION_DAYS} -delete + +echo "=== Backup completed at $(date) ===" diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml new file mode 100644 index 0000000..be02518 --- /dev/null +++ b/roles/common/defaults/main.yml @@ -0,0 +1,24 @@ +--- +common_packages: + - htop + - iotop + - curl + - wget + - git + - jq + - unzip + - net-tools + - ncdu + - tmux + - rsync + - lsof + - vim + - ufw + - fail2ban + +common_timezone: America/Chicago +common_hostname: "{{ hostname | default(inventory_hostname) }}" +sshd_port: 22 +sshd_permit_root: "no" +sshd_password_auth: "no" +sshd_pubkey_auth: "yes" diff --git a/roles/common/handlers/main.yml b/roles/common/handlers/main.yml new file mode 100644 index 0000000..e3e8b1c --- /dev/null +++ b/roles/common/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Restart sshd + ansible.builtin.systemd: + name: sshd + state: restarted diff --git a/roles/common/tasks/main.yml b/roles/common/tasks/main.yml new file mode 100644 index 0000000..57022f2 --- /dev/null +++ b/roles/common/tasks/main.yml @@ -0,0 +1,98 @@ +--- +- name: Set hostname + ansible.builtin.hostname: + name: "{{ common_hostname }}" + tags: [common, hostname] + +- name: Set timezone + community.general.timezone: + name: "{{ common_timezone }}" + tags: [common, timezone] + +- name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + tags: [common, packages] + +- name: Install common packages + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + tags: [common, packages] + +- name: Upgrade all packages + ansible.builtin.apt: + upgrade: safe + tags: [common, packages, upgrade] + when: apt_upgrade | default(false) | bool + +- name: Configure SSH daemon + ansible.builtin.template: + src: sshd_config.j2 + dest: /etc/ssh/sshd_config + owner: root + group: root + mode: "0644" + validate: "sshd -t -f %s" + notify: Restart sshd + tags: [common, ssh] + +- name: Ensure SSH is running + ansible.builtin.systemd: + name: sshd + state: started + enabled: true + tags: [common, ssh] + +- name: Configure UFW defaults + community.general.ufw: + direction: incoming + policy: deny + tags: [common, firewall] + +- name: Allow SSH through UFW + community.general.ufw: + rule: allow + port: "{{ sshd_port }}" + proto: tcp + tags: [common, firewall] + +- name: Allow traffic from local network + community.general.ufw: + rule: allow + src: 192.168.86.0/24 + tags: [common, firewall] + +- name: Enable UFW + community.general.ufw: + state: enabled + tags: [common, firewall] + +- name: Configure fail2ban + ansible.builtin.systemd: + name: fail2ban + state: started + enabled: true + tags: [common, security] + +- name: Set vm.swappiness + ansible.posix.sysctl: + name: vm.swappiness + value: "10" + state: present + reload: true + tags: [common, sysctl] + +- name: Configure NTP + ansible.builtin.apt: + name: chrony + state: present + tags: [common, ntp] + +- name: Start chrony + ansible.builtin.systemd: + name: chrony + state: started + enabled: true + tags: [common, ntp] diff --git a/roles/common/templates/sshd_config.j2 b/roles/common/templates/sshd_config.j2 new file mode 100644 index 0000000..f8af40a --- /dev/null +++ b/roles/common/templates/sshd_config.j2 @@ -0,0 +1,26 @@ +# Managed by Ansible - do not edit manually +Port {{ sshd_port }} +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 + +PermitRootLogin {{ sshd_permit_root }} +PubkeyAuthentication {{ sshd_pubkey_auth }} +PasswordAuthentication {{ sshd_password_auth }} +ChallengeResponseAuthentication no +UsePAM yes + +X11Forwarding no +PrintMotd no +AcceptEnv LANG LC_* + +MaxAuthTries 3 +MaxSessions 10 +ClientAliveInterval 300 +ClientAliveCountMax 2 + +Subsystem sftp /usr/lib/openssh/sftp-server diff --git a/roles/docker/defaults/main.yml b/roles/docker/defaults/main.yml new file mode 100644 index 0000000..c8a4a7c --- /dev/null +++ b/roles/docker/defaults/main.yml @@ -0,0 +1,13 @@ +--- +docker_edition: ce +docker_users: + - glyptic +docker_install_compose_plugin: true +docker_daemon_options: + log-driver: json-file + log-opts: + max-size: "10m" + max-file: "3" + storage-driver: overlay2 + live-restore: true +docker_network_name: homelab_network diff --git a/roles/docker/handlers/main.yml b/roles/docker/handlers/main.yml new file mode 100644 index 0000000..37b5807 --- /dev/null +++ b/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Restart docker + ansible.builtin.systemd: + name: docker + state: restarted diff --git a/roles/docker/tasks/main.yml b/roles/docker/tasks/main.yml new file mode 100644 index 0000000..bdfa8e8 --- /dev/null +++ b/roles/docker/tasks/main.yml @@ -0,0 +1,76 @@ +--- +- name: Install Docker prerequisites + ansible.builtin.apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + - python3-pip + - python3-docker + state: present + tags: [docker, install] + +- name: Add Docker GPG key + ansible.builtin.apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + tags: [docker, install] + +- name: Add Docker repository + ansible.builtin.apt_repository: + repo: "deb https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + tags: [docker, install] + +- name: Install Docker CE + ansible.builtin.apt: + name: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-compose-plugin + state: present + update_cache: true + tags: [docker, install] + +- name: Configure Docker daemon + ansible.builtin.copy: + content: "{{ docker_daemon_options | to_nice_json }}" + dest: /etc/docker/daemon.json + owner: root + group: root + mode: "0644" + notify: Restart docker + tags: [docker, config] + +- name: Add users to docker group + ansible.builtin.user: + name: "{{ item }}" + groups: docker + append: true + loop: "{{ docker_users }}" + tags: [docker, users] + +- name: Ensure Docker is started and enabled + ansible.builtin.systemd: + name: docker + state: started + enabled: true + tags: [docker] + +- name: Create homelab Docker network + community.docker.docker_network: + name: "{{ docker_network_name }}" + driver: bridge + tags: [docker, network] + +- name: Ensure docker-compose base directory exists + ansible.builtin.file: + path: "{{ docker_compose_base }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + tags: [docker] diff --git a/roles/frigate/defaults/main.yml b/roles/frigate/defaults/main.yml new file mode 100644 index 0000000..43201ac --- /dev/null +++ b/roles/frigate/defaults/main.yml @@ -0,0 +1,6 @@ +--- +frigate_compose_dir: /opt/frigate +frigate_config_dir: /opt/frigate/config +frigate_media_dir: /media/frigate +frigate_image: "ghcr.io/blakeblackshear/frigate:stable" +frigate_host: "frigate.{{ domain }}" diff --git a/roles/frigate/tasks/main.yml b/roles/frigate/tasks/main.yml new file mode 100644 index 0000000..e89a8c8 --- /dev/null +++ b/roles/frigate/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- name: Create Frigate directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + loop: + - "{{ frigate_compose_dir }}" + - "{{ frigate_config_dir }}" + - "{{ frigate_media_dir }}" + tags: [frigate] + +- name: Deploy Frigate compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ frigate_compose_dir }}/docker-compose.yml" + owner: glyptic + group: docker + mode: "0644" + tags: [frigate] + +- name: Start Frigate + community.docker.docker_compose_v2: + project_src: "{{ frigate_compose_dir }}" + state: present + tags: [frigate] diff --git a/roles/frigate/templates/docker-compose.yml.j2 b/roles/frigate/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..2f92ea2 --- /dev/null +++ b/roles/frigate/templates/docker-compose.yml.j2 @@ -0,0 +1,30 @@ +services: + frigate: + image: {{ frigate_image }} + container_name: frigate + restart: unless-stopped + privileged: true + shm_size: "256mb" + ports: + - "5000:5000" + - "8554:8554" + - "8555:8555/tcp" + - "8555:8555/udp" + volumes: + - {{ frigate_config_dir }}:/config + - {{ frigate_media_dir }}:/media/frigate + - /etc/localtime:/etc/localtime:ro + environment: + FRIGATE_RTSP_PASSWORD: "{{ vault_frigate_rtsp_password | default('changeme') }}" + labels: + traefik.enable: "true" + traefik.http.routers.frigate.rule: "Host(`{{ frigate_host }}`)" + traefik.http.routers.frigate.entrypoints: websecure + traefik.http.routers.frigate.tls.certresolver: cloudflare + traefik.http.services.frigate.loadbalancer.server.port: "5000" + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true diff --git a/roles/gitea/defaults/main.yml b/roles/gitea/defaults/main.yml new file mode 100644 index 0000000..fefd938 --- /dev/null +++ b/roles/gitea/defaults/main.yml @@ -0,0 +1,8 @@ +--- +gitea_compose_dir: "{{ docker_compose_base }}/automation" +gitea_data_dir: /data/gitea +gitea_image: "gitea/gitea:latest" +gitea_db_image: "postgres:16-alpine" +gitea_host: "git.{{ domain }}" +gitea_ssh_port: 2222 +gitea_db_password: "{{ vault_gitea_db_password }}" diff --git a/roles/gitea/tasks/main.yml b/roles/gitea/tasks/main.yml new file mode 100644 index 0000000..7f47cd8 --- /dev/null +++ b/roles/gitea/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- name: Create Gitea directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + loop: + - "{{ gitea_compose_dir }}" + - "{{ gitea_data_dir }}" + - "{{ gitea_data_dir }}/db" + tags: [gitea, automation] + +- name: Deploy Gitea compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ gitea_compose_dir }}/docker-compose.yml" + owner: glyptic + group: docker + mode: "0644" + tags: [gitea, automation] + +- name: Start Gitea stack + community.docker.docker_compose_v2: + project_src: "{{ gitea_compose_dir }}" + state: present + tags: [gitea, automation] diff --git a/roles/gitea/templates/docker-compose.yml.j2 b/roles/gitea/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..53e481d --- /dev/null +++ b/roles/gitea/templates/docker-compose.yml.j2 @@ -0,0 +1,50 @@ +services: + gitea: + image: {{ gitea_image }} + container_name: gitea + restart: unless-stopped + environment: + USER_UID: "1000" + USER_GID: "1000" + GITEA__database__DB_TYPE: postgres + GITEA__database__HOST: gitea-db:5432 + GITEA__database__NAME: gitea + GITEA__database__USER: gitea + GITEA__database__PASSWD: {{ gitea_db_password }} + GITEA__server__DOMAIN: {{ gitea_host }} + GITEA__server__SSH_DOMAIN: {{ gitea_host }} + GITEA__server__SSH_PORT: {{ gitea_ssh_port }} + GITEA__server__ROOT_URL: "https://{{ gitea_host }}/" + volumes: + - {{ gitea_data_dir }}:/data + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "{{ gitea_ssh_port }}:22" + depends_on: + - gitea-db + labels: + traefik.enable: "true" + traefik.http.routers.gitea.rule: "Host(`{{ gitea_host }}`)" + traefik.http.routers.gitea.entrypoints: websecure + traefik.http.routers.gitea.tls.certresolver: cloudflare + traefik.http.services.gitea.loadbalancer.server.port: "3000" + networks: + - {{ docker_network_name }} + + gitea-db: + image: {{ gitea_db_image }} + container_name: gitea-db + restart: unless-stopped + environment: + POSTGRES_USER: gitea + POSTGRES_PASSWORD: {{ gitea_db_password }} + POSTGRES_DB: gitea + volumes: + - {{ gitea_data_dir }}/db:/var/lib/postgresql/data + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true diff --git a/roles/home_assistant/defaults/main.yml b/roles/home_assistant/defaults/main.yml new file mode 100644 index 0000000..2cefd84 --- /dev/null +++ b/roles/home_assistant/defaults/main.yml @@ -0,0 +1,12 @@ +--- +smarthome_compose_dir: "{{ docker_compose_base }}/smarthome" +ha_data_dir: /data/homeassistant +esphome_data_dir: /data/esphome +mosquitto_data_dir: /data/mosquitto + +ha_image: "ghcr.io/home-assistant/home-assistant:stable" +esphome_image: "ghcr.io/esphome/esphome:latest" +mosquitto_image: "eclipse-mosquitto:latest" + +ha_host: "ha.{{ domain }}" +esphome_host: "esphome.{{ domain }}" diff --git a/roles/home_assistant/tasks/main.yml b/roles/home_assistant/tasks/main.yml new file mode 100644 index 0000000..cf391ad --- /dev/null +++ b/roles/home_assistant/tasks/main.yml @@ -0,0 +1,32 @@ +--- +- name: Create smart home directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + loop: + - "{{ smarthome_compose_dir }}" + - "{{ ha_data_dir }}" + - "{{ esphome_data_dir }}" + - "{{ mosquitto_data_dir }}" + - "{{ mosquitto_data_dir }}/config" + - "{{ mosquitto_data_dir }}/data" + - "{{ mosquitto_data_dir }}/log" + tags: [smarthome, homeassistant] + +- name: Deploy smart home compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ smarthome_compose_dir }}/docker-compose.yml" + owner: glyptic + group: docker + mode: "0644" + tags: [smarthome, homeassistant] + +- name: Start smart home stack + community.docker.docker_compose_v2: + project_src: "{{ smarthome_compose_dir }}" + state: present + tags: [smarthome, homeassistant] diff --git a/roles/home_assistant/templates/docker-compose.yml.j2 b/roles/home_assistant/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..88e458b --- /dev/null +++ b/roles/home_assistant/templates/docker-compose.yml.j2 @@ -0,0 +1,52 @@ +services: + homeassistant: + image: {{ ha_image }} + container_name: homeassistant + restart: unless-stopped + privileged: true + environment: + TZ: {{ timezone }} + volumes: + - {{ ha_data_dir }}:/config + labels: + traefik.enable: "true" + traefik.http.routers.ha.rule: "Host(`{{ ha_host }}`)" + traefik.http.routers.ha.entrypoints: websecure + traefik.http.routers.ha.tls.certresolver: cloudflare + traefik.http.services.ha.loadbalancer.server.port: "8123" + networks: + - {{ docker_network_name }} + + esphome: + image: {{ esphome_image }} + container_name: esphome + restart: unless-stopped + environment: + TZ: {{ timezone }} + volumes: + - {{ esphome_data_dir }}:/config + labels: + traefik.enable: "true" + traefik.http.routers.esphome.rule: "Host(`{{ esphome_host }}`)" + traefik.http.routers.esphome.entrypoints: websecure + traefik.http.routers.esphome.tls.certresolver: cloudflare + traefik.http.services.esphome.loadbalancer.server.port: "6052" + networks: + - {{ docker_network_name }} + + mosquitto: + image: {{ mosquitto_image }} + container_name: mosquitto + restart: unless-stopped + ports: + - "1883:1883" + volumes: + - {{ mosquitto_data_dir }}/config:/mosquitto/config + - {{ mosquitto_data_dir }}/data:/mosquitto/data + - {{ mosquitto_data_dir }}/log:/mosquitto/log + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true diff --git a/roles/matrix/defaults/main.yml b/roles/matrix/defaults/main.yml new file mode 100644 index 0000000..51580e3 --- /dev/null +++ b/roles/matrix/defaults/main.yml @@ -0,0 +1,7 @@ +--- +matrix_compose_dir: "{{ docker_compose_base }}/communication" +synapse_data_dir: /data/synapse +synapse_image: "matrixdotorg/synapse:latest" +matrix_host: "matrix.{{ domain }}" +matrix_server_name: "{{ domain }}" +matrix_shared_secret: "{{ vault_matrix_shared_secret }}" diff --git a/roles/matrix/tasks/main.yml b/roles/matrix/tasks/main.yml new file mode 100644 index 0000000..5898a94 --- /dev/null +++ b/roles/matrix/tasks/main.yml @@ -0,0 +1,27 @@ +--- +- name: Create Matrix directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + loop: + - "{{ matrix_compose_dir }}" + - "{{ synapse_data_dir }}" + tags: [matrix, communication] + +- name: Deploy Matrix compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ matrix_compose_dir }}/docker-compose.yml" + owner: glyptic + group: docker + mode: "0644" + tags: [matrix, communication] + +- name: Start Matrix stack + community.docker.docker_compose_v2: + project_src: "{{ matrix_compose_dir }}" + state: present + tags: [matrix, communication] diff --git a/roles/matrix/templates/docker-compose.yml.j2 b/roles/matrix/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..39e0444 --- /dev/null +++ b/roles/matrix/templates/docker-compose.yml.j2 @@ -0,0 +1,22 @@ +services: + synapse: + image: {{ synapse_image }} + container_name: synapse + restart: unless-stopped + environment: + SYNAPSE_SERVER_NAME: {{ matrix_server_name }} + SYNAPSE_REPORT_STATS: "no" + volumes: + - {{ synapse_data_dir }}:/data + labels: + traefik.enable: "true" + traefik.http.routers.synapse.rule: "Host(`{{ matrix_host }}`)" + traefik.http.routers.synapse.entrypoints: websecure + traefik.http.routers.synapse.tls.certresolver: cloudflare + traefik.http.services.synapse.loadbalancer.server.port: "8008" + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true diff --git a/roles/media/defaults/main.yml b/roles/media/defaults/main.yml new file mode 100644 index 0000000..9722613 --- /dev/null +++ b/roles/media/defaults/main.yml @@ -0,0 +1,27 @@ +--- +media_compose_dir: "{{ docker_compose_base }}/media" +jellyfin_data_dir: /data/jellyfin +sonarr_data_dir: /data/sonarr +radarr_data_dir: /data/radarr +sabnzbd_data_dir: /data/sabnzbd +nzbhydra_data_dir: /data/nzbhydra2 +jellyseerr_data_dir: /data/jellyseerr + +jellyfin_image: "jellyfin/jellyfin:latest" +sonarr_image: "lscr.io/linuxserver/sonarr:latest" +radarr_image: "lscr.io/linuxserver/radarr:latest" +sabnzbd_image: "lscr.io/linuxserver/sabnzbd:latest" +nzbhydra_image: "lscr.io/linuxserver/nzbhydra2:latest" +jellyseerr_image: "fallenbagel/jellyseerr:latest" + +media_path: /mnt/nas/media +downloads_path: /mnt/nas/downloads + +jellyfin_host: "jellyfin.{{ domain }}" +sonarr_host: "sonarr.{{ domain }}" +radarr_host: "radarr.{{ domain }}" +sabnzbd_host: "sabnzbd.{{ domain }}" +jellyseerr_host: "jellyseerr.{{ domain }}" + +puid: "1000" +pgid: "1000" diff --git a/roles/media/tasks/main.yml b/roles/media/tasks/main.yml new file mode 100644 index 0000000..070717e --- /dev/null +++ b/roles/media/tasks/main.yml @@ -0,0 +1,32 @@ +--- +- name: Create media directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + loop: + - "{{ media_compose_dir }}" + - "{{ jellyfin_data_dir }}" + - "{{ sonarr_data_dir }}" + - "{{ radarr_data_dir }}" + - "{{ sabnzbd_data_dir }}" + - "{{ nzbhydra_data_dir }}" + - "{{ jellyseerr_data_dir }}" + tags: [media] + +- name: Deploy media compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ media_compose_dir }}/docker-compose.yml" + owner: glyptic + group: docker + mode: "0644" + tags: [media] + +- name: Start media stack + community.docker.docker_compose_v2: + project_src: "{{ media_compose_dir }}" + state: present + tags: [media] diff --git a/roles/media/templates/docker-compose.yml.j2 b/roles/media/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..5650de4 --- /dev/null +++ b/roles/media/templates/docker-compose.yml.j2 @@ -0,0 +1,116 @@ +services: + jellyfin: + image: {{ jellyfin_image }} + container_name: jellyfin + restart: unless-stopped + environment: + JELLYFIN_PublishedServerUrl: "https://{{ jellyfin_host }}" + volumes: + - {{ jellyfin_data_dir }}/config:/config + - {{ jellyfin_data_dir }}/cache:/cache + - {{ media_path }}:/media + labels: + traefik.enable: "true" + traefik.http.routers.jellyfin.rule: "Host(`{{ jellyfin_host }}`)" + traefik.http.routers.jellyfin.entrypoints: websecure + traefik.http.routers.jellyfin.tls.certresolver: cloudflare + traefik.http.services.jellyfin.loadbalancer.server.port: "8096" + networks: + - {{ docker_network_name }} + + sonarr: + image: {{ sonarr_image }} + container_name: sonarr + restart: unless-stopped + environment: + PUID: "{{ puid }}" + PGID: "{{ pgid }}" + TZ: {{ timezone }} + volumes: + - {{ sonarr_data_dir }}:/config + - {{ media_path }}/tv:/tv + - {{ downloads_path }}:/downloads + labels: + traefik.enable: "true" + traefik.http.routers.sonarr.rule: "Host(`{{ sonarr_host }}`)" + traefik.http.routers.sonarr.entrypoints: websecure + traefik.http.routers.sonarr.tls.certresolver: cloudflare + traefik.http.services.sonarr.loadbalancer.server.port: "8989" + networks: + - {{ docker_network_name }} + + radarr: + image: {{ radarr_image }} + container_name: radarr + restart: unless-stopped + environment: + PUID: "{{ puid }}" + PGID: "{{ pgid }}" + TZ: {{ timezone }} + volumes: + - {{ radarr_data_dir }}:/config + - {{ media_path }}/movies:/movies + - {{ downloads_path }}:/downloads + labels: + traefik.enable: "true" + traefik.http.routers.radarr.rule: "Host(`{{ radarr_host }}`)" + traefik.http.routers.radarr.entrypoints: websecure + traefik.http.routers.radarr.tls.certresolver: cloudflare + traefik.http.services.radarr.loadbalancer.server.port: "7878" + networks: + - {{ docker_network_name }} + + sabnzbd: + image: {{ sabnzbd_image }} + container_name: sabnzbd + restart: unless-stopped + environment: + PUID: "{{ puid }}" + PGID: "{{ pgid }}" + TZ: {{ timezone }} + volumes: + - {{ sabnzbd_data_dir }}:/config + - {{ downloads_path }}:/downloads + labels: + traefik.enable: "true" + traefik.http.routers.sabnzbd.rule: "Host(`{{ sabnzbd_host }}`)" + traefik.http.routers.sabnzbd.entrypoints: websecure + traefik.http.routers.sabnzbd.tls.certresolver: cloudflare + traefik.http.services.sabnzbd.loadbalancer.server.port: "8080" + networks: + - {{ docker_network_name }} + + nzbhydra2: + image: {{ nzbhydra_image }} + container_name: nzbhydra2 + restart: unless-stopped + environment: + PUID: "{{ puid }}" + PGID: "{{ pgid }}" + TZ: {{ timezone }} + volumes: + - {{ nzbhydra_data_dir }}:/config + - {{ downloads_path }}:/downloads + networks: + - {{ docker_network_name }} + + jellyseerr: + image: {{ jellyseerr_image }} + container_name: jellyseerr + restart: unless-stopped + environment: + TZ: {{ timezone }} + volumes: + - {{ jellyseerr_data_dir }}:/app/config + labels: + traefik.enable: "true" + traefik.http.routers.jellyseerr.rule: "Host(`{{ jellyseerr_host }}`)" + traefik.http.routers.jellyseerr.entrypoints: websecure + traefik.http.routers.jellyseerr.tls.certresolver: cloudflare + traefik.http.services.jellyseerr.loadbalancer.server.port: "5055" + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true diff --git a/roles/monitoring/defaults/main.yml b/roles/monitoring/defaults/main.yml new file mode 100644 index 0000000..5d0f480 --- /dev/null +++ b/roles/monitoring/defaults/main.yml @@ -0,0 +1,18 @@ +--- +monitoring_compose_dir: "{{ docker_compose_base }}/monitoring" +prometheus_data_dir: /data/prometheus +grafana_data_dir: /data/grafana +alertmanager_data_dir: /data/alertmanager + +prometheus_image: "prom/prometheus:latest" +grafana_image: "grafana/grafana:latest" +alertmanager_image: "prom/alertmanager:latest" +node_exporter_image: "prom/node-exporter:latest" +cadvisor_image: "gcr.io/cadvisor/cadvisor:latest" + +prometheus_retention: 30d +prometheus_scrape_interval: 15s +grafana_admin_password: "{{ vault_grafana_admin_password }}" +grafana_host: "grafana.{{ domain }}" +prometheus_host: "prometheus.{{ domain }}" +alertmanager_host: "alertmanager.{{ domain }}" diff --git a/roles/monitoring/handlers/main.yml b/roles/monitoring/handlers/main.yml new file mode 100644 index 0000000..726aade --- /dev/null +++ b/roles/monitoring/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Restart monitoring stack + community.docker.docker_compose_v2: + project_src: "{{ monitoring_compose_dir }}" + state: present + recreate: always diff --git a/roles/monitoring/tasks/main.yml b/roles/monitoring/tasks/main.yml new file mode 100644 index 0000000..84aeb1b --- /dev/null +++ b/roles/monitoring/tasks/main.yml @@ -0,0 +1,61 @@ +--- +- name: Create monitoring directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + loop: + - "{{ monitoring_compose_dir }}" + - "{{ prometheus_data_dir }}" + - "{{ prometheus_data_dir }}/config" + - "{{ grafana_data_dir }}" + - "{{ alertmanager_data_dir }}" + - "{{ alertmanager_data_dir }}/config" + tags: [monitoring] + +- name: Set Grafana data directory ownership + ansible.builtin.file: + path: "{{ grafana_data_dir }}" + state: directory + owner: "472" + group: "472" + mode: "0755" + tags: [monitoring, grafana] + +- name: Deploy Prometheus configuration + ansible.builtin.template: + src: prometheus.yml.j2 + dest: "{{ prometheus_data_dir }}/config/prometheus.yml" + owner: glyptic + group: docker + mode: "0644" + notify: Restart monitoring stack + tags: [monitoring, prometheus] + +- name: Deploy Alertmanager configuration + ansible.builtin.template: + src: alertmanager.yml.j2 + dest: "{{ alertmanager_data_dir }}/config/alertmanager.yml" + owner: glyptic + group: docker + mode: "0644" + notify: Restart monitoring stack + tags: [monitoring, alertmanager] + +- name: Deploy monitoring compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ monitoring_compose_dir }}/docker-compose.yml" + owner: glyptic + group: docker + mode: "0644" + notify: Restart monitoring stack + tags: [monitoring] + +- name: Start monitoring stack + community.docker.docker_compose_v2: + project_src: "{{ monitoring_compose_dir }}" + state: present + tags: [monitoring] diff --git a/roles/monitoring/templates/alertmanager.yml.j2 b/roles/monitoring/templates/alertmanager.yml.j2 new file mode 100644 index 0000000..0ee35dc --- /dev/null +++ b/roles/monitoring/templates/alertmanager.yml.j2 @@ -0,0 +1,29 @@ +global: + resolve_timeout: 5m +{% if alertmanager_smtp_host is defined and alertmanager_smtp_host %} + smtp_smarthost: "{{ alertmanager_smtp_host }}" + smtp_from: "{{ alertmanager_smtp_from }}" +{% endif %} + +route: + group_by: ["alertname", "instance"] + group_wait: 30s + group_interval: 5m + repeat_interval: 4h + receiver: default + +receivers: + - name: default +{% if alertmanager_slack_webhook is defined and alertmanager_slack_webhook %} + slack_configs: + - api_url: "{{ alertmanager_slack_webhook }}" + channel: "#alerts" + send_resolved: true +{% endif %} + +inhibit_rules: + - source_match: + severity: critical + target_match: + severity: warning + equal: ["alertname", "instance"] diff --git a/roles/monitoring/templates/docker-compose.yml.j2 b/roles/monitoring/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..40e4186 --- /dev/null +++ b/roles/monitoring/templates/docker-compose.yml.j2 @@ -0,0 +1,86 @@ +services: + prometheus: + image: {{ prometheus_image }} + container_name: prometheus + restart: unless-stopped + command: + - --config.file=/etc/prometheus/prometheus.yml + - --storage.tsdb.path=/prometheus + - --storage.tsdb.retention.time={{ prometheus_retention }} + - --web.enable-lifecycle + volumes: + - {{ prometheus_data_dir }}/config/prometheus.yml:/etc/prometheus/prometheus.yml:ro + - {{ prometheus_data_dir }}/data:/prometheus + labels: + traefik.enable: "true" + traefik.http.routers.prometheus.rule: "Host(`{{ prometheus_host }}`)" + traefik.http.routers.prometheus.entrypoints: websecure + traefik.http.routers.prometheus.tls.certresolver: cloudflare + traefik.http.services.prometheus.loadbalancer.server.port: "9090" + networks: + - {{ docker_network_name }} + + grafana: + image: {{ grafana_image }} + container_name: grafana + restart: unless-stopped + environment: + GF_SECURITY_ADMIN_PASSWORD: {{ grafana_admin_password }} + GF_SERVER_ROOT_URL: "https://{{ grafana_host }}" + GF_SERVER_DOMAIN: "{{ grafana_host }}" + volumes: + - {{ grafana_data_dir }}:/var/lib/grafana + labels: + traefik.enable: "true" + traefik.http.routers.grafana.rule: "Host(`{{ grafana_host }}`)" + traefik.http.routers.grafana.entrypoints: websecure + traefik.http.routers.grafana.tls.certresolver: cloudflare + traefik.http.services.grafana.loadbalancer.server.port: "3000" + networks: + - {{ docker_network_name }} + + alertmanager: + image: {{ alertmanager_image }} + container_name: alertmanager + restart: unless-stopped + command: + - --config.file=/etc/alertmanager/alertmanager.yml + volumes: + - {{ alertmanager_data_dir }}/config/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro + labels: + traefik.enable: "true" + traefik.http.routers.alertmanager.rule: "Host(`{{ alertmanager_host }}`)" + traefik.http.routers.alertmanager.entrypoints: websecure + traefik.http.routers.alertmanager.tls.certresolver: cloudflare + traefik.http.services.alertmanager.loadbalancer.server.port: "9093" + networks: + - {{ docker_network_name }} + + node-exporter: + image: {{ node_exporter_image }} + container_name: node-exporter + restart: unless-stopped + command: + - --path.rootfs=/host + volumes: + - /:/host:ro,rslave + pid: host + networks: + - {{ docker_network_name }} + + cadvisor: + image: {{ cadvisor_image }} + container_name: cadvisor + restart: unless-stopped + privileged: true + volumes: + - /:/rootfs:ro + - /var/run:/var/run:ro + - /sys:/sys:ro + - /var/lib/docker:/var/lib/docker:ro + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true diff --git a/roles/monitoring/templates/prometheus.yml.j2 b/roles/monitoring/templates/prometheus.yml.j2 new file mode 100644 index 0000000..cc6f990 --- /dev/null +++ b/roles/monitoring/templates/prometheus.yml.j2 @@ -0,0 +1,37 @@ +global: + scrape_interval: {{ prometheus_scrape_interval }} + evaluation_interval: {{ prometheus_scrape_interval }} + +alerting: + alertmanagers: + - static_configs: + - targets: + - alertmanager:9093 + +scrape_configs: + - job_name: prometheus + static_configs: + - targets: ["localhost:9090"] + + - job_name: node-exporter + static_configs: + - targets: + - node-exporter:9100 + labels: + instance: peggy + - targets: + - 192.168.86.181:9100 + labels: + instance: bart + + - job_name: cadvisor + static_configs: + - targets: ["cadvisor:8080"] + + - job_name: traefik + static_configs: + - targets: ["traefik:8080"] + + - job_name: adguard + static_configs: + - targets: ["adguard-exporter:9617"] diff --git a/roles/openclaw/defaults/main.yml b/roles/openclaw/defaults/main.yml new file mode 100644 index 0000000..84cc328 --- /dev/null +++ b/roles/openclaw/defaults/main.yml @@ -0,0 +1,5 @@ +--- +openclaw_compose_dir: "{{ docker_compose_base }}/automation" +openclaw_data_dir: /data/openclaw +openclaw_image: "ghcr.io/openclaw/openclaw:latest" +openclaw_host: "openclaw.{{ domain }}" diff --git a/roles/openclaw/tasks/main.yml b/roles/openclaw/tasks/main.yml new file mode 100644 index 0000000..4a5e57f --- /dev/null +++ b/roles/openclaw/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- name: Create OpenClaw directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + loop: + - "{{ openclaw_data_dir }}" + tags: [openclaw, automation] + +- name: Deploy OpenClaw compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ openclaw_compose_dir }}/docker-compose.openclaw.yml" + owner: glyptic + group: docker + mode: "0644" + tags: [openclaw, automation] + +- name: Start OpenClaw + community.docker.docker_compose_v2: + project_src: "{{ openclaw_compose_dir }}" + files: + - docker-compose.openclaw.yml + state: present + tags: [openclaw, automation] diff --git a/roles/openclaw/templates/docker-compose.yml.j2 b/roles/openclaw/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..137334e --- /dev/null +++ b/roles/openclaw/templates/docker-compose.yml.j2 @@ -0,0 +1,22 @@ +services: + openclaw: + image: {{ openclaw_image }} + container_name: openclaw + restart: unless-stopped + environment: + TZ: {{ timezone }} + volumes: + - {{ openclaw_data_dir }}:/data + - /var/run/docker.sock:/var/run/docker.sock:ro + labels: + traefik.enable: "true" + traefik.http.routers.openclaw.rule: "Host(`{{ openclaw_host }}`)" + traefik.http.routers.openclaw.entrypoints: websecure + traefik.http.routers.openclaw.tls.certresolver: cloudflare + traefik.http.services.openclaw.loadbalancer.server.port: "3000" + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true diff --git a/roles/security/defaults/main.yml b/roles/security/defaults/main.yml new file mode 100644 index 0000000..4570705 --- /dev/null +++ b/roles/security/defaults/main.yml @@ -0,0 +1,5 @@ +--- +security_sysctl_hardening: true +security_audit_enabled: true +security_auto_updates: true +security_ssh_max_auth_tries: 3 diff --git a/roles/security/handlers/main.yml b/roles/security/handlers/main.yml new file mode 100644 index 0000000..211652f --- /dev/null +++ b/roles/security/handlers/main.yml @@ -0,0 +1,9 @@ +--- +- name: Reload sysctl + ansible.builtin.command: sysctl --system + changed_when: true + +- name: Restart auditd + ansible.builtin.systemd: + name: auditd + state: restarted diff --git a/roles/security/tasks/main.yml b/roles/security/tasks/main.yml new file mode 100644 index 0000000..e145534 --- /dev/null +++ b/roles/security/tasks/main.yml @@ -0,0 +1,73 @@ +--- +- name: Deploy hardened sysctl configuration + ansible.builtin.template: + src: sysctl.conf.j2 + dest: /etc/sysctl.d/99-hardening.conf + owner: root + group: root + mode: "0644" + notify: Reload sysctl + when: security_sysctl_hardening + tags: [security, sysctl] + +- name: Install auditd + ansible.builtin.apt: + name: + - auditd + - audispd-plugins + state: present + when: security_audit_enabled + tags: [security, audit] + +- name: Deploy audit rules + ansible.builtin.template: + src: audit.rules.j2 + dest: /etc/audit/rules.d/hardening.rules + owner: root + group: root + mode: "0640" + notify: Restart auditd + when: security_audit_enabled + tags: [security, audit] + +- name: Enable unattended upgrades + ansible.builtin.apt: + name: unattended-upgrades + state: present + when: security_auto_updates + tags: [security, updates] + +- name: Configure unattended upgrades + ansible.builtin.copy: + content: | + APT::Periodic::Update-Package-Lists "1"; + APT::Periodic::Unattended-Upgrade "1"; + APT::Periodic::AutocleanInterval "7"; + dest: /etc/apt/apt.conf.d/20auto-upgrades + owner: root + group: root + mode: "0644" + when: security_auto_updates + tags: [security, updates] + +- name: Disable core dumps + ansible.posix.sysctl: + name: fs.suid_dumpable + value: "0" + state: present + reload: true + tags: [security] + +- name: Set secure permissions on cron directories + ansible.builtin.file: + path: "{{ item }}" + owner: root + group: root + mode: "0700" + loop: + - /etc/cron.d + - /etc/cron.daily + - /etc/cron.hourly + - /etc/cron.weekly + - /etc/cron.monthly + tags: [security] diff --git a/roles/security/templates/audit.rules.j2 b/roles/security/templates/audit.rules.j2 new file mode 100644 index 0000000..002c2d5 --- /dev/null +++ b/roles/security/templates/audit.rules.j2 @@ -0,0 +1,24 @@ +# Audit rules - Managed by Ansible +# Monitor changes to authentication configuration +-w /etc/pam.d/ -p wa -k auth_changes +-w /etc/nsswitch.conf -p wa -k auth_changes +-w /etc/ssh/sshd_config -p wa -k sshd_config + +# Monitor user/group changes +-w /etc/passwd -p wa -k user_changes +-w /etc/group -p wa -k group_changes +-w /etc/shadow -p wa -k shadow_changes + +# Monitor sudo usage +-w /etc/sudoers -p wa -k sudoers +-w /etc/sudoers.d/ -p wa -k sudoers + +# Monitor Docker +-w /usr/bin/docker -p rwxa -k docker +-w /var/lib/docker -p wa -k docker +-w /etc/docker -p wa -k docker +-w /usr/lib/systemd/system/docker.service -p wa -k docker + +# Monitor cron +-w /etc/crontab -p wa -k cron +-w /etc/cron.d/ -p wa -k cron diff --git a/roles/security/templates/sysctl.conf.j2 b/roles/security/templates/sysctl.conf.j2 new file mode 100644 index 0000000..f356c99 --- /dev/null +++ b/roles/security/templates/sysctl.conf.j2 @@ -0,0 +1,24 @@ +# Network hardening - Managed by Ansible +net.ipv4.conf.all.send_redirects = 0 +net.ipv4.conf.default.send_redirects = 0 +net.ipv4.conf.all.accept_redirects = 0 +net.ipv4.conf.default.accept_redirects = 0 +net.ipv4.conf.all.accept_source_route = 0 +net.ipv4.conf.default.accept_source_route = 0 +net.ipv4.conf.all.log_martians = 1 +net.ipv4.conf.default.log_martians = 1 +net.ipv4.icmp_echo_ignore_broadcasts = 1 +net.ipv4.icmp_ignore_bogus_error_responses = 1 +net.ipv4.conf.all.rp_filter = 1 +net.ipv4.conf.default.rp_filter = 1 +net.ipv4.tcp_syncookies = 1 +net.ipv6.conf.all.accept_redirects = 0 +net.ipv6.conf.default.accept_redirects = 0 + +# Memory hardening +kernel.randomize_va_space = 2 +fs.protected_hardlinks = 1 +fs.protected_symlinks = 1 + +# Docker needs IP forwarding +net.ipv4.ip_forward = 1 diff --git a/roles/semaphore/defaults/main.yml b/roles/semaphore/defaults/main.yml new file mode 100644 index 0000000..cebec90 --- /dev/null +++ b/roles/semaphore/defaults/main.yml @@ -0,0 +1,6 @@ +--- +semaphore_compose_dir: "{{ docker_compose_base }}/automation" +semaphore_data_dir: /data/semaphore +semaphore_image: "semaphoreui/semaphore:latest" +semaphore_host: "semaphore.{{ domain }}" +semaphore_db_password: "{{ vault_semaphore_db_password }}" diff --git a/roles/semaphore/tasks/main.yml b/roles/semaphore/tasks/main.yml new file mode 100644 index 0000000..32f8367 --- /dev/null +++ b/roles/semaphore/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- name: Create Semaphore directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + loop: + - "{{ semaphore_data_dir }}" + tags: [semaphore, automation] + +- name: Deploy Semaphore compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ semaphore_compose_dir }}/docker-compose.semaphore.yml" + owner: glyptic + group: docker + mode: "0644" + tags: [semaphore, automation] + +- name: Start Semaphore + community.docker.docker_compose_v2: + project_src: "{{ semaphore_compose_dir }}" + files: + - docker-compose.semaphore.yml + state: present + tags: [semaphore, automation] diff --git a/roles/semaphore/templates/docker-compose.yml.j2 b/roles/semaphore/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..74a2316 --- /dev/null +++ b/roles/semaphore/templates/docker-compose.yml.j2 @@ -0,0 +1,26 @@ +services: + semaphore: + image: {{ semaphore_image }} + container_name: semaphore + restart: unless-stopped + environment: + SEMAPHORE_DB_DIALECT: bolt + SEMAPHORE_ADMIN_PASSWORD: "{{ vault_semaphore_admin_password | default('changeme') }}" + SEMAPHORE_ADMIN_NAME: admin + SEMAPHORE_ADMIN_EMAIL: admin@{{ domain }} + SEMAPHORE_ADMIN: admin + SEMAPHORE_ACCESS_KEY_ENCRYPTION: "{{ vault_semaphore_encryption_key | default('generate-a-key') }}" + volumes: + - {{ semaphore_data_dir }}:/tmp/semaphore + labels: + traefik.enable: "true" + traefik.http.routers.semaphore.rule: "Host(`{{ semaphore_host }}`)" + traefik.http.routers.semaphore.entrypoints: websecure + traefik.http.routers.semaphore.tls.certresolver: cloudflare + traefik.http.services.semaphore.loadbalancer.server.port: "3000" + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true diff --git a/roles/traefik/defaults/main.yml b/roles/traefik/defaults/main.yml new file mode 100644 index 0000000..d97ad23 --- /dev/null +++ b/roles/traefik/defaults/main.yml @@ -0,0 +1,10 @@ +--- +traefik_version: "v3.1" +traefik_image: "traefik:{{ traefik_version }}" +traefik_compose_dir: "{{ docker_compose_base }}/core-infrastructure" +traefik_data_dir: /data/traefik +traefik_acme_email: "{{ vault_acme_email }}" +cloudflare_api_token: "{{ vault_cloudflare_api_token }}" +traefik_log_level: INFO +traefik_dashboard_enabled: true +traefik_dashboard_host: "traefik.{{ domain }}" diff --git a/roles/traefik/handlers/main.yml b/roles/traefik/handlers/main.yml new file mode 100644 index 0000000..eaf4c70 --- /dev/null +++ b/roles/traefik/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Restart traefik stack + community.docker.docker_compose_v2: + project_src: "{{ traefik_compose_dir }}" + state: present + recreate: always diff --git a/roles/traefik/tasks/main.yml b/roles/traefik/tasks/main.yml new file mode 100644 index 0000000..c1fb585 --- /dev/null +++ b/roles/traefik/tasks/main.yml @@ -0,0 +1,41 @@ +--- +- name: Create Traefik directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + loop: + - "{{ traefik_compose_dir }}" + - "{{ traefik_data_dir }}" + - "{{ traefik_data_dir }}/acme" + - "{{ traefik_data_dir }}/dynamic" + tags: [traefik] + +- name: Ensure acme.json exists with correct permissions + ansible.builtin.file: + path: "{{ traefik_data_dir }}/acme/acme.json" + state: touch + owner: root + group: root + mode: "0600" + modification_time: preserve + access_time: preserve + tags: [traefik] + +- name: Deploy Traefik compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ traefik_compose_dir }}/docker-compose.yml" + owner: glyptic + group: docker + mode: "0644" + notify: Restart traefik stack + tags: [traefik] + +- name: Start Traefik stack + community.docker.docker_compose_v2: + project_src: "{{ traefik_compose_dir }}" + state: present + tags: [traefik] diff --git a/roles/traefik/templates/docker-compose.yml.j2 b/roles/traefik/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..4e90df8 --- /dev/null +++ b/roles/traefik/templates/docker-compose.yml.j2 @@ -0,0 +1,46 @@ +services: + traefik: + image: {{ traefik_image }} + container_name: traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + ports: + - "80:80" + - "443:443" + environment: + CF_DNS_API_TOKEN: {{ cloudflare_api_token }} + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - {{ traefik_data_dir }}/acme/acme.json:/acme.json + - {{ traefik_data_dir }}/dynamic:/dynamic + command: + - --api.dashboard={{ traefik_dashboard_enabled | lower }} + - --log.level={{ traefik_log_level }} + - --providers.docker=true + - --providers.docker.exposedbydefault=false + - --providers.docker.network={{ docker_network_name }} + - --providers.file.directory=/dynamic + - --providers.file.watch=true + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.websecure.address=:443 + - --certificatesresolvers.cloudflare.acme.dnschallenge=true + - --certificatesresolvers.cloudflare.acme.dnschallenge.provider=cloudflare + - --certificatesresolvers.cloudflare.acme.email={{ traefik_acme_email }} + - --certificatesresolvers.cloudflare.acme.storage=/acme.json + - --certificatesresolvers.cloudflare.acme.dnschallenge.resolvers=1.1.1.1:53,1.0.0.1:53 + labels: + traefik.enable: "true" + traefik.http.routers.traefik.rule: "Host(`{{ traefik_dashboard_host }}`)" + traefik.http.routers.traefik.entrypoints: websecure + traefik.http.routers.traefik.tls.certresolver: cloudflare + traefik.http.routers.traefik.tls.domains[0].main: "{{ domain }}" + traefik.http.routers.traefik.tls.domains[0].sans: "*.{{ domain }}" + traefik.http.routers.traefik.service: api@internal + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true diff --git a/roles/vaultwarden/defaults/main.yml b/roles/vaultwarden/defaults/main.yml new file mode 100644 index 0000000..cf11ba0 --- /dev/null +++ b/roles/vaultwarden/defaults/main.yml @@ -0,0 +1,7 @@ +--- +vaultwarden_image: "vaultwarden/server:latest" +vaultwarden_compose_dir: "{{ docker_compose_base }}/core-infrastructure" +vaultwarden_data_dir: /data/vaultwarden +vaultwarden_host: "vault.{{ domain }}" +vaultwarden_admin_token: "{{ vault_vaultwarden_admin_token }}" +vaultwarden_signups_allowed: false diff --git a/roles/vaultwarden/tasks/main.yml b/roles/vaultwarden/tasks/main.yml new file mode 100644 index 0000000..cf31587 --- /dev/null +++ b/roles/vaultwarden/tasks/main.yml @@ -0,0 +1,26 @@ +--- +- name: Create Vaultwarden data directory + ansible.builtin.file: + path: "{{ vaultwarden_data_dir }}" + state: directory + owner: glyptic + group: docker + mode: "0755" + tags: [vaultwarden] + +- name: Deploy Vaultwarden compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ vaultwarden_compose_dir }}/docker-compose.vaultwarden.yml" + owner: glyptic + group: docker + mode: "0644" + tags: [vaultwarden] + +- name: Start Vaultwarden + community.docker.docker_compose_v2: + project_src: "{{ vaultwarden_compose_dir }}" + files: + - docker-compose.vaultwarden.yml + state: present + tags: [vaultwarden] diff --git a/roles/vaultwarden/templates/docker-compose.yml.j2 b/roles/vaultwarden/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..2aae729 --- /dev/null +++ b/roles/vaultwarden/templates/docker-compose.yml.j2 @@ -0,0 +1,24 @@ +services: + vaultwarden: + image: {{ vaultwarden_image }} + container_name: vaultwarden + restart: unless-stopped + environment: + ADMIN_TOKEN: {{ vaultwarden_admin_token }} + SIGNUPS_ALLOWED: "{{ vaultwarden_signups_allowed | lower }}" + DOMAIN: "https://{{ vaultwarden_host }}" + WEBSOCKET_ENABLED: "true" + volumes: + - {{ vaultwarden_data_dir }}:/data + labels: + traefik.enable: "true" + traefik.http.routers.vaultwarden.rule: "Host(`{{ vaultwarden_host }}`)" + traefik.http.routers.vaultwarden.entrypoints: websecure + traefik.http.routers.vaultwarden.tls.certresolver: cloudflare + traefik.http.services.vaultwarden.loadbalancer.server.port: "80" + networks: + - {{ docker_network_name }} + +networks: + {{ docker_network_name }}: + external: true