From 0c76cd5b447f41a8015bc22198214c5089e9a886 Mon Sep 17 00:00:00 2001 From: syrell Date: Wed, 1 Mar 2023 22:52:00 +0100 Subject: [PATCH] Added Wireguard role --- roles/wireguard/README.rst | 128 ++++++++++++++++++++++ roles/wireguard/defaults/main.yml | 51 +++++++++ roles/wireguard/meta/main.yml | 52 +++++++++ roles/wireguard/tasks/clients.yml | 51 +++++++++ roles/wireguard/tasks/firewall.yml | 32 ++++++ roles/wireguard/tasks/init.yml | 14 +++ roles/wireguard/tasks/main.yml | 26 +++++ roles/wireguard/tasks/server.yml | 75 +++++++++++++ roles/wireguard/templates/clients.conf.j2 | 13 +++ roles/wireguard/templates/peer.j2 | 5 + roles/wireguard/templates/peers.j2 | 9 ++ roles/wireguard/templates/server.conf.j2 | 6 + roles/wireguard/vars/main.yml | 6 + 13 files changed, 468 insertions(+) create mode 100644 roles/wireguard/README.rst create mode 100644 roles/wireguard/defaults/main.yml create mode 100644 roles/wireguard/meta/main.yml create mode 100644 roles/wireguard/tasks/clients.yml create mode 100644 roles/wireguard/tasks/firewall.yml create mode 100644 roles/wireguard/tasks/init.yml create mode 100644 roles/wireguard/tasks/main.yml create mode 100644 roles/wireguard/tasks/server.yml create mode 100644 roles/wireguard/templates/clients.conf.j2 create mode 100644 roles/wireguard/templates/peer.j2 create mode 100644 roles/wireguard/templates/peers.j2 create mode 100644 roles/wireguard/templates/server.conf.j2 create mode 100644 roles/wireguard/vars/main.yml diff --git a/roles/wireguard/README.rst b/roles/wireguard/README.rst new file mode 100644 index 0000000..c488016 --- /dev/null +++ b/roles/wireguard/README.rst @@ -0,0 +1,128 @@ +Wireguard setup +=============== + +Wireguard setup role. This role extends `this codebase `_ to my needs. It's a bit simpler and adds more idempotence, e.g. when replaying the role to add another client to the server. + +Requirements +------------ + +This role was written for Debian (11) and requires root privileges. It also requires to have several collections installed on your ansible host you won't necessarily have depending on your Ansible installation: + +- ansible.posix +- community.general (iptables_save module) +- ansible.utils (network filters) +- netaddr (python package) + +Role Variables +-------------- + +Variables can be found in the `default vars `_ + +.. code-block:: yaml + + wireguard_dir: /etc/wireguard + wireguard_clients_dir: "{{ wireguard_dir }}/clients" + wireguard_clients_download_dir: clients/ + wireguard_download_clients: false + wireguard_serverkeys_download_dir: server/ + wireguard_download_serverkeys: false + +Defines basic arborescence to store Wireguard files. ``wireguard_download_clients`` and ``wireguard_download_serverkeys`` can optionally set to true in order to download respectively clients and server's keys from the target host. + +.. code-block:: yaml + + wireguard_restore_serverkeys_dir: "" + +Use this variable if you want to use pre-existing keys from a directory to bootstrap Wireguard. Must ends with '/'. + +.. code-block:: yaml + + wireguard_packages: + - wireguard + +List of packages to install. + +.. code-block:: yaml + + wireguard_port: 51810 + +Port which Wireguard will listen to. + +.. code-block:: yaml + + wireguard_hostname: "{{ inventory_hostname }}" + +Hostname the client will use to connect to the server. + +.. code-block:: yaml + + wireguard_interface: wg0 + +Interface which will be mounted to the server. + +.. code-block:: yaml + + nat_out_interface: eth0 + +Interface where the traffic will be NATed to on the server. + +.. code-block:: yaml + + wireguard_address: 10.213.213.0/24 + +Subnet definition for the VPN network. + +.. code-block:: yaml + + wireguard_keepalive: 25 + +Uses this if you wanna specify a keepalive value. See `this `_ for more information on keepalive. + +.. code-block:: yaml + + wireguard_peers: [] + +Lits of peers (clients) you wanna create. You can define specific name, address, allowedIPs, DNS and keepalive for each peer. See playbook below for example. + +.. code-block:: yaml + + filter_forward: false + other_interface: + +Set ``filter_forward`` to true and specify an interface name for ``other_interface`` if you wanna drop packets from ``wireguard_interface`` to this interface. + +Dependencies +------------ + +None. + +Example Playbook +---------------- + +.. code-block:: yaml + + - name: Deploy Wireguard + hosts: wireguard_hosts + become: true + vars: + wireguard_hostname: "mywireguard.server.com" + wireguard_address: 10.10.10.0/24 + wireguard_peers: + - name: client_001 + allowed_ip: "0.0.0.0/0, ::/0" + address: "10.10.10.2" + - name: client_002 + allowed_ip: "0.0.0.0/0, ::/0" + address: "10.10.10.3" + roles: + - wireguard + +License +------- + +BSD-3 + +Author Information +------------------ + +Role created by `syrell `_ diff --git a/roles/wireguard/defaults/main.yml b/roles/wireguard/defaults/main.yml new file mode 100644 index 0000000..080a58a --- /dev/null +++ b/roles/wireguard/defaults/main.yml @@ -0,0 +1,51 @@ +--- +# defaults file for wireguard +# Directory to store WireGuard configuration on the remote hosts +wireguard_dir: /etc/wireguard +wireguard_clients_dir: "{{ wireguard_dir }}/clients" + +# Download client configs +wireguard_clients_download_dir: clients/ +wireguard_download_clients: false + +# Download private, public and preshared keys +wireguard_serverkeys_download_dir: server/ +wireguard_download_serverkeys: false + +# Path to Wireguard keys +wireguard_privatekey_path: "{{ wireguard_dir }}/pk" +wireguard_publickey_path: "{{ wireguard_dir }}/pubk" +wireguard_presharedkey_path: "{{ wireguard_dir }}/psk" + +# When defined, Ansible will restore wireguard keys (private key, public key, preshared key) from this directory. +# NOTE: The directory path must end with "/" +wireguard_restore_serverkeys_dir: "" + +# List of packages to install +wireguard_packages: + - wireguard + +# The default port WireGuard will listen if not specified otherwise. +wireguard_port: 51810 + +# Client destination Hostname +wireguard_hostname: "{{ inventory_hostname }}" + +# The default interface name that wireguard should use if not specified otherwise. +wireguard_interface: wg0 + +# Interface to NAT traffic to +nat_out_interface: eth0 + +# Base wireguard subnet +wireguard_address: 10.213.213.0/24 + +# Defines a keepalive value for peers +wireguard_keepalive: 0 + +# List of peers +wireguard_peers: [] + +# Add additional forward rule to drop packets to other_interface +filter_forward: false +other_interface: \ No newline at end of file diff --git a/roles/wireguard/meta/main.yml b/roles/wireguard/meta/main.yml new file mode 100644 index 0000000..c572acc --- /dev/null +++ b/roles/wireguard/meta/main.yml @@ -0,0 +1,52 @@ +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/roles/wireguard/tasks/clients.yml b/roles/wireguard/tasks/clients.yml new file mode 100644 index 0000000..373b6ff --- /dev/null +++ b/roles/wireguard/tasks/clients.yml @@ -0,0 +1,51 @@ +- name: Create client configs directories + ansible.builtin.file: + path: "{{ wireguard_clients_dir }}/{{ item.FriendlyName }}" + mode: 0755 + state: directory + register: existing_client_config + +- name: Wireguard client keys block + block: + + - name: Generate WireGuard client private and public keys + ansible.builtin.shell: | + set -o pipefail + umask 077 && wg genkey | tee pk | wg pubkey > pubk + args: + executable: /bin/bash + chdir: "{{ wireguard_clients_dir }}/{{ item.FriendlyName }}" + + - name: Read publickey + ansible.builtin.slurp: + src: "{{ wireguard_clients_dir }}/{{ item.FriendlyName }}/pubk" + register: _client_pubkey_value + + - name: Read privatekey + ansible.builtin.slurp: + src: "{{ wireguard_clients_dir }}/{{ item.FriendlyName }}/pk" + register: _privkey_value + + - name: Create client config + ansible.builtin.template: + src: "clients.conf.j2" + dest: "{{ wireguard_clients_dir }}/{{ item.FriendlyName }}/{{ item.FriendlyName }}.conf" + mode: 0644 + vars: + server_public_key: "{{ _pubkey_value['content'] | b64decode | trim }}" + preshared_key: "{{ _pskkey_value['content'] | b64decode | trim }}" + + - name: Download client configs + ansible.builtin.fetch: + src: "{{ wireguard_clients_dir }}/{{ item.FriendlyName }}.conf" + dest: "{{ wireguard_clients_download_dir }}/{{ inventory_hostname }}/" + flat: true + when: wireguard_download_clients | bool + + - name: Append peer to server config + ansible.builtin.blockinfile: + dest: "{{ wireguard_dir }}/{{ wireguard_interface }}.conf" + block: "{{ lookup('template', 'templates/peer.j2') }}" + marker: "### {mark} ANSIBLE MANAGED BLOCK FOR {{ item.FriendlyName }} ###" + + when: existing_client_config.changed == true diff --git a/roles/wireguard/tasks/firewall.yml b/roles/wireguard/tasks/firewall.yml new file mode 100644 index 0000000..bb3043c --- /dev/null +++ b/roles/wireguard/tasks/firewall.yml @@ -0,0 +1,32 @@ +- name: Install iptables-persistent + ansible.builtin.apt: + name: + - iptables + - iptables-persistent + state: present + +- name: Filter FORWARD packets + ansible.builtin.iptables: + chain: FORWARD + jump: DROP + in_interface: "{{ wireguard_interface }}" + out_interface: "{{ other_interface }}" + when: + - filter_forward | bool + - other_interface | length > 0 + + +- name: Setup ipv4 IP forward + ansible.posix.sysctl: + name: net.ipv4.ip_forward + value: '1' + sysctl_set: true + reload: true + +- name: Save current firewall state + community.general.iptables_state: + state: saved + path: /etc/iptables/rules.v4 + when: + - filter_forward | bool + - other_interface | length > 0 diff --git a/roles/wireguard/tasks/init.yml b/roles/wireguard/tasks/init.yml new file mode 100644 index 0000000..3cc51aa --- /dev/null +++ b/roles/wireguard/tasks/init.yml @@ -0,0 +1,14 @@ +- name: Create required dirs + ansible.builtin.file: + path: "{{ item }}" + mode: 0755 + state: directory + loop: + - "{{ wireguard_dir }}" + - "{{ wireguard_clients_dir }}" + +- name: Install WireGuard + ansible.builtin.apt: + name: "{{ wireguard_packages }}" + update_cache: true + state: present diff --git a/roles/wireguard/tasks/main.yml b/roles/wireguard/tasks/main.yml new file mode 100644 index 0000000..25abda8 --- /dev/null +++ b/roles/wireguard/tasks/main.yml @@ -0,0 +1,26 @@ +--- +# tasks file for wireguard +- name: Init tasks + import_tasks: init.yml + +- name: Deploy server + import_tasks: server.yml + +- name: Firewalling + import_tasks: firewall.yml + +- name: Include Client configs + include_tasks: clients.yml + loop: "{{ peers | list }}" + +- name: Cleanup secrets from memory + ansible.builtin.set_fact: + _pskkey_value: "" + _pubkey_value: "" + _privkey_value: "" + +- name: Restart wg-quick + ansible.builtin.systemd: + name: wg-quick@wg0.service + enabled: yes + state: restarted \ No newline at end of file diff --git a/roles/wireguard/tasks/server.yml b/roles/wireguard/tasks/server.yml new file mode 100644 index 0000000..20d26f7 --- /dev/null +++ b/roles/wireguard/tasks/server.yml @@ -0,0 +1,75 @@ +- name: Wireguard keys block + block: + - name: Test if private key is already present + ansible.builtin.stat: + path: "{{ wireguard_privatekey_path }}" + register: _priv_key + + - name: Generate WireGuard server private and public keys + ansible.builtin.shell: | + set -o pipefail + umask 077 && wg genkey | tee {{ wireguard_privatekey_path }} | wg pubkey > {{ wireguard_publickey_path }} + args: + executable: /bin/bash + when: + - not _priv_key.stat.exists + - wireguard_restore_serverkeys_dir | length == 0 + + - name: Restore WireGuard private, public and preshared keys + ansible.builtin.copy: + src: "{{ wireguard_restore_serverkeys_dir }}" + dest: "{{ wireguard_dir }}" + mode: '0644' + when: + - not _priv_key.stat.exists + - wireguard_restore_serverkeys_dir | length > 0 + + - name: Read publickey + ansible.builtin.slurp: + src: "{{ wireguard_publickey_path }}" + register: _pubkey_value + + - name: Read privatekey + ansible.builtin.slurp: + src: "{{ wireguard_privatekey_path }}" + register: _privkey_value + + - name: Test if preshared key is already present + ansible.builtin.stat: + path: "{{ wireguard_presharedkey_path }}" + register: _psk_key + + - name: Generate WireGuard preshared key + ansible.builtin.shell: | + set -o pipefail + umask 077 && wg genpsk | tee {{ wireguard_presharedkey_path }} + args: + executable: /bin/bash + when: not _psk_key.stat.exists + + - name: Read presharedkey + ansible.builtin.slurp: + src: "{{ wireguard_presharedkey_path }}" + register: _pskkey_value + + - name: Create server config + ansible.builtin.template: + src: server.conf.j2 + dest: "{{ wireguard_dir }}/{{ wireguard_interface }}.conf" + mode: 0700 + force: no + +- name: Create peers variable from template + ansible.builtin.set_fact: + peers: "{{ lookup('template', 'templates/peers.j2') | from_yaml }}" + +- name: Download server private key + ansible.builtin.fetch: + src: "{{ item }}" + dest: "{{ wireguard_serverkeys_download_dir }}/{{ inventory_hostname }}/" + flat: true + loop: + - "{{ wireguard_privatekey_path }}" + - "{{ wireguard_publickey_path }}" + - "{{ wireguard_presharedkey_path }}" + when: wireguard_download_serverkeys | bool \ No newline at end of file diff --git a/roles/wireguard/templates/clients.conf.j2 b/roles/wireguard/templates/clients.conf.j2 new file mode 100644 index 0000000..db8b96d --- /dev/null +++ b/roles/wireguard/templates/clients.conf.j2 @@ -0,0 +1,13 @@ +[Interface] +Address = {{ item.Address }} +ListenPort = {{ wireguard_port }} +PrivateKey = {{ _privkey_value['content'] | b64decode | trim }} +{% if item.DNS|length > 0 %}DNS = {{ item.DNS }} +{% endif %} + +[Peer] +PublicKey = {{ server_public_key }} +PresharedKey = {{ preshared_key }} +AllowedIPs = {{ item.AllowedIPs }} +Endpoint = {{ wireguard_hostname }}:{{ wireguard_port }} +PersistentKeepalive = {{ item.PersistentKeepalive | default(wireguard_keepalive) }} \ No newline at end of file diff --git a/roles/wireguard/templates/peer.j2 b/roles/wireguard/templates/peer.j2 new file mode 100644 index 0000000..5ca2c9f --- /dev/null +++ b/roles/wireguard/templates/peer.j2 @@ -0,0 +1,5 @@ +[peer] +# peer_{{ item.FriendlyName }} +PublicKey = {{ _client_pubkey_value['content'] | b64decode | trim }} +PresharedKey = {{ item.PresharedKey }} +AllowedIPs = {{ item.Address }}/32 \ No newline at end of file diff --git a/roles/wireguard/templates/peers.j2 b/roles/wireguard/templates/peers.j2 new file mode 100644 index 0000000..b3d0897 --- /dev/null +++ b/roles/wireguard/templates/peers.j2 @@ -0,0 +1,9 @@ +{% for peer in wireguard_peers %} +- WireGuardPeer: + FriendlyName: {{ peer.name }} + Address: {{ peer.address }} + AllowedIPs: "{{ peer.allowed_ip }}{% if not '/' in peer.allowed_ip %}/32{% endif %}" + DNS: "{% if peer.dns is defined %}{{ peer.dns }}{% endif %}" + PresharedKey: "{{ _pskkey_value['content'] | b64decode | trim }}" + PersistentKeepalive: {{ peer.keepalive | default(wireguard_keepalive) }} +{% endfor %} diff --git a/roles/wireguard/templates/server.conf.j2 b/roles/wireguard/templates/server.conf.j2 new file mode 100644 index 0000000..f688246 --- /dev/null +++ b/roles/wireguard/templates/server.conf.j2 @@ -0,0 +1,6 @@ +[Interface] +Address = {{ wireguard_server_ip }} +ListenPort = {{ wireguard_port }} +PrivateKey = {{ _privkey_value['content'] | b64decode | trim }} +PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -s {{ wireguard_address }} -o {{ nat_out_interface }} -j MASQUERADE +PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -s {{ wireguard_address }} -o {{ nat_out_interface }} -j MASQUERADE diff --git a/roles/wireguard/vars/main.yml b/roles/wireguard/vars/main.yml new file mode 100644 index 0000000..a776b8f --- /dev/null +++ b/roles/wireguard/vars/main.yml @@ -0,0 +1,6 @@ +--- +# vars file for wireguard +_wireguard_interface_addr: "{{ ansible_default_ipv4.address | default(ansible_all_ipv4_addresses[0]) }}/{{ ansible_default_ipv4.netmask }}" +wireguard_server_ip: "{{ wireguard_address | ansible.utils.ipaddr('network') | ansible.utils.ipmath(1) }}" +wireguard_subnetmask: "{{ wireguard_address | ansible.utils.ipaddr('prefix') }}" +wireguard_peers_allowed_ips: "{{ ([(_wireguard_interface_addr | ansible.utils.ipaddr('network/prefix'))] + (wireguard_additional_routes | default([]))) | join(\", \") }}" \ No newline at end of file