Create WireGuard VPN with Ansible

Create WireGuard VPN with Ansible

Currently we are using Cisco AnyConnect for our VPN solution. It works well, but it is a bit of a pain to install and configure. We are retiring the environment that the ASA is located in and it’s time to retire this piece of equipment since it doesn’t fit into our current infrastructure or future plans.

We are a fully remote company and as a result all of our resources and assets are also in the cloud. That said we would like to control access to our resources and have some ability to quickly revoke access if needed. Since we may have multiple endpoints or environments we would need to connect to we would like it to be simple and flexible. Additionally we would like to reduce expenses as much as possible so open-source is certainly preferred.

That said the simple and honest choices were between OpenVPN and WireGuard. Discussions with the technical resources and managers quickly settled on WireGuard. WireGuard is simple, lightweight, easy to configure and troubleshoot and has wide acceptance in the market. We can have multiple configurations on a single system without conflict. It’s simple enough to deploy and manage that if we needed to have multiple environments it’s pretty straight forward.

But we can always do better!

Scenario

Here is the basic scenario:

  • We are a small firm and we have a small presence in a co-located datacenter and we need to give our employees access to the datacenter resources. Management does not want us to use the resources provided by the datacenter to terminate the VPN due to cost, however we are able to spin up as many virtual machines as we want without incurring any additional cost.

Seems simple enough and this is what we’re going to work through. We have a bunch of remote users that need to connect to the datacenter and we don’t have any real central method of authentication here so using files is acceptable to this company’s risk profile.

WHY VPN?

So it’s the end of 2023 and I’m about to explain why a VPN is important. People don’t understand the costs or risks of being spied on, either in a corporate sense or personal sense either. This is worth its own post which I’ll link here when I get to it :)

  • Corporate: We want to protect our customer’s data and prevent internet snoops1 from being able to read or prioritize our private data.
  • Personal: We don’t want to be a larger target for advertising, data theft, man-in-the-middle attacks, etc. Using a VPN reduces the risks of a lot of these, even at home. Using a VPN will keep your ISP from being able to tell where you like to shop, what you like to watch, what you’re searching. Additionally, in some people’s lives it’s a matter of personal security.
  • Government: I live in the United States where currently (Dec, 2023) we are not exceptionally concerned with government persecuting us for our political views however, if you’re in a country where this is a problem a VPN may be a life-line.

Planning

Generally when developing a playbook you’re going to complete a series of steps that would normally be completed on the commandline, (Linux assumptions here) so I like to run through those steps to build out my playbook(s) or role(s).

  1. Configure Server

    • Install Software (WireGuard package)

    • Create server config file

    • Create WireGuard service

    • Configure service to start on boot

  2. Create Client Configurations

    • Create list of users

    • Create client configuration

  3. TEST

    • TEST

    • TEST

    • TEST

  4. Start playbook development

As you can tell we are just running through the process right now to make sure everything works as planned. We aren’t building the playbook or writing any code just doing the job. Take copious amounts of notes while doing this and make sure you’ve gotten every step. That is going to be the template you write the playbook from.2

Doing

Create WireGuard Server

  1. Install WireGuard and firewall

    sudo apt install wireguard ufw -y
    
  2. Create your WireGuard keys

    Create the private key with wg genkey which will output the private key. This private key is the input for the next step which derives the public key. echo <key> | wg pubkey

    wg genkey
    GO/b82NFwTXApR1CzO2MHtwYg0qWSpGgGgO//GgL5Xo=  # Create and use your own keys!
    
    echo GO/b82NFwTXApR1CzO2MHtwYg0qWSpGgGgO//GgL5Xo= | wg pubkey
    6Gj/JTnJjfFnqnAIA8l6pr718rfEGYK94G9RttzTUwE=
    

    These are the private key and public key you will need for the server. The clients get the public key while the server retains the private key in the configuration file. These values will eventually be stored in the ansible vault so we can start creating that yml file now.

  3. (Side Quest) Create vault.yml file

    server:
      private_key: GO/b82NFwTXApR1CzO2MHtwYg0qWSpGgGgO//GgL5Xo=
      public_key: 6Gj/JTnJjfFnqnAIA8l6pr718rfEGYK94G9RttzTUwE=
    
  4. Create the server config file /etc/wireguard/wg0.conf

    [Interface]
    Address = 172.20.100 .1/23 # Private Address IPv4
    Address = fd0d:27ad:deda::1/64 # Private IPv6
    SaveConfig = false
    PostUp = ufw route allow in on wg0 out on eth0
    PostUp = iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE
    PostUp = ip6tables -t nat -I POSTROUTING -o eth0 -j MASQUERADE
    PreDown = ufw route delete allow in on wg0 out on eth0
    PreDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
    PreDown = ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
    ListenPort = 51820
    PrivateKey = GO/b82NFwTXApR1CzO2MHtwYg0qWSpGgGgO//GgL5Xo=
    
  5. Update server networking so it can route and forward packets

    sudo echo net.ipv4.ip_forward=1 >> /etc/sysctl.conf
    sudo echo net.ipv6.conf.all.forwarding=1 >> /etc/sysctl.conf
    sudo sysctl -p
    
  6. Update firewall so it can accept VPN connections and SSH. This block allows the correct traffic through, resets the firewall service then prints out the current status.

    sudo ufw allow 51820/udp
    sudo ufw allow OpenSSH
    sudo ufw disable
    sudo ufw enable
    sudo ufw status
    
  7. Start and enable the WireGuard service

    sudo systemctl enable wg-quick@wg0.service
    sudo systemctl start wg-quick@wg0.service
    sleep 10
    sudo systemctl status wg-quick@wg0.service
    

At this point you should get a lot of output but it should be green and show a status of ‘Running’

YAY! We have a server running!

But it can’t accept any connections… And we have no clients…

Creating Clients

Part of what makes WireGuard so easy to use is one o the thngs that makes it difficlyt to adopt. Each configuratin is different and there is no method to distribute or update cient configurations after deployment. We will distribute these files using features of HashiCorp’s Vault.

Since we are using ansible to configure the server and it already has the keys for the server let’s leverage that to create the clients files.

  1. Review the client config anatomomy

    [Interface]
    PrivateKey = <CLIENT PRIVATE KEY>
    Address = <CLIENT STATIC VPN IP ADDRESS>/32
    DNS = 9.9.9.11, 149.112.112.11, 2620:fe::11, 2620:fe::fe:11 # DNS Servers to use when connected
    
    [Peer]
    PublicKEy = <SERVER PUBLIC KEY>
    AllowedIPs = 0.0.0.0/0, ::/0  # IPs to route through VPN
    Endpoint = vpn.domain.com:51820 # VPN server DNS address
    
  2. Server Updates for client - We need to add the [Peer] Section for each

    # Entry for each peer on server
    PublicKey = <SERVER PUBLIC KEY>
    AllowedIPs = <CLIENT STATIC VPN IP ADDRESS>/32
    

Looking at this we need four pieces of information to make our configs:

  • Client Private Key
  • Client Public Key
  • Server Public Key
  • IP Address for client when connected to VPN

Since all this needs to be retained and can’t change we are going to store this information in ansible vault. The datastructure is simple:

   users:
      ex_user_001:
         key: 6AGIjiJIHoas8Ew0vENLJvslbsZGo6d+rerRPK++lUI=
         pubkey: R08mgG3FAfNOMn/qjSUMWO7L83XV7y5IFpjHRg6CKiY=
         number: 148

      ex_user_002:
         key: MPl1exnEnE2vAfhUoUKy9BZnWAucUpYtk5HAaTJnDUc=
         pubkey: 4q9IpJxBCZ6HRtWbHgogPlDY9G2LvUkmwQNupLmXrlQ=
         number: 149

This gives us all the information we need to create all of our config entries for the clients. It’s tedious to create this for a bunch of users and keep it straight so I created a shell script to create the users. Execute the script to create the users we will copy the relevant entries into your vault file later.

cat /dev/null > new_clients.yml
echo "users:"
for i in {1..150}; do
    KEY=$(wg genkey)
    PUBKEY=$(echo $KEY | wg pubkey)
    echo "  user-$i": >> new_clients.yml
    echo "    key: $KEY" >> new_clients.yml
    echo "    pubkey: $PUBKEY" >> new_clients.yml
    echo "    number: $i" >> new_clients.yml
    echo "" >> new_clients.yml
done

Create Ansible Playbook

Now we have all the steps we need to accomplish to make this work and all the data required.

  1. Create MOTD that tells people this is an Ansible Managed system
  2. Deploy WireGuard to the server
  3. Create Server Configurations
  4. Create Client configs that need to be distributed to clients
  • Secondary Objective: Configure so that it can configure new server from scratch

    Create the project directory structure

Create the project directory and the required files. We are also creating and populating the pwfile for ansible vault bucause I know you would never store this in a public location or repo if it wasn’t encrypted right.

export PROJECT_DIR=~/wg_project
mkdir -P $PROJECT_DIR/ansible
mkdir -p $PROJECT_DIR/ansible/handlers
mkdir -p $PROJECT_DIR/ansible/templates
mkdir -p $PROJECT_DIR/ansible/vars
touch $PROJECT_DIR/ansible/inventory.yml
touch $PROJECT_DIR/ansible/handlers/main.yml
touch $PROJECT_DIR/ansible/vars/vars.yml
echo "ansible/pwfile.txt" >> .gitignore
openssl rand -base64 32 > $PROJECT_DIR/ansible/pwfile.txt  # Ansible Vault PW File

Build Data & Ansible Files

  1. Populate Vault file Populate the values with what you have previously or generate new values. These values will be used by the templates to create the config files. This keeps us from needing to have the key values in an unsecured code repo.
    wireguard_private_key: OPq2MSt6jd48rwz1Rl+xerrNGoD9x5nQwQm3aC15UlM=
    wireguard_public_key: Z1lsQcYzaGSrQhm8I9kDOezr+wWjPqJFS1JvuLgjM1A=
    
    users:
       cool-user:
          key: GALbYcYdUZAdj1Vimr/Dgd9+ig+O675jEas8/58GAUA=
          pubkey: PUNU2maOfqX+Z1gCj4BG7pdVjxw7maDK+U/lHQ1nrFM=
          number: 15
    
       bob:
          key: CO2K6Z5hqCOIth5e6DXDzD9mPXhkNadi99ytPNxHR0g=
          pubkey: sTyfg//FYtNeTyUk78ZT0+16E8DhwoAeiPMWUzHa4WI=
          number: 46
    
       jake:
          key: UF2zByFRLA63qj9aZ/ZGAdxrigWPasnfLjLojnPubnI=
          pubkey: dQACpWMf93+JO3oPJqoIchT6UOW8BLBTNe9g5koNUls=
          number: 47
    
       ishmail:
          key: ECu4XIhX9HZdwkshU/lvGQcaCNt4/OBZO9xXfmUaMF4=
          pubkey: uo8iMNSFrQQ/Ynqp3FtBJuRsXgZwqx3luzU+VxMfwUM=
          number: 48
    
       # Repeat for each user
    
  2. Define our inventory file
    • We define the group of one so it can be incorporated into larger workflows later
    all:
       hosts:
          wireguard-server:
             ansible_host: 172.20.200.15
    
  3. Define the top matter for our playbook in main.yml.

    ---
    - name: Update and Install WireGuard on Localhost
      hosts: wireguard-server
      become: true
      gather_facts: true
    
      vars_files:
        - vars/vars.yml
        - vars/vault.yml
    
    • This will only be executed on the WireGuard server(s) not clients as limited to the group wireguard-server in the inventory file
    • We are logging as not root server and will become root via sudo
    • We are going to collect inventory of the host(s)
  4. Create the MOTD Template - templates/motd.j2

       
       
    ================================================================================
    
    This system is managed and configured via Ansible.  The Ansible playbooks are
    located in the `ansible` directory of the repo.  
    
    Repo: "{{ repo }}"
    Last Execution: "{{ ansible_date_time.year }}-{{ ansible_date_time.month }}-{{ ansible_date_time.day }} {{ ansible_date_time.hour }}:{{ ansible_date_time.minute }} UTC"
    
    
    ================================================================================
       
       
    
    • Update the message as appropriate for your organization
    • Ensure the repo variable is defined - vars/vars.yaml
    • Last Execution fills in the values from Ansible
  5. Execute the Ansible template to create the MOTD

       tasks:
          - name: "Update MOTD"
             ansible.builtin.template:
             src: "motd.j2"
             dest: "/etc/motd"
             owner: root
             group: root
             mode: "0644"
    
    • name: Defines the name of the task and allows it to be identified if needed
    • ansible.builtin.template: this is the full reference to the module we are using
    • src: (required) the filename in the templates/ directory to use
    • owner: (required)
    • group: (required)
    • mode: (required) input as string and allow ansible to interpret - hence quotes
  6. Install WireGuard and Firewall

       - name: Install WireGuard and Firewall
         ansible.builtin.apt:
           state: present
           pkg:
            - wireguard
            - ufw
           update_cache: true
    
  7. Create the template for the server config - templates/wg.conf.j2

          
       [Interface]
       Address = 172.20.100.1/23
       Address = fd0d:27ad:abcd::1/64
       SaveConfig = false
       PostUp = ufw route allow in on wg0 out on eth0
       PostUp = iptables -t nat -I POSTROUTING -o eth0 -j MASQUERADE
       PostUp = ip6tables -t nat -I POSTROUTING -o eth0 -j MASQUERADE
       PreDown = ufw route delete allow in on wg0 out on eth0
       PreDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
       PreDown = ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
       ListenPort = 51820
       PrivateKey = {{ wireguard_private_key }}
    
    
       {% for user_name, user_data in users.items() %}
    
       [Peer]
       # {{ user_name }}
       PublicKey = {{ user_data.pubkey }}
       Address = 172.20.100.{{ user_data.number }}, fd0d:27ad:abcd::{{ user_data.number }}/128
    
       {% endfor %}
          
    
    • This is a copy of the configuration we had prior with the sensitive and repetitive variables swapped out
    • wireguard_private_key is in the vault from a prior step
    • The data for the users was placed in the vault from a prior step and is looped through creating a [Peer] stanza for each
    • user_name: is the id in the data structure in vault
    • user_data.pubkey: pubkey in the vault
    • user_data.number: defines the IP address of the client since it must be static
  8. Create Ansible step to create config from template
       
     - name: Create WireGuard Server Config
       ansible.builtin.template:
         src: "wg0.conf.j2"
         dest: "/etc/wireguard/wg0.conf"
         owner: root
         group: root
         mode: "0600"
       # notify: Restart WireGuard Server
    
    • Using the fine templates/wg0.conf.j2
    • Create the file based off the template in /etc/wireguard/wg0.conf
  9. Create the Client Config Directory This needs to be treated reasonably securely - I’m putting it in the directory here as an example but it should really be retained in Vault or someplace similar.

       - name: Create Client config directory
         ansible.builtin.file:
           dest: "/etc/wireguard/clients"
           owner: root
           group: root
           state: directory
           mode: "0600"
    
    
  10. Create the template for the client configs - templates/client.conf.j2

          
       # Configuration for {{ item.key }} ONLY
       [Interface]
       PrivateKey = {{ item.value.key }}
       Address = 172.20.100.{{ item.value.number }}/32, fd0d:27ad:abcd::{{ item.value.number }}/128
    
       [Peer]
       PublicKey = {{ wireguard_public_key}}
       AllowedIPs = 0.0.0.0/0, ::/0
       Endpoint = vpn.domain.com:51820
    
    
       
    

    This loops through the times and creates the file for each of them.

  11. Create Ansible step to create configs from template
       
       - name: Create Client Configurations
         ansible.builtin.template:
           src: "client.conf.j2"
           dest: "/etc/wireguard/clients/{{ item.key }}.conf"
           owner: root
           group: root
           mode: "0600"
         loop: "{{ lookup('dict', users) }}"
         loop_control:
           label: "{{ item.key }}"
       
    
    • This loops through every item of users
    • Creates an individual file for each item with the contents of the template
  12. Setting services to start on boot and starting them

    
       - name: Enable wg-quick@wg0.service
          ansible.builtin.systemd:
          name: wg-quick@wg0.service
          enabled: yes
    
       - name: Start wg-quick@wg0.service
          ansible.builtin.systemd:
          name: wg-quick@wg0.service
          state: started
    
  13. Check the services

       
       - name: Check status of wg-quick@wg0.service
          ansible.builtin.command:
          cmd: systemctl status wg-quick@wg0.service
          register: service_status
    
       - name: Show service status
          ansible.builtin.debug:
          msg: "{{ service_status.stdout_lines }}"
       
    

    Honestly - this is a personal preference that I’m getting into the habit of doing as a final check. This will eventually be put into OpenSearch but that’s a topic for another day. ;)

  14. Define Handler(s) handlers/main.yml

    The purpose of the handler is to take some action when called. Multiple tasks can call the handler but it will only execute once when all the actions that call that handler are complete. This can be anything that needs to happen - here we are using it to restart the service.

       ---
       - name: Restart_wireguard
          ansible.builtin.systemd:
             name: wg-quick@wg0.service  # Replace wg0 with your WireGuard interface name if different
             state: restarted
             daemon_reload: true
    
  15. Register Handler in playbook ${PROJECT_DIR}/main.yml

    In the top matter of the file include the include lines. This tells Ansible where to look for the handlers.

    
       handlers:
          - include: handlers/main.yml
       
    

Execute Ansible Playbook

  1. Encrypt the Vault
    cd $PROJECT_DIR/ansible
    ansible-vault encrypt vars/vault.yml --vault-password-file=pwfilt.txt
    
  2. Execute Playbook
    ansible-playbook -i inventory.yml playbook.yml --vault-password-file=pwfile.txt
    
  3. Output3

    ansible-playbook -i inventory.yml main.yml --vault-password-file=pwfile.txt --limit wireguard-test
       [DEPRECATION WARNING]: "include" is deprecated, use include_tasks/import_tasks instead. See https://docs.ansible.com/ansible-core/2.14/user_guide/playbooks_reuse_includes.html for details. This feature will be 
       removed in version 2.16. Deprecation warnings can be disabled by setting deprecation_warnings=False in ansible.cfg.
    
       PLAY [Update and Install WireGuard on Localhost] ******************************************************************************************************************************************************************
       skipping: no hosts matched
    
       PLAY [Configure WireGuard Server] *********************************************************************************************************************************************************************************
    
       TASK [Gathering Facts] ********************************************************************************************************************************************************************************************
       ok: [wireguard-test]
    
       TASK [Update MOTD] ************************************************************************************************************************************************************************************************
       changed: [wireguard-test]
    
       TASK [Install WireGuard and Firewall] *****************************************************************************************************************************************************************************
       ok: [wireguard-test]
    
       TASK [Create WireGuard Server Config] *****************************************************************************************************************************************************************************
       ok: [wireguard-test]
    
       TASK [Create Client config directory] *****************************************************************************************************************************************************************************
       ok: [wireguard-test]
    
       TASK [Create Client Configurations] *******************************************************************************************************************************************************************************
       ok: [wireguard-test] => (item=temp_user_10)
       ok: [wireguard-test] => (item=temp_user_20)
       ok: [wireguard-test] => (item=temp_user_30)
       ok: [wireguard-test] => (item=temp_user_40)
       ok: [wireguard-test] => (item=temp_user_50)
       ok: [wireguard-test] => (item=temp_user_60)
    
       TASK [Enable wg-quick@wg0.service] ********************************************************************************************************************************************************************************
       changed: [wireguard-test]
    
       TASK [Start wg-quick@wg0.service] *********************************************************************************************************************************************************************************
       changed: [wireguard-test]
    
       TASK [Check status of wg-quick@wg0.service] ***********************************************************************************************************************************************************************
       changed: [wireguard-test]
    
       TASK [Show service status] ****************************************************************************************************************************************************************************************
       ok: [wireguard-test] => {
          "msg": [
             "● wg-quick@wg0.service - WireGuard via wg-quick(8) for wg0",
             "     Loaded: loaded (/lib/systemd/system/wg-quick@.service; enabled; preset: enabled)",
             "     Active: active (exited) since Tue 2023-12-26 15:12:18 UTC; 1s ago",
             "       Docs: man:wg-quick(8)",
             "             man:wg(8)",
             "             https://www.wireguard.com/",
             "             https://www.wireguard.com/quickstart/",
             "             https://git.zx2c4.com/wireguard-tools/about/src/man/wg-quick.8",
             "             https://git.zx2c4.com/wireguard-tools/about/src/man/wg.8",
             "    Process: 4897 ExecStart=/usr/bin/wg-quick up wg0 (code=exited, status=0/SUCCESS)",
             "   Main PID: 4897 (code=exited, status=0/SUCCESS)",
             "        CPU: 152ms"
          ]
       }
    
       PLAY RECAP ********************************************************************************************************************************************************************************************************
       wireguard-test             : ok=10   changed=4    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
    
    
    

Conclusion

This process should successfully execute and configure your WireGuard server from start to finish. Initially, we manually configured the server to develop the Ansible playbook. While this approach was sufficient to get the server up and running, it lacked the scalability and replicability essential for efficient management. Without Ansible, each new user configuration would require manual setup, a time-consuming and error-prone process. Now, by simply making an entry into the vault and executing the playbook, everything is deployed in a standardized and consistent manner. This automation not only saves time but also ensures uniformity across different environments or machines. Imagine the complexity of explaining the addition of a new user over the phone using the manual method versus the streamlined process we have now established with Ansible. This approach significantly simplifies the management of the VPN server, making it more accessible and manageable.

– Cheers

Ansible for DevOps - If you’re just starting with Ansible this is the book you want. Get it from Leanpub and if there are any updates to the book you will also receive them. (Digital Only)

  1. Corporate entities that handle your data such as ATT, Google, Amazon, Microsoft are known to read or analyze their customer’s un-encrypted data or network traffic. ↩︎

  2. Do not use the keys in the examples. I intentionally change them so they won’t work if you copy and paste. Generating them is simple and I really want you to be secure ↩︎

  3. I’m executing this against the testing server not the production wireguard servers. You are too, right? ↩︎