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 calledno_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 tono_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 theno_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 withsystemd-tty-ask-password-agent
when the tunnel is starting or reconnecting.
- Ensures your VPN service account’s username/password are current in
- 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:
Variable | Description | Default |
---|---|---|
ap_dhcp_network | The first three octets of the AP’s network. | 192.168.220 |
ap_dhcp_lease_time | The DHCP lease time of the AP’s clients. | 12h |
ap_dhcp_dns_server | The DNS server assigned to the AP’s clients. | 1.1.1.1 |
ap_channel | The AP’s wireless channel. | 6 |
ap_interface | The name of the AP’s (wireless) interface. | wlan0 |
ap_ssid | The SSID (network name) advertised by the AP. | private |
*ap_wpa_passphrase | The AP’s WPA passphrase that clients must use to connect. | (none) |
no_vpn_dest_addresses | A list of IP address destinations that should NOT be routed over the VPN. | |
no_vpn_dest_hostnames | A list of hostname destinations that should NOT be routed over the VPN. | |
*vpn_username | The VPN service account’s username. | (none) |
*vpn_password | The VPN service account’s password. | (none) |
*vpn_service | The VPN service to be used; must be “nord” or “vypr”. | (none) |
*vpn_name | The name of the VPN configuration (sometimes called “server”) to use. See example playbooks, below. | (none) |
nvpn_nord_transport | The protocol used by NordVPN; must be “tcp” or “udp”. | tcp |
vvpn_vypr_bits | The 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.