Setting up WireGuard is not a difficult process but I wanted to automate it among hosts by using a simple playbook that can be executed against the hosts and get it configured and deployed in a simple way.
I also wanted to require the minimum possible number of values in the inventory, so tried to automate lot of the information required, leaving in the end only some required values:
wireguard: True
wgrole: 'master' or 'something else'
wgport: port number to use
The first step was to create the private and public key once the wireguard
package is installed.
This was more or less easy, just run and store the output in the folder for WireGuard.
# Create private key
wg genkey | tee privatekey
# Create public key
wg pubkey < privatekey | tee publickey
Later, I could set via Ansible’s set_fact
:
- name: Set WireGuard keys
set_fact:
wgprivatekey: "{{ lookup('file', 'privatekey') }}"
wgpublickey: "{{ lookup('file', 'publickey') }}"
But I got to the first problem… the facts are set by the host running the playbook, and for setting master/client connection I need to use the master’s public key available on the client, and the client’s public key on the master.
As the fact was setup at execution, other hosts couldn’t check them via hostvars
…
To solve this issue I went by creating a custom fact
that is copied over the hosts, and that causes the host to refresh if something has been copied, this made the fact available in the execution:
Facts for private key:
#!/bin/bash
echo "{\"wgprivkey\" : \"$(cat /etc/wireguard/privatekey)\"}"
Fact for public key:
#!/bin/bash
echo "{\"wgpubkey\" : \"$(cat /etc/wireguard/publickey)\"}"
Note that both, provide output in a JSON compatible format.
We now need to copy the facts to the hosts and refresh the data if needed:
- name: Create directory for ansible custom facts
file:
state: directory
recurse: yes
path: /etc/ansible/facts.d
register: facts_dir_created
- name: Copy custom facts
copy:
src: "{{ item }}"
dest: /etc/ansible/facts.d/
owner: root
group: root
mode: 0755
with_fileglob:
- "facts/*.fact"
register: facts_copied
- name: "Re-run setup to use custom facts"
setup: ~
when: facts_copied.changed
Now… all hosts have the facts for private and public key so that can be used… but we need to actually create WireGuard configuration file for it… but wait… we need to know which host is the master
that will receive all connections from the clients.
So, let’s detect the master by the wgrole
value:
- name: Set wireguard master server host
set_fact:
wgmaster: "{{ item }}"
with_items: "{{ groups.all }}"
when: hostvars[item].wgrole is defined and hostvars[item].wgrole == 'master' and wireguard == True
Above task will loop across all hosts, check for the wgrole
defined and equal to master
(with wireguard
enabled), and set the wgmaster
fact to the hostname.
We will also need to calculate the IP to use for the master and the client in a private range… so let’s just get the number of item in the list for this:
- name: Calculate IP for host
set_fact:
wgip: "10.0.0.{{ lookup('ansible.utils.index_of', groups.all, 'eq', inventory_hostname) }}"
Now, each host will get a ‘fact’ wgip
with the content similar to 10.0.0.1
that we can use to connect them.
So… we should have all the information to create the configuration file for each client host:
- name: Create configuration file for client
copy:
dest: "/etc/wireguard/wg0.conf"
mode: 0644
content: |
[Interface]
ListenPort = {{ wgport }}
PrivateKey = {{ ansible_local.wgprivkey.wgprivkey }}
[Peer]
PublicKey = {{ hostvars[wgmaster].ansible_local.wgpubkey.wgpubkey }}
AllowedIPs = {{ hostvars[wgmaster].wgip }}/32
Endpoint = {{ hostvars[wgmaster].inventory_hostname }}:{{ hostvars[wgmaster].wgport }}
when: wireguard == True and wgrole is defined and wgrole != 'master'
In above example, check that we use hostvars
with the wgmaster
value we obtained, in order to fill-in the values for the master, and make use of ansible_local
to grab the values that our custom facts generated.
So far, it has been more or less easy… the problem is that in the master, we need to build a base section ([Interface
]) and then, ad the client section ([Peer]
) with the client’s public key and the IP of the client.
Next, let’s check the one for the master:
- name: Create configuration file for master
copy:
dest: "/etc/wireguard/wg0.conf"
mode: 0644
content: |
[Interface]
ListenPort = {{ wgport }}
PrivateKey = {{ hostvars[wgmaster].ansible_local.wgprivkey.wgprivkey }}
{%- for item in hostvars -%}
{% if hostvars[item].wgrole is defined and hostvars[item].wgrole != 'master' %}
[Peer]
PublicKey = {{ hostvars[item].ansible_local.wgpubkey.wgpubkey }}
AllowedIPs = {{ hostvars[item].wgip }} /32
Endpoint = {{ hostvars[item].inventory_hostname }}:{{ hostvars[item].wgport }}
{% endif %}
{%- endfor -%}
when: wireguard == True and wgrole is defined and wgrole == 'master' and item == wgmaster
with_inventory_hostnames:
- all
More or less, the logic should be clear… we iterate over all the hosts with wireguard: True
and wgrole
not master to add their section…. and this only runs on the master
host, a the clients would have get the previous task instead.
Of course, extra tasks would be needed for:
- Bringing service up
- Open firewall ports
- etc.
Hope you liked it!, it got me busy for some time until all pieces matched together :)
Enjoy! (and if you do, you can Buy Me a Coffee )