This commit is contained in:
Sergei Poljanski 2025-11-01 17:15:42 +02:00
commit b81f7e15b2
41 changed files with 1632 additions and 0 deletions

215
ansible/README.md Normal file
View file

@ -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

20
ansible/ansible.cfg Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,4 @@
---
# SSH Hardening Configuration
# Restrict SSH to VPC and WireGuard networks only
ssh_allow_public_git: false

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,6 @@
---
- name: Deploy Forgejo Runner
hosts: asxpio-services
become: true
roles:
- forgejo_runner

View file

@ -0,0 +1,6 @@
---
- name: Deploy Forgejo Git server
hosts: asxpio-ops
become: true
roles:
- forgejo

View file

@ -0,0 +1,6 @@
---
- name: Deploy Traefik reverse proxy
hosts: asxpio-ops
become: true
roles:
- traefik

View file

@ -0,0 +1,6 @@
---
- name: Restore Forgejo to new server
hosts: asxpio-ops
become: true
roles:
- forgejo_restore

View file

@ -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

View file

@ -0,0 +1,6 @@
---
- name: Harden SSH configuration
hosts: asxpio
become: true
roles:
- ssh_hardening

View file

@ -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

View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -0,0 +1,6 @@
---
- name: Setup WireGuard VPN
hosts: ops_servers
become: true
roles:
- wireguard

View file

@ -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"

View file

@ -0,0 +1,5 @@
---
- name: Restart Forgejo
community.docker.docker_compose_v2:
project_src: /opt/forgejo
state: restarted

View file

@ -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

View file

@ -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') }}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
---
- name: Restart Forgejo Runner
community.docker.docker_compose_v2:
project_src: /opt/forgejo-runner
state: restarted

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,6 @@
---
- name: restart sshd
ansible.builtin.systemd:
name: sshd
state: restarted
enabled: true

View file

@ -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)

View file

@ -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 %}

View file

@ -0,0 +1,5 @@
---
- name: Restart Traefik
community.docker.docker_compose_v2:
project_src: /opt/traefik
state: restarted

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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: []

View file

@ -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

View file

@ -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

View file

@ -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) }}

View file

@ -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 %}