From b81f7e15b2b66912486fc35c2171f2012bdbac54 Mon Sep 17 00:00:00 2001 From: Sergei Poljanski Date: Sat, 1 Nov 2025 17:15:42 +0200 Subject: [PATCH] ansible --- ansible/README.md | 215 +++++++++++++++++ ansible/ansible.cfg | 20 ++ ansible/group_vars/all.yaml | 20 ++ ansible/inventory/host_vars/asxpio-ops.yaml | 47 ++++ .../inventory/host_vars/asxpio-services.yaml | 4 + ansible/inventory/hosts.yaml | 45 ++++ ansible/playbooks/backup-forgejo.yaml | 8 + ansible/playbooks/deploy-forgejo-runner.yaml | 6 + ansible/playbooks/deploy-forgejo.yaml | 6 + ansible/playbooks/deploy-traefik.yaml | 6 + ansible/playbooks/restore-forgejo.yaml | 6 + ansible/playbooks/setup-servers.yaml | 32 +++ ansible/playbooks/ssh-harden.yaml | 6 + ansible/playbooks/wireguard-client.yaml | 59 +++++ .../playbooks/wireguard-configs/.gitignore | 2 + ansible/playbooks/wireguard.yaml | 6 + ansible/roles/common/tasks/main.yaml | 16 ++ ansible/roles/forgejo/handlers/main.yaml | 5 + ansible/roles/forgejo/tasks/main.yaml | 39 ++++ ansible/roles/forgejo/templates/app.ini.j2 | 83 +++++++ .../forgejo/templates/docker-compose.yml.j2 | 31 +++ ansible/roles/forgejo_backup/tasks/main.yaml | 79 +++++++ ansible/roles/forgejo_restore/tasks/main.yaml | 129 +++++++++++ .../roles/forgejo_runner/handlers/main.yaml | 5 + ansible/roles/forgejo_runner/tasks/main.yaml | 45 ++++ .../forgejo_runner/templates/config.yaml.j2 | 43 ++++ .../templates/docker-compose.yml.j2 | 11 + .../roles/ssh_hardening/defaults/main.yaml | 60 +++++ .../roles/ssh_hardening/handlers/main.yaml | 6 + ansible/roles/ssh_hardening/tasks/main.yaml | 80 +++++++ .../templates/99-ssh-hardening.conf.j2 | 32 +++ ansible/roles/traefik/handlers/main.yaml | 5 + ansible/roles/traefik/tasks/main.yaml | 53 +++++ .../traefik/templates/docker-compose.yml.j2 | 26 +++ .../roles/traefik/templates/dynamic.yml.j2 | 17 ++ .../roles/traefik/templates/traefik.yml.j2 | 40 ++++ ansible/roles/wireguard/defaults/main.yaml | 39 ++++ ansible/roles/wireguard/handlers/main.yaml | 21 ++ ansible/roles/wireguard/tasks/main.yaml | 219 ++++++++++++++++++ .../roles/wireguard/templates/client.conf.j2 | 21 ++ ansible/roles/wireguard/templates/wg0.conf.j2 | 39 ++++ 41 files changed, 1632 insertions(+) create mode 100644 ansible/README.md create mode 100644 ansible/ansible.cfg create mode 100644 ansible/group_vars/all.yaml create mode 100644 ansible/inventory/host_vars/asxpio-ops.yaml create mode 100644 ansible/inventory/host_vars/asxpio-services.yaml create mode 100644 ansible/inventory/hosts.yaml create mode 100644 ansible/playbooks/backup-forgejo.yaml create mode 100644 ansible/playbooks/deploy-forgejo-runner.yaml create mode 100644 ansible/playbooks/deploy-forgejo.yaml create mode 100644 ansible/playbooks/deploy-traefik.yaml create mode 100644 ansible/playbooks/restore-forgejo.yaml create mode 100644 ansible/playbooks/setup-servers.yaml create mode 100644 ansible/playbooks/ssh-harden.yaml create mode 100644 ansible/playbooks/wireguard-client.yaml create mode 100644 ansible/playbooks/wireguard-configs/.gitignore create mode 100644 ansible/playbooks/wireguard.yaml create mode 100644 ansible/roles/common/tasks/main.yaml create mode 100644 ansible/roles/forgejo/handlers/main.yaml create mode 100644 ansible/roles/forgejo/tasks/main.yaml create mode 100644 ansible/roles/forgejo/templates/app.ini.j2 create mode 100644 ansible/roles/forgejo/templates/docker-compose.yml.j2 create mode 100644 ansible/roles/forgejo_backup/tasks/main.yaml create mode 100644 ansible/roles/forgejo_restore/tasks/main.yaml create mode 100644 ansible/roles/forgejo_runner/handlers/main.yaml create mode 100644 ansible/roles/forgejo_runner/tasks/main.yaml create mode 100644 ansible/roles/forgejo_runner/templates/config.yaml.j2 create mode 100644 ansible/roles/forgejo_runner/templates/docker-compose.yml.j2 create mode 100644 ansible/roles/ssh_hardening/defaults/main.yaml create mode 100644 ansible/roles/ssh_hardening/handlers/main.yaml create mode 100644 ansible/roles/ssh_hardening/tasks/main.yaml create mode 100644 ansible/roles/ssh_hardening/templates/99-ssh-hardening.conf.j2 create mode 100644 ansible/roles/traefik/handlers/main.yaml create mode 100644 ansible/roles/traefik/tasks/main.yaml create mode 100644 ansible/roles/traefik/templates/docker-compose.yml.j2 create mode 100644 ansible/roles/traefik/templates/dynamic.yml.j2 create mode 100644 ansible/roles/traefik/templates/traefik.yml.j2 create mode 100644 ansible/roles/wireguard/defaults/main.yaml create mode 100644 ansible/roles/wireguard/handlers/main.yaml create mode 100644 ansible/roles/wireguard/tasks/main.yaml create mode 100644 ansible/roles/wireguard/templates/client.conf.j2 create mode 100644 ansible/roles/wireguard/templates/wg0.conf.j2 diff --git a/ansible/README.md b/ansible/README.md new file mode 100644 index 0000000..b11d0cb --- /dev/null +++ b/ansible/README.md @@ -0,0 +1,215 @@ +# ASXP.IO Ansible + +Post-cloud-init configuration for asxpio servers. + +## Playbooks + +### setup-servers.yaml +Post-deployment verification (cloud-init handles base setup). + +```bash +ansible-playbook playbooks/setup-servers.yaml +``` + +### wireguard.yaml +Deploy WireGuard VPN server on asxpio-ops (10.20.0.10). + +```bash +# Initial deployment +ansible-playbook playbooks/wireguard.yaml + +# Add new clients to inventory/host_vars/asxpio-ops.yaml, then re-run +ansible-playbook playbooks/wireguard.yaml +``` + +Client configs fetched to `./wireguard-configs/asxpio-ops/` + +### wireguard-client.yaml +Deploy WireGuard client configs to servers. + +```bash +ansible-playbook playbooks/wireguard-client.yaml +``` + +Deploys asxpio-services.conf (10.20.0.20) to asxpio-services server. + +### ssh-harden.yaml +Harden SSH configuration with network restrictions and strong cryptography. + +```bash +ansible-playbook playbooks/ssh-harden.yaml +``` + +**Security measures:** +- Shell access (asxpi/deploy/root): Only from VPC (10.10.0.0/20) or WireGuard (10.20.0.0/24) +- Forgejo git user: Public access on asxpio-ops for git operations (ssh://forgejo@git.asxp.io/...) +- Strong crypto: ChaCha20-Poly1305, AES-GCM, RSA-SHA2, Ed25519 +- iptables: SSH blocked on asxpio-services except from trusted networks +- Enhanced sshd_config: MaxAuthTries 3, MaxSessions 5, no X11 forwarding + +**Access after hardening:** +- Admin access: Connect via WireGuard VPN first, then SSH +- Git operations: Public access to asxpio-ops (Forgejo handles authentication) +- Internal VPC: Servers can SSH to each other via 10.10.0.0/20 + +### deploy-traefik.yaml +Deploy Traefik reverse proxy with Let's Encrypt on asxpio-ops. + +```bash +ansible-playbook playbooks/deploy-traefik.yaml +``` + +**Features:** +- Automatic HTTPS with Let's Encrypt (HTTP challenge) +- Dashboard accessible only via WireGuard (http://10.20.0.10:8080) +- Shared Docker network for service discovery +- Certificates stored in `/opt/traefik/letsencrypt/` +- Logs in `/opt/traefik/logs/` + +### deploy-forgejo.yaml +Deploy fresh Forgejo Git server on asxpio-ops (no data migration). + +```bash +ansible-playbook playbooks/deploy-forgejo.yaml +``` + +**Configuration:** +- Forgejo 12.x with SQLite database +- HTTP: port 3000 (internal), exposed via Traefik at git.asxp.io +- SSH: port 2222 (external) → 22 (container) +- Actions enabled with 365-day artifact retention +- LFS enabled, OAuth2 enabled +- Data stored in `/opt/forgejo/data/` + +**Important:** Configure secrets in `inventory/host_vars/asxpio-ops.yaml`: +```yaml +forgejo_secret_key: "YOUR-SECRET-KEY" +forgejo_internal_token: "YOUR-INTERNAL-TOKEN" +forgejo_jwt_secret: "YOUR-JWT-SECRET" +forgejo_lfs_jwt_secret: "YOUR-LFS-JWT-SECRET" +letsencrypt_email: "admin@asxp.io" +``` + +### deploy-forgejo-runner.yaml +Deploy Forgejo Runner on asxpio-services for CI/CD. + +```bash +ansible-playbook playbooks/deploy-forgejo-runner.yaml +``` + +**Configuration:** +- Forgejo Runner 11.x with Docker execution +- 14 pre-configured labels (ubuntu, debian, arch, node, python, nginx, alpine) +- Connects to Forgejo via VPC (http://10.10.0.10:3000) +- Requires manual registration after deployment + +**Post-deployment registration:** +```bash +# 1. Get registration token from Forgejo: +# Site Administration > Actions > Runners > Generate Token + +# 2. Register the runner: +ssh asxpio-services +docker exec -it forgejo-runner forgejo-runner register --no-interactive \ + --token YOUR_REGISTRATION_TOKEN \ + --name runner-asxpio-services \ + --instance http://10.10.0.10:3000 + +# 3. Restart runner: +cd /opt/forgejo-runner && docker compose restart +``` + +### backup-forgejo.yaml +Backup Forgejo from old server (10.0.0.1). + +```bash +ansible-playbook playbooks/backup-forgejo.yaml +``` + +**What it does:** +1. Stops Forgejo service temporarily +2. Creates dump: `forgejo dump -c /etc/forgejo/app.ini` +3. Creates additional archive of: + - Database: `/srv/gitea/gitea.db` + - Repositories: `/srv/gitea/data/` + - SSH keys: `/var/lib/forgejo/.ssh/` + - Runner registration: `/var/lib/forgejo-runner/.runner` +4. Fetches backups to local machine: `/tmp/forgejo-backups/` +5. Restarts Forgejo service + +**Backup includes:** Database, repositories (~513MB), configuration, SSH keys, runner registration. + +### restore-forgejo.yaml +Restore Forgejo backup to new server (10.20.0.10 via WireGuard). + +```bash +ansible-playbook playbooks/restore-forgejo.yaml +``` + +**Prerequisites:** +- Traefik deployed and running +- Forgejo deployed (fresh instance) +- Backup files in `/tmp/forgejo-backups/` + +**What it does:** +1. Stops Forgejo container +2. Extracts dump backup to `/opt/forgejo/` +3. Restores database, repositories, SSH keys +4. Sets correct ownership (UID 1000) +5. Starts Forgejo container +6. Waits for health check + +**Post-restore verification:** +```bash +# Test web interface +curl -I https://git.asxp.io + +# Test git clone +git clone https://git.asxp.io/asxpi/infrastructure.git + +# Test SSH access +git clone git@git.asxp.io:asxpi/infrastructure.git +``` + +## Deployment Order + +### Phase 1: Infrastructure (Prerequisites) +```bash +# 1. Deploy VPN +ansible-playbook playbooks/wireguard.yaml +ansible-playbook playbooks/wireguard-client.yaml + +# 2. Harden SSH +ansible-playbook playbooks/ssh-harden.yaml + +# 3. Deploy reverse proxy +ansible-playbook playbooks/deploy-traefik.yaml +``` + +### Phase 2: Forgejo Fresh Deployment +```bash +# 4. Deploy Forgejo (empty instance) +ansible-playbook playbooks/deploy-forgejo.yaml + +# 5. Deploy runner +ansible-playbook playbooks/deploy-forgejo-runner.yaml + +# 6. Register runner (manual step - see instructions above) +``` + +### Phase 3: Data Migration (When Ready) +```bash +# 7. Backup from old server +ansible-playbook playbooks/backup-forgejo.yaml + +# 8. Restore to new server +ansible-playbook playbooks/restore-forgejo.yaml + +# 9. Update DNS: git.asxp.io → 65.109.14.216 +# 10. Re-register runner with migrated instance +``` + +## Inventory + +- **asxpio-ops** (65.109.14.216) - Traefik, Forgejo, monitoring, WireGuard server +- **asxpio-services** (95.217.19.240) - Forgejo Runner, Immich, game servers, XMPP diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000..5d3de2e --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,20 @@ +[defaults] +interpreter_python = auto_silent +gathering = smart +pipelining = True +retry_files_enabled = False +inventory = inventory/hosts.yaml +roles_path = roles +host_key_checking = False +module_name = shell +display_skipped_hosts = no +timeout=30 +remote_user = root + +[connection] +pipelining=True + +[ssh_connection] +pipelining=True +control_path = /tmp/%%r@%%h:%%p +ssh_args = -o ControlPersist=10m -o PreferredAuthentications=publickey -o ControlMaster=no -o ForwardAgent=yes diff --git a/ansible/group_vars/all.yaml b/ansible/group_vars/all.yaml new file mode 100644 index 0000000..594b2e8 --- /dev/null +++ b/ansible/group_vars/all.yaml @@ -0,0 +1,20 @@ +--- +# Shared configuration for all servers + +# Docker network for Traefik +traefik_network: traefik + +# Forgejo configuration +forgejo_version: "12" +forgejo_domain: git.asxp.io +forgejo_http_port: 3000 +forgejo_ssh_port: 2222 # External port (container uses 22) + +# Forgejo Runner configuration +forgejo_runner_version: "11" +forgejo_runner_capacity: 1 +forgejo_runner_timeout: 3h + +# Backup configuration +backup_dest_dir: /tmp/forgejo-backups +backup_old_server: 10.0.0.1 diff --git a/ansible/inventory/host_vars/asxpio-ops.yaml b/ansible/inventory/host_vars/asxpio-ops.yaml new file mode 100644 index 0000000..40d43b4 --- /dev/null +++ b/ansible/inventory/host_vars/asxpio-ops.yaml @@ -0,0 +1,47 @@ +--- +# WireGuard VPN Configuration for asxpio-ops +wireguard_port: 60101 +wireguard_interface: wg0 +wireguard_ipv4: 10.20.0.10 +wireguard_ipv6: fd69:69:69::1 +wireguard_ipv4_network: 10.20.0.0/24 +wireguard_ipv6_network: fd69:69:69::/64 + +# Enable UFW firewall rules (set to false to use iptables PostUp/PostDown) +wireguard_enable_ufw: false + +# WireGuard clients +wireguard_clients: + - name: asxpio-services + ipv4: 10.20.0.20 + ipv6: fd69:69:69::20 + preshared_key: true + persistent_keepalive: 25 + - name: laptop + ipv4: 10.20.0.2 + ipv6: fd69:69:69::2 + preshared_key: true + persistent_keepalive: 25 + - name: phone + ipv4: 10.20.0.3 + ipv6: fd69:69:69::3 + preshared_key: true + persistent_keepalive: 25 + - name: pixel9 + ipv4: 10.20.0.4 + ipv6: fd69:69:69::4 + preshared_key: true + persistent_keepalive: 25 + +# Optional: DNS servers for clients +wireguard_dns_servers: + - 1.1.1.1 + - 1.0.0.1 + +# Optional: Additional routes for split tunneling +# wireguard_additional_routes: +# - 192.168.1.0/24 + +# SSH Hardening Configuration +# Allow public SSH access for Forgejo git operations +ssh_allow_public_git: true diff --git a/ansible/inventory/host_vars/asxpio-services.yaml b/ansible/inventory/host_vars/asxpio-services.yaml new file mode 100644 index 0000000..0b89036 --- /dev/null +++ b/ansible/inventory/host_vars/asxpio-services.yaml @@ -0,0 +1,4 @@ +--- +# SSH Hardening Configuration +# Restrict SSH to VPC and WireGuard networks only +ssh_allow_public_git: false diff --git a/ansible/inventory/hosts.yaml b/ansible/inventory/hosts.yaml new file mode 100644 index 0000000..4181d42 --- /dev/null +++ b/ansible/inventory/hosts.yaml @@ -0,0 +1,45 @@ +--- +# Ansible inventory for asxpio infrastructure +# Update IP addresses after running terraform apply + +all: + vars: + ansible_user: root + ansible_python_interpreter: /usr/bin/python3 + + children: + asxpio: + vars: + # Common variables for all asxpio servers + timezone: UTC + locale: en_US.UTF-8 + + children: + ops_servers: + hosts: + asxpio-ops: + ansible_host: 10.20.0.10 #65.109.14.216 + server_role: ops + server_type: cx22 + services: + - traefik + - forgejo + - grafana + - prometheus + - loki + - static_sites + + services_servers: + hosts: + asxpio-services: + ansible_host: 10.20.0.20 #95.217.15.42 + server_role: services + server_type: cx43 + services: + - forgejo_runner + - immich + - minecraft + - valheim + - factorio + - prosody + - coturn diff --git a/ansible/playbooks/backup-forgejo.yaml b/ansible/playbooks/backup-forgejo.yaml new file mode 100644 index 0000000..1a53604 --- /dev/null +++ b/ansible/playbooks/backup-forgejo.yaml @@ -0,0 +1,8 @@ +--- +- name: Backup Forgejo from old server + hosts: "{{ backup_old_server }}" + become: true + vars: + ansible_host: "{{ backup_old_server }}" + roles: + - forgejo_backup diff --git a/ansible/playbooks/deploy-forgejo-runner.yaml b/ansible/playbooks/deploy-forgejo-runner.yaml new file mode 100644 index 0000000..754e857 --- /dev/null +++ b/ansible/playbooks/deploy-forgejo-runner.yaml @@ -0,0 +1,6 @@ +--- +- name: Deploy Forgejo Runner + hosts: asxpio-services + become: true + roles: + - forgejo_runner diff --git a/ansible/playbooks/deploy-forgejo.yaml b/ansible/playbooks/deploy-forgejo.yaml new file mode 100644 index 0000000..3862545 --- /dev/null +++ b/ansible/playbooks/deploy-forgejo.yaml @@ -0,0 +1,6 @@ +--- +- name: Deploy Forgejo Git server + hosts: asxpio-ops + become: true + roles: + - forgejo diff --git a/ansible/playbooks/deploy-traefik.yaml b/ansible/playbooks/deploy-traefik.yaml new file mode 100644 index 0000000..75e5758 --- /dev/null +++ b/ansible/playbooks/deploy-traefik.yaml @@ -0,0 +1,6 @@ +--- +- name: Deploy Traefik reverse proxy + hosts: asxpio-ops + become: true + roles: + - traefik diff --git a/ansible/playbooks/restore-forgejo.yaml b/ansible/playbooks/restore-forgejo.yaml new file mode 100644 index 0000000..5f32859 --- /dev/null +++ b/ansible/playbooks/restore-forgejo.yaml @@ -0,0 +1,6 @@ +--- +- name: Restore Forgejo to new server + hosts: asxpio-ops + become: true + roles: + - forgejo_restore diff --git a/ansible/playbooks/setup-servers.yaml b/ansible/playbooks/setup-servers.yaml new file mode 100644 index 0000000..ef656b5 --- /dev/null +++ b/ansible/playbooks/setup-servers.yaml @@ -0,0 +1,32 @@ +--- +# Main playbook for setting up asxpio servers +# Usage: ansible-playbook playbooks/setup-servers.yml +# +# NOTE: Cloud-init handles base system setup: +# - System packages (fail2ban, curl, htop, ncdu, tree, etc.) +# - User management (asxpi + deploy users with sudo and docker access) +# - SSH hardening (key-only auth, prohibit root password) +# - Docker installation (docker-ce, docker-ce-cli, containerd.io, docker-compose-plugin) +# - Bash prompt styling for asxpi user +# - Docker image pruning cron job +# - Directory structure creation +# +# This playbook handles post-cloud-init verification and additional configuration. +# Application deployment (Traefik, Forgejo, monitoring, etc.) +# will be handled via Forgejo Actions pipelines + +- name: Setup asxpio infrastructure servers + hosts: asxpio + become: yes + gather_facts: yes + + roles: + - common + + post_tasks: + - name: Display server info + debug: + msg: | + Server {{ inventory_hostname }} post-cloud-init setup complete! + Role: {{ server_role }} + Next: Deploy services via Forgejo Actions pipelines diff --git a/ansible/playbooks/ssh-harden.yaml b/ansible/playbooks/ssh-harden.yaml new file mode 100644 index 0000000..72b2f26 --- /dev/null +++ b/ansible/playbooks/ssh-harden.yaml @@ -0,0 +1,6 @@ +--- +- name: Harden SSH configuration + hosts: asxpio + become: true + roles: + - ssh_hardening diff --git a/ansible/playbooks/wireguard-client.yaml b/ansible/playbooks/wireguard-client.yaml new file mode 100644 index 0000000..9892d9d --- /dev/null +++ b/ansible/playbooks/wireguard-client.yaml @@ -0,0 +1,59 @@ +--- +- name: Deploy WireGuard client configuration + hosts: services_servers + become: true + vars: + # Map each server to its client config name + wireguard_client_configs: + asxpio-services: asxpio-services + tasks: + - name: Get client config name for this host + ansible.builtin.set_fact: + client_config_name: "{{ wireguard_client_configs[inventory_hostname] }}" + when: inventory_hostname in wireguard_client_configs + + - name: Install WireGuard + ansible.builtin.apt: + name: + - wireguard + - wireguard-tools + state: present + update_cache: true + + - name: Create WireGuard config directory + ansible.builtin.file: + path: /etc/wireguard + state: directory + mode: '0700' + owner: root + group: root + + - name: Copy WireGuard client configuration + ansible.builtin.copy: + src: "../playbooks/wireguard-configs/asxpio-ops/{{ client_config_name }}.conf" + dest: "/etc/wireguard/{{ client_config_name }}.conf" + mode: '0600' + owner: root + group: root + when: client_config_name is defined + notify: restart wireguard client + + - name: Enable WireGuard interface + ansible.builtin.systemd: + name: "wg-quick@{{ client_config_name }}" + enabled: true + when: client_config_name is defined + + - name: Start WireGuard interface + ansible.builtin.systemd: + name: "wg-quick@{{ client_config_name }}" + state: started + when: client_config_name is defined + + handlers: + - name: restart wireguard client + ansible.builtin.shell: | + wg-quick down {{ client_config_name }} || true + wg-quick up {{ client_config_name }} + args: + executable: /bin/bash diff --git a/ansible/playbooks/wireguard-configs/.gitignore b/ansible/playbooks/wireguard-configs/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/ansible/playbooks/wireguard-configs/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/ansible/playbooks/wireguard.yaml b/ansible/playbooks/wireguard.yaml new file mode 100644 index 0000000..2461f1e --- /dev/null +++ b/ansible/playbooks/wireguard.yaml @@ -0,0 +1,6 @@ +--- +- name: Setup WireGuard VPN + hosts: ops_servers + become: true + roles: + - wireguard diff --git a/ansible/roles/common/tasks/main.yaml b/ansible/roles/common/tasks/main.yaml new file mode 100644 index 0000000..450e1bb --- /dev/null +++ b/ansible/roles/common/tasks/main.yaml @@ -0,0 +1,16 @@ +# Cloud-init already handles: +# - Package installation (fail2ban, curl, git, htop, ncdu, tree, tmux, etc.) +# - fail2ban service enabled and started +# - User creation (asxpi with sudo + docker, deploy with sudo + docker) +# - SSH hardening (key-only auth, prohibit root password) +# - Docker installation and configuration +# - Docker image pruning cron job (weekly at 3am Monday) +# - Directory structure creation +# +# This role is now for post-cloud-init configuration only + +- name: Verify cloud-init completed successfully + command: cloud-init status --wait + register: cloud_init_status + changed_when: false + failed_when: "'done' not in cloud_init_status.stdout" diff --git a/ansible/roles/forgejo/handlers/main.yaml b/ansible/roles/forgejo/handlers/main.yaml new file mode 100644 index 0000000..16a41e5 --- /dev/null +++ b/ansible/roles/forgejo/handlers/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Restart Forgejo + community.docker.docker_compose_v2: + project_src: /opt/forgejo + state: restarted diff --git a/ansible/roles/forgejo/tasks/main.yaml b/ansible/roles/forgejo/tasks/main.yaml new file mode 100644 index 0000000..8566b04 --- /dev/null +++ b/ansible/roles/forgejo/tasks/main.yaml @@ -0,0 +1,39 @@ +--- +- name: Create Forgejo directories + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /opt/forgejo + - /opt/forgejo/data + - /opt/forgejo/config + +- name: Deploy Forgejo app.ini configuration + template: + src: app.ini.j2 + dest: /opt/forgejo/config/app.ini + mode: '0644' + notify: Restart Forgejo + +- name: Deploy Forgejo docker-compose + template: + src: docker-compose.yml.j2 + dest: /opt/forgejo/docker-compose.yml + mode: '0644' + notify: Restart Forgejo + +- name: Start Forgejo + community.docker.docker_compose_v2: + project_src: /opt/forgejo + state: present + register: forgejo_output + +- name: Wait for Forgejo to be healthy + uri: + url: http://localhost:{{ forgejo_http_port }}/ + status_code: 200 + retries: 15 + delay: 5 + register: result + until: result.status == 200 diff --git a/ansible/roles/forgejo/templates/app.ini.j2 b/ansible/roles/forgejo/templates/app.ini.j2 new file mode 100644 index 0000000..9df93a7 --- /dev/null +++ b/ansible/roles/forgejo/templates/app.ini.j2 @@ -0,0 +1,83 @@ +APP_NAME = Gitea: Git with a cup of tea together with asxpi +RUN_MODE = prod + +[server] +PROTOCOL = http +DOMAIN = {{ forgejo_domain }} +ROOT_URL = https://{{ forgejo_domain }}/ +HTTP_PORT = 3000 +DISABLE_SSH = false +SSH_DOMAIN = {{ forgejo_domain }} +SSH_PORT = {{ forgejo_ssh_port }} +SSH_LISTEN_PORT = 22 +START_SSH_SERVER = true +LFS_START_SERVER = true +OFFLINE_MODE = false + +[database] +DB_TYPE = sqlite3 +PATH = /data/gitea/gitea.db +LOG_SQL = false + +[repository] +ROOT = /data/git/repositories +SCRIPT_TYPE = bash +DEFAULT_BRANCH = main +DEFAULT_PRIVATE = private +ENABLE_PUSH_CREATE_USER = true +ENABLE_PUSH_CREATE_ORG = false +DISABLED_REPO_UNITS = +DEFAULT_REPO_UNITS = repo.code,repo.releases,repo.issues,repo.pulls,repo.wiki,repo.projects,repo.packages,repo.actions + +[ui] +THEMES = forgejo-auto,forgejo-light,forgejo-dark,gitea-auto,gitea-light,gitea-dark +DEFAULT_THEME = forgejo-auto + +[security] +INSTALL_LOCK = true +SECRET_KEY = {{ forgejo_secret_key | default('CHANGE-ME-PLEASE-GENERATE-NEW-SECRET') }} +INTERNAL_TOKEN = {{ forgejo_internal_token | default('CHANGE-ME-PLEASE-GENERATE-NEW-TOKEN') }} +PASSWORD_HASH_ALGO = pbkdf2 + +[service] +DISABLE_REGISTRATION = false +REQUIRE_SIGNIN_VIEW = false +REGISTER_EMAIL_CONFIRM = false +ENABLE_NOTIFY_MAIL = false +DEFAULT_KEEP_EMAIL_PRIVATE = true +DEFAULT_ALLOW_CREATE_ORGANIZATION = true +DEFAULT_ENABLE_TIMETRACKING = true +NO_REPLY_ADDRESS = noreply.{{ forgejo_domain }} + +[oauth2] +ENABLED = true +JWT_SECRET = {{ forgejo_jwt_secret | default('CHANGE-ME-PLEASE-GENERATE-NEW-JWT-SECRET') }} + +[mailer] +ENABLED = false + +[session] +PROVIDER = file + +[log] +MODE = console +LEVEL = info +ROOT_PATH = /data/gitea/log + +[actions] +ENABLED = true +DEFAULT_ACTIONS_URL = https://github.com +ARTIFACT_RETENTION_DAYS = 365 + +[repository.upload] +ENABLED = true + +[repository.pull-request] +DEFAULT_MERGE_STYLE = merge + +[repository.signing] +DEFAULT_TRUST_MODEL = collaborator + +[lfs] +PATH = /data/git/lfs +LFS_JWT_SECRET = {{ forgejo_lfs_jwt_secret | default('CHANGE-ME-PLEASE-GENERATE-NEW-LFS-JWT-SECRET') }} diff --git a/ansible/roles/forgejo/templates/docker-compose.yml.j2 b/ansible/roles/forgejo/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..14128e6 --- /dev/null +++ b/ansible/roles/forgejo/templates/docker-compose.yml.j2 @@ -0,0 +1,31 @@ +services: + forgejo: + image: codeberg.org/forgejo/forgejo:{{ forgejo_version }} + container_name: forgejo + restart: unless-stopped + environment: + - USER_UID=1000 + - USER_GID=1000 + - FORGEJO__database__DB_TYPE=sqlite3 + - FORGEJO__database__PATH=/data/gitea/gitea.db + volumes: + - ./data:/data + - ./config/app.ini:/data/gitea/conf/app.ini + - /etc/timezone:/etc/timezone:ro + - /etc/localtime:/etc/localtime:ro + ports: + - "{{ forgejo_http_port }}:3000" + - "{{ forgejo_ssh_port }}:22" + networks: + - {{ traefik_network }} + labels: + - "traefik.enable=true" + - "traefik.http.routers.forgejo.rule=Host(`{{ forgejo_domain }}`)" + - "traefik.http.routers.forgejo.entrypoints=websecure" + - "traefik.http.routers.forgejo.tls=true" + - "traefik.http.routers.forgejo.tls.certresolver=letsencrypt" + - "traefik.http.services.forgejo.loadbalancer.server.port=3000" + +networks: + {{ traefik_network }}: + external: true diff --git a/ansible/roles/forgejo_backup/tasks/main.yaml b/ansible/roles/forgejo_backup/tasks/main.yaml new file mode 100644 index 0000000..18df7fa --- /dev/null +++ b/ansible/roles/forgejo_backup/tasks/main.yaml @@ -0,0 +1,79 @@ +--- +- name: Create local backup directory + file: + path: "{{ backup_dest_dir }}" + state: directory + mode: '0755' + delegate_to: localhost + become: false + +- name: Check if Forgejo service is running on old server + systemd: + name: forgejo + register: forgejo_service + +- name: Stop Forgejo service temporarily for backup + systemd: + name: forgejo + state: stopped + when: forgejo_service.status.ActiveState == "active" + +- name: Create Forgejo dump backup + command: > + /usr/bin/forgejo dump + -c /etc/forgejo/app.ini + -w /srv/gitea + --tempdir /tmp + args: + chdir: /srv/gitea + register: dump_output + +- name: Get dump filename + find: + paths: /srv/gitea + patterns: "gitea-dump-*.zip" + register: dump_file + +- name: Create additional backup archive + archive: + path: + - /srv/gitea/gitea.db + - /srv/gitea/data + - /var/lib/forgejo/.ssh + - /var/lib/forgejo-runner/.runner + dest: /tmp/forgejo-additional-{{ ansible_date_time.date }}.tar.gz + format: gz + ignore_errors: true + +- name: Fetch dump file to local machine + fetch: + src: "{{ dump_file.files[0].path }}" + dest: "{{ backup_dest_dir }}/" + flat: true + when: dump_file.files | length > 0 + +- name: Fetch additional backup to local machine + fetch: + src: /tmp/forgejo-additional-{{ ansible_date_time.date }}.tar.gz + dest: "{{ backup_dest_dir }}/" + flat: true + ignore_errors: true + +- name: Start Forgejo service + systemd: + name: forgejo + state: started + when: forgejo_service.status.ActiveState == "active" + +- name: Display backup information + debug: + msg: | + Forgejo backup completed successfully! + + Backup files saved to: {{ backup_dest_dir }}/ + - Forgejo dump: {{ dump_file.files[0].path | basename }} + - Additional data: forgejo-additional-{{ ansible_date_time.date }}.tar.gz + + Next steps: + 1. Verify backup integrity + 2. Run restore playbook: ansible-playbook playbooks/restore-forgejo.yaml diff --git a/ansible/roles/forgejo_restore/tasks/main.yaml b/ansible/roles/forgejo_restore/tasks/main.yaml new file mode 100644 index 0000000..7d3a348 --- /dev/null +++ b/ansible/roles/forgejo_restore/tasks/main.yaml @@ -0,0 +1,129 @@ +--- +- name: Stop Forgejo container + community.docker.docker_compose_v2: + project_src: /opt/forgejo + state: stopped + ignore_errors: true + +- name: Find backup dump file locally + find: + paths: "{{ backup_dest_dir }}" + patterns: "gitea-dump-*.zip" + delegate_to: localhost + become: false + register: local_dump_file + +- name: Find additional backup file locally + find: + paths: "{{ backup_dest_dir }}" + patterns: "forgejo-additional-*.tar.gz" + delegate_to: localhost + become: false + register: local_additional_file + +- name: Fail if no backup files found + fail: + msg: "No backup files found in {{ backup_dest_dir }}. Please run backup-forgejo.yaml first." + when: local_dump_file.files | length == 0 + +- name: Copy dump backup to server + copy: + src: "{{ local_dump_file.files[0].path }}" + dest: /tmp/forgejo-dump.zip + mode: '0644' + +- name: Copy additional backup to server + copy: + src: "{{ local_additional_file.files[0].path }}" + dest: /tmp/forgejo-additional.tar.gz + mode: '0644' + when: local_additional_file.files | length > 0 + +- name: Extract Forgejo dump + unarchive: + src: /tmp/forgejo-dump.zip + dest: /opt/forgejo/ + remote_src: true + creates: /opt/forgejo/app.ini + +- name: Extract additional backup + unarchive: + src: /tmp/forgejo-additional.tar.gz + dest: /tmp/forgejo-restore/ + remote_src: true + when: local_additional_file.files | length > 0 + +- name: Ensure data directory exists + file: + path: /opt/forgejo/data + state: directory + mode: '0755' + +- name: Copy database from dump to data directory + copy: + src: /opt/forgejo/data/gitea.db + dest: /opt/forgejo/data/gitea.db + remote_src: true + force: true + ignore_errors: true + +- name: Copy repository data from additional backup + synchronize: + src: /tmp/forgejo-restore/srv/gitea/data/ + dest: /opt/forgejo/data/ + delegate_to: "{{ inventory_hostname }}" + when: local_additional_file.files | length > 0 + ignore_errors: true + +- name: Copy SSH keys from additional backup + synchronize: + src: /tmp/forgejo-restore/var/lib/forgejo/.ssh/ + dest: /opt/forgejo/data/.ssh/ + delegate_to: "{{ inventory_hostname }}" + when: local_additional_file.files | length > 0 + ignore_errors: true + +- name: Set correct ownership for Forgejo data + file: + path: /opt/forgejo/data + owner: 1000 + group: 1000 + recurse: true + +- name: Start Forgejo container + community.docker.docker_compose_v2: + project_src: /opt/forgejo + state: present + +- name: Wait for Forgejo to be healthy + uri: + url: http://localhost:{{ forgejo_http_port }}/ + status_code: 200 + retries: 15 + delay: 5 + register: result + until: result.status == 200 + +- name: Clean up temporary files + file: + path: "{{ item }}" + state: absent + loop: + - /tmp/forgejo-dump.zip + - /tmp/forgejo-additional.tar.gz + - /tmp/forgejo-restore + +- name: Display restore information + debug: + msg: | + Forgejo restore completed successfully! + + Forgejo is now running at: + - Internal: http://localhost:{{ forgejo_http_port }} + - External: https://{{ forgejo_domain }} + + Next steps: + 1. Test git operations: git clone https://{{ forgejo_domain }}/user/repo.git + 2. Test SSH access: git clone git@{{ forgejo_domain }}:user/repo.git + 3. Test web interface at https://{{ forgejo_domain }} + 4. Register runners with the new instance diff --git a/ansible/roles/forgejo_runner/handlers/main.yaml b/ansible/roles/forgejo_runner/handlers/main.yaml new file mode 100644 index 0000000..4ece759 --- /dev/null +++ b/ansible/roles/forgejo_runner/handlers/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Restart Forgejo Runner + community.docker.docker_compose_v2: + project_src: /opt/forgejo-runner + state: restarted diff --git a/ansible/roles/forgejo_runner/tasks/main.yaml b/ansible/roles/forgejo_runner/tasks/main.yaml new file mode 100644 index 0000000..ba6ddf8 --- /dev/null +++ b/ansible/roles/forgejo_runner/tasks/main.yaml @@ -0,0 +1,45 @@ +--- +- name: Create Forgejo Runner directories + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /opt/forgejo-runner + - /opt/forgejo-runner/data + - /opt/forgejo-runner/config + +- name: Deploy Forgejo Runner configuration + template: + src: config.yaml.j2 + dest: /opt/forgejo-runner/config/config.yaml + mode: '0644' + notify: Restart Forgejo Runner + +- name: Deploy Forgejo Runner docker-compose + template: + src: docker-compose.yml.j2 + dest: /opt/forgejo-runner/docker-compose.yml + mode: '0644' + notify: Restart Forgejo Runner + +- name: Start Forgejo Runner + community.docker.docker_compose_v2: + project_src: /opt/forgejo-runner + state: present + register: runner_output + +- name: Display runner registration instructions + debug: + msg: | + Forgejo Runner deployed successfully! + + To register the runner with Forgejo: + 1. Log in to Forgejo at https://{{ forgejo_domain }} + 2. Go to Site Administration > Actions > Runners + 3. Generate a registration token + 4. Run: docker exec -it forgejo-runner forgejo-runner register --no-interactive \ + --token YOUR_TOKEN \ + --name runner-{{ inventory_hostname }} \ + --instance http://10.10.0.10:{{ forgejo_http_port }} + 5. Restart the runner: cd /opt/forgejo-runner && docker compose restart diff --git a/ansible/roles/forgejo_runner/templates/config.yaml.j2 b/ansible/roles/forgejo_runner/templates/config.yaml.j2 new file mode 100644 index 0000000..fdf99bd --- /dev/null +++ b/ansible/roles/forgejo_runner/templates/config.yaml.j2 @@ -0,0 +1,43 @@ +log: + level: info + job_level: info + +runner: + file: /data/.runner + capacity: {{ forgejo_runner_capacity }} + envs: {} + env_file: .env + timeout: {{ forgejo_runner_timeout }} + shutdown_timeout: {{ forgejo_runner_timeout }} + insecure: false + fetch_timeout: 5s + fetch_interval: 2s + report_interval: 1s + labels: + - "ubuntu-latest:docker://node:20-bookworm" + - "ubuntu-22.04:docker://node:20-bookworm" + - "ubuntu-24.04:docker://node:22-bookworm" + - "debian-latest:docker://debian:bookworm" + - "debian-12:docker://debian:bookworm" + - "debian-11:docker://debian:bullseye" + - "arch-latest:docker://archlinux:latest" + - "nginx-latest:docker://nginx:latest" + - "nginx-alpine:docker://nginx:alpine" + - "node-20:docker://node:20" + - "node-22:docker://node:22" + - "python-3.11:docker://python:3.11" + - "python-3.12:docker://python:3.12" + - "alpine-latest:docker://alpine:latest" + +cache: + enabled: true + port: 0 + dir: "" + +container: + network: "host" + enable_ipv6: false + privileged: false + docker_host: "-" + force_pull: false + force_rebuild: false diff --git a/ansible/roles/forgejo_runner/templates/docker-compose.yml.j2 b/ansible/roles/forgejo_runner/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..129dcb0 --- /dev/null +++ b/ansible/roles/forgejo_runner/templates/docker-compose.yml.j2 @@ -0,0 +1,11 @@ +services: + forgejo-runner: + image: code.forgejo.org/forgejo/runner:{{ forgejo_runner_version }} + container_name: forgejo-runner + restart: unless-stopped + volumes: + - ./data:/data + - ./config/config.yaml:/config.yaml + - /var/run/docker.sock:/var/run/docker.sock + environment: + - CONFIG_FILE=/config.yaml diff --git a/ansible/roles/ssh_hardening/defaults/main.yaml b/ansible/roles/ssh_hardening/defaults/main.yaml new file mode 100644 index 0000000..af6cd15 --- /dev/null +++ b/ansible/roles/ssh_hardening/defaults/main.yaml @@ -0,0 +1,60 @@ +--- +# SSH Hardening Role Default Variables + +# Networks allowed to access SSH (VPC and WireGuard) +ssh_allowed_networks: + - 10.20.0.0/24 # WireGuard VPN + - 10.10.0.0/20 # Internal VPC + +# Whether to allow public SSH access for git operations (Forgejo) +# Set to true on Forgejo host, false on other servers +ssh_allow_public_git: false + +# Shell users that require network restrictions +ssh_shell_users: + - asxpi + - deploy + - root + +# Git user for Forgejo (allowed from anywhere when ssh_allow_public_git is true) +ssh_git_user: forgejo + +# SSH hardening settings +ssh_max_auth_tries: 3 +ssh_max_sessions: 5 +ssh_client_alive_interval: 300 +ssh_client_alive_count_max: 2 + +# Strong cryptography settings +ssh_ciphers: + - chacha20-poly1305@openssh.com + - aes256-gcm@openssh.com + - aes128-gcm@openssh.com + - aes256-ctr + - aes192-ctr + - aes128-ctr + +ssh_macs: + - hmac-sha2-512-etm@openssh.com + - hmac-sha2-256-etm@openssh.com + - hmac-sha2-512 + - hmac-sha2-256 + +ssh_kex_algorithms: + - sntrup761x25519-sha512@openssh.com # Post-quantum hybrid (OpenSSH 9.0+) + - curve25519-sha256 + - curve25519-sha256@libssh.org + - diffie-hellman-group-exchange-sha256 + - diffie-hellman-group16-sha512 + - diffie-hellman-group18-sha512 + +# Public key algorithms (includes RSA-SHA2 for existing keys) +ssh_pubkey_accepted_algorithms: + - ssh-ed25519 + - ssh-ed25519-cert-v01@openssh.com + - sk-ssh-ed25519@openssh.com + - ecdsa-sha2-nistp256 + - ecdsa-sha2-nistp384 + - ecdsa-sha2-nistp521 + - rsa-sha2-512 + - rsa-sha2-256 diff --git a/ansible/roles/ssh_hardening/handlers/main.yaml b/ansible/roles/ssh_hardening/handlers/main.yaml new file mode 100644 index 0000000..c4bd317 --- /dev/null +++ b/ansible/roles/ssh_hardening/handlers/main.yaml @@ -0,0 +1,6 @@ +--- +- name: restart sshd + ansible.builtin.systemd: + name: sshd + state: restarted + enabled: true diff --git a/ansible/roles/ssh_hardening/tasks/main.yaml b/ansible/roles/ssh_hardening/tasks/main.yaml new file mode 100644 index 0000000..4fe758a --- /dev/null +++ b/ansible/roles/ssh_hardening/tasks/main.yaml @@ -0,0 +1,80 @@ +--- +- name: Install iptables and iptables-persistent + ansible.builtin.apt: + name: + - iptables + - iptables-persistent + - netfilter-persistent + state: present + update_cache: true + +- name: Deploy SSH hardening configuration + ansible.builtin.template: + src: 99-ssh-hardening.conf.j2 + dest: /etc/ssh/sshd_config.d/99-ssh-hardening.conf + mode: '0644' + owner: root + group: root + validate: 'sshd -t -f %s' + notify: restart sshd + +- name: Configure iptables for SSH restrictions (non-Forgejo servers) + block: + - name: Accept SSH from WireGuard network + ansible.builtin.iptables: + chain: INPUT + protocol: tcp + destination_port: '22' + source: 10.20.0.0/24 + jump: ACCEPT + comment: Allow SSH from WireGuard VPN + + - name: Accept SSH from VPC network + ansible.builtin.iptables: + chain: INPUT + protocol: tcp + destination_port: '22' + source: 10.10.0.0/20 + jump: ACCEPT + comment: Allow SSH from internal VPC + + - name: Accept established SSH connections + ansible.builtin.iptables: + chain: INPUT + protocol: tcp + destination_port: '22' + ctstate: ESTABLISHED,RELATED + jump: ACCEPT + comment: Allow established SSH connections + + - name: Drop all other SSH connections + ansible.builtin.iptables: + chain: INPUT + protocol: tcp + destination_port: '22' + jump: DROP + comment: Drop SSH from untrusted networks + + when: not ssh_allow_public_git + +- name: Save iptables rules + ansible.builtin.shell: | + iptables-save > /etc/iptables/rules.v4 + netfilter-persistent save + args: + executable: /bin/bash + changed_when: true + when: not ssh_allow_public_git + +- name: Display SSH access information + ansible.builtin.debug: + msg: | + SSH hardening applied to {{ inventory_hostname }}: + - Shell users ({{ ssh_shell_users | join(', ') }}): Only from {{ ssh_allowed_networks | join(', ') }} + {% if ssh_allow_public_git %} + - Git user ({{ ssh_git_user }}): Public access allowed for Forgejo + - No iptables restrictions (sshd AllowUsers controls access) + {% else %} + - iptables rules: SSH blocked except from trusted networks + {% endif %} + - Strong cryptography enforced (RSA-SHA2, Ed25519, ChaCha20, AES-GCM) diff --git a/ansible/roles/ssh_hardening/templates/99-ssh-hardening.conf.j2 b/ansible/roles/ssh_hardening/templates/99-ssh-hardening.conf.j2 new file mode 100644 index 0000000..36f8293 --- /dev/null +++ b/ansible/roles/ssh_hardening/templates/99-ssh-hardening.conf.j2 @@ -0,0 +1,32 @@ +# SSH Hardening Configuration +# Generated by Ansible on {{ ansible_date_time.iso8601 }} +# Server: {{ inventory_hostname }} + +# Connection settings +MaxAuthTries {{ ssh_max_auth_tries }} +MaxSessions {{ ssh_max_sessions }} +ClientAliveInterval {{ ssh_client_alive_interval }} +ClientAliveCountMax {{ ssh_client_alive_count_max }} + +# Security hardening +StrictModes yes +X11Forwarding no +PrintMotd no + +# Strong cryptography only +Ciphers {{ ssh_ciphers | join(',') }} +MACs {{ ssh_macs | join(',') }} +KexAlgorithms {{ ssh_kex_algorithms | join(',') }} +PubkeyAcceptedAlgorithms {{ ssh_pubkey_accepted_algorithms | join(',') }} + +# Access control - restrict shell users to trusted networks only +{% for user in ssh_shell_users %} +{% for network in ssh_allowed_networks %} +AllowUsers {{ user }}@{{ network }} +{% endfor %} +{% endfor %} + +{% if ssh_allow_public_git %} +# Allow Forgejo git user from anywhere for git operations +AllowUsers {{ ssh_git_user }} +{% endif %} diff --git a/ansible/roles/traefik/handlers/main.yaml b/ansible/roles/traefik/handlers/main.yaml new file mode 100644 index 0000000..3bebae5 --- /dev/null +++ b/ansible/roles/traefik/handlers/main.yaml @@ -0,0 +1,5 @@ +--- +- name: Restart Traefik + community.docker.docker_compose_v2: + project_src: /opt/traefik + state: restarted diff --git a/ansible/roles/traefik/tasks/main.yaml b/ansible/roles/traefik/tasks/main.yaml new file mode 100644 index 0000000..09de7f7 --- /dev/null +++ b/ansible/roles/traefik/tasks/main.yaml @@ -0,0 +1,53 @@ +--- +- name: Create Traefik directories + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: + - /opt/traefik + - /opt/traefik/config + - /opt/traefik/letsencrypt + - /opt/traefik/logs + +- name: Create Traefik Docker network + community.docker.docker_network: + name: "{{ traefik_network }}" + driver: bridge + state: present + +- name: Deploy Traefik static configuration + template: + src: traefik.yml.j2 + dest: /opt/traefik/traefik.yml + mode: '0644' + notify: Restart Traefik + +- name: Deploy Traefik dynamic configuration + template: + src: dynamic.yml.j2 + dest: /opt/traefik/config/dynamic.yml + mode: '0644' + notify: Restart Traefik + +- name: Deploy Traefik docker-compose + template: + src: docker-compose.yml.j2 + dest: /opt/traefik/docker-compose.yml + mode: '0644' + notify: Restart Traefik + +- name: Start Traefik + community.docker.docker_compose_v2: + project_src: /opt/traefik + state: present + register: traefik_output + +- name: Wait for Traefik to be healthy + uri: + url: http://localhost:8080/ping + status_code: 200 + retries: 10 + delay: 3 + register: result + until: result.status == 200 diff --git a/ansible/roles/traefik/templates/docker-compose.yml.j2 b/ansible/roles/traefik/templates/docker-compose.yml.j2 new file mode 100644 index 0000000..79bddf6 --- /dev/null +++ b/ansible/roles/traefik/templates/docker-compose.yml.j2 @@ -0,0 +1,26 @@ +services: + traefik: + image: traefik:v3.2 + container_name: traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - {{ traefik_network }} + ports: + - "80:80" + - "443:443" + - "10.20.0.10:8080:8080" # Dashboard only on WireGuard IP + environment: + - TZ=UTC + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./traefik.yml:/traefik.yml:ro + - ./config:/config:ro + - ./letsencrypt:/letsencrypt + - ./logs:/var/log/traefik + +networks: + {{ traefik_network }}: + external: true diff --git a/ansible/roles/traefik/templates/dynamic.yml.j2 b/ansible/roles/traefik/templates/dynamic.yml.j2 new file mode 100644 index 0000000..c9ed6aa --- /dev/null +++ b/ansible/roles/traefik/templates/dynamic.yml.j2 @@ -0,0 +1,17 @@ +http: + middlewares: + # Security headers + secure-headers: + headers: + sslRedirect: true + forceSTSHeader: true + stsIncludeSubdomains: true + stsPreload: true + stsSeconds: 31536000 + + # Rate limiting + rate-limit: + rateLimit: + average: 100 + burst: 50 + period: 1s diff --git a/ansible/roles/traefik/templates/traefik.yml.j2 b/ansible/roles/traefik/templates/traefik.yml.j2 new file mode 100644 index 0000000..a873956 --- /dev/null +++ b/ansible/roles/traefik/templates/traefik.yml.j2 @@ -0,0 +1,40 @@ +api: + dashboard: true + insecure: true # Dashboard on port 8080 (bound to WireGuard IP only) + +entryPoints: + web: + address: ":80" + http: + redirections: + entryPoint: + to: websecure + scheme: https + permanent: true + websecure: + address: ":443" + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false + network: {{ traefik_network }} + file: + directory: /config + watch: true + +certificatesResolvers: + letsencrypt: + acme: + email: {{ letsencrypt_email | default('admin@asxp.io') }} + storage: /letsencrypt/acme.json + httpChallenge: + entryPoint: web + +log: + level: INFO + filePath: /var/log/traefik/traefik.log + +accessLog: + filePath: /var/log/traefik/access.log + bufferingSize: 100 diff --git a/ansible/roles/wireguard/defaults/main.yaml b/ansible/roles/wireguard/defaults/main.yaml new file mode 100644 index 0000000..f6ac1e5 --- /dev/null +++ b/ansible/roles/wireguard/defaults/main.yaml @@ -0,0 +1,39 @@ +--- +# WireGuard default configuration +wireguard_interface: wg0 +wireguard_port: 60101 +wireguard_ipv4: 10.20.0.10 +wireguard_ipv6: fd69:69:69::1 +wireguard_ipv4_network: 10.20.0.0/24 +wireguard_ipv6_network: fd69:69:69::/64 + +# Directory paths +wireguard_config_dir: /etc/wireguard +wireguard_keys_dir: "{{ wireguard_config_dir }}/keys" +wireguard_clients_dir: "{{ wireguard_config_dir }}/clients" + +# Enable IPv4 forwarding +wireguard_enable_forwarding: true + +# UFW configuration (if false, will use iptables rules in PostUp/PostDown) +# Default to false since most Debian servers don't have UFW enabled by default +wireguard_enable_ufw: false + +# Client configurations +wireguard_clients: [] +# Example client structure: +# wireguard_clients: +# - name: laptop +# ipv4: 10.20.0.2 +# ipv6: fd69:69:69::2 +# preshared_key: true # Optional: generate preshared key for extra security +# - name: phone +# ipv4: 10.20.0.3 +# ipv6: fd69:69:69::3 +# preshared_key: true + +# DNS servers for clients (optional) +wireguard_dns_servers: [] + +# Additional routes for split tunneling (optional) +wireguard_additional_routes: [] diff --git a/ansible/roles/wireguard/handlers/main.yaml b/ansible/roles/wireguard/handlers/main.yaml new file mode 100644 index 0000000..362fd15 --- /dev/null +++ b/ansible/roles/wireguard/handlers/main.yaml @@ -0,0 +1,21 @@ +--- +- name: restart wireguard + ansible.builtin.shell: | + wg syncconf {{ wireguard_interface }} <(wg-quick strip {{ wireguard_interface }}) + args: + executable: /bin/bash + ignore_errors: true + register: wg_sync_result + +- name: restart wireguard (fallback) + ansible.builtin.systemd: + name: "wg-quick@{{ wireguard_interface }}" + state: restarted + enabled: true + when: wg_sync_result is failed + +- name: reload wireguard + ansible.builtin.shell: | + wg syncconf {{ wireguard_interface }} <(wg-quick strip {{ wireguard_interface }}) + args: + executable: /bin/bash diff --git a/ansible/roles/wireguard/tasks/main.yaml b/ansible/roles/wireguard/tasks/main.yaml new file mode 100644 index 0000000..767128b --- /dev/null +++ b/ansible/roles/wireguard/tasks/main.yaml @@ -0,0 +1,219 @@ +--- +- name: Install WireGuard + ansible.builtin.apt: + name: + - wireguard + - wireguard-tools + state: present + update_cache: true + +- name: Install UFW (if needed) + ansible.builtin.apt: + name: ufw + state: present + when: wireguard_enable_ufw + +- name: Create WireGuard directories + ansible.builtin.file: + path: "{{ item }}" + state: directory + mode: '0700' + owner: root + group: root + loop: + - "{{ wireguard_config_dir }}" + - "{{ wireguard_keys_dir }}" + - "{{ wireguard_clients_dir }}" + +- name: Check if server private key exists + ansible.builtin.stat: + path: "{{ wireguard_keys_dir }}/server_private.key" + register: server_private_key_stat + +- name: Generate server private key + ansible.builtin.shell: | + wg genkey > {{ wireguard_keys_dir }}/server_private.key + chmod 600 {{ wireguard_keys_dir }}/server_private.key + when: not server_private_key_stat.stat.exists + +- name: Generate server public key + ansible.builtin.shell: | + wg pubkey < {{ wireguard_keys_dir }}/server_private.key > {{ wireguard_keys_dir }}/server_public.key + chmod 600 {{ wireguard_keys_dir }}/server_public.key + when: not server_private_key_stat.stat.exists + +- name: Read server private key + ansible.builtin.slurp: + src: "{{ wireguard_keys_dir }}/server_private.key" + register: server_private_key_content + +- name: Read server public key + ansible.builtin.slurp: + src: "{{ wireguard_keys_dir }}/server_public.key" + register: server_public_key_content + +- name: Set server key facts + ansible.builtin.set_fact: + wireguard_server_private_key: "{{ server_private_key_content.content | b64decode | trim }}" + wireguard_server_public_key: "{{ server_public_key_content.content | b64decode | trim }}" + +- name: Generate client keys + block: + - name: Check if client private key exists + ansible.builtin.stat: + path: "{{ wireguard_keys_dir }}/{{ item.name }}_private.key" + register: client_private_key_stats + loop: "{{ wireguard_clients }}" + + - name: Generate client private keys + ansible.builtin.shell: | + wg genkey > {{ wireguard_keys_dir }}/{{ item.item.name }}_private.key + chmod 600 {{ wireguard_keys_dir }}/{{ item.item.name }}_private.key + loop: "{{ client_private_key_stats.results }}" + when: not item.stat.exists + + - name: Check if client public key exists + ansible.builtin.stat: + path: "{{ wireguard_keys_dir }}/{{ item.name }}_public.key" + register: client_public_key_stats + loop: "{{ wireguard_clients }}" + + - name: Generate client public keys + ansible.builtin.shell: | + wg pubkey < {{ wireguard_keys_dir }}/{{ item.item.name }}_private.key > {{ wireguard_keys_dir }}/{{ item.item.name }}_public.key + chmod 600 {{ wireguard_keys_dir }}/{{ item.item.name }}_public.key + loop: "{{ client_public_key_stats.results }}" + when: not item.stat.exists + + - name: Check if client preshared key exists + ansible.builtin.stat: + path: "{{ wireguard_keys_dir }}/{{ item.name }}_preshared.key" + register: client_preshared_key_stats + loop: "{{ wireguard_clients }}" + when: item.preshared_key | default(false) + + - name: Generate client preshared keys (if enabled) + ansible.builtin.shell: | + wg genpsk > {{ wireguard_keys_dir }}/{{ item.item.name }}_preshared.key + chmod 600 {{ wireguard_keys_dir }}/{{ item.item.name }}_preshared.key + loop: "{{ client_preshared_key_stats.results | default([]) }}" + when: + - client_preshared_key_stats.results is defined + - not item.stat.exists + + - name: Read client private keys + ansible.builtin.slurp: + src: "{{ wireguard_keys_dir }}/{{ item.name }}_private.key" + register: client_private_keys + loop: "{{ wireguard_clients }}" + + - name: Read client public keys + ansible.builtin.slurp: + src: "{{ wireguard_keys_dir }}/{{ item.name }}_public.key" + register: client_public_keys + loop: "{{ wireguard_clients }}" + + - name: Read client preshared keys + ansible.builtin.slurp: + src: "{{ wireguard_keys_dir }}/{{ item.name }}_preshared.key" + register: client_preshared_keys + loop: "{{ wireguard_clients }}" + when: item.preshared_key | default(false) + ignore_errors: true + + when: wireguard_clients | length > 0 + +- name: Process client keys into structured data + ansible.builtin.set_fact: + wireguard_client_configs: "{{ wireguard_client_configs | default([]) + [client_config] }}" + loop: "{{ wireguard_clients }}" + loop_control: + index_var: client_index + vars: + private_key: "{{ client_private_keys.results[client_index].content | b64decode | trim }}" + public_key: "{{ client_public_keys.results[client_index].content | b64decode | trim }}" + preshared_key: "{{ client_preshared_keys.results[client_index].content | b64decode | trim if (item.preshared_key | default(false) and client_preshared_keys.results[client_index] is defined and not client_preshared_keys.results[client_index].failed | default(false)) else '' }}" + client_config: + name: "{{ item.name }}" + ipv4: "{{ item.ipv4 }}" + ipv6: "{{ item.ipv6 }}" + private_key: "{{ private_key }}" + public_key: "{{ public_key }}" + preshared_key: "{{ preshared_key }}" + persistent_keepalive: "{{ item.persistent_keepalive | default(25) }}" + when: wireguard_clients | length > 0 + +- name: Enable IPv4 forwarding + ansible.posix.sysctl: + name: net.ipv4.ip_forward + value: '1' + sysctl_set: true + state: present + reload: true + when: wireguard_enable_forwarding + +- name: Enable IPv6 forwarding + ansible.posix.sysctl: + name: net.ipv6.conf.all.forwarding + value: '1' + sysctl_set: true + state: present + reload: true + when: wireguard_enable_forwarding + +- name: Get default network interface + ansible.builtin.shell: ip route | grep default | awk '{print $5}' | head -n1 + register: default_interface + changed_when: false + +- name: Set default interface fact + ansible.builtin.set_fact: + wireguard_default_interface: "{{ default_interface.stdout }}" + +- name: Deploy WireGuard server configuration + ansible.builtin.template: + src: wg0.conf.j2 + dest: "{{ wireguard_config_dir }}/{{ wireguard_interface }}.conf" + mode: '0600' + owner: root + group: root + notify: restart wireguard + +- name: Configure UFW for WireGuard + block: + - name: Allow WireGuard port through UFW + community.general.ufw: + rule: allow + port: "{{ wireguard_port }}" + proto: udp + + - name: Allow forwarding on WireGuard interface + community.general.ufw: + rule: allow + route: true + interface_in: "{{ wireguard_interface }}" + when: wireguard_enable_ufw + +- name: Enable and start WireGuard service + ansible.builtin.systemd: + name: "wg-quick@{{ wireguard_interface }}" + enabled: true + state: started + +- name: Generate client configuration files + ansible.builtin.template: + src: client.conf.j2 + dest: "{{ wireguard_clients_dir }}/{{ item.name }}.conf" + mode: '0600' + owner: root + group: root + loop: "{{ wireguard_client_configs | default([]) }}" + when: wireguard_clients | length > 0 + +- name: Fetch client configuration files to local machine + ansible.builtin.fetch: + src: "{{ wireguard_clients_dir }}/{{ item.name }}.conf" + dest: "./wireguard-configs/{{ inventory_hostname }}/{{ item.name }}.conf" + flat: true + loop: "{{ wireguard_clients }}" + when: wireguard_clients | length > 0 diff --git a/ansible/roles/wireguard/templates/client.conf.j2 b/ansible/roles/wireguard/templates/client.conf.j2 new file mode 100644 index 0000000..ab974f4 --- /dev/null +++ b/ansible/roles/wireguard/templates/client.conf.j2 @@ -0,0 +1,21 @@ +# WireGuard Client Configuration - {{ item.name }} +# Generated by Ansible on {{ ansible_date_time.iso8601 }} +# Server: {{ inventory_hostname }} + +[Interface] +PrivateKey = {{ item.private_key }} +Address = {{ item.ipv4 }}/32,{{ item.ipv6 }}/128 +{% if wireguard_dns_servers | length > 0 %} +DNS = {{ wireguard_dns_servers | join(', ') }} +{% endif %} + +[Peer] +PublicKey = {{ wireguard_server_public_key }} +{% if item.preshared_key %} +PresharedKey = {{ item.preshared_key }} +{% endif %} +Endpoint = {{ ansible_host }}:{{ wireguard_port }} +# Allow all VPN traffic (peer-to-peer connectivity) +AllowedIPs = {{ wireguard_ipv4_network }},{{ wireguard_ipv6_network }}{% if wireguard_additional_routes | length > 0 %},{{ wireguard_additional_routes | join(',') }}{% endif %} + +PersistentKeepalive = {{ item.persistent_keepalive | default(25) }} diff --git a/ansible/roles/wireguard/templates/wg0.conf.j2 b/ansible/roles/wireguard/templates/wg0.conf.j2 new file mode 100644 index 0000000..ce7bf91 --- /dev/null +++ b/ansible/roles/wireguard/templates/wg0.conf.j2 @@ -0,0 +1,39 @@ +# WireGuard Server Configuration - {{ wireguard_interface }} +# Generated by Ansible on {{ ansible_date_time.iso8601 }} + +[Interface] +Address = {{ wireguard_ipv4 }}/{{ wireguard_ipv4_network.split('/')[-1] }},{{ wireguard_ipv6 }}/{{ wireguard_ipv6_network.split('/')[-1] }} +ListenPort = {{ wireguard_port }} +PrivateKey = {{ wireguard_server_private_key }} + +{% if wireguard_enable_ufw %} +# UFW handles firewall rules +{% else %} +# Firewall rules for iptables +PostUp = iptables -I INPUT -p udp --dport {{ wireguard_port }} -j ACCEPT +PostUp = iptables -I FORWARD -i {{ wireguard_default_interface }} -o {{ wireguard_interface }} -j ACCEPT +PostUp = iptables -I FORWARD -i {{ wireguard_interface }} -j ACCEPT +PostUp = iptables -t nat -A POSTROUTING -o {{ wireguard_default_interface }} -j MASQUERADE +PostUp = ip6tables -I FORWARD -i {{ wireguard_interface }} -j ACCEPT +PostUp = ip6tables -t nat -A POSTROUTING -o {{ wireguard_default_interface }} -j MASQUERADE +PostDown = iptables -D INPUT -p udp --dport {{ wireguard_port }} -j ACCEPT +PostDown = iptables -D FORWARD -i {{ wireguard_default_interface }} -o {{ wireguard_interface }} -j ACCEPT +PostDown = iptables -D FORWARD -i {{ wireguard_interface }} -j ACCEPT +PostDown = iptables -t nat -D POSTROUTING -o {{ wireguard_default_interface }} -j MASQUERADE +PostDown = ip6tables -D FORWARD -i {{ wireguard_interface }} -j ACCEPT +PostDown = ip6tables -t nat -D POSTROUTING -o {{ wireguard_default_interface }} -j MASQUERADE +{% endif %} + +{% for client in wireguard_client_configs | default([]) %} +### Client {{ client.name }} +[Peer] +PublicKey = {{ client.public_key }} +{% if client.preshared_key %} +PresharedKey = {{ client.preshared_key }} +{% endif %} +AllowedIPs = {{ client.ipv4 }}/32,{{ client.ipv6 }}/128 +{% if client.persistent_keepalive | default(25) %} +PersistentKeepalive = {{ client.persistent_keepalive }} +{% endif %} + +{% endfor %}