Welcome to Warp Zone! Easy VPN AP Setup with Ansible

VPNs are useful for many reasons. But if you have many devices it can be annoying to install, configure, authenticate, reconfigure, and keep a bunch of different VPN client apps updated. And if you have a device or two that you never want to be attributed to your real IP address and your VPN client drops or you forget to use the client altogether, you’re burned.

A great way to mitigate these pain points and risks is to use a device that creates a WiFi network and bridges that network to a VPN tunnel. That way you simply connect to that wireless network and boom: all their traffic goes over the encrypted tunnel and appears to come from the region you’ve configured. And if a non-attrib device is only configured to use that access point it should never expose your real IP address to any services it uses.

A standard Raspberry Pi (not a Zero or Pico) is a great option for this kind of thing: it’s cheap, has all the required interfaces, and can be a stand-alone, single-purpose device independent from your workstation or anything else on your network.

There is a great PiMyLifeUp tutorial detailing how to create a VPN Access Point, and I highly recommend reading through that if you’re interested in learning each step and building your access point manually. But if you’d prefer skipping the step-by-step process and have things Just Work–or if you need to re-configure your VPN AP months later and don’t want to have to dig through the tutorial to remember what needs to change–we’ve created an Ansible role to take care of the grunt work: https://github.com/CofenseLabs/ansible-cfl-vpn-ap

Prerequisites

We suggest using a Pi 3 or later, since they have built-in ethernet and wireless, but any Pi may work as long as it has at least one wireless network interface plus one other network interface.

The role has been tested on the current (as of this lab note’s publication) Raspberry Pi OS release and we will do our best to update it to work on future releases, as time allows. It might also work on other fairly modern Debian-based distributions. Pull requests are always welcome. 😉

If you don’t know what Ansible is, you should google that. You might also want to google the basics of creating a simple Ansible playbook if you haven’t before. And finally, you may want to familiarize yourself with the fundamentals of using Ansible Roles in a playbook if that is new to you.

Make sure you can ssh using public key authentication (without having to enter the remote user’s password) from the machine where you’re running ansible-playbook to the Raspberry Pi as the pi user, and that the pi user can sudo without having to enter a password (that should already work on a fresh install). Assuming your Pi’s hostname is vpn-ap.local, you’re ready to go when you can do this:

[you@workstation ~] % ssh pi@vpn-ap.local sudo whoami
root
[you@workstation ~] % # look ma, no passwords!

High-Level Summary of this Role’s Tasks

Take a look at tasks/main.yml for the nitty-gritty, but essentially this role does the following:

  • Configures static routes for IP addresses that should NOT use the VPN tunnel. Ansible needs to connect out to certain things in order to do what it needs to do, and if the openvpn service is broken and needs to be reconfigured these static routes will ensure that it can get what it needs.
    • Gets the default gateway IP address (i.e., your router’s internal IP–192.168.1.1 or something like that) and default network interface (probably eth0) from the Pi.
    • Compiles a list of hostnames that Ansible relies on. This includes apt sources, a NordVPN hostname, and a VyprVPN hostname. These will be added to a list that you can optionally define in your playbook called no_vpn_dest_hostnames.
    • Does a DNS lookup of each hostname in no_vpn_dest_hostnames (hostnames often have more than one associated IP address) and append each address to no_vpn_dest_addresses (which you can also define in your playbook if there are other destinations that you don’t want using the VPN).
    • Creates or updates /etc/network/if-up.d/no_vpn, which adds static routes for each IP address.
    • Updates /etc/rc.d/rc.local to re-source the no_vpn script later during the boot sequence. (This is a workaround, but it seems to work reliably.)
  • Configures /etc/dhcpcd.conf and /etc/dnsmasq.conf to manage the wireless network clients.
  • Creates or updates /etc/hostapd.conf to advertise the wireless network and manage authentication of wireless clients.
  • Configures openvpn:
    • Ensures your VPN service account’s username/password are current in /etc/openvpn/auth.txt.
    • Downloads the OpenVPN configuration file from NordVPN or VyprVPN for the vpn_name endpoint specified in your playbook.
    • Modifies the OpenVPN configuration to use /etc/openvpn/auth.txt, which prevents you from having to login interactively with systemd-tty-ask-password-agent when the tunnel is starting or reconnecting.
  • Configures IP forwarding and iptables masquerading to route wireless clients’ traffic through the VPN tunnel’s network interface (tun0).

…and of course restarting services as required, depending on what changes are made.

Role Variables

Your playbook can set these variables for the role; note that some of them are REQUIRED:

VariableDescriptionDefault
ap_dhcp_networkThe first three octets of the AP’s network.192.168.220
ap_dhcp_lease_timeThe DHCP lease time of the AP’s clients.12h
ap_dhcp_dns_serverThe DNS server assigned to the AP’s clients.1.1.1.1
ap_channelThe AP’s wireless channel.6
ap_interfaceThe name of the AP’s (wireless) interface.wlan0
ap_ssidThe SSID (network name) advertised by the AP.private
*ap_wpa_passphraseThe AP’s WPA passphrase that clients must use to connect.(none)
no_vpn_dest_addressesA list of IP address destinations that should NOT be routed over the VPN.[ ]
no_vpn_dest_hostnamesA list of hostname destinations that should NOT be routed over the VPN.[ ]
*vpn_usernameThe VPN service account’s username.(none)
*vpn_passwordThe VPN service account’s password.(none)
*vpn_serviceThe VPN service to be used; must be “nord” or “vypr”.(none)
*vpn_nameThe name of the VPN configuration (sometimes called “server”) to use. See example playbooks, below.(none)
nvpn_nord_transportThe protocol used by NordVPN; must be “tcp” or “udp”.tcp
vvpn_vypr_bitsThe VPN encryption level used for VyprVPN; must be 160 or 256.256

* required
n applies only to NordVPN
v applies only to VyprVPN

Example Playbooks

NordVPN

Here is minimal playbook using the role to configure a NordVPN tunnel:

---
- hosts: all
  roles:
    - role: cfl-vpnap
      vars:
        ap_wpa_passphrase: a_convenient_password
        vpn_service: nord
        vpn_name: us9059 # Chicago
        vpn_username: you@example.com
        vpn_password: your_nordvpn_password

You can find NordVPN’s suggested vpn_name for for your location–or choose another country that you’d like your traffic to come out of–by visiting https://nordvpn.com/servers/tools/.

If that’s too much pointin’-and-clickin’ for ya, you can also get the names and locations of Nord’s servers from the command line using curl and jq:

% curl -s https://api.nordvpn.com/v1/servers | jq -r '.'

But that particular JSON is kind of a pain to slice and dice with jq, so we created a little script called nordservers.py to make it super easy to select a NordVPN server.

% ./nordservers.py croatia
[*] Fetching https://api.nordvpn.com/v1/servers?limit=9999999 ... received 5334 bytes
ID    Name         Egress Country  Egress City  Categories    Load
----  -----------  --------------  -----------  ------------  ----
hr32  Croatia #32  Croatia         Zagreb       p2p,standard  14
hr25  Croatia #25  Croatia         Zagreb       p2p,standard  20
hr26  Croatia #26  Croatia         Zagreb       p2p,standard  27
hr27  Croatia #27  Croatia         Zagreb       p2p,standard  30
hr29  Croatia #29  Croatia         Zagreb       p2p,standard  30
hr30  Croatia #30  Croatia         Zagreb       p2p,standard  30
hr28  Croatia #28  Croatia         Zagreb       p2p,standard  44
hr31  Croatia #31  Croatia         Zagreb       p2p,standard  46

Check out it’s README for usage instructions, and if you’re interested in some of the more advanced server types that NordVPN provides, be sure to check out Nord VPN: Expert Mode.

VyprVPN

Here is minimal playbook using the role to configure a VyprVPN tunnel:

---
- hosts: all
  roles:
    - role: cfl-vpnap
      vars:
        ap_wpa_passphrase: a_convenient_password
        vpn_service: vypr
        vpn_name: USA - New York
        vpn_username: you@example.com
        vpn_password: your_vyprvpn_password

Choosing a VyprVPN server name is simpler than Nord, because Vypr has a very limited feature set. You just need to visit https://www.vyprvpn.com/server-locations and copy/paste a location to the vpn_name variable. Vypr does not provide a JSON formatted sever list, and while you can find various scripts floating around the internet people have made to scrape and parse vyprvpn.com web pages, I wouldn’t rely on them.

Example Output

How you run your playbook varies depending on how you like to set things up. Typically I have an ansible.cfg that looks something like this:

[defaults]
inventory = inventory
remote_user = pi
retry_files_enabled = False

An inventory file like this:

[development]
pi-nord.local

And then I run my playbook with:

% ansible-playbook site.yml -l development

Those specifics are really up to your own preferences, but once the playbook is run you should see some output similar to this below and after 1m43s or so your VPN-backed WiFi access point is in business!

 

All third-party trademarks referenced by Cofense whether in logo form, name form or product form, or otherwise, remain the property of their respective holders, and use of these trademarks in no way indicates any relationship between Cofense and the holders of the trademarks.