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
This commit is contained in:
+12
@@ -0,0 +1,12 @@
|
||||
*.retry
|
||||
.vault_pass
|
||||
vault_password_file
|
||||
*.pyc
|
||||
__pycache__/
|
||||
.fact_cache/
|
||||
*.log
|
||||
*.bak
|
||||
*.swp
|
||||
*~
|
||||
.env
|
||||
secrets/
|
||||
@@ -1,3 +1,80 @@
|
||||
# ansible-homelab
|
||||
# Ansible Homelab - hobbs.farm
|
||||
|
||||
Ansible automation for hobbs.farm infrastructure
|
||||
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
|
||||
```
|
||||
|
||||
+25
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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('') }}"
|
||||
@@ -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
|
||||
@@ -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."
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: Deploy communication stack
|
||||
hosts: peggy
|
||||
roles:
|
||||
- role: docker
|
||||
tags: [docker]
|
||||
- role: matrix
|
||||
tags: [communication, matrix]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: Deploy media stack
|
||||
hosts: peggy
|
||||
roles:
|
||||
- role: docker
|
||||
tags: [docker]
|
||||
- role: media
|
||||
tags: [media, jellyfin, sonarr, radarr]
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: Deploy monitoring stack
|
||||
hosts: peggy
|
||||
roles:
|
||||
- role: docker
|
||||
tags: [docker]
|
||||
- role: monitoring
|
||||
tags: [monitoring, prometheus, grafana, alertmanager]
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: Apply security hardening
|
||||
hosts: docker_hosts
|
||||
roles:
|
||||
- role: common
|
||||
tags: [common]
|
||||
- role: security
|
||||
tags: [security]
|
||||
@@ -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
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
- name: Deploy smart home stack
|
||||
hosts: peggy
|
||||
roles:
|
||||
- role: docker
|
||||
tags: [docker]
|
||||
- role: home_assistant
|
||||
tags: [smarthome, homeassistant, esphome]
|
||||
@@ -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"
|
||||
@@ -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 }}"
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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: []
|
||||
@@ -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]
|
||||
@@ -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) ==="
|
||||
@@ -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"
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: Restart sshd
|
||||
ansible.builtin.systemd:
|
||||
name: sshd
|
||||
state: restarted
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
- name: Restart docker
|
||||
ansible.builtin.systemd:
|
||||
name: docker
|
||||
state: restarted
|
||||
@@ -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]
|
||||
@@ -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 }}"
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Restart monitoring stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ monitoring_compose_dir }}"
|
||||
state: present
|
||||
recreate: always
|
||||
@@ -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]
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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"]
|
||||
@@ -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 }}"
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
security_sysctl_hardening: true
|
||||
security_audit_enabled: true
|
||||
security_auto_updates: true
|
||||
security_ssh_max_auth_tries: 3
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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 }}"
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
- name: Restart traefik stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ traefik_compose_dir }}"
|
||||
state: present
|
||||
recreate: always
|
||||
@@ -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]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user