From b15d869de6a7844b3a1f652de21a4abccd0701cd Mon Sep 17 00:00:00 2001
From: jawher <jawher.moussa@mirakl.com>
Date: Thu, 25 Mar 2021 12:15:35 +0100
Subject: [PATCH] Point of inception

---
 Makefile                  |   7 +++
 README.md                 | 111 +++++++++++++++++++++++++++++++++++++
 inventories/inventory.yml |  33 +++++++++++
 ping.yml                  |   7 +++
 templates/systemd.netdev  |  19 +++++++
 templates/systemd.network |   5 ++
 wireguard.yml             | 114 ++++++++++++++++++++++++++++++++++++++
 7 files changed, 296 insertions(+)
 create mode 100644 Makefile
 create mode 100644 README.md
 create mode 100644 inventories/inventory.yml
 create mode 100644 ping.yml
 create mode 100644 templates/systemd.netdev
 create mode 100644 templates/systemd.network
 create mode 100644 wireguard.yml

diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0b38f92
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,7 @@
+INVENTORY = inventory
+
+apply:
+	ansible-playbook -i "inventories/${INVENTORY}.yml" "wireguard.yml"
+
+test:
+	ansible-playbook -i "inventories/${INVENTORY}.yml" "ping.yml"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..0867c21
--- /dev/null
+++ b/README.md
@@ -0,0 +1,111 @@
+# Multi server Wireguard mesh with ansible
+
+A playbook which given an inventory file with:
+
+* a list of hosts
+    * for each host a `wireguard_ip` variable with the desired host (private) Wireguard IP
+* `wireguard_mask_bits` variable with the number of the wireguard (private) network mask bits
+* `wireguard_port` variable with the UDP port to use
+
+will:
+
+* install wireguard in all hosts
+* generate public/private key pairs in all hosts
+* generate the pre-shared keys for all host pairs
+* create a `wg0` virtual network device and a `wg0` network
+
+optionally, when the `ufw_enabled` variable is set to `true`:
+
+* enable ufw on all hosts
+* reject everything by default
+* allow ssh protocol from all sources
+* allow traffic from all the inventory wireguard IPs
+
+More details and explanation can be found in this blog post: https://jawher.me/wireguard-ansible-systemd-ubuntu/
+
+## Example
+
+In this example, we'll create 3 Hetzner cloud CX11 servers (~3€/month) using [Hetzner's cli](https://github.com/hetznercloud/cli),
+1 in each of their 3 datacenters (Nuremberg, Falkenstein & Helsinki):
+
+```shell
+env_id=wireguard-test
+server_type=cx11
+image=ubuntu-20.04
+
+args=()
+
+for k in $(hcloud ssh-key list -o=noheader -ocolumns=name); do
+  args+=("--ssh-key=$k")
+done
+
+for datacenter in nbg1-dc3 fsn1-dc14 hel1-dc2; do
+    hcloud server create "${args[@]}" \
+    --datacenter="${datacenter}" \
+    --type="${server_type}" \
+    --image="${image}" \
+    --label=env="${env_id}" \
+    --name="${env_id}-${datacenter}"
+done
+```
+
+
+### Inventory
+
+Next you need to prepapre an inventory file with the 3 servers we created in `inventories/inventory.yml`:
+
+Run `hcloud server list -l env=wireguard-test`:
+
+```
+ID         NAME                       STATUS    IPV4             IPV6                      DATACENTER
+10889123   wireguard-test-nbg1-dc3    running   xxx.xx.xxx.xx    2a01:xxxx:xxxx:xxxx::/64   nbg1-dc3
+10889126   wireguard-test-fsn1-dc14   running   xxx.xx.xxx.xxx   2a01:xxxx:xxxx:xxxx::/64   fsn1-dc14
+10889127   wireguard-test-hel1-dc2    running   xx.xxx.xx.xxx    2a01:xxxx:xxxx:xxxx::/64   hel1-dc2
+```
+
+And use the server names and IPv4s to build your inventory:
+
+```yml
+all:
+  hosts:
+
+    host1:
+      pipelining: true
+      ansible_ssh_user: root
+      ansible_host: "$host1_public_ip"
+      ansible_ssh_port: 22
+
+      wireguard_ip: 192.168.0.1
+
+    host2:
+      pipelining: true
+      ansible_ssh_user: root
+      ansible_host: "$host2_public_ip"
+      ansible_ssh_port: 22
+
+      wireguard_ip: 192.168.0.2
+
+    host3:
+      pipelining: true
+      ansible_ssh_user: root
+      ansible_host: "$host3_public_ip"
+      ansible_ssh_port: 22
+
+      wireguard_ip: 192.168.0.3
+
+  vars:
+    ansible_become_method: su 
+
+    wireguard_mask_bits: 24
+    wireguard_port: 51871
+```
+
+### Apply
+
+Run `make apply`
+
+### Test connectivity
+
+Run `make test`, which will perform ping tests between the 3 servers using their wireguard private IPs.
+
+You could also ssh to each/any host and run `ping` manually if you prefer.
diff --git a/inventories/inventory.yml b/inventories/inventory.yml
new file mode 100644
index 0000000..4822d98
--- /dev/null
+++ b/inventories/inventory.yml
@@ -0,0 +1,33 @@
+all:
+  hosts:
+
+    host1:
+      pipelining: true
+      ansible_ssh_user: root
+      ansible_host: "$host1_public_ip"
+      ansible_ssh_port: 22
+
+      wireguard_ip: 192.168.0.1
+
+    host2:
+      pipelining: true
+      ansible_ssh_user: root
+      ansible_host: "$host2_public_ip"
+      ansible_ssh_port: 22
+
+      wireguard_ip: 192.168.0.2
+
+    host3:
+      pipelining: true
+      ansible_ssh_user: root
+      ansible_host: "$host3_public_ip"
+      ansible_ssh_port: 22
+
+      wireguard_ip: 192.168.0.3
+
+  vars:
+    ansible_become_method: su 
+
+    wireguard_mask_bits: 24
+    wireguard_port: 51871
+
diff --git a/ping.yml b/ping.yml
new file mode 100644
index 0000000..942c42a
--- /dev/null
+++ b/ping.yml
@@ -0,0 +1,7 @@
+---
+- hosts: all
+  gather_facts: yes
+  tasks:
+    - name: ping
+      command: "ping -c3 {{ hostvars[item].wireguard_ip }}"
+      with_items: "{{ groups['all'] }}"
diff --git a/templates/systemd.netdev b/templates/systemd.netdev
new file mode 100644
index 0000000..7d52361
--- /dev/null
+++ b/templates/systemd.netdev
@@ -0,0 +1,19 @@
+[NetDev]
+Name=wg0
+Kind=wireguard
+Description=WireGuard tunnel wg0
+
+[WireGuard]
+ListenPort={{ wireguard_port }}
+PrivateKey={{ wireguard_private_key.stdout }}
+
+{% for peer in groups['all'] %}
+{% if peer != inventory_hostname %}
+
+[WireGuardPeer]
+PublicKey={{ hostvars[peer].wireguard_public_key.stdout }}
+PresharedKey={{ wireguard_preshared_keys[peer] if inventory_hostname < peer else hostvars[peer].wireguard_preshared_keys[inventory_hostname] }}
+AllowedIPs={{ hostvars[peer].wireguard_ip }}/32
+Endpoint={{ hostvars[peer].ansible_host }}:{{ wireguard_port }}
+{% endif %}
+{% endfor %}
diff --git a/templates/systemd.network b/templates/systemd.network
new file mode 100644
index 0000000..4a584ff
--- /dev/null
+++ b/templates/systemd.network
@@ -0,0 +1,5 @@
+[Match]
+Name=wg0
+
+[Network]
+Address={{ wireguard_ip }}/{{ wireguard_mask_bits }}
diff --git a/wireguard.yml b/wireguard.yml
new file mode 100644
index 0000000..5ad3aff
--- /dev/null
+++ b/wireguard.yml
@@ -0,0 +1,114 @@
+---
+- hosts: all
+  any_errors_fatal: true
+  gather_facts: yes
+  tasks:
+    - name: update packages
+      apt:
+        update_cache: yes
+        cache_valid_time: 3600
+      become: yes
+
+    - name: Allow SSH in UFW
+      ufw:
+        rule: allow
+        port: "{{ ansible_ssh_port }}"
+        proto: tcp
+      become: yes
+      when: ufw_enabled
+
+    - name: Set ufw logging
+      ufw:
+        logging: "on"
+      become: yes
+      when: ufw_enabled
+
+    - name: inter-node Wireguard UFW connectivity
+      ufw:
+        rule: allow
+        src: "{{ hostvars[item].wireguard_ip }}"
+      with_items: "{{ groups['all'] }}"
+      become: yes
+      when: ufw_enabled and item != inventory_hostname
+
+    - name: Reject everything and enable UFW
+      ufw:
+        state: enabled
+        policy: reject
+        log: yes
+      become: yes
+      when: ufw_enabled
+
+    - name: Install wireguard
+      apt:
+        name: wireguard
+        state: present
+      become: yes
+
+    - name: Generate Wireguard keypair
+      shell: wg genkey | tee /etc/wireguard/privatekey | wg pubkey | tee /etc/wireguard/publickey
+      args:
+        creates: /etc/wireguard/privatekey
+      become: yes
+
+    - name: register private key
+      shell: cat /etc/wireguard/privatekey
+      register: wireguard_private_key
+      changed_when: false
+      become: yes
+
+    - name: register public key
+      shell: cat /etc/wireguard/publickey
+      register: wireguard_public_key
+      changed_when: false
+      become: yes
+
+    - name: generate Preshared keyskeypair
+      shell: "wg genpsk > /etc/wireguard/psk-{{ item }}"
+      args:
+        creates: "/etc/wireguard/psk-{{ item }}"
+      when: inventory_hostname < item
+      with_items: "{{ groups['all'] }}"
+      become: yes
+
+    - name: register preshared key
+      shell: "cat /etc/wireguard/psk-{{ item }}"
+      register: wireguard_preshared_key
+      changed_when: false
+      when: inventory_hostname < item
+      with_items: "{{ groups['all'] }}"
+      become: yes
+
+    - name: massage preshared keys
+      set_fact: "wireguard_preshared_keys={{ wireguard_preshared_keys|default({}) | combine( {item.item: item.stdout} ) }}"
+      when: item.skipped is not defined
+      with_items: "{{ wireguard_preshared_key.results }}"
+      become: yes
+
+    - name: Setup wg0 device
+      template:
+        src: ./templates/systemd.netdev
+        dest: /etc/systemd/network/99-wg0.netdev
+        owner: root
+        group: systemd-network
+        mode: 0640
+      become: yes
+      notify: systemd network restart
+
+    - name: Setup wg0 network
+      template:
+        src: ./templates/systemd.network
+        dest: /etc/systemd/network/99-wg0.network
+        owner: root
+        group: systemd-network
+        mode: 0640
+      become: yes
+      notify: systemd network restart
+
+  handlers:
+    - name: systemd network restart
+      service:
+        name: systemd-networkd
+        state: restarted
+        enabled: yes
+      become: yes