Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
megalinter-reports/
inventories/production
.galaxy/
inventories/test/wireguard_configs
1 change: 1 addition & 0 deletions ansible.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ inventory = inventories/default/hosts
interpreter_python = /usr/bin/python3.12
no_log = ${ANSIBLE_NO_LOG:true}
inventory_ignore_patterns = ^wireguard_configs/.*$,^netplan_configs/.*$,^ssh_keys/.*$
inventory_ignore_extensions = .orig,.bak,.swp,.vaulted
1 change: 1 addition & 0 deletions inventories/test/group_vars/all/vars.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,4 @@ bootstrap_user: "test-bootstrap"
dns1: 8.8.8.8
dns2: 8.8.4.4
disable_ipv6: false
manage_ufw: false
8 changes: 6 additions & 2 deletions inventories/test/group_vars/wireguard/vars.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
---
wireguard_port: 51821
wireguard_port: 51842
wireguard_endpoint: "vpn.example.com"
wireguard_allowed_ips: "10.10.0.1/32, 192.168.0.1/24"
wireguard_allowed_lan_ips: "10.10.0.1/32, 192.168.0.1/24"
wireguard_dns_servers: [8.8.8.8]
wireguard_client_peers:
- name: "alice"
ip: "10.10.0.2/32"
- name: "bob"
ip: "10.10.0.3/32"
# default is inventory_dir/wireguard_configs
# moved outside of inventory_dir since this breaks molecule tests
local_wireguard_config_dir: "/tmp/molecule/wireguard_configs"
3 changes: 3 additions & 0 deletions inventories/test/hosts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
[example_group]
#testhost ansible_host=10.42.42.42

[wireguard]
testhost-ubuntu-24.04
Empty file.
2 changes: 1 addition & 1 deletion molecule/wireguard/converge.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
- name: Converge
hosts: all
hosts: wireguard
become: yes

roles:
Expand Down
1 change: 1 addition & 0 deletions molecule/wireguard/molecule.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ provisioner:
group_vars: "../../inventories/test/group_vars/"
host_vars: "../../inventories/test/host_vars/"
hosts: "../../inventories/test/hosts"

19 changes: 19 additions & 0 deletions molecule/wireguard/tests/test_wireguard-client-configs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from pathlib import Path

def test_wireguard_config_dir_exists_locally():
client_config_dir: Path = Path("/tmp/molecule/wireguard_configs/clients/alice")

assert client_config_dir.exists(), f"{client_config_dir} missing on controller"
assert client_config_dir.is_dir(), f"{client_config_dir} is not a directory on controller"

def test_wireguard_client_config():
client_config_dir: Path = Path("/tmp/molecule/wireguard_configs/clients/alice")

assert client_config_dir.exists(), f"{client_config_dir} missing on controller"
assert client_config_dir.is_dir(), f"{client_config_dir} is not a directory on controller"

client_all_config_file: Path = client_config_dir / "vpn-acme-all.conf.vaulted"
assert client_all_config_file.exists(), f"{client_all_config_file} missing in local inventory"

client_lan_config_file: Path = client_config_dir / "vpn-acme-lan.conf.vaulted"
assert client_lan_config_file.exists(), f"{client_lan_config_file} missing in local inventory"
45 changes: 35 additions & 10 deletions molecule/wireguard/tests/test_wireguard-server.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import re
import pytest

def test_wireguard_config_file_exists(host):
config_file = host.file("/etc/wireguard/wg0.conf")

Expand All @@ -6,7 +9,6 @@ def test_wireguard_config_file_exists(host):


def test_wireguard_config_file_permissions(host):
"""Test that the WireGuard configuration file has correct permissions."""
config_file = host.file("/etc/wireguard/wg0.conf")

# WireGuard config files should be readable only by root for security
Expand All @@ -15,15 +17,38 @@ def test_wireguard_config_file_permissions(host):
assert config_file.mode == 0o600


def test_wireguard_config_file_content(host):
"""Test that the WireGuard configuration file contains expected content."""
def test_wireguard_server_conf_content(host):
config_file = host.file("/etc/wireguard/wg0.conf")

content = config_file.content_string
wg0_conf_content = config_file.content_string

# server part
assert "[Interface]" in wg0_conf_content
assert "# Ansible managed" in wg0_conf_content
assert "Address = 10.10.0.1/24" in wg0_conf_content
assert "ListenPort = 51842" in wg0_conf_content
assert "PrivateKey = " in wg0_conf_content
assert "DNS = 8.8.8.8" in wg0_conf_content

# client peer part
alice_peer = (
r'# BEGIN peer alice\n'
r'\[Peer\]\n'
r'PublicKey = [A-Za-z0-9+/]{43}=\n'
r'AllowedIPs = 10\.10\.0\.2/32\n'
r'PreSharedKey = [A-Za-z0-9+/]{43}=\n'
r'# END peer alice'
)

assert re.search(alice_peer, wg0_conf_content), "alice peer section not found or invalid format"

bob_peer = (
r'# BEGIN peer bob\n'
r'\[Peer\]\n'
r'PublicKey = [A-Za-z0-9+/]{43}=\n'
r'AllowedIPs = 10\.10\.0\.3/32\n'
r'PreSharedKey = [A-Za-z0-9+/]{43}=\n'
r'# END peer bob'
)

# Check for required sections and parameters
assert "[Interface]" in content
assert "# Ansible managed" in content
assert "Address = 10.10.0.1/24" in content
assert "ListenPort = 51820" in content
assert "PrivateKey = " in content
assert re.search(bob_peer, wg0_conf_content), "bob peer section not found or invalid format"
28 changes: 28 additions & 0 deletions roles/wireguard/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Wireguard

this role takes care of installing and configuring wireguard:

- installs wireguard on the server
- generates the server config
- generates the client config (stored in local inventory)
- sets up ufw config (NAT) for wireguard (ufw rules have to configured in ufw role)

## Client config

configure clients in inventory `group_vars/wireguard` like so:

```yaml
wireguard_client_peers:
- name: "alice"
ip: "10.10.0.2/32"
```

configs are stored and encrypted (ansible-vault) locally in `inventories/production/wireguard_configs/clients`

to generate the QR code for the client config, run:

`ansible-vault view vpn-acme-all.conf.vault | qrencode -t ansiutf8`

or import the config into network manager after decrypting:

`nmcli connection import type wireguard file vpn-acme-all.conf`
9 changes: 7 additions & 2 deletions roles/wireguard/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,27 @@
---
wireguard_company: "acme"
wireguard_interface: wg0

wireguard_internal_server_ip: "10.10.0.1/24"
wireguard_subnet: "10.10.0.0/24"
wireguard_external_interface: "eth0"
wireguard_port: 51820

# Peers configuration
wireguard_client_peers: []
# Example:
# wireguard_peers:
# wireguard_client_peers:
# - name: "alice"
# ip: "10.10.0.2/32"

# DNS servers (optional)
wireguard_dns: []
wireguard_dns_servers: [10.10.0.1]

# MTU (optional)
wireguard_mtu: ""

# PostUp and PostDown commands (optional)
wireguard_post_up: []
wireguard_post_down: []

manage_ufw: true
4 changes: 4 additions & 0 deletions roles/wireguard/handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@
ansible.builtin.systemd:
name: "wg-quick@{{ wireguard_interface }}"
state: restarted

- name: reload ufw
community.general.ufw:
state: reloaded
81 changes: 4 additions & 77 deletions roles/wireguard/tasks/2_server-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,79 +7,12 @@
owner: root
group: root

- name: Set wg config file path
ansible.builtin.set_fact:
wireguard_config_path: "/etc/wireguard/{{ wireguard_interface }}.conf"

- name: Check if WireGuard config exists
stat:
path: "{{ wireguard_config_path }}"
register: wireguard_config_stat

- name: When WireGuard config exists, extract existing key
when: wireguard_config_stat.stat.exists
block:
- name: Extract WireGuard private key if present
shell: "grep '^PrivateKey' {{ wireguard_config_path }} | awk '{print $3}'"
register: wireguard_private_key
changed_when: false
when: wireguard_config_stat.stat.exists
- name: Set private key fact
ansible.builtin.set_fact:
wireguard_private_key: "{{ wireguard_private_key.stdout }}"
no_log: true
rescue:
- name: there was an error extracting the private key
debug:
msg: "there was an error extracting wireguard private key in existing config on {{ inventory_hostname }}."
- name: Fail explicitly after extracting private key error
fail:
msg: "extracting wireguard private key failed on {{ inventory_hostname }}."
always:
- name: extract wireguard private key block finished (success or failure)
debug:
msg: "extracting wireguard private key block finished on {{ inventory_hostname }}."

- name: When WireGuard config does not yet exist
when: not wireguard_config_stat.stat.exists
block:
- name: Generate WireGuard private key
ansible.builtin.shell: |
wg genkey
register: wireguard_private_key_generated
changed_when: false
when: not wireguard_config_stat.stat.exists
no_log: true
- name: Set private key fact
ansible.builtin.set_fact:
wireguard_private_key: "{{ wireguard_private_key_generated.stdout }}"
no_log: true
rescue:
- name: there was an error generating the private key
debug:
msg: "there was an error generating wireguard private key on {{ inventory_hostname }}."
- name: Fail explicitly after wireguard private key generation error
fail:
msg: "wireguard private key generation failed failed on {{ inventory_hostname }}."
always:
- name: generating wireguard private key block finished (success or failure)
debug:
msg: "generating wireguard private key block finished on {{ inventory_hostname }}."

- name: Generate WireGuard public key
ansible.builtin.shell: |
echo "{{ wireguard_private_key }}" | wg pubkey
register: wireguard_public_key
changed_when: false
no_log: true

- name: Set public key fact
ansible.builtin.set_fact:
wireguard_public_key: "{{ wireguard_public_key.stdout }}"
- name: When WireGuard config exists, extract existing keys
include_tasks: "process-server-keys.yml"

- name: Display public key
ansible.builtin.debug:
msg: "WireGuard Public Key: {{ wireguard_public_key }}"
msg: "WireGuard Public Key: {{ wireguard_server_public_key }}"

- name: Deploy WireGuard interface configuration
ansible.builtin.template:
Expand All @@ -88,10 +21,4 @@
mode: '0600'
owner: root
group: root
notify: Restart WireGuard

- name: Enable and start WireGuard interface
ansible.builtin.systemd:
name: "wg-quick@{{ wireguard_interface }}"
enabled: true
state: started
changed_when: false
15 changes: 10 additions & 5 deletions roles/wireguard/tasks/3_clients.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
---
- name: Load common wireguard facts
include_tasks: "facts.yml"

- name: Set local_user
ansible.builtin.set_fact:
local_user: "{{ lookup('env', 'USER') }}"

- name: Set local_wireguard_config_dir
ansible.builtin.set_fact:
local_wireguard_config_dir: "{{ inventory_dir }}/wireguard_configs"

- name: Set local_wireguard_client_config_dir
ansible.builtin.set_fact:
local_wireguard_client_config_dir: "{{ inventory_dir }}/wireguard_configs/clients"
local_wireguard_client_config_dir: "{{ local_wireguard_config_dir }}/clients"

- name: Make sure client config directory exists for given inventory
delegate_to: localhost
Expand All @@ -20,3 +19,9 @@
owner: "{{ local_user }}"
group: "{{ local_user }}"
changed_when: false

- name: Create client config files
include_tasks: "create-client-config.yml"
loop_control:
loop_var: client
loop: "{{ wireguard_client_peers }}"
4 changes: 4 additions & 0 deletions roles/wireguard/tasks/4_ufw.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
- name: Setup wireguard ufw config
include_tasks: "ufw.yml"
when: manage_ufw
6 changes: 6 additions & 0 deletions roles/wireguard/tasks/5_service.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
- name: Enable and start WireGuard interface
ansible.builtin.systemd:
name: "wg-quick@{{ wireguard_interface }}"
enabled: true
state: restarted
22 changes: 15 additions & 7 deletions roles/wireguard/tasks/9_local-config-backup.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
---
- name: Load common wireguard facts
include_tasks: "facts.yml"

- name: Set local_user
ansible.builtin.set_fact:
local_user: "{{ lookup('env', 'USER') }}"
Expand All @@ -23,10 +26,8 @@

- name: Make sure wireguard_configs directory exists for given inventory
delegate_to: localhost
become: yes
become_user: "{{ local_user }}"
ansible.builtin.file:
path: "{{ inventory_dir }}/wireguard_configs"
path: "{{ local_wireguard_config_dir }}"
state: directory
mode: "0700"
owner: "{{ local_user }}"
Expand All @@ -50,13 +51,20 @@
run_once: true
changed_when: false

- name: Encrypt the fetched file with Ansible Vault
- name: Encrypt server config for if it has changed
delegate_to: localhost
become: yes
become_user: "{{ local_user }}"
command: "ansible-vault encrypt {{ local_tmp_server_bkp_config_path }} --output {{ inventory_dir }}/wireguard_configs/wg0.conf.vault"
ignore_errors: yes
run_once: true
shell: |
set -euo pipefail
if [ -f "{{ local_wireguard_config_dir }}/wg0.conf.vaulted" ]; then
if ansible-vault view "{{ local_wireguard_config_dir }}/wg0.conf.vaulted" | cmp -s - "{{ local_tmp_server_bkp_config_path }}"; then
exit 0
fi
fi
ansible-vault encrypt "{{ local_tmp_server_bkp_config_path }}" --output "{{ local_wireguard_config_dir }}/wg0.conf.vaulted"
args:
executable: /bin/bash
changed_when: false

- name: Remove the unencrypted fetched file
Expand Down
Loading
Loading