refactor(deployment): Remove WireGuard VPN dependency and restore public service access

Remove WireGuard integration from production deployment to simplify infrastructure:
- Remove docker-compose-direct-access.yml (VPN-bound services)
- Remove VPN-only middlewares from Grafana, Prometheus, Portainer
- Remove WireGuard middleware definitions from Traefik
- Remove WireGuard IPs (10.8.0.0/24) from Traefik forwarded headers

All monitoring services now publicly accessible via subdomains:
- grafana.michaelschiemer.de (with Grafana native auth)
- prometheus.michaelschiemer.de (with Basic Auth)
- portainer.michaelschiemer.de (with Portainer native auth)

All services use Let's Encrypt SSL certificates via Traefik.
This commit is contained in:
2025-11-05 12:48:25 +01:00
parent 7c52065aae
commit 95147ff23e
215 changed files with 29490 additions and 368 deletions

View File

@@ -179,6 +179,141 @@ sudo ufw allow 51820/udp comment 'WireGuard VPN'
sudo iptables -A INPUT -p udp --dport 51820 -j ACCEPT
```
## Split-Tunnel Routing & NAT Fix
### A. Quick Fix Commands (manuell auf dem Server)
```bash
WAN_IF=${WAN_IF:-eth0}
WG_IF=${WG_IF:-wg0}
WG_NET=${WG_NET:-10.8.0.0/24}
WG_PORT=${WG_PORT:-51820}
EXTRA_NETS=${EXTRA_NETS:-"192.168.178.0/24 172.20.0.0/16"}
sudo sysctl -w net.ipv4.ip_forward=1
sudo tee /etc/sysctl.d/99-${WG_IF}-forward.conf >/dev/null <<'EOF'
# WireGuard Forwarding
net.ipv4.ip_forward=1
EOF
sudo sysctl --system
# iptables Variante
sudo iptables -t nat -C POSTROUTING -s ${WG_NET} -o ${WAN_IF} -j MASQUERADE 2>/dev/null \
|| sudo iptables -t nat -A POSTROUTING -s ${WG_NET} -o ${WAN_IF} -j MASQUERADE
sudo iptables -C FORWARD -i ${WG_IF} -s ${WG_NET} -o ${WAN_IF} -j ACCEPT 2>/dev/null \
|| sudo iptables -A FORWARD -i ${WG_IF} -s ${WG_NET} -o ${WAN_IF} -j ACCEPT
sudo iptables -C FORWARD -o ${WG_IF} -d ${WG_NET} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null \
|| sudo iptables -A FORWARD -o ${WG_IF} -d ${WG_NET} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
for NET in ${EXTRA_NETS}; do
sudo iptables -C FORWARD -i ${WG_IF} -d ${NET} -j ACCEPT 2>/dev/null || sudo iptables -A FORWARD -i ${WG_IF} -d ${NET} -j ACCEPT
done
# nftables Variante
sudo nft list table inet wireguard_${WG_IF} >/dev/null 2>&1 || sudo nft add table inet wireguard_${WG_IF}
sudo nft list chain inet wireguard_${WG_IF} postrouting >/dev/null 2>&1 \
|| sudo nft add chain inet wireguard_${WG_IF} postrouting '{ type nat hook postrouting priority srcnat; }'
sudo nft list chain inet wireguard_${WG_IF} forward >/dev/null 2>&1 \
|| sudo nft add chain inet wireguard_${WG_IF} forward '{ type filter hook forward priority filter; policy accept; }'
sudo nft list chain inet wireguard_${WG_IF} postrouting | grep -q "${WAN_IF}" \
|| sudo nft add rule inet wireguard_${WG_IF} postrouting oifname "${WAN_IF}" ip saddr ${WG_NET} masquerade
sudo nft list chain inet wireguard_${WG_IF} forward | grep -q "iifname \"${WG_IF}\"" \
|| sudo nft add rule inet wireguard_${WG_IF} forward iifname "${WG_IF}" ip saddr ${WG_NET} counter accept
sudo nft list chain inet wireguard_${WG_IF} forward | grep -q "oifname \"${WG_IF}\"" \
|| sudo nft add rule inet wireguard_${WG_IF} forward oifname "${WG_IF}" ip daddr ${WG_NET} ct state established,related counter accept
for NET in ${EXTRA_NETS}; do
sudo nft list chain inet wireguard_${WG_IF} forward | grep -q "${NET}" \
|| sudo nft add rule inet wireguard_${WG_IF} forward iifname "${WG_IF}" ip daddr ${NET} counter accept
done
# Firewall Hooks
if command -v ufw >/dev/null && sudo ufw status | grep -iq "Status: active"; then
sudo sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
sudo ufw allow ${WG_PORT}/udp
sudo ufw route allow in on ${WG_IF} out on ${WAN_IF} to any
fi
if command -v firewall-cmd >/dev/null && sudo firewall-cmd --state >/dev/null 2>&1; then
sudo firewall-cmd --permanent --zone=${FIREWALLD_ZONE:-public} --add-port=${WG_PORT}/udp
sudo firewall-cmd --permanent --zone=${FIREWALLD_ZONE:-public} --add-masquerade
sudo firewall-cmd --reload
fi
sudo systemctl enable --now wg-quick@${WG_IF}
sudo wg show
```
### B. Skript: `deployment/ansible/scripts/setup-wireguard-routing.sh`
```bash
cd deployment/ansible
sudo WAN_IF=eth0 WG_IF=wg0 WG_NET=10.8.0.0/24 EXTRA_NETS="192.168.178.0/24 172.20.0.0/16" \
./scripts/setup-wireguard-routing.sh
```
*Erkennt automatisch iptables/nftables und konfiguriert optional UFW/Firewalld.*
### C. Ansible Playbook: `playbooks/wireguard-routing.yml`
```bash
cd deployment/ansible
ansible-playbook -i inventory/production.yml playbooks/wireguard-routing.yml \
-e "wg_interface=wg0 wg_addr=10.8.0.1/24 wg_net=10.8.0.0/24 wan_interface=eth0" \
-e '{"extra_nets":["192.168.178.0/24","172.20.0.0/16"],"firewall_backend":"iptables","manage_ufw":true}'
```
*Variablen:* `wg_interface`, `wg_addr`, `wg_net`, `wan_interface`, `extra_nets`, `firewall_backend` (`iptables|nftables`), `manage_ufw`, `manage_firewalld`, `firewalld_zone`.
### D. Beispiel `wg0.conf` Ausschnitt
```ini
[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = <ServerPrivateKey>
# iptables
PostUp = iptables -t nat -C POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE 2>/dev/null || iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostUp = iptables -C FORWARD -i wg0 -s 10.8.0.0/24 -j ACCEPT 2>/dev/null || iptables -A FORWARD -i wg0 -s 10.8.0.0/24 -j ACCEPT
PostUp = iptables -C FORWARD -o wg0 -d 10.8.0.0/24 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || iptables -A FORWARD -o wg0 -d 10.8.0.0/24 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE 2>/dev/null || true
PostDown = iptables -D FORWARD -i wg0 -s 10.8.0.0/24 -j ACCEPT 2>/dev/null || true
PostDown = iptables -D FORWARD -o wg0 -d 10.8.0.0/24 -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
# nftables (stattdessen)
# PostUp = nft -f /etc/nftables.d/wireguard-wg0.nft
# PostDown = nft delete table inet wireguard_wg0 2>/dev/null || true
[Peer]
PublicKey = <ClientPublicKey>
AllowedIPs = 10.8.0.5/32, 192.168.178.0/24, 172.20.0.0/16
PersistentKeepalive = 25
```
### E. Windows Client (AllowedIPs & Tests)
```ini
[Interface]
Address = 10.8.0.5/32
DNS = 10.8.0.1 # optional
[Peer]
PublicKey = <ServerPublicKey>
Endpoint = vpn.example.com:51820
AllowedIPs = 10.8.0.0/24, 192.168.178.0/24, 172.20.0.0/16
PersistentKeepalive = 25
```
PowerShell:
```powershell
wg show
Test-Connection -Source 10.8.0.5 -ComputerName 10.8.0.1
Test-Connection 192.168.178.1
Test-NetConnection -ComputerName 192.168.178.10 -Port 22
```
Optional: `Set-DnsClientNrptRule -Namespace "internal.lan" -NameServers 10.8.0.1`.
### F. Troubleshooting & Rollback
- Checks: `ip r`, `ip route get <target>`, `iptables -t nat -S`, `nft list ruleset`, `sysctl net.ipv4.ip_forward`, `wg show`, `tcpdump -i wg0`, `tcpdump -i eth0 host 10.8.0.5`.
- Häufige Fehler: falsches WAN-Interface, Forwarding/NAT fehlt, doppelte Firewalls (iptables + nftables), Docker-NAT kollidiert, Policy-Routing aktiv.
- Rollback:
- `sudo rm /etc/sysctl.d/99-wg0-forward.conf && sudo sysctl -w net.ipv4.ip_forward=0`
- iptables: Regeln mit `iptables -D` entfernen (siehe oben).
- nftables: `sudo nft delete table inet wireguard_wg0`.
- UFW: `sudo ufw delete allow 51820/udp`, Route-Regeln entfernen, `DEFAULT_FORWARD_POLICY` zurücksetzen.
- Firewalld: `firewall-cmd --permanent --remove-port=51820/udp`, `--remove-masquerade`, `--reload`.
- Dienst: `sudo systemctl disable --now wg-quick@wg0`.
## Troubleshooting
### WireGuard startet nicht
@@ -281,4 +416,4 @@ Bei Problemen:
1. Prüfe Logs: `sudo journalctl -u wg-quick@wg0`
2. Prüfe Status: `sudo wg show`
3. Prüfe Firewall: `sudo ufw status`
4. Teste Connectivity: `ping 10.8.0.1` (vom Client)
4. Teste Connectivity: `ping 10.8.0.1` (vom Client)

View File

@@ -1,205 +0,0 @@
---
- name: Add WireGuard Client
hosts: production
become: yes
gather_facts: yes
vars:
wireguard_interface: "wg0"
wireguard_config_path: "/etc/wireguard"
wireguard_config_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}.conf"
wireguard_client_configs_path: "/etc/wireguard/clients"
wireguard_local_client_configs_dir: "{{ playbook_dir }}/../wireguard-clients"
wireguard_dns_servers: []
pre_tasks:
- name: Set WireGuard network
set_fact:
wireguard_network: "{{ wireguard_network | default('10.8.0.0/24') }}"
- name: Set WireGuard other variables with defaults
set_fact:
wireguard_port: "{{ wireguard_port | default(51820) }}"
client_ip: "{{ client_ip | default('') }}"
# IMPORTANT: Default to VPN network only (not 0.0.0.0/0)
# This ensures SSH access via normal IP remains available
allowed_ips: "{{ allowed_ips | default(wireguard_network) }}"
tasks:
- name: Validate client name
fail:
msg: "client_name is required. Usage: ansible-playbook ... -e 'client_name=myclient'"
when: client_name is not defined or client_name == ""
- name: Get server external IP address
uri:
url: https://api.ipify.org
return_content: yes
register: server_external_ip
changed_when: false
failed_when: false
- name: Set server external IP
set_fact:
server_external_ip_content: "{{ ansible_host | default(server_external_ip.content | default('')) }}"
- name: Check if WireGuard config exists
stat:
path: "{{ wireguard_config_file }}"
register: wireguard_config_exists
- name: Fail if WireGuard not configured
fail:
msg: "WireGuard server not configured. Please run setup-wireguard.yml first."
when: not wireguard_config_exists.stat.exists
- name: Read WireGuard server config
slurp:
src: "{{ wireguard_config_file }}"
register: wireguard_server_config_read
- name: Extract server IP from config
set_fact:
server_vpn_ip: "{{ (wireguard_server_config_read.content | b64decode | regex_search('Address = ([0-9.]+)', '\\1')) | first | default('10.8.0.1') }}"
- name: Extract WireGuard server IP octets
set_fact:
wireguard_server_ip_octets: "{{ server_vpn_ip.split('.') }}"
when: client_ip == ""
- name: Gather existing client addresses
set_fact:
existing_client_ips: "{{ (wireguard_server_config_read.content | b64decode | regex_findall('AllowedIPs = ([0-9A-Za-z.]+)/32', '\\1')) }}"
when: client_ip == ""
- name: Calculate client IP if not provided
vars:
existing_last_octets: "{{ (existing_client_ips | default([])) | map('regex_replace', '^(?:\\d+\\.\\d+\\.\\d+\\.)', '') | select('match', '^[0-9]+$') | map('int') | list }}"
server_last_octet: "{{ wireguard_server_ip_octets[3] | int }}"
next_octet_candidate: "{{ (existing_last_octets + [server_last_octet]) | map('int') | list | max + 1 if (existing_last_octets + [server_last_octet]) else server_last_octet + 1 }}"
set_fact:
client_ip: "{{ [
wireguard_server_ip_octets[0],
wireguard_server_ip_octets[1],
wireguard_server_ip_octets[2],
next_octet_candidate
] | join('.') }}"
when: client_ip == ""
- name: Generate client private key
command: "wg genkey"
register: client_private_key
changed_when: true
no_log: yes
- name: Generate client public key
command: "wg pubkey"
args:
stdin: "{{ client_private_key.stdout }}"
register: client_public_key
changed_when: false
no_log: yes
- name: Add client to WireGuard server config
blockinfile:
path: "{{ wireguard_config_file }}"
block: |
# Client: {{ client_name }}
[Peer]
PublicKey = {{ client_public_key.stdout }}
AllowedIPs = {{ client_ip }}/32
marker: "# {mark} ANSIBLE MANAGED BLOCK - Client: {{ client_name }}"
register: wireguard_client_block
- name: Ensure client configs directory exists
file:
path: "{{ wireguard_client_configs_path }}"
state: directory
mode: '0700'
owner: root
group: root
- name: Ensure local client configs directory exists
file:
path: "{{ wireguard_local_client_configs_dir }}"
state: directory
mode: '0700'
delegate_to: localhost
become: no
run_once: true
- name: Get server public key
shell: "cat {{ wireguard_config_path }}/{{ wireguard_interface }}_private.key | wg pubkey"
register: server_public_key_cmd
changed_when: false
no_log: yes
failed_when: false
- name: Create client configuration file
template:
src: "{{ playbook_dir }}/../templates/wireguard-client.conf.j2"
dest: "{{ wireguard_client_configs_path }}/{{ client_name }}.conf"
mode: '0600'
owner: root
group: root
- name: Download client configuration to control machine
fetch:
src: "{{ wireguard_client_configs_path }}/{{ client_name }}.conf"
dest: "{{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf"
flat: yes
mode: '0600'
- name: Ensure local client configuration has strict permissions
file:
path: "{{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf"
mode: '0600'
delegate_to: localhost
become: no
- name: Read WireGuard server config to find server IP
slurp:
src: "{{ wireguard_config_file }}"
register: wireguard_server_config_read
- name: Restart WireGuard service
systemd:
name: "wg-quick@{{ wireguard_interface }}"
state: restarted
when: wireguard_client_block.changed
- name: Display client configuration
debug:
msg: |
========================================
WireGuard Client Added: {{ client_name }}
========================================
Client Configuration File:
{{ wireguard_client_configs_path }}/{{ client_name }}.conf
Local Copy:
{{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf
Client IP: {{ client_ip }}
Server Endpoint: {{ server_external_ip_content }}:{{ wireguard_port }}
To use this configuration:
1. Copy the config file to your client machine
2. Install WireGuard client
3. Run: sudo wg-quick up {{ client_name }}
Or scan the QR code (if qrencode installed):
qrencode -t ansiutf8 < {{ wireguard_client_configs_path }}/{{ client_name }}.conf
========================================
- name: Generate QR code for client config
command: "qrencode -t ansiutf8 -r {{ wireguard_client_configs_path }}/{{ client_name }}.conf"
register: qr_code
changed_when: false
failed_when: false
- name: Display QR code
debug:
msg: "{{ qr_code.stdout }}"
when: qr_code.rc == 0

View File

@@ -0,0 +1,229 @@
---
# WireGuard Client Configuration Generator
# Usage: ansible-playbook playbooks/generate-wireguard-client.yml -e "client_name=michael-laptop"
- name: Generate WireGuard Client Configuration
hosts: server
become: true
gather_facts: true
vars:
# Default values (can be overridden with -e)
wireguard_config_dir: "/etc/wireguard"
wireguard_interface: "wg0"
wireguard_server_endpoint: "{{ ansible_default_ipv4.address }}"
wireguard_server_port: 51820
wireguard_vpn_network: "10.8.0.0/24"
wireguard_server_ip: "10.8.0.1"
# Client output directory (local)
client_config_dir: "{{ playbook_dir }}/../wireguard/configs"
# Required variable (must be passed via -e)
# client_name: "device-name"
tasks:
- name: Validate client_name is provided
assert:
that:
- client_name is defined
- client_name | length > 0
fail_msg: "ERROR: client_name must be provided via -e client_name=<name>"
success_msg: "Generating config for client: {{ client_name }}"
- name: Validate client_name format (alphanumeric and hyphens only)
assert:
that:
- client_name is match('^[a-zA-Z0-9-]+$')
fail_msg: "ERROR: client_name must contain only letters, numbers, and hyphens"
success_msg: "Client name format is valid"
- name: Check if WireGuard server is configured
stat:
path: "{{ wireguard_config_dir }}/{{ wireguard_interface }}.conf"
register: server_config
- name: Fail if server config doesn't exist
fail:
msg: "WireGuard server config not found. Run setup-wireguard-host.yml first."
when: not server_config.stat.exists
- name: Read server public key
slurp:
src: "{{ wireguard_config_dir }}/server_public.key"
register: server_public_key_raw
- name: Set server public key fact
set_fact:
server_public_key: "{{ server_public_key_raw.content | b64decode | trim }}"
- name: Get next available IP address
shell: |
# Parse existing peer IPs from wg0.conf
existing_ips=$(grep -oP 'AllowedIPs\s*=\s*\K[0-9.]+' {{ wireguard_config_dir }}/{{ wireguard_interface }}.conf 2>/dev/null || echo "")
# Start from .2 (server is .1)
i=2
while [ $i -le 254 ]; do
ip="10.8.0.$i"
if ! echo "$existing_ips" | grep -q "^$ip$"; then
echo "$ip"
exit 0
fi
i=$((i + 1))
done
echo "ERROR: No free IP addresses" >&2
exit 1
register: next_ip_result
changed_when: false
- name: Set client IP fact
set_fact:
client_ip: "{{ next_ip_result.stdout | trim }}"
- name: Display client IP assignment
debug:
msg: "Assigned IP for {{ client_name }}: {{ client_ip }}"
- name: Check if client already exists
shell: |
grep -q "# Client: {{ client_name }}" {{ wireguard_config_dir }}/{{ wireguard_interface }}.conf
register: client_exists
changed_when: false
failed_when: false
- name: Warn if client already exists
debug:
msg: "WARNING: Client '{{ client_name }}' already exists in server config. Creating new keys anyway."
when: client_exists.rc == 0
- name: Generate client private key
shell: wg genkey
register: client_private_key_result
changed_when: true
no_log: true
- name: Generate client public key
shell: echo "{{ client_private_key_result.stdout }}" | wg pubkey
register: client_public_key_result
changed_when: false
no_log: true
- name: Generate preshared key
shell: wg genpsk
register: preshared_key_result
changed_when: true
no_log: true
- name: Set client key facts
set_fact:
client_private_key: "{{ client_private_key_result.stdout | trim }}"
client_public_key: "{{ client_public_key_result.stdout | trim }}"
preshared_key: "{{ preshared_key_result.stdout | trim }}"
no_log: true
- name: Create client config directory on control node
delegate_to: localhost
file:
path: "{{ client_config_dir }}"
state: directory
mode: '0755'
become: false
- name: Generate client WireGuard configuration
delegate_to: localhost
copy:
content: |
[Interface]
# Client: {{ client_name }}
# Generated: {{ ansible_date_time.iso8601 }}
PrivateKey = {{ client_private_key }}
Address = {{ client_ip }}/32
DNS = 1.1.1.1, 8.8.8.8
[Peer]
# WireGuard Server
PublicKey = {{ server_public_key }}
PresharedKey = {{ preshared_key }}
Endpoint = {{ wireguard_server_endpoint }}:{{ wireguard_server_port }}
AllowedIPs = {{ wireguard_vpn_network }}
PersistentKeepalive = 25
dest: "{{ client_config_dir }}/{{ client_name }}.conf"
mode: '0600'
become: false
no_log: true
- name: Add client peer to server configuration
blockinfile:
path: "{{ wireguard_config_dir }}/{{ wireguard_interface }}.conf"
marker: "# {mark} ANSIBLE MANAGED BLOCK - Client: {{ client_name }}"
block: |
[Peer]
# Client: {{ client_name }}
PublicKey = {{ client_public_key }}
PresharedKey = {{ preshared_key }}
AllowedIPs = {{ client_ip }}/32
no_log: true
- name: Reload WireGuard configuration
shell: wg syncconf {{ wireguard_interface }} <(wg-quick strip {{ wireguard_interface }})
args:
executable: /bin/bash
- name: Generate QR code (ASCII)
delegate_to: localhost
shell: |
qrencode -t ansiutf8 < {{ client_config_dir }}/{{ client_name }}.conf > {{ client_config_dir }}/{{ client_name }}.qr.txt
become: false
changed_when: true
- name: Generate QR code (PNG)
delegate_to: localhost
shell: |
qrencode -t png -o {{ client_config_dir }}/{{ client_name }}.qr.png < {{ client_config_dir }}/{{ client_name }}.conf
become: false
changed_when: true
- name: Display QR code for mobile devices
delegate_to: localhost
shell: cat {{ client_config_dir }}/{{ client_name }}.qr.txt
register: qr_code_output
become: false
changed_when: false
- name: Client configuration summary
debug:
msg:
- "========================================="
- "WireGuard Client Configuration Created!"
- "========================================="
- ""
- "Client: {{ client_name }}"
- "IP Address: {{ client_ip }}/32"
- "Public Key: {{ client_public_key }}"
- ""
- "Configuration Files:"
- " Config: {{ client_config_dir }}/{{ client_name }}.conf"
- " QR Code (ASCII): {{ client_config_dir }}/{{ client_name }}.qr.txt"
- " QR Code (PNG): {{ client_config_dir }}/{{ client_name }}.qr.png"
- ""
- "Server Configuration:"
- " Endpoint: {{ wireguard_server_endpoint }}:{{ wireguard_server_port }}"
- " Allowed IPs: {{ wireguard_vpn_network }}"
- ""
- "Next Steps:"
- " Linux/macOS: sudo cp {{ client_config_dir }}/{{ client_name }}.conf /etc/wireguard/ && sudo wg-quick up {{ client_name }}"
- " Windows: Import {{ client_name }}.conf in WireGuard GUI"
- " iOS/Android: Scan QR code with WireGuard app"
- ""
- "Test Connection:"
- " ping {{ wireguard_server_ip }}"
- " curl -k https://{{ wireguard_server_ip }}:8080 # Traefik Dashboard"
- ""
- "========================================="
- name: Display QR code
debug:
msg: "{{ qr_code_output.stdout_lines }}"

View File

@@ -1,206 +0,0 @@
---
- name: Regenerate WireGuard Client - Fresh Config
hosts: production
become: yes
gather_facts: yes
vars:
wireguard_interface: "wg0"
wireguard_config_path: "/etc/wireguard"
wireguard_config_file: "{{ wireguard_config_path }}/{{ wireguard_interface }}.conf"
wireguard_client_configs_path: "/etc/wireguard/clients"
wireguard_local_client_configs_dir: "{{ playbook_dir }}/../wireguard-clients"
tasks:
- name: Validate client name
fail:
msg: "client_name is required. Usage: ansible-playbook ... -e 'client_name=myclient'"
when: client_name is not defined or client_name == ""
- name: Check if old client config exists
stat:
path: "{{ wireguard_client_configs_path }}/{{ client_name }}.conf"
register: old_client_config
failed_when: false
- name: Backup old client config
copy:
src: "{{ wireguard_client_configs_path }}/{{ client_name }}.conf"
dest: "{{ wireguard_client_configs_path }}/{{ client_name }}.conf.backup-{{ ansible_date_time.epoch }}"
remote_src: yes
when: old_client_config.stat.exists
register: backup_result
failed_when: false
- name: Display backup info
debug:
msg: "Alte Config wurde gesichert als: {{ backup_result.dest | default('N/A') }}"
when: old_client_config.stat.exists
- name: Remove old client from WireGuard server config
shell: |
# Entferne den [Peer] Block f?r den Client aus wg0.conf
sed -i '/# BEGIN ANSIBLE MANAGED BLOCK - Client: {{ client_name }}/,/^# END ANSIBLE MANAGED BLOCK - Client: {{ client_name }}/d' {{ wireguard_config_file }}
# Fallback: Entferne auch ohne Marker
sed -i '/# Client: {{ client_name }}/,/{/d' {{ wireguard_config_file }}
sed -i '/PublicKey = .*/d' {{ wireguard_config_file }} || true
sed -i '/AllowedIPs = .*\/32$/d' {{ wireguard_config_file }} || true
args:
executable: /bin/bash
register: remove_result
failed_when: false
changed_when: false
- name: Set WireGuard network
set_fact:
wireguard_network: "{{ wireguard_network | default('10.8.0.0/24') }}"
- name: Set WireGuard other variables with defaults
set_fact:
wireguard_port: "{{ wireguard_port | default(51820) }}"
client_ip: "{{ client_ip | default('') }}"
allowed_ips: "{{ allowed_ips | default(wireguard_network) }}"
- name: Get server external IP address
uri:
url: https://api.ipify.org
return_content: yes
register: server_external_ip
changed_when: false
failed_when: false
- name: Set server external IP
set_fact:
server_external_ip_content: "{{ ansible_host | default(server_external_ip.content | default('')) }}"
- name: Read WireGuard server config
slurp:
src: "{{ wireguard_config_file }}"
register: wireguard_server_config_read
- name: Extract server IP from config
set_fact:
server_vpn_ip: "{{ (wireguard_server_config_read.content | b64decode | regex_findall('Address\\s*=\\s*([0-9.]+)') | first) | default('10.8.0.1') }}"
failed_when: false
- name: Extract WireGuard server IP octets
set_fact:
wireguard_server_ip_octets: "{{ (server_vpn_ip | default('')).split('.') }}"
when: client_ip == ""
- name: Fail if server VPN IP is invalid
fail:
msg: "Server VPN IP '{{ server_vpn_ip }}' ist ungültig bitte wg0.conf prüfen."
when: client_ip == "" and (wireguard_server_ip_octets | length) < 4
- name: Gather existing client addresses
set_fact:
existing_client_ips: "{{ (wireguard_server_config_read.content | b64decode | regex_findall('AllowedIPs = ([0-9A-Za-z.]+)/32', '\\\\1')) }}"
when: client_ip == ""
- name: Calculate client IP if not provided
vars:
existing_last_octets: "{{ (existing_client_ips | default([])) | map('regex_replace', '^(?:\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.)', '') | select('match', '^[0-9]+$') | map('int') | list }}"
server_last_octet: "{{ wireguard_server_ip_octets[3] | int }}"
next_octet_candidate: "{{ (existing_last_octets + [server_last_octet]) | map('int') | list | max + 1 if (existing_client_ips | default([]) | length > 0) else server_last_octet + 1 }}"
set_fact:
client_ip: "{{ [
wireguard_server_ip_octets[0],
wireguard_server_ip_octets[1],
wireguard_server_ip_octets[2],
next_octet_candidate
] | join('.') }}"
when: client_ip == "" and (wireguard_server_ip_octets | length) >= 4
- name: Generate NEW client private key
command: "wg genkey"
register: client_private_key
changed_when: true
no_log: yes
- name: Generate NEW client public key
command: "wg pubkey"
args:
stdin: "{{ client_private_key.stdout }}"
register: client_public_key
changed_when: false
no_log: yes
- name: Add NEW client to WireGuard server config
blockinfile:
path: "{{ wireguard_config_file }}"
block: |
# Client: {{ client_name }}
[Peer]
PublicKey = {{ client_public_key.stdout }}
AllowedIPs = {{ client_ip }}/32
marker: "# {mark} ANSIBLE MANAGED BLOCK - Client: {{ client_name }}"
register: wireguard_client_block
- name: Ensure client configs directory exists
file:
path: "{{ wireguard_client_configs_path }}"
state: directory
mode: '0700'
owner: root
group: root
- name: Ensure local client configs directory exists
file:
path: "{{ wireguard_local_client_configs_dir }}"
state: directory
mode: '0700'
delegate_to: localhost
become: no
run_once: true
- name: Get server public key
shell: "cat {{ wireguard_config_path }}/{{ wireguard_interface }}_private.key | wg pubkey"
register: server_public_key_cmd
changed_when: false
no_log: yes
failed_when: false
- name: Create NEW client configuration file
template:
src: "{{ playbook_dir }}/../templates/wireguard-client.conf.j2"
dest: "{{ wireguard_client_configs_path }}/{{ client_name }}.conf"
mode: '0600'
owner: root
group: root
- name: Download NEW client configuration to control machine
fetch:
src: "{{ wireguard_client_configs_path }}/{{ client_name }}.conf"
dest: "{{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf"
flat: yes
mode: '0600'
- name: Restart WireGuard service
systemd:
name: "wg-quick@{{ wireguard_interface }}"
state: restarted
- name: Display NEW client configuration
debug:
msg: |
========================================
WireGuard Client REGENERATED: {{ client_name }}
========================================
Neue Client-IP: {{ client_ip }}
Server Endpoint: {{ server_external_ip_content }}:{{ wireguard_port }}
Neue Client-Konfiguration:
{{ wireguard_local_client_configs_dir }}/{{ client_name }}.conf
WICHTIG:
1. Lade die neue Config-Datei herunter
2. Importiere sie in WireGuard (ersetze die alte!)
3. Verbinde mit dem VPN
4. Teste: ping 10.8.0.1
5. Teste: https://grafana.michaelschiemer.de
Alte Config gesichert als:
{{ backup_result.dest | default('N/A') }}
========================================

View File

@@ -0,0 +1,309 @@
---
# Ansible Playbook: WireGuard Host-based VPN Setup
# Purpose: Deploy minimalistic WireGuard VPN for admin access
# Architecture: Host-based (systemd), no Docker, no DNS
- name: Setup WireGuard VPN (Host-based)
hosts: all
become: yes
vars:
# WireGuard Configuration
wg_interface: wg0
wg_network: 10.8.0.0/24
wg_server_ip: 10.8.0.1
wg_netmask: 24
wg_port: 51820
# Network Configuration
wan_interface: eth0 # Change to your WAN interface (eth0, ens3, etc.)
# Admin Service Ports (VPN-only access)
admin_service_ports:
- 8080 # Traefik Dashboard
- 9090 # Prometheus
- 3001 # Grafana
- 9000 # Portainer
- 8001 # Redis Insight
# Public Service Ports
public_service_ports:
- 80 # HTTP
- 443 # HTTPS
- 22 # SSH
# Rate Limiting
wg_enable_rate_limit: true
# Paths
wg_config_dir: /etc/wireguard
wg_backup_dir: /root/wireguard-backup
nft_config_file: /etc/nftables.d/wireguard.nft
tasks:
# ========================================
# 1. Pre-flight Checks
# ========================================
- name: Check if running as root
assert:
that: ansible_user_id == 'root'
fail_msg: "This playbook must be run as root"
- name: Detect WAN interface
shell: ip route | grep default | awk '{print $5}' | head -n1
register: detected_wan_interface
changed_when: false
- name: Set WAN interface if not specified
set_fact:
wan_interface: "{{ detected_wan_interface.stdout }}"
when: wan_interface == 'eth0' and detected_wan_interface.stdout != ''
- name: Display detected network configuration
debug:
msg:
- "WAN Interface: {{ wan_interface }}"
- "VPN Network: {{ wg_network }}"
- "VPN Server IP: {{ wg_server_ip }}"
# ========================================
# 2. Backup Existing Configuration
# ========================================
- name: Create backup directory
file:
path: "{{ wg_backup_dir }}"
state: directory
mode: '0700'
- name: Backup existing WireGuard config (if exists)
shell: |
if [ -d {{ wg_config_dir }} ]; then
tar -czf {{ wg_backup_dir }}/wireguard-backup-$(date +%Y%m%d-%H%M%S).tar.gz {{ wg_config_dir }}
echo "Backup created"
else
echo "No existing config"
fi
register: backup_result
changed_when: "'Backup created' in backup_result.stdout"
# ========================================
# 3. Install WireGuard
# ========================================
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
when: ansible_os_family == 'Debian'
- name: Install WireGuard and dependencies
apt:
name:
- wireguard
- wireguard-tools
- qrencode # For QR code generation
- nftables
state: present
when: ansible_os_family == 'Debian'
- name: Ensure WireGuard kernel module is loaded
modprobe:
name: wireguard
state: present
- name: Verify WireGuard module is available
shell: lsmod | grep -q wireguard
register: wg_module_check
failed_when: wg_module_check.rc != 0
changed_when: false
# ========================================
# 4. Generate Server Keys (if not exist)
# ========================================
- name: Create WireGuard config directory
file:
path: "{{ wg_config_dir }}"
state: directory
mode: '0700'
- name: Check if server private key exists
stat:
path: "{{ wg_config_dir }}/server_private.key"
register: server_private_key_stat
- name: Generate server private key
shell: wg genkey > {{ wg_config_dir }}/server_private.key
when: not server_private_key_stat.stat.exists
- name: Set server private key permissions
file:
path: "{{ wg_config_dir }}/server_private.key"
mode: '0600'
- name: Generate server public key
shell: cat {{ wg_config_dir }}/server_private.key | wg pubkey > {{ wg_config_dir }}/server_public.key
when: not server_private_key_stat.stat.exists
- name: Read server private key
slurp:
src: "{{ wg_config_dir }}/server_private.key"
register: server_private_key_content
- name: Read server public key
slurp:
src: "{{ wg_config_dir }}/server_public.key"
register: server_public_key_content
- name: Set server key facts
set_fact:
wg_server_private_key: "{{ server_private_key_content.content | b64decode | trim }}"
wg_server_public_key: "{{ server_public_key_content.content | b64decode | trim }}"
- name: Display server public key
debug:
msg: "Server Public Key: {{ wg_server_public_key }}"
# ========================================
# 5. Configure WireGuard
# ========================================
- name: Deploy WireGuard server configuration
template:
src: ../templates/wg0.conf.j2
dest: "{{ wg_config_dir }}/wg0.conf"
mode: '0600'
notify: restart wireguard
- name: Enable IP forwarding
sysctl:
name: net.ipv4.ip_forward
value: '1'
sysctl_set: yes
state: present
reload: yes
# ========================================
# 6. Configure nftables Firewall
# ========================================
- name: Create nftables config directory
file:
path: /etc/nftables.d
state: directory
mode: '0755'
- name: Deploy WireGuard firewall rules
template:
src: ../templates/wireguard-host-firewall.nft.j2
dest: "{{ nft_config_file }}"
mode: '0644'
notify: reload nftables
- name: Include WireGuard rules in main nftables config
lineinfile:
path: /etc/nftables.conf
line: 'include "{{ nft_config_file }}"'
create: yes
state: present
notify: reload nftables
- name: Enable nftables service
systemd:
name: nftables
enabled: yes
state: started
# ========================================
# 7. Enable and Start WireGuard
# ========================================
- name: Enable WireGuard interface
systemd:
name: wg-quick@wg0
enabled: yes
state: started
- name: Verify WireGuard is running
command: wg show wg0
register: wg_status
changed_when: false
- name: Display WireGuard status
debug:
msg: "{{ wg_status.stdout_lines }}"
# ========================================
# 8. Health Checks
# ========================================
- name: Check WireGuard interface exists
command: ip link show wg0
register: wg_interface_check
failed_when: wg_interface_check.rc != 0
changed_when: false
- name: Check firewall rules applied
command: nft list ruleset
register: nft_rules
failed_when: "'wireguard_firewall' not in nft_rules.stdout"
changed_when: false
- name: Verify admin ports are blocked from public
shell: nft list chain inet wireguard_firewall input | grep -q "admin_service_ports.*drop"
register: admin_port_block_check
failed_when: admin_port_block_check.rc != 0
changed_when: false
# ========================================
# 9. Post-Installation Summary
# ========================================
- name: Create post-installation summary
debug:
msg:
- "========================================="
- "WireGuard VPN Setup Complete!"
- "========================================="
- ""
- "Server Configuration:"
- " Interface: wg0"
- " Server IP: {{ wg_server_ip }}/{{ wg_netmask }}"
- " Listen Port: {{ wg_port }}"
- " Public Key: {{ wg_server_public_key }}"
- ""
- "Network Configuration:"
- " VPN Network: {{ wg_network }}"
- " WAN Interface: {{ wan_interface }}"
- ""
- "Admin Service Access (VPN-only):"
- " Traefik Dashboard: https://{{ wg_server_ip }}:8080"
- " Prometheus: http://{{ wg_server_ip }}:9090"
- " Grafana: https://{{ wg_server_ip }}:3001"
- " Portainer: http://{{ wg_server_ip }}:9000"
- " Redis Insight: http://{{ wg_server_ip }}:8001"
- ""
- "Next Steps:"
- " 1. Generate client config: ./scripts/generate-client-config.sh <device-name>"
- " 2. Import config on client device"
- " 3. Connect and verify access"
- ""
- "Firewall Status: ACTIVE (nftables)"
- " - Public ports: 80, 443, 22"
- " - VPN port: {{ wg_port }}"
- " - Admin services: VPN-only access"
- ""
- "========================================="
handlers:
- name: restart wireguard
systemd:
name: wg-quick@wg0
state: restarted
- name: reload nftables
systemd:
name: nftables
state: reloaded

View File

@@ -1,287 +0,0 @@
---
- name: Setup WireGuard VPN Server
hosts: production
become: yes
gather_facts: yes
vars:
# WireGuard variables are defined in group_vars/production.yml
# Can be overridden via -e flag if needed
wireguard_port: "{{ wireguard_port_default | default(51820) }}"
wireguard_network: "{{ wireguard_network_default | default('10.8.0.0/24') }}"
wireguard_server_ip: "{{ wireguard_server_ip_default | default('10.8.0.1') }}"
pre_tasks:
- name: Optionally load wireguard secrets from vault
include_vars:
file: "{{ playbook_dir }}/../secrets/production.vault.yml"
no_log: yes
ignore_errors: yes
delegate_to: localhost
become: no
tasks:
- name: Check if WireGuard is already installed
command: which wg
register: wireguard_installed
changed_when: false
failed_when: false
- name: Update package cache
apt:
update_cache: yes
cache_valid_time: 3600
when: not wireguard_installed.rc == 0
- name: Install WireGuard
apt:
name:
- wireguard
- wireguard-tools
- qrencode
state: present
when: not wireguard_installed.rc == 0
notify: restart wireguard
- name: Ensure WireGuard config directory exists
file:
path: "{{ wireguard_config_path }}"
state: directory
mode: '0700'
owner: root
group: root
- name: Ensure WireGuard client configs directory exists
file:
path: "{{ wireguard_client_configs_path }}"
state: directory
mode: '0700'
owner: root
group: root
- name: Check if WireGuard server keys exist
stat:
path: "{{ wireguard_private_key_file }}"
register: server_private_key_exists
- name: Generate WireGuard server private key
command: "wg genkey"
register: server_private_key
changed_when: true
when: not server_private_key_exists.stat.exists
no_log: yes
- name: Save WireGuard server private key
copy:
content: "{{ server_private_key.stdout }}"
dest: "{{ wireguard_private_key_file }}"
mode: '0600'
owner: root
group: root
when: not server_private_key_exists.stat.exists
no_log: yes
- name: Read WireGuard server private key
slurp:
src: "{{ wireguard_private_key_file }}"
register: server_private_key_content
when: server_private_key_exists.stat.exists
- name: Generate WireGuard server public key
command: "wg pubkey"
args:
stdin: "{{ server_private_key.stdout if not server_private_key_exists.stat.exists else server_private_key_content.content | b64decode | trim }}"
register: server_public_key
changed_when: false
when: not server_private_key_exists.stat.exists
no_log: yes
- name: Get existing server public key
shell: "cat {{ wireguard_private_key_file }} | wg pubkey"
register: existing_server_public_key
changed_when: false
when: server_private_key_exists.stat.exists
no_log: yes
failed_when: false
- name: Set server public key fact
set_fact:
server_public_key_value: "{{ server_public_key.stdout if not server_private_key_exists.stat.exists else existing_server_public_key.stdout }}"
- name: Save WireGuard server public key
copy:
content: "{{ server_public_key_value }}"
dest: "{{ wireguard_public_key_file }}"
mode: '0644'
owner: root
group: root
- name: Enable IP forwarding
sysctl:
name: net.ipv4.ip_forward
value: '1'
state: present
sysctl_set: yes
reload: yes
when: wireguard_enable_ip_forwarding
- name: Make IP forwarding persistent
lineinfile:
path: /etc/sysctl.conf
regexp: '^net\.ipv4\.ip_forward'
line: 'net.ipv4.ip_forward=1'
state: present
when: wireguard_enable_ip_forwarding
- name: Get server external IP address
uri:
url: https://api.ipify.org
return_content: yes
register: server_external_ip
changed_when: false
failed_when: false
- name: Set server external IP from inventory if API fails
set_fact:
server_external_ip_content: "{{ ansible_host | default(server_external_ip.content | default('')) }}"
when: server_external_ip.content is defined
- name: Get server external IP from ansible_host
set_fact:
server_external_ip_content: "{{ ansible_host }}"
when: server_external_ip.content is not defined
- name: Read server private key for config
slurp:
src: "{{ wireguard_private_key_file }}"
register: server_private_key_file_content
when: server_private_key_exists.stat.exists
- name: Set server private key for template (new key)
set_fact:
server_private_key_for_config: "{{ server_private_key.stdout }}"
when: not server_private_key_exists.stat.exists
- name: Set server private key for template (existing key)
set_fact:
server_private_key_for_config: "{{ server_private_key_file_content.content | b64decode | trim }}"
when: server_private_key_exists.stat.exists
- name: Get network interface name
shell: "ip route | grep default | awk '{print $5}' | head -1"
register: default_interface
changed_when: false
failed_when: false
- name: Set default interface
set_fact:
wireguard_interface_name: "{{ default_interface.stdout | default('eth0') }}"
- name: Check if WireGuard config exists
stat:
path: "{{ wireguard_config_file }}"
register: wireguard_config_exists
- name: Create WireGuard server configuration
template:
src: "{{ playbook_dir }}/../templates/wireguard-server.conf.j2"
dest: "{{ wireguard_config_file }}"
mode: '0600'
owner: root
group: root
notify: restart wireguard
- name: Check if WireGuard service is enabled
systemd:
name: "wg-quick@{{ wireguard_interface }}"
register: wireguard_service_status
failed_when: false
changed_when: false
- name: Enable WireGuard service
systemd:
name: "wg-quick@{{ wireguard_interface }}"
enabled: yes
daemon_reload: yes
when: not wireguard_service_status.status.ActiveState is defined or wireguard_service_status.status.ActiveState != 'active'
- name: Start WireGuard service
systemd:
name: "wg-quick@{{ wireguard_interface }}"
state: started
notify: restart wireguard
- name: Check if UFW firewall is installed
command: which ufw
register: ufw_installed
changed_when: false
failed_when: false
- name: Verify SSH access is allowed in UFW
command: "ufw status | grep -q '22/tcp' || echo 'SSH not found'"
register: ssh_ufw_check
changed_when: false
failed_when: false
when: ufw_installed.rc == 0
- name: Warn if SSH is not explicitly allowed
debug:
msg: |
?? WARNING: SSH (port 22) might not be explicitly allowed in UFW!
Please ensure SSH access is configured before proceeding.
Run: sudo ufw allow 22/tcp
when: ufw_installed.rc == 0 and 'SSH not found' in ssh_ufw_check.stdout
- name: Allow WireGuard port in UFW firewall
ufw:
rule: allow
port: "{{ wireguard_port }}"
proto: udp
comment: "WireGuard VPN"
when: ufw_installed.rc == 0
- name: Allow WireGuard port in UFW firewall (alternative)
shell: "ufw allow {{ wireguard_port }}/udp comment 'WireGuard VPN'"
when: ufw_installed.rc == 0
failed_when: false
changed_when: false
- name: Check WireGuard status
command: "wg show {{ wireguard_interface }}"
register: wireguard_status
changed_when: false
failed_when: false
- name: Display WireGuard status
debug:
msg: |
WireGuard Status:
{{ wireguard_status.stdout if wireguard_status.rc == 0 else 'WireGuard interface not active' }}
- name: Display server public key
debug:
msg: |
========================================
WireGuard Server Setup Complete!
========================================
Server Public Key:
{{ server_public_key_value }}
Server IP: {{ wireguard_server_ip }}
Server Endpoint: {{ server_external_ip_content }}:{{ wireguard_port }}
Network: {{ wireguard_network }}
To add a client, run:
ansible-playbook -i inventory/production.yml playbooks/add-wireguard-client.yml -e "client_name=myclient"
Client configs are stored in:
{{ wireguard_client_configs_path }}/
========================================
handlers:
- name: restart wireguard
systemd:
name: "wg-quick@{{ wireguard_interface }}"
state: restarted

View File

@@ -1,168 +0,0 @@
---
- name: Test WireGuard Connection from Docker Container
hosts: production
become: yes
gather_facts: yes
vars:
test_container_name: "wireguard-test-client"
wireguard_config_path: "/tmp/wireguard-test"
tasks:
- name: Validate client name
fail:
msg: "client_name is required. Usage: ansible-playbook ... -e 'client_name=grafana-test'"
when: client_name is not defined or client_name == ""
- name: Check if WireGuard client config exists
stat:
path: "{{ playbook_dir }}/../wireguard-clients/{{ client_name }}.conf"
register: client_config_exists
delegate_to: localhost
become: no
- name: Fail if client config not found
fail:
msg: "Client config not found: {{ playbook_dir }}/../wireguard-clients/{{ client_name }}.conf"
when: not client_config_exists.stat.exists
- name: Read client config
slurp:
src: "{{ playbook_dir }}/../wireguard-clients/{{ client_name }}.conf"
register: client_config_content
delegate_to: localhost
become: no
- name: Extract client IP from config
set_fact:
client_vpn_ip: "{{ (client_config_content.content | b64decode | regex_findall('Address\\s*=\\s*([0-9.]+)') | first) | default('10.8.0.7') }}"
failed_when: false
- name: Display extracted client IP
debug:
msg: "Client VPN IP: {{ client_vpn_ip }}"
- name: Stop and remove existing test container
shell: |
docker stop {{ test_container_name }} || true
docker rm {{ test_container_name }} || true
args:
executable: /bin/bash
ignore_errors: yes
failed_when: false
- name: Create temporary directory for WireGuard config
file:
path: "{{ wireguard_config_path }}"
state: directory
mode: '0700'
- name: Copy client config to server
copy:
content: "{{ client_config_content.content | b64decode }}"
dest: "{{ wireguard_config_path }}/{{ client_name }}.conf"
mode: '0600'
- name: Start WireGuard test container
shell: |
docker run -d \
--name {{ test_container_name }} \
--cap-add=NET_ADMIN \
--cap-add=SYS_MODULE \
--sysctl net.ipv4.conf.all.src_valid_mark=1 \
-v {{ wireguard_config_path }}/{{ client_name }}.conf:/etc/wireguard/{{ client_name }}.conf:ro \
--device /dev/net/tun \
ghcr.io/linuxserver/wireguard:latest
args:
executable: /bin/bash
register: container_result
ignore_errors: yes
- name: Wait for container to start
pause:
seconds: 5
- name: Check container status
shell: docker ps -a --filter "name={{ test_container_name }}" --format "{{ '{{' }}.Status{{ '}}' }}"
register: container_status
failed_when: false
- name: Display container status
debug:
msg: "Container Status: {{ container_status.stdout }}"
- name: Get container logs
shell: docker logs {{ test_container_name }} --tail 50
register: container_logs
failed_when: false
- name: Display container logs
debug:
msg: "{{ container_logs.stdout_lines }}"
- name: Test ping to VPN server from container
shell: |
docker exec {{ test_container_name }} ping -c 4 10.8.0.1 || true
register: ping_result
failed_when: false
- name: Display ping result
debug:
msg: "{{ ping_result.stdout_lines }}"
- name: Test curl to Grafana from container
shell: |
docker exec {{ test_container_name }} curl -s -o /dev/null -w "%{http_code}" --max-time 10 https://grafana.michaelschiemer.de/ || echo "FAILED"
register: curl_result
failed_when: false
- name: Display curl result
debug:
msg: "HTTP Status Code: {{ curl_result.stdout }}"
- name: Get container IP
shell: |
docker exec {{ test_container_name }} ip addr show wg0 | grep "inet " | awk '{print $2}' | cut -d/ -f1 || echo "No WireGuard IP"
register: container_wg_ip
failed_when: false
- name: Display container WireGuard IP
debug:
msg: "Container WireGuard IP: {{ container_wg_ip.stdout }}"
- name: Test DNS resolution from container
shell: |
docker exec {{ test_container_name }} nslookup grafana.michaelschiemer.de || true
register: dns_result
failed_when: false
- name: Display DNS result
debug: "{{ dns_result.stdout_lines }}"
- name: Check Traefik logs for container access
shell: |
cd ~/deployment/stacks/traefik
tail -100 logs/access.log | grep -i grafana | tail -10 | grep -oP '"ClientHost":"[^"]*"' | sed 's/"ClientHost":"//;s/"//' | sort -u
register: traefik_client_ips
failed_when: false
- name: Display Traefik client IPs
debug:
msg: "{{ traefik_client_ips.stdout_lines }}"
- name: Cleanup instructions
debug:
msg: |
========================================
TEST ABGESCHLOSSEN
========================================
Container-Name: {{ test_container_name }}
Um Container zu entfernen:
docker stop {{ test_container_name }}
docker rm {{ test_container_name }}
Um Config zu entfernen:
rm -rf {{ wireguard_config_path }}
========================================

View File

@@ -0,0 +1,212 @@
---
- name: Configure WireGuard split tunnel routing
hosts: production
become: true
gather_facts: true
vars:
wg_interface: wg0
wg_addr: 10.8.0.1/24
wg_net: 10.8.0.0/24
wan_interface: eth0
listening_port: 51820
extra_nets:
- 192.168.178.0/24
- 172.20.0.0/16
firewall_backend: iptables # or nftables
manage_ufw: false
manage_firewalld: false
firewalld_zone: public
pre_tasks:
- name: Ensure required collections are installed (documentation note)
debug:
msg: >
Install collections if missing:
ansible-galaxy collection install ansible.posix community.general
when: false
tasks:
- name: Ensure WireGuard config directory exists
ansible.builtin.file:
path: "/etc/wireguard"
state: directory
mode: "0700"
owner: root
group: root
- name: Persist IPv4 forwarding
ansible.builtin.copy:
dest: "/etc/sysctl.d/99-{{ wg_interface }}-forward.conf"
owner: root
group: root
mode: "0644"
content: |
# Managed by Ansible - WireGuard {{ wg_interface }}
net.ipv4.ip_forward=1
- name: Enable IPv4 forwarding runtime
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: "1"
state: present
reload: true
- name: Configure MASQUERADE (iptables)
community.general.iptables:
table: nat
chain: POSTROUTING
out_interface: "{{ wan_interface }}"
source: "{{ wg_net }}"
jump: MASQUERADE
state: present
when: firewall_backend == "iptables"
- name: Allow forwarding wg -> wan (iptables)
community.general.iptables:
table: filter
chain: FORWARD
in_interface: "{{ wg_interface }}"
out_interface: "{{ wan_interface }}"
source: "{{ wg_net }}"
jump: ACCEPT
state: present
when: firewall_backend == "iptables"
- name: Allow forwarding wan -> wg (iptables)
community.general.iptables:
table: filter
chain: FORWARD
out_interface: "{{ wg_interface }}"
destination: "{{ wg_net }}"
ctstate: RELATED,ESTABLISHED
jump: ACCEPT
state: present
when: firewall_backend == "iptables"
- name: Allow forwarding to extra nets (iptables)
community.general.iptables:
table: filter
chain: FORWARD
in_interface: "{{ wg_interface }}"
destination: "{{ item }}"
jump: ACCEPT
state: present
loop: "{{ extra_nets }}"
when: firewall_backend == "iptables"
- name: Allow return from extra nets (iptables)
community.general.iptables:
table: filter
chain: FORWARD
source: "{{ item }}"
out_interface: "{{ wg_interface }}"
ctstate: RELATED,ESTABLISHED
jump: ACCEPT
state: present
loop: "{{ extra_nets }}"
when: firewall_backend == "iptables"
- name: Deploy nftables WireGuard rules
ansible.builtin.template:
src: "{{ playbook_dir }}/../templates/wireguard-nftables.nft.j2"
dest: "/etc/nftables.d/wireguard-{{ wg_interface }}.nft"
owner: root
group: root
mode: "0644"
when: firewall_backend == "nftables"
notify: Reload nftables
- name: Ensure nftables main config includes WireGuard rules
ansible.builtin.lineinfile:
path: /etc/nftables.conf
regexp: '^include "/etc/nftables.d/wireguard-{{ wg_interface }}.nft";$'
line: 'include "/etc/nftables.d/wireguard-{{ wg_interface }}.nft";'
create: true
when: firewall_backend == "nftables"
notify: Reload nftables
- name: Manage UFW forward policy
ansible.builtin.lineinfile:
path: /etc/default/ufw
regexp: '^DEFAULT_FORWARD_POLICY='
line: 'DEFAULT_FORWARD_POLICY="ACCEPT"'
when: manage_ufw
- name: Allow WireGuard port in UFW
community.general.ufw:
rule: allow
port: "{{ listening_port }}"
proto: udp
comment: "WireGuard VPN"
when: manage_ufw
- name: Allow routed traffic via UFW (wg -> wan)
ansible.builtin.command:
cmd: "ufw route allow in on {{ wg_interface }} out on {{ wan_interface }} to any"
register: ufw_route_result
changed_when: "'Skipping' not in ufw_route_result.stdout"
when: manage_ufw
- name: Allow extra nets via UFW
ansible.builtin.command:
cmd: "ufw route allow in on {{ wg_interface }} to {{ item }}"
loop: "{{ extra_nets }}"
register: ufw_extra_result
changed_when: "'Skipping' not in ufw_extra_result.stdout"
when: manage_ufw
- name: Allow WireGuard port in firewalld
ansible.posix.firewalld:
zone: "{{ firewalld_zone }}"
port: "{{ listening_port }}/udp"
permanent: true
state: enabled
when: manage_firewalld
- name: Enable firewalld masquerade
ansible.posix.firewalld:
zone: "{{ firewalld_zone }}"
masquerade: true
permanent: true
state: enabled
when: manage_firewalld
- name: Allow forwarding from WireGuard via firewalld
ansible.posix.firewalld:
permanent: true
state: enabled
immediate: false
rich_rule: 'rule family="ipv4" source address="{{ wg_net }}" accept'
when: manage_firewalld
- name: Allow extra nets via firewalld
ansible.posix.firewalld:
permanent: true
state: enabled
immediate: false
rich_rule: 'rule family="ipv4" source address="{{ item }}" accept'
loop: "{{ extra_nets }}"
when: manage_firewalld
- name: Ensure wg-quick service enabled and restarted
ansible.builtin.systemd:
name: "wg-quick@{{ wg_interface }}"
enabled: true
state: restarted
- name: Show WireGuard status
ansible.builtin.command: "wg show {{ wg_interface }}"
register: wg_status
changed_when: false
failed_when: false
- name: Render routing summary
ansible.builtin.debug:
msg: |
WireGuard routing updated for {{ wg_interface }}
{{ wg_status.stdout }}
handlers:
- name: Reload nftables
ansible.builtin.command: nft -f /etc/nftables.conf

View File

@@ -0,0 +1,156 @@
#!/usr/bin/env bash
# setup-wireguard-routing.sh
# Idempotent WireGuard split-tunnel routing helper.
# Detects iptables/nftables and optional UFW/Firewalld to configure forwarding + NAT.
set -euo pipefail
WAN_IF=${WAN_IF:-eth0}
WG_IF=${WG_IF:-wg0}
WG_NET=${WG_NET:-10.8.0.0/24}
WG_ADDR=${WG_ADDR:-10.8.0.1/24}
WG_PORT=${WG_PORT:-51820}
EXTRA_NETS_DEFAULT="192.168.178.0/24 172.20.0.0/16"
EXTRA_NETS="${EXTRA_NETS:-$EXTRA_NETS_DEFAULT}"
FIREWALL_BACKEND=${FIREWALL_BACKEND:-auto}
FIREWALLD_ZONE=${FIREWALLD_ZONE:-public}
read -r -a EXTRA_NETS_ARRAY <<< "${EXTRA_NETS}"
abort() {
echo "Error: $1" >&2
exit 1
}
require_root() {
if [[ "${EUID}" -ne 0 ]]; then
abort "please run as root (sudo ./setup-wireguard-routing.sh)"
fi
}
detect_backend() {
case "${FIREWALL_BACKEND}" in
iptables|nftables) echo "${FIREWALL_BACKEND}"; return 0 ;;
auto)
if command -v nft >/dev/null 2>&1; then
echo "nftables"; return 0
fi
if command -v iptables >/dev/null 2>&1; then
echo "iptables"; return 0
fi
;;
esac
abort "no supported firewall backend found (install iptables or nftables)"
}
ensure_sysctl() {
local sysctl_file="/etc/sysctl.d/99-${WG_IF}-forward.conf"
cat <<EOF > "${sysctl_file}"
# Managed by setup-wireguard-routing.sh
net.ipv4.ip_forward=1
EOF
sysctl -w net.ipv4.ip_forward=1 >/dev/null
sysctl --system >/dev/null
}
apply_iptables() {
iptables -t nat -C POSTROUTING -s "${WG_NET}" -o "${WAN_IF}" -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -s "${WG_NET}" -o "${WAN_IF}" -j MASQUERADE
iptables -C FORWARD -i "${WG_IF}" -s "${WG_NET}" -o "${WAN_IF}" -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -i "${WG_IF}" -s "${WG_NET}" -o "${WAN_IF}" -j ACCEPT
iptables -C FORWARD -o "${WG_IF}" -d "${WG_NET}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -o "${WG_IF}" -d "${WG_NET}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
for net in "${EXTRA_NETS_ARRAY[@]}"; do
[[ -z "${net}" ]] && continue
iptables -C FORWARD -i "${WG_IF}" -d "${net}" -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -i "${WG_IF}" -d "${net}" -j ACCEPT
iptables -C FORWARD -s "${net}" -o "${WG_IF}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -s "${net}" -o "${WG_IF}" -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
done
}
apply_nftables() {
local table="inet wireguard_${WG_IF}"
nft list table ${table} >/dev/null 2>&1 || nft add table ${table}
nft list chain ${table} postrouting >/dev/null 2>&1 || \
nft add chain ${table} postrouting '{ type nat hook postrouting priority srcnat; }'
nft list chain ${table} forward >/dev/null 2>&1 || \
nft add chain ${table} forward '{ type filter hook forward priority filter; policy accept; }'
nft list chain ${table} postrouting | grep -q "oifname \"${WAN_IF}\" ip saddr ${WG_NET}" || \
nft add rule ${table} postrouting oifname "${WAN_IF}" ip saddr ${WG_NET} masquerade
nft list chain ${table} forward | grep -q "iifname \"${WG_IF}\" ip saddr ${WG_NET}" || \
nft add rule ${table} forward iifname "${WG_IF}" ip saddr ${WG_NET} counter accept
nft list chain ${table} forward | grep -q "oifname \"${WG_IF}\" ip daddr ${WG_NET}" || \
nft add rule ${table} forward oifname "${WG_IF}" ip daddr ${WG_NET} ct state established,related counter accept
for net in "${EXTRA_NETS_ARRAY[@]}"; do
[[ -z "${net}" ]] && continue
nft list chain ${table} forward | grep -q "iifname \"${WG_IF}\" ip daddr ${net}" || \
nft add rule ${table} forward iifname "${WG_IF}" ip daddr ${net} counter accept
done
}
configure_ufw() {
if command -v ufw >/dev/null 2>&1 && ufw status | grep -iq "Status: active"; then
sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
ufw allow "${WG_PORT}"/udp >/dev/null
ufw route allow in on "${WG_IF}" out on "${WAN_IF}" to any >/dev/null 2>&1 || true
for net in "${EXTRA_NETS_ARRAY[@]}"; do
[[ -z "${net}" ]] && continue
ufw route allow in on "${WG_IF}" to "${net}" >/dev/null 2>&1 || true
done
ufw reload >/dev/null
fi
}
configure_firewalld() {
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
firewall-cmd --permanent --zone="${FIREWALLD_ZONE}" --add-port=${WG_PORT}/udp >/dev/null
firewall-cmd --permanent --zone="${FIREWALLD_ZONE}" --add-masquerade >/dev/null
firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 \
"iif ${WG_IF} oif ${WAN_IF} -s ${WG_NET} -j ACCEPT" >/dev/null 2>&1 || true
firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 \
"oif ${WG_IF} -d ${WG_NET} -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT" >/dev/null 2>&1 || true
for net in "${EXTRA_NETS_ARRAY[@]}"; do
[[ -z "${net}" ]] && continue
firewall-cmd --permanent --direct --add-rule ipv4 filter FORWARD 0 \
"iif ${WG_IF} -d ${net} -j ACCEPT" >/dev/null 2>&1 || true
done
firewall-cmd --reload >/dev/null
fi
}
ensure_service() {
systemctl enable "wg-quick@${WG_IF}" >/dev/null
systemctl restart "wg-quick@${WG_IF}"
}
show_status() {
echo "WireGuard routing configured with ${WG_IF} (${WG_ADDR}) via ${WAN_IF}"
wg show "${WG_IF}" || true
ip route show table main | grep "${WG_NET}" || true
}
main() {
require_root
ensure_sysctl
backend=$(detect_backend)
case "${backend}" in
iptables) apply_iptables ;;
nftables) apply_nftables ;;
esac
configure_ufw
configure_firewalld
ensure_service
show_status
}
main "$@"

View File

@@ -0,0 +1,50 @@
# WireGuard Server Configuration
# Interface: wg0
# Network: {{ wg_network }}
# Server IP: {{ wg_server_ip }}
[Interface]
PrivateKey = {{ wg_server_private_key }}
Address = {{ wg_server_ip }}/{{ wg_netmask }}
ListenPort = {{ wg_port | default(51820) }}
# Enable IP forwarding for VPN routing
PostUp = sysctl -w net.ipv4.ip_forward=1
# nftables: Setup VPN routing and firewall
PostUp = nft add table inet wireguard
PostUp = nft add chain inet wireguard postrouting { type nat hook postrouting priority srcnat\; }
PostUp = nft add chain inet wireguard forward { type filter hook forward priority filter\; }
# NAT for VPN traffic (masquerade to WAN)
PostUp = nft add rule inet wireguard postrouting oifname "{{ wan_interface }}" ip saddr {{ wg_network }} masquerade
# Allow VPN traffic forwarding
PostUp = nft add rule inet wireguard forward iifname "wg0" ip saddr {{ wg_network }} accept
PostUp = nft add rule inet wireguard forward oifname "wg0" ip daddr {{ wg_network }} ct state established,related accept
# Cleanup on shutdown
PostDown = nft delete table inet wireguard
# Peers (automatically managed)
# Format:
# [Peer]
# # Description: device-name
# PublicKey = peer_public_key
# PresharedKey = peer_preshared_key
# AllowedIPs = 10.8.0.X/32
# PersistentKeepalive = 25 # Optional: for clients behind NAT
{% for peer in wg_peers | default([]) %}
[Peer]
# {{ peer.name }}
PublicKey = {{ peer.public_key }}
{% if peer.preshared_key is defined %}
PresharedKey = {{ peer.preshared_key }}
{% endif %}
AllowedIPs = {{ peer.allowed_ips }}
{% if peer.persistent_keepalive | default(true) %}
PersistentKeepalive = 25
{% endif %}
{% endfor %}

View File

@@ -1,29 +0,0 @@
# WireGuard Client Configuration for {{ client_name }}
# Generated by Ansible - DO NOT EDIT MANUALLY
[Interface]
# Client private key
PrivateKey = {{ client_private_key.stdout }}
# Client IP address in VPN network
Address = {{ client_ip }}/24
{% if wireguard_dns_servers | length > 0 %}
# DNS servers provided via Ansible (optional)
DNS = {{ wireguard_dns_servers | join(', ') }}
{% endif %}
[Peer]
# Server public key
PublicKey = {{ server_public_key_cmd.stdout }}
# Server endpoint
Endpoint = {{ server_external_ip_content }}:{{ wireguard_port }}
# Allowed IPs (routes through VPN)
# IMPORTANT: Only VPN network is routed through VPN by default
# SSH access via normal IP ({{ server_external_ip_content }}) remains available
AllowedIPs = {{ allowed_ips }}
# Keep connection alive
PersistentKeepalive = 25

View File

@@ -0,0 +1,116 @@
#!/usr/sbin/nft -f
# WireGuard VPN Firewall Rules
# Purpose: Isolate admin services behind VPN, allow public access only to ports 80, 443, 22
# Generated by Ansible - DO NOT EDIT MANUALLY
table inet wireguard_firewall {
# Define sets for easy management
set vpn_network {
type ipv4_addr
flags interval
elements = { {{ wg_network }} }
}
set admin_service_ports {
type inet_service
elements = {
8080, # Traefik Dashboard
9090, # Prometheus
3001, # Grafana
9000, # Portainer
8001, # Redis Insight
{% for port in additional_admin_ports | default([]) %}
{{ port }}, # {{ port }}
{% endfor %}
}
}
set public_service_ports {
type inet_service
elements = {
80, # HTTP
443, # HTTPS
22, # SSH
{% for port in additional_public_ports | default([]) %}
{{ port }}, # {{ port }}
{% endfor %}
}
}
# Input chain - Handle incoming traffic
chain input {
type filter hook input priority filter; policy drop;
# Allow established/related connections
ct state established,related accept
# Allow loopback
iifname "lo" accept
# Allow ICMP (ping)
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
# Allow SSH (public)
tcp dport 22 accept
# Allow WireGuard port (public)
udp dport {{ wg_port | default(51820) }} accept comment "WireGuard VPN"
# Allow public web services (HTTP/HTTPS)
tcp dport @public_service_ports accept comment "Public services"
# Allow VPN network to access admin services
ip saddr @vpn_network tcp dport @admin_service_ports accept comment "VPN admin access"
# Block public access to admin services
tcp dport @admin_service_ports counter log prefix "BLOCKED_ADMIN_SERVICE: " drop
# Log and drop all other traffic
counter log prefix "BLOCKED_INPUT: " drop
}
# Forward chain - Handle routed traffic (VPN to services)
chain forward {
type filter hook forward priority filter; policy drop;
# Allow established/related connections
ct state established,related accept
# Allow VPN clients to access local services
iifname "wg0" ip saddr @vpn_network accept comment "VPN to services"
# Allow return traffic to VPN clients
oifname "wg0" ip daddr @vpn_network ct state established,related accept
# Log and drop all other forwarded traffic
counter log prefix "BLOCKED_FORWARD: " drop
}
# Output chain - Allow all outgoing traffic
chain output {
type filter hook output priority filter; policy accept;
}
# NAT chain - Masquerade VPN traffic to WAN
chain postrouting {
type nat hook postrouting priority srcnat;
# Masquerade VPN traffic going to WAN
oifname "{{ wan_interface }}" ip saddr @vpn_network masquerade comment "VPN NAT"
}
}
# Optional: Rate limiting for VPN port (DDoS protection)
{% if wg_enable_rate_limit | default(true) %}
table inet wireguard_ratelimit {
chain input {
type filter hook input priority -10;
# Rate limit WireGuard port: 10 connections per second per IP
udp dport {{ wg_port | default(51820) }} \
meter vpn_ratelimit { ip saddr limit rate over 10/second } \
counter log prefix "VPN_RATELIMIT: " drop
}
}
{% endif %}

View File

@@ -0,0 +1,15 @@
table inet wireguard_{{ wg_interface }} {
chain postrouting {
type nat hook postrouting priority srcnat;
oifname "{{ wan_interface }}" ip saddr {{ wg_net }} masquerade
}
chain forward {
type filter hook forward priority filter;
iifname "{{ wg_interface }}" ip saddr {{ wg_net }} counter accept
oifname "{{ wg_interface }}" ip daddr {{ wg_net }} ct state established,related counter accept
{% for net in extra_nets %}
iifname "{{ wg_interface }}" ip daddr {{ net }} counter accept
{% endfor %}
}
}

View File

@@ -1,22 +0,0 @@
# WireGuard Server Configuration
# Generated by Ansible - DO NOT EDIT MANUALLY
[Interface]
# Server private key
PrivateKey = {{ server_private_key_for_config }}
# Server IP address in VPN network
Address = {{ wireguard_server_ip }}/24
# Port to listen on
ListenPort = {{ wireguard_port }}
# Enable NAT for VPN clients to access internet
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o {{ wireguard_interface_name }} -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o {{ wireguard_interface_name }} -j MASQUERADE
# Clients will be added here by the add-wireguard-client playbook
# Example:
# [Peer]
# PublicKey = <client_public_key>
# AllowedIPs = 10.8.0.2/32

View File

@@ -0,0 +1,14 @@
[Interface]
# Client: michael-pc
# Generated: 2025-11-05T01:02:14Z
PrivateKey = MHgxUzmEHQ15EB3v4TaXEcJAZNRaBd54/ZDcN6nN8lI=
Address = 10.8.0.2/32
DNS = 1.1.1.1, 8.8.8.8
[Peer]
# WireGuard Server
PublicKey = SFxxHe4bunfQ1Xid5AMXbBgY+AjlxNtRHQ5uYjSib3E=
PresharedKey = WsnvFp6WrF/y9fQwn3RgOTmwMS2UHoqIBRKrTPZ5lW8=
Endpoint = 94.16.110.151:51820
AllowedIPs = 10.8.0.0/24
PersistentKeepalive = 25