├── Makefile ├── README.md ├── inventories └── inventory.yml ├── ping.yml ├── templates ├── systemd.netdev └── systemd.network └── wireguard.yml /Makefile: -------------------------------------------------------------------------------- 1 | INVENTORY = inventory 2 | 3 | apply: 4 | ansible-playbook -i "inventories/${INVENTORY}.yml" "wireguard.yml" 5 | 6 | test: 7 | ansible-playbook -i "inventories/${INVENTORY}.yml" "ping.yml" 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multi server Wireguard mesh with ansible 2 | 3 | A playbook which given an inventory file with: 4 | 5 | * a list of hosts 6 | * for each host a `wireguard_ip` variable with the desired host (private) Wireguard IP 7 | * `wireguard_mask_bits` variable with the number of the wireguard (private) network mask bits 8 | * `wireguard_port` variable with the UDP port to use 9 | 10 | will: 11 | 12 | * install wireguard in all hosts 13 | * generate public/private key pairs in all hosts 14 | * generate the pre-shared keys for all host pairs 15 | * create a `wg0` virtual network device and a `wg0` network 16 | 17 | optionally, when the `ufw_enabled` variable is set to `true`: 18 | 19 | * enable ufw on all hosts 20 | * reject everything by default 21 | * allow ssh protocol from all sources 22 | * allow traffic from all the inventory wireguard IPs 23 | 24 | More details and explanation can be found in this blog post: https://jawher.me/wireguard-ansible-systemd-ubuntu/ 25 | 26 | ## Example 27 | 28 | In this example, we'll create 3 Hetzner cloud CX11 servers (~3€/month) using [Hetzner's cli](https://github.com/hetznercloud/cli), 29 | 1 in each of their 3 datacenters (Nuremberg, Falkenstein & Helsinki): 30 | 31 | ```shell 32 | env_id=wireguard-test 33 | server_type=cx11 34 | image=ubuntu-20.04 35 | 36 | args=() 37 | 38 | for k in $(hcloud ssh-key list -o=noheader -ocolumns=name); do 39 | args+=("--ssh-key=$k") 40 | done 41 | 42 | for datacenter in nbg1-dc3 fsn1-dc14 hel1-dc2; do 43 | hcloud server create "${args[@]}" \ 44 | --datacenter="${datacenter}" \ 45 | --type="${server_type}" \ 46 | --image="${image}" \ 47 | --label=env="${env_id}" \ 48 | --name="${env_id}-${datacenter}" 49 | done 50 | ``` 51 | 52 | 53 | ### Inventory 54 | 55 | Next you need to prepapre an inventory file with the 3 servers we created in `inventories/inventory.yml`: 56 | 57 | Run `hcloud server list -l env=wireguard-test`: 58 | 59 | ``` 60 | ID NAME STATUS IPV4 IPV6 DATACENTER 61 | 10889123 wireguard-test-nbg1-dc3 running xxx.xx.xxx.xx 2a01:xxxx:xxxx:xxxx::/64 nbg1-dc3 62 | 10889126 wireguard-test-fsn1-dc14 running xxx.xx.xxx.xxx 2a01:xxxx:xxxx:xxxx::/64 fsn1-dc14 63 | 10889127 wireguard-test-hel1-dc2 running xx.xxx.xx.xxx 2a01:xxxx:xxxx:xxxx::/64 hel1-dc2 64 | ``` 65 | 66 | And use the server names and IPv4s to build your inventory: 67 | 68 | ```yml 69 | all: 70 | hosts: 71 | 72 | host1: 73 | pipelining: true 74 | ansible_ssh_user: root 75 | ansible_host: "$host1_public_ip" 76 | ansible_ssh_port: 22 77 | 78 | wireguard_ip: 192.168.0.1 79 | 80 | host2: 81 | pipelining: true 82 | ansible_ssh_user: root 83 | ansible_host: "$host2_public_ip" 84 | ansible_ssh_port: 22 85 | 86 | wireguard_ip: 192.168.0.2 87 | 88 | host3: 89 | pipelining: true 90 | ansible_ssh_user: root 91 | ansible_host: "$host3_public_ip" 92 | ansible_ssh_port: 22 93 | 94 | wireguard_ip: 192.168.0.3 95 | 96 | vars: 97 | ansible_become_method: su 98 | 99 | wireguard_mask_bits: 24 100 | wireguard_port: 51871 101 | ``` 102 | 103 | ### Apply 104 | 105 | Run `make apply` 106 | 107 | ### Test connectivity 108 | 109 | Run `make test`, which will perform ping tests between the 3 servers using their wireguard private IPs. 110 | 111 | You could also ssh to each/any host and run `ping` manually if you prefer. 112 | -------------------------------------------------------------------------------- /inventories/inventory.yml: -------------------------------------------------------------------------------- 1 | all: 2 | hosts: 3 | 4 | host1: 5 | pipelining: true 6 | ansible_ssh_user: root 7 | ansible_host: "$host1_public_ip" 8 | ansible_ssh_port: 22 9 | 10 | wireguard_ip: 192.168.0.1 11 | 12 | host2: 13 | pipelining: true 14 | ansible_ssh_user: root 15 | ansible_host: "$host2_public_ip" 16 | ansible_ssh_port: 22 17 | 18 | wireguard_ip: 192.168.0.2 19 | 20 | host3: 21 | pipelining: true 22 | ansible_ssh_user: root 23 | ansible_host: "$host3_public_ip" 24 | ansible_ssh_port: 22 25 | 26 | wireguard_ip: 192.168.0.3 27 | 28 | vars: 29 | ansible_become_method: su 30 | 31 | wireguard_mask_bits: 24 32 | wireguard_port: 51871 33 | 34 | -------------------------------------------------------------------------------- /ping.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | gather_facts: yes 4 | tasks: 5 | - name: ping 6 | command: "ping -c3 {{ hostvars[item].wireguard_ip }}" 7 | with_items: "{{ groups['all'] }}" 8 | -------------------------------------------------------------------------------- /templates/systemd.netdev: -------------------------------------------------------------------------------- 1 | [NetDev] 2 | Name=wg0 3 | Kind=wireguard 4 | Description=WireGuard tunnel wg0 5 | 6 | [WireGuard] 7 | ListenPort={{ wireguard_port }} 8 | PrivateKey={{ wireguard_private_key.stdout }} 9 | 10 | {% for peer in groups['all'] %} 11 | {% if peer != inventory_hostname %} 12 | 13 | [WireGuardPeer] 14 | PublicKey={{ hostvars[peer].wireguard_public_key.stdout }} 15 | PresharedKey={{ wireguard_preshared_keys[peer] if inventory_hostname < peer else hostvars[peer].wireguard_preshared_keys[inventory_hostname] }} 16 | AllowedIPs={{ hostvars[peer].wireguard_ip }}/32 17 | Endpoint={{ hostvars[peer].ansible_host }}:{{ wireguard_port }} 18 | PersistentKeepalive=25 19 | {% endif %} 20 | {% endfor %} 21 | -------------------------------------------------------------------------------- /templates/systemd.network: -------------------------------------------------------------------------------- 1 | [Match] 2 | Name=wg0 3 | 4 | [Network] 5 | Address={{ wireguard_ip }}/{{ wireguard_mask_bits }} 6 | -------------------------------------------------------------------------------- /wireguard.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: all 3 | any_errors_fatal: true 4 | gather_facts: yes 5 | tasks: 6 | - name: update packages 7 | apt: 8 | update_cache: yes 9 | cache_valid_time: 3600 10 | become: yes 11 | 12 | - name: Allow SSH in UFW 13 | ufw: 14 | rule: allow 15 | port: "{{ ansible_ssh_port }}" 16 | proto: tcp 17 | become: yes 18 | when: ufw_enabled 19 | 20 | - name: Set ufw logging 21 | ufw: 22 | logging: "on" 23 | become: yes 24 | when: ufw_enabled 25 | 26 | - name: inter-node Wireguard UFW connectivity 27 | ufw: 28 | rule: allow 29 | src: "{{ hostvars[item].wireguard_ip }}" 30 | with_items: "{{ groups['all'] }}" 31 | become: yes 32 | when: ufw_enabled and item != inventory_hostname 33 | 34 | - name: Reject everything and enable UFW 35 | ufw: 36 | state: enabled 37 | policy: reject 38 | log: yes 39 | become: yes 40 | when: ufw_enabled 41 | 42 | - name: Install wireguard 43 | apt: 44 | name: wireguard 45 | state: present 46 | become: yes 47 | 48 | - name: Generate Wireguard keypair 49 | shell: wg genkey | tee /etc/wireguard/privatekey | wg pubkey | tee /etc/wireguard/publickey 50 | args: 51 | creates: /etc/wireguard/privatekey 52 | become: yes 53 | 54 | - name: register private key 55 | shell: cat /etc/wireguard/privatekey 56 | register: wireguard_private_key 57 | changed_when: false 58 | become: yes 59 | 60 | - name: register public key 61 | shell: cat /etc/wireguard/publickey 62 | register: wireguard_public_key 63 | changed_when: false 64 | become: yes 65 | 66 | - name: generate Preshared keyskeypair 67 | shell: "wg genpsk > /etc/wireguard/psk-{{ item }}" 68 | args: 69 | creates: "/etc/wireguard/psk-{{ item }}" 70 | when: inventory_hostname < item 71 | with_items: "{{ groups['all'] }}" 72 | become: yes 73 | 74 | - name: register preshared key 75 | shell: "cat /etc/wireguard/psk-{{ item }}" 76 | register: wireguard_preshared_key 77 | changed_when: false 78 | when: inventory_hostname < item 79 | with_items: "{{ groups['all'] }}" 80 | become: yes 81 | 82 | - name: massage preshared keys 83 | set_fact: "wireguard_preshared_keys={{ wireguard_preshared_keys|default({}) | combine( {item.item: item.stdout} ) }}" 84 | when: item.skipped is not defined 85 | with_items: "{{ wireguard_preshared_key.results }}" 86 | become: yes 87 | 88 | - name: Setup wg0 device 89 | template: 90 | src: ./templates/systemd.netdev 91 | dest: /etc/systemd/network/99-wg0.netdev 92 | owner: root 93 | group: systemd-network 94 | mode: 0640 95 | become: yes 96 | notify: systemd network restart 97 | 98 | - name: Setup wg0 network 99 | template: 100 | src: ./templates/systemd.network 101 | dest: /etc/systemd/network/99-wg0.network 102 | owner: root 103 | group: systemd-network 104 | mode: 0640 105 | become: yes 106 | notify: systemd network restart 107 | 108 | handlers: 109 | - name: systemd network restart 110 | service: 111 | name: systemd-networkd 112 | state: restarted 113 | enabled: yes 114 | become: yes 115 | --------------------------------------------------------------------------------