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

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