Step-by-step scripts to semi-automatically setup an OpenVPN server on Raspberry Pi.
Here are the things you need to get your device up and running.
You don’t need a fancy new model just for OpenVPN. A smaller one would work. Install Raspbian on the SD card. You will need some way to get it to connect to network and SSH turned on.
- Check the SSH documentation to figure out how to enable SSH including on headless configuration
- Also, Setting up a Raspberry Pi headless contain more information including setting up WiFi before the first boot.
This part is hopefully written as a shell script, if it works. It does the following:
- Install common tools
- Turn off swap, stopping more disk writes.
- Install
unattended-upgrades
and ask it to automatically install all package updates except for known packages that touches/boot
. - Set the file system to be readonly and move all the mutable states to
tmpfs
. This is done so that an unexpected power cycle won’t corrupt the file system and prevent the device from booting up. This is very important in order for devices that is hard to service. - Also installed a
remount
command-line tool for easy toggle between read-write and read-only.
To execute bootstrap.sh
on it, run this on your terminal:
ssh pi@raspberrypi.local -o UserKnownHostsFile=./known_hosts bash < ./bootstrap.sh
where raspberrypi.local
should be the mDNS hostname of your device in the same LAN. You are more than welcome to comment out the section that you don't need before running the script.
We'll setup two services so the connection will be more reliable.
We will setup the first OpenVPN server by running the openvpn-install script on the device.
wget https://raw.githubusercontent.com/Nyr/openvpn-install/92d90dac/openvpn-install.sh -O - | ssh pi@raspberrypi.local -o UserKnownHostsFile=./known_hosts -- "cat > /tmp/openssh-install.sh"
ssh pi@raspberrypi.local -o UserKnownHostsFile=./known_hosts "sudo remount rw"
ssh pi@raspberrypi.local -o UserKnownHostsFile=./known_hosts "sudo bash /tmp/openssh-install.sh"
ssh pi@raspberrypi.local -o UserKnownHostsFile=./known_hosts "sudo remount ro"
When prompted, set the OpenVPN server to use UDP/443, and a proper external hostname (see section 4.2 on dynamic DNS), and name the client file as client
.
We will use the HTTPS/QUIC port to establish our TLS connection to avoid blockage.
This script will do the following
- Stop the UDP OpenVPN server
- Make the necessary adjustment on the configuration file.
- Copy the config to add a TCP server and also make the necessary adjustment.
- Modify the client file.
- Remove the iptables systemd script and replace it with a crontab.
ssh pi@raspberrypi.local -o UserKnownHostsFile=./known_hosts bash < ./openvpn.sh
To gain access to the machine inside an ordinary home network setup, you'll need three things
- Track the external IP via Dynamic DNS
- Set the router IPv4 NAT to forward the incoming ports to a specific LAN IP
- Get the device to be on that specific LAN IP
Note that even though the device may receive an IPv6 address, most home routers block all incoming IPv6 connections and there is not way to configure it otherwise. Until that changes, we will keep working with IPv4.
The NAT port forwarding and LAN IP setup is substituted with UPnP (Universal Plug and Play). Just ensure that UPnP is turned on on the router. The script will try to configure it every hour and forward the port listed in the script. You can skip this part if you are sure that you can achieve (2) and (3) by manually configure the router, and the router configuration will stick.
ssh pi@raspberrypi.local -o UserKnownHostsFile=./known_hosts bash < ./upnp.sh
To setup a Dynamic DNS, you will need a Dynamic DNS service. It may be the DNS come with your domain registrar, for example Namecheap. It may be a free service where you CNAME
your subdomain hostname to the dynamic record. You could use the hostname provided by the dyanmic DNS service directly. Regardless, you will need to figure out the URL that curl
should hit and edit ddns.sh
to fill that in. The script will set it up in the crontab to run every hour.
ssh pi@raspberrypi.local -o UserKnownHostsFile=./known_hosts bash < ./ddns.sh
This part is completely optional. I have a heartbeat script living on the gist that I wish the device to run every hour. This is the way to achieve it; it's documented in gist.sh
.
ssh pi@raspberrypi.local -o UserKnownHostsFile=./known_hosts bash < ./gist.sh
With everything done you should have a /root/client.ovpn
that you can import into any OpenVPN client.
ssh pi@raspberrypi.local -o UserKnownHostsFile=./known_hosts sudo cat /root/client.ovpn > ./client.ovpn
On macOS I recommend TunnelBlick. On iOS there is OpenVPN Connect. Don't download software from unofficial source and always keep it up-to-date!
macOS Client options I am using:
-
Connect: Manually
-
Set DNS/WINS: Set nameserver
-
OpenVPN version: Default (2.4.7 - OpenSSL v1.0.2t)
-
VPN log level: OpenVPN level 3 - normal output
-
Monitor network settings: checked
-
Route all IPv4 traffic through the VPN: checked
-
Disable IPv6 unless the VPN server is accessed using IPv6: checked
-
Check if appearant public IP address changed after connecting: checked
iOS Client options I am using:
- Seamless Tunnel: checked
- VPN Protocol: Adaptive
- IPv6: IPv4-Only Tunnel
- Connection Timeout: 30 sec
- Allow Compression (insecure): Full
- AES-CBC Cipher Algorithm: checked
- Minimum TLS Version: Profile Default
- DNS Fallback: not checked
- Connect Via: Any network
- Layer 2 Reachability: checked
Reboot the device. Once it come back, you should have
- Two OpenVPN daemon up and running. You can verify that with
sudo systemctl status openvpn@server.service
andsudo systemctl status openvpn@server-tcp.service
. - The system filesystem should be read-only. You can verify that by running
remount
and see if it saysCurrent mount options: ro
instead ofrw
.
Run /etc/cron.hourly/iptables
to trigger iptables check: sudo /etc/cron.hourly/iptables
. There should be exactly one rule in the nat
table. Inspect the output of sudo iptables -t nat -L POSTROUTING
.
Run /etc/cron.hourly/upnp
and /etc/cron.hourly/ddns
on your own to trigger UPnP update: sudo /etc/cron.hourly/vpn && sudo /etc/cron.hourly/ddns
, after that you can verify that the external incomming connection works. The scripts save thier outputs at /var/log/upnp.log
and /var/log/ddns.log
.
To test the OpenVPN server on the TCP port, stop the UDP server and try to connect the client with it. The UDP server should timeout and the client should fallback to TCP.
Remember to change the SSH password! Noted that fail2ban
is not setup because it won't be able to tell the remote IP addresses behind an NAT.
Typing remount
will give you current mount option of the root filesystem, and usage.
bootstrap.sh
would patch a few scripts for them to remount the disk read-write temporary, but sometimes, remount
may fail to set the disk to read-only again.
When that happens, the offending processes may be identified in /var/log/remount.log
.
It would be wise to have some monitoring in place to verify the current disk state.
I am doing that in my Gist which runs hourly.
These script are developed with a QEMU image builder and test scripts controlled by a Makefile
.
The resulting image cannot be flashed into an SD card.
OpenVPN service don't actually work on the emulation because of native configuration, but it is useful enough to verify the script.
- Install qemu and wget:
brew install qemu wget
. - Run
make
- The resulting disk image can be found in
dist
. - Boot up the device with
make boot
. - On a separate terminal, test out each of the script with the following commands:
make test-bootstrap
,make test-openvpn-install
,make test-openvpn
,make test-upnp
,make test-ddns
, andmake test-gist
.
- RPI Qemu
- How to Setup QEMU Output to Console and Automate Using Shell Script
- Makefile cheatsheet
- Turn on SSH
- Moniter a string in shell until it is found
- Protect your Raspberry PI SD card, use Read-Only filesystem: very useful but the assumptions on directories are outdated.
- ReadonlyRoot: Also generic but useful.
- OpenVPN road warrior installer for Debian, Ubuntu and CentOS.
- Configuring
unattended-upgrades
on Raspbian Stretch - Test UDP connection