Introduction
WireGuard is the modern VPN protocol — faster, simpler, and more secure than OpenVPN or IPsec. Ansible automates WireGuard deployment across your infrastructure: generate key pairs, template configurations, manage peers, configure firewall rules, and build full mesh networks — all from a single playbook.
Prerequisites
# WireGuard is built into Linux kernel 5.6+
# For older kernels, install the module:
# Ubuntu/Debian
sudo apt install wireguard
# RHEL/CentOS
sudo dnf install wireguard-tools
Key Concepts
┌──────────────┐ WireGuard tunnel ┌──────────────┐
│ Server │◄─────────────────────►│ Client │
│ 10.0.0.1/24 │ UDP port 51820 │ 10.0.0.2/24 │
│ PublicKey: A │ │ PublicKey: B │
│ ListenPort: │ │ │
│ 51820 │ │ │
└──────────────┘ └──────────────┘
Each peer has:
- Private key — kept secret, never shared
- Public key — derived from private key, shared with peers
- Allowed IPs — which traffic routes through this peer
Point-to-Site VPN (Road Warrior)
Generate Keys
---
- name: Generate WireGuard keys
hosts: vpn_server
become: true
tasks:
- name: Install WireGuard
ansible.builtin.package:
name: wireguard-tools
state: present
- name: Generate server private key
ansible.builtin.shell: wg genkey
register: server_private_key
changed_when: false
no_log: true
- name: Derive server public key
ansible.builtin.shell: "echo '{{ server_private_key.stdout }}' | wg pubkey"
register: server_public_key
changed_when: false
- name: Generate client keys
ansible.builtin.shell: |
PRIV=$(wg genkey)
PUB=$(echo "$PRIV" | wg pubkey)
PSK=$(wg genpsk)
echo "$PRIV|$PUB|$PSK"
register: client_keys
changed_when: false
loop: "{{ wireguard_clients }}"
no_log: true
Configure Server
- name: Deploy WireGuard server config
hosts: vpn_server
become: true
vars:
wg_interface: wg0
wg_port: 51820
wg_network: 10.100.0.0/24
wg_server_ip: 10.100.0.1/24
server_public_ip: vpn.example.com
wireguard_clients:
- { name: laptop, ip: "10.100.0.2/32" }
- { name: phone, ip: "10.100.0.3/32" }
- { name: tablet, ip: "10.100.0.4/32" }
tasks:
- name: Deploy server config
ansible.builtin.template:
src: wg-server.conf.j2
dest: /etc/wireguard/{{ wg_interface }}.conf
mode: '0600'
notify: restart wireguard
- name: Enable IP forwarding
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
sysctl_set: true
reload: true
- name: Allow WireGuard port
ansible.posix.firewalld:
port: "{{ wg_port }}/udp"
permanent: true
state: enabled
immediate: true
- name: Enable WireGuard service
ansible.builtin.service:
name: "wg-quick@{{ wg_interface }}"
state: started
enabled: true
handlers:
- name: restart wireguard
ansible.builtin.service:
name: "wg-quick@{{ wg_interface }}"
state: restarted
# templates/wg-server.conf.j2
[Interface]
Address = {{ wg_server_ip }}
ListenPort = {{ wg_port }}
PrivateKey = {{ server_private_key }}
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
{% for client in wireguard_clients %}
# {{ client.name }}
[Peer]
PublicKey = {{ client.public_key }}
PresharedKey = {{ client.psk }}
AllowedIPs = {{ client.ip }}
{% endfor %}
Generate Client Configs
- name: Generate client configs
ansible.builtin.template:
src: wg-client.conf.j2
dest: "/etc/wireguard/clients/{{ item.name }}.conf"
mode: '0600'
loop: "{{ wireguard_clients }}"
# templates/wg-client.conf.j2
[Interface]
Address = {{ item.ip }}
PrivateKey = {{ item.private_key }}
DNS = 1.1.1.1, 1.0.0.1
[Peer]
PublicKey = {{ server_public_key }}
PresharedKey = {{ item.psk }}
Endpoint = {{ server_public_ip }}:{{ wg_port }}
AllowedIPs = 0.0.0.0/0 # Route all traffic through VPN
# AllowedIPs = 10.0.0.0/8 # Split tunnel — only private networks
PersistentKeepalive = 25
Site-to-Site VPN
---
- name: Site-to-site WireGuard VPN
hosts: vpn_gateways
become: true
vars:
sites:
site_a:
wg_ip: 10.200.0.1/30
public_ip: 203.0.113.1
local_network: 10.0.1.0/24
site_b:
wg_ip: 10.200.0.2/30
public_ip: 198.51.100.1
local_network: 10.0.2.0/24
tasks:
- name: Deploy WireGuard config
ansible.builtin.template:
src: wg-site.conf.j2
dest: /etc/wireguard/wg0.conf
mode: '0600'
notify: restart wireguard
- name: Enable IP forwarding
ansible.posix.sysctl:
name: net.ipv4.ip_forward
value: '1'
sysctl_set: true
# templates/wg-site.conf.j2 (for site_a)
[Interface]
Address = {{ sites[site_name].wg_ip }}
ListenPort = 51820
PrivateKey = {{ vault_wg_private_key }}
[Peer]
PublicKey = {{ peer_public_key }}
Endpoint = {{ peer_public_ip }}:51820
AllowedIPs = {{ peer_wg_ip }}, {{ peer_local_network }}
PersistentKeepalive = 25
Full Mesh VPN
For connecting all servers directly to each other:
---
- name: Deploy WireGuard full mesh
hosts: all
become: true
vars:
wg_port: 51820
wg_network: "10.200.0"
tasks:
- name: Assign mesh IP
ansible.builtin.set_fact:
wg_ip: "{{ wg_network }}.{{ groups['all'].index(inventory_hostname) + 1 }}/24"
- name: Deploy mesh config
ansible.builtin.template:
src: wg-mesh.conf.j2
dest: /etc/wireguard/wg0.conf
mode: '0600'
notify: restart wireguard
# templates/wg-mesh.conf.j2
[Interface]
Address = {{ wg_ip }}
ListenPort = {{ wg_port }}
PrivateKey = {{ vault_wg_keys[inventory_hostname].private }}
{% for host in groups['all'] if host != inventory_hostname %}
# {{ host }}
[Peer]
PublicKey = {{ vault_wg_keys[host].public }}
Endpoint = {{ hostvars[host].ansible_host }}:{{ wg_port }}
AllowedIPs = {{ wg_network }}.{{ groups['all'].index(host) + 1 }}/32
PersistentKeepalive = 25
{% endfor %}
Manage Peers Dynamically
Add a Peer
- name: Add new peer
ansible.builtin.command: >
wg set wg0 peer {{ new_peer_public_key }}
allowed-ips {{ new_peer_ip }}/32
preshared-key /tmp/psk
notify: save wireguard config
- name: Save config
ansible.builtin.command: wg-quick save wg0
Remove a Peer
- name: Remove peer
ansible.builtin.command: >
wg set wg0 peer {{ removed_peer_public_key }} remove
notify: save wireguard config
Monitor WireGuard
- name: Show WireGuard status
ansible.builtin.command: wg show wg0
register: wg_status
changed_when: false
- name: Check peer handshakes
ansible.builtin.shell: |
wg show wg0 latest-handshakes | while read key handshake; do
if [ "$handshake" -lt "$(date -d '5 minutes ago' +%s)" ] 2>/dev/null; then
echo "STALE: $key last handshake $(date -d @$handshake)"
fi
done
register: stale_peers
changed_when: false
Troubleshooting
No Handshake
- Verify UDP port is open:
ss -ulnp | grep 51820 - Check firewall allows UDP 51820
- Verify endpoint IP/DNS resolves correctly
- Check public keys match on both sides
Can't Reach Remote Network
# Check routing
ip route show
# Verify forwarding
sysctl net.ipv4.ip_forward
# Check WireGuard interface
wg show wg0
Related Articles
Conclusion
Ansible automates WireGuard VPN deployment for any topology — point-to-site for remote access, site-to-site for office connectivity, or full mesh for server-to-server communication. Template configurations from inventory variables, manage keys with Ansible Vault, and dynamically add/remove peers. WireGuard's simplicity (one config file per interface) makes it a perfect fit for Ansible's template-based approach.