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
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 firstname.lastname@example.org 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
aptsources, a NordVPN hostname, and a VyprVPN hostname. These will be added to a list that you can optionally define in your playbook called
- 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.
/etc/rc.d/rc.localto re-source the
no_vpnscript later during the boot sequence. (This is a workaround, but it seems to work reliably.)
/etc/dnsmasq.confto manage the wireless network clients.
- Creates or updates
/etc/hostapd.confto advertise the wireless network and manage authentication of wireless clients.
- Ensures your VPN service account’s username/password are current in
- Downloads the OpenVPN configuration file from NordVPN or VyprVPN for the
vpn_nameendpoint 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-agentwhen the tunnel is starting or reconnecting.
- Ensures your VPN service account’s username/password are current in
- Configures IP forwarding and
iptablesmasquerading 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.
Your playbook can set these variables for the role; note that some of them are REQUIRED:
|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.||188.8.131.52|
|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|
n applies only to NordVPN
v applies only to VyprVPN
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: email@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 -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.
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: firstname.lastname@example.org 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.
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
inventory file like this:
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!