diff --git a/README.md b/README.md index 8bba763..b6ac61f 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ The role is currently heavily reworked for increased flexibility and DebOps supp The reworked role should cover all valid use cases, for this, the code is complex but you can decide using a few inventory variables how you want to role to behave. +You will need to have `python3-future` installed on your remote hosts. + ## Previous not up-to-date docs follow This Ansible role is used in my blog series [Kubernetes the not so hard way with Ansible](https://www.tauceti.blog/post/kubernetes-the-not-so-hard-way-with-ansible-wireguard/) but can be used standalone of course. I use WireGuard and this Ansible role to setup a fully meshed VPN between all nodes of my little Kubernetes cluster. This VPN also includes two clients so that I can communicate securely with the Kubernetes API server. Also my Postfix mailserver running as K8s DaemonSet forwards mails to my internal Postfix through WireGuard VPN. diff --git a/defaults/main.yml b/defaults/main.yml index b1ad8e3..ca5032f 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -1,5 +1,6 @@ --- # Copyright (C) 2018-2020 Robert Wimmer +# Copyright (C) 2020 Robin Schneider # SPDX-License-Identifier: GPL-3.0-or-later ####################################### @@ -7,7 +8,12 @@ ####################################### # Directory to store WireGuard configuration on the remote hosts -wireguard_remote_directory: "{{ '/etc/wireguard' if not ansible_os_family == 'Darwin' else '/opt/local/etc/wireguard' }}" +wireguard_remote_directory: '{{ ("/opt/local/etc/wireguard" + if (ansible_os_family == "Darwin") + else "/etc/wireguard") + if (wireguard__config_target == "host") + else (wireguard__secret_directory + "/config") }}' + wireguard__keys_directory: '{{ wireguard_remote_directory + "/keys" }}' @@ -44,20 +50,29 @@ wireguard_ubuntu_cache_valid_time: "3600" # FIXME: Update docs # Name of the WireGuard network in case it is different to the interface name. # Might be the case if wg0 is already taken on some peers or if the network otherwise just has a more fitting name for the whole virtual network. -wireguard_inventory_group: "wireguard_wg0" +wireguard_inventory_group: 'wireguard_wg0' # Either "host" or "ansible_controller". -wireguard__secret_authority: "ansible_controller" +wireguard__secret_authority: 'ansible_controller' # Key templating mode. Either "inline" or "file". -wireguard__key_templating: "file" +wireguard__key_templating: '{{ "file" if (wireguard__config_target == "host") else "inline" }}' + +# Either "host" or "ansible_controller". +wireguard__config_target: 'host' + +# File path on the Ansible controller where files will be redirected to with wireguard__config_target == "ansible_controller". +# The idea behind this directory is to act as a faked / (root) directory for that "host". +wireguard__controller_host_dir_path: '{{ (inventory_dir + "../../../root-fs-by-host/" + inventory_hostname) | realpath }}' +wireguard__controller_host_owner: '{{ omit}}' +wireguard__controller_host_group: '{{ omit}}' +wireguard__controller_host_mode: '{{ omit}}' # .. envvar:: wireguard__secret_directory [[[ # # Secret directory to use on the Ansible controller for key management and # generating configuration files for unmanaged peers. wireguard__secret_directory: '{{ secret + "/wireguard/" + wireguard_inventory_group }}' - # + + "/" + ansible_fqdn # ]]] # Configuration for other Ansible roles [[[ diff --git a/handlers/main.yml b/handlers/main.yml index 45c2549..60825e5 100644 --- a/handlers/main.yml +++ b/handlers/main.yml @@ -6,4 +6,4 @@ service: name: "wg-quick@{{ wireguard_interface }}" state: "reloaded" - when: (ansible_os_family != 'Darwin') + when: (wireguard__config_target == "host" and ansible_os_family != 'Darwin') diff --git a/tasks/main.yml b/tasks/main.yml index 4775220..1713e08 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -6,6 +6,13 @@ - import_role: name: 'secret' +- name: Assert that inventory configuration is valid + assert: + that: + - '(wireguard__config_target == "ansible_controller" and wireguard__secret_authority == "ansible_controller") or wireguard__config_target != "ansible_controller"' + run_once: True + delegate_to: 'localhost' + # Installing and load WireGuard [[[1 - include_tasks: "{{ item }}" with_first_found: @@ -14,6 +21,7 @@ - "setup-{{ ansible_distribution|lower }}-{{ ansible_distribution_release }}.yml" - "setup-{{ ansible_distribution|lower }}.yml" - "setup-{{ ansible_os_family|lower }}.yml" + when: wireguard__config_target == "host" - name: Install patched version of wg-quick copy: @@ -24,6 +32,7 @@ mode: "0755" tags: - wg-install + when: wireguard__config_target == "host" - name: Enable WireGuard kernel module modprobe: @@ -34,14 +43,14 @@ retries: 10 delay: 10 failed_when: wireguard__register_module_enabled is failure - when: ansible_os_family == 'Darwin' + when: wireguard__config_target == "host" and ansible_os_family == 'Darwin' tags: - wg-install # Prepare WireGuard configuration directory [[[1 - name: Create WireGuard configuration directory file: - dest: "{{ wireguard_remote_directory }}" + dest: '{{ wireguard_remote_directory }}' state: directory mode: 0700 tags: @@ -75,7 +84,7 @@ creates: '{{ wireguard__secret_directory + "/" + item + ".privkey" }}' delegate_to: "localhost" run_once: True - loop: '{{ ansible_play_hosts }}' + loop: '{{ groups[wireguard_inventory_group]|d([]) }}' tags: - wg-config @@ -87,7 +96,7 @@ - name: Set private key fact from Ansible controller set_fact: - wireguard__fact_private_key: "{{ wireguard__register_private_key['content'] | b64decode }}" + wireguard__fact_private_key: '{{ (wireguard__register_private_key["content"] | b64decode).strip() }}' when: wireguard__secret_authority == "ansible_controller" # Private key handling on remote [[[1 @@ -143,6 +152,7 @@ group: "{{ wireguard_conf_group }}" mode: "{{ wireguard_conf_mode }}" no_log: True + when: wireguard__key_templating == "file" tags: - wg-config notify: @@ -159,7 +169,7 @@ - name: Set public key fact set_fact: - wireguard__fact_public_key: "{{ wireguard__register_public_key.stdout }}" + wireguard__fact_public_key: '{{ wireguard__register_public_key.stdout }}' tags: - wg-config @@ -215,6 +225,7 @@ group: "{{ wireguard_conf_group }}" mode: "{{ wireguard_conf_mode }}" loop: '{{ wireguard__combos }}' + when: wireguard__config_target == "host" tags: - wg-config @@ -226,15 +237,25 @@ owner: "{{ wireguard_conf_owner }}" group: "{{ wireguard_conf_group }}" mode: "{{ wireguard_conf_mode }}" + when: wireguard__config_target == "host" tags: - wg-config notify: - Reload WireGuard interface +- name: Generate WireGuard configuration file on Ansible controller + template: + src: 'etc/wireguard/wg.conf.j2' + dest: '{{ wireguard_remote_directory + "/" + inventory_hostname + "_" + wireguard_interface + ".conf" }}' + when: wireguard__config_target == "ansible_controller" + tags: + - wg-config + - name: Ensure legacy reload-module-on-update is absent file: dest: "{{ wireguard_remote_directory }}/.reload-module-on-update" state: absent + when: wireguard__config_target == "host" tags: - wg-config @@ -244,6 +265,7 @@ dest: "/etc/systemd/system/wg-quick@.service.d" state: directory mode: 0755 + when: wireguard__config_target == "host" - name: Create systemd drop-in file for wg-quick@.service template: @@ -252,6 +274,7 @@ owner: "root" group: "root" mode: "0644" + when: wireguard__config_target == "host" - name: Start and enable WireGuard service service: @@ -259,26 +282,48 @@ name: "wg-quick@{{ wireguard_interface }}" state: started enabled: True - when: not ansible_os_family == 'Darwin' + when: (wireguard__config_target == "host" and ansible_os_family != 'Darwin') # Save WireGuard local facts [[[1 - name: Make sure that Ansible local facts directory exists file: - path: '/etc/ansible/facts.d' + path: '{{ (wireguard__controller_host_dir_path + if (wireguard__config_target == "ansible_controller") + else "") + "/etc/ansible/facts.d" }}' state: 'directory' - owner: 'root' - group: 'root' - mode: '0755' + owner: '{{ wireguard__controller_host_owner + if (wireguard__config_target == "ansible_controller") + else "root" }}' + group: '{{ wireguard__controller_host_group + if (wireguard__config_target == "ansible_controller") + else "root" }}' + mode: '{{ wireguard__controller_host_mode + if (wireguard__config_target == "ansible_controller") + else "0755" }}' - name: Save WireGuard local facts template: - src: 'etc/ansible/facts.d/wireguard.fact.j2' - dest: '/etc/ansible/facts.d/wireguard.fact' - owner: 'root' - group: 'root' - mode: '0755' + src: '{{ "etc/ansible/facts.d/" + + ("wireguard_static.fact.j2" + if (wireguard__config_target == "ansible_controller") + else "wireguard.fact.j2") }}' + dest: '{{ (wireguard__controller_host_dir_path + if (wireguard__config_target == "ansible_controller") + else "") + "/etc/ansible/facts.d/wireguard.fact" }}' + owner: '{{ wireguard__controller_host_owner + if (wireguard__config_target == "ansible_controller") + else "root" }}' + group: '{{ wireguard__controller_host_group + if (wireguard__config_target == "ansible_controller") + else "root" }}' + mode: '{{ "0644" + if (wireguard__config_target == "ansible_controller") + else "0755" }}' register: wireguard__register_facts - name: Update Ansible facts if they were modified - action: setup + setup: + fact_path: '{{ (wireguard__controller_host_dir_path + "/etc/ansible/facts.d") + if (wireguard__config_target == "ansible_controller") + else omit }}' when: wireguard__register_facts is changed diff --git a/tasks/setup-debian-vanilla.yml b/tasks/setup-debian-vanilla.yml index 63a9d4e..35212d1 100644 --- a/tasks/setup-debian-vanilla.yml +++ b/tasks/setup-debian-vanilla.yml @@ -26,6 +26,7 @@ apt: name: - "wireguard" + - "python3-future" state: present tags: - wg-install diff --git a/templates/etc/ansible/facts.d/wireguard.fact.j2 b/templates/etc/ansible/facts.d/wireguard.fact.j2 index acc3d9c..bc70d2e 100644 --- a/templates/etc/ansible/facts.d/wireguard.fact.j2 +++ b/templates/etc/ansible/facts.d/wireguard.fact.j2 @@ -33,7 +33,7 @@ for wg_conf in glob('/etc/wireguard/*.conf'): stderr=subprocess.PIPE, shell=False, ).decode('utf8') - config = ConfigParser() + config = ConfigParser(strict=False) config.read_string(config_string) pubkey = subprocess.check_output( @@ -41,7 +41,7 @@ for wg_conf in glob('/etc/wireguard/*.conf'): input=config.get('Interface', 'PrivateKey').encode('utf8'), stderr=subprocess.PIPE, shell=False, - ).decode('utf8') + ).decode('utf8').strip() output['interface'][interface].setdefault('Interface', {}) output['interface'][interface]['Interface']['PublicKey'] = pubkey diff --git a/templates/etc/ansible/facts.d/wireguard_static.fact.j2 b/templates/etc/ansible/facts.d/wireguard_static.fact.j2 new file mode 100644 index 0000000..359d4cc --- /dev/null +++ b/templates/etc/ansible/facts.d/wireguard_static.fact.j2 @@ -0,0 +1,18 @@ +{# + # Copyright (C) 2020 Robin Schneider + # Copyright (C) 2020 DebOps + # SPDX-License-Identifier: GPL-3.0-or-later + #} +{% set facts_interfaces = {} %} +{% for host in groups[wireguard_inventory_group]|d([]) %} +{% set _ = facts_interfaces.update({ + wireguard_interface: { + 'Interface': { + 'PublicKey': hostvars[host].wireguard__fact_public_key, + } + } +}) %} +{% endfor %} +{ + "interface": {{ facts_interfaces | to_json }} +} diff --git a/templates/etc/wireguard/wg.conf.j2 b/templates/etc/wireguard/wg.conf.j2 index 4bfda49..0a09fea 100644 --- a/templates/etc/wireguard/wg.conf.j2 +++ b/templates/etc/wireguard/wg.conf.j2 @@ -12,9 +12,10 @@ Address = {{ wireguard_address }} PrivateKey = {{ wireguard__fact_private_key }} {% else %} PostUp = wg set %i private-key {{ wireguard__keys_directory }}/%i.privkey -{% for host in ansible_play_hosts %} +{% for host in groups[wireguard_inventory_group]|d([]) %} {% if host != inventory_hostname %} -PostUp = wg set %i peer {{ hostvars[host].wireguard__fact_public_key }} preshared-key {{ wireguard__keys_directory }}/%i,{{ [inventory_hostname, host] | sort | join(",") }}.psk +PostUp = wg set %i peer {{ hostvars[host].wireguard__fact_public_key + |d(hostvars[host].ansible_local.wireguard.interface[hostvars[host].wireguard_interface].Interface.PublicKey) }} preshared-key {{ wireguard__keys_directory }}/%i,{{ [inventory_hostname, host] | sort | join(",") }}.psk {% endif %} {% endfor %} {% endif %} @@ -54,12 +55,13 @@ PostDown = {{ wg_postdown }} {% if wireguard_save_config is defined %} SaveConfig = true {% endif %} -{% for host in ansible_play_hosts %} +{% for host in groups[wireguard_inventory_group]|d([]) %} {% if host != inventory_hostname %} [Peer] # {{ host }} -PublicKey = {{ hostvars[host].wireguard__fact_public_key }} +PublicKey = {{ hostvars[host].wireguard__fact_public_key + |d(hostvars[host].ansible_local.wireguard.interface[hostvars[host].wireguard_interface].Interface.PublicKey) }} {% if wireguard__key_templating == 'inline' and host in wireguard__fact_psks %} PresharedKey = {{ wireguard__fact_psks[host] }} {% endif %}