How to Configure a Personal VPN
This post is going to be a quick, high-level summary of the VPN I just set up for my personal use yesterday. I won’t go into a lot of details, but I’m going to add a few things I learned along the way.
The Root CA
The trust chain is a representation of how computers know the other is a “good guy”. I’ll start with an analogy.
How do you make friends? One way is through other friends. The two new friends are usually introduced through a third, trusted party. In our situation, the server, my desktop, and my phone are all new friends, and the trusted intermediary is…the intermediary CA! But then the new question is, “how did the server, my desktop, and my phone meet the intermediary friend?” This is the same question as how you solve the secure introduction problem, which is usually solved at provision time. The intermediary creates the leaves using OpenSSL. This will create the public certificate and private key for the new leaf in the trust tree.
Before going into this, I’ll explain the file naming convention because I’ve noticed that there isn’t a strong convention here. I prefer to do file_name.key.pem to be specific about what a key is and what a cert is. The .pem part describes the format of the private key or public certificate. I’ve been saying private key and public certificate a lot, but only to be verbose. They’re commonly just called the key and cert.
Generating the root certificate and private key is pretty easy:
# Generate the private key
# https://www.openssl.org/docs/man1.1.0/man1/genpkey.html
openssl genpkey \
-out "/tmp/root_ca.key.pem" \
-algorithm rsa \
-outform PEM \
-pkeyopt rsa_keygen_bits:4096
# Generate the public certificate
# req PKCS#10 X.509 Certificate Signing Request (CSR) Management.
openssl req \
-x509 \
-new \
-nodes \
-inform PEM \
-outform PEM \
-config openssl-req-config-infile-root.cnf \ # More on this file later
-key "/tmp/root_ca.key.pem" \ # Keep this private
-sha256 \
-pkeyopt rsa_keygen_bits:4096 \
-days 3650 \
-out "/tmp/root_ca.cert.pem" # This is public
This will generate
-----BEGIN PRIVATE KEY-----
MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDH5RXhfPzvA5Sh
Oc2k1CfwCxQKwTZpk+JtBBL7UFdksFo0
-----END PRIVATE KEY-----
and
-----BEGIN CERTIFICATE-----
MIIFYzCCA0ugAwIBAgIUIe3P/wgr+lcnFoX51UmKWqFxOX4wDQYJKoZIhvcNAQEL
RoGjz4aGze2qyxZuNx0S5Ot0kPhe0c6G2/NAvXTn2uI290i0CH4Z
-----END CERTIFICATE-----
The openssl-req-config-infile-root.cnf has some defaults that I set. There’s a great tutorial on Jamie Nguyen’s blog for the specific CA parts. This config adds more options in addition to the options you set on the command line.
When you do this kind of work at a company, provisioning the root CA is a huge deal. These kinds of operations will sometimes get done on a specific laptop. Then the Root CA will sign a few intermediate CA’s while physically on the laptop. Once the CA’s are generated, they’re moved from the laptop to another secure place like Hashicorp Vault, AWS KMS, or some other secure place. Then that laptop is physically thrown into a safe. I’ve heard of stories where this magical machine is thrown into an actual vault, and requires 3 people with their own key to access the vault in the bunker. In my case, I did something less extreme. Just to be extra sure I don’t mess with the certificates after I create them, I put a USB drive in my PC, then started a Vagrant VM and mounted the USB drive to the VM I started and did all my operations on the drive. Then I put the drive in a Lego Millenium Falcon I have for safe keeping.
The Server
I deployed a t3.nano in AWS with a leaf of the trust tree. One leaf of the trust tree is the VPN server, one leaf is my desktop, and one leaf is my phone. In the VPN configs, I say which is which. I’ll walk through my config a bit. In each certificate, there’s a little setting that I use to designate which leaf is the server and which is the client as well. Here is an example but I added some more comments, deleted some unnecessary options, and inserted a few things to be more expressive.
Deploying in AWS
# Turn on server mode and supply a VPN subnet
# for OpenVPN to draw client addresses from.
# The server will take 10.121.69.1 for itself,
# the rest will be made available to clients.
# Each client will be able to reach the server
# on 10.121.69.1. Comment this line out if you are
# ethernet bridging. See the man page for more info.
# The 255.255.255.0 defines the bitmask that is allowed.
# In this case, we're "reserving the first 3 and leaving
# the last open, giving us 255 addresses to chose from.
# This would be equivalent to 10.121.69.0/24. This means
# the new clients will get anything between 10.121.69.2 and
# 10.121.69.255
server 10.121.69.0 255.255.255.0
# Define what port this server should listen on. Notice that
# I'm not defining a network interface here. When OpenVPN starts,
# it needs to be run as root so that it can create a network
# interface. Later in the config, we can set the process to
# de-escalate it's priveledges and go to a different user.
# This will bind to 0.0.0.0 (all interfaces). See netstat -nulp
# for more info once it's up.
port 1194
# TCP or UDP server? It's most common to use udp here.
# proto tcp # is the other option here.
proto udp
# I chose to use dev tun here and create a tunnel as opposed
# to dev tap. tap is more commonly used when you're also creating
# a bridged system. This is also how we set things up at my last
# job.
# dev tap # is the other option here
dev tun
# Any X509 key management system can be used.
# There are two ways to configure how the config finds
# the files. One is to specify it like the following...
# ca leaf-chain.cert.pem
# cert leaf.cert.pem
# key leaf.key.pem # This file should be kept secret
# The other way is to nest it in the file using HTML like
# tags. I prefer this way because it means fewer files when you
# provision a client or server. Then the .ovpn itself becomes a
# secret file since it has the private key in it. It's easier
# just give users an ovpn file than to give them a chain, cert
# and key file and ask them to put it in known, relative directories.
<ca>
-----BEGIN CERTIFICATE-----
MIIFYzCCA0ugAwIBAgIUSq82ZF1hC6AukqXeVVh7rjZoR3gwDQYJKoZIhvcNAQEL
aidLXA8FoSzQMg2662QP1L0vx2jFtxHqnUuqy0FfT7cREtdT/sFq
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFajCCA1KgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwOTELMAkGA1UEBhMCVVMx
DzVx1E4auMyZtVeTJ+/iGPoTfbotywHwzE1gelOAQJVL2WGkYRBhJ7ebEwWekA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIGJTCCBA2gAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwOTELMAkGA1UEBhMCVVMx
kN+a9C54b9erA740mkmv9dtN7WCk5NbQ3g+7Sj8zJCjiKxL2jmxRmh0=
-----END CERTIFICATE-----
</ca>
# cert leaf.cert.pem
<cert>
-----BEGIN CERTIFICATE-----
MIIGJTCCBA2gAwIBAgICEAEwDQYJKoZIhvcNAQELBQAwOTELMAkGA1UEBhMCVVMx
kN+a9C54b9erA740mkmv9dtN7WCk5NbQ3g+7Sj8zJCjiKxL2jmxRmh0=
-----END CERTIFICATE-----
</cert>
# key leaf.key.pem # Keep this secret
<key>
-----BEGIN PRIVATE KEY-----
MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDNCucM24d8H44p
1ISP0V8/GNRQhjcJMQfjI37c0e2bzQ==
-----END PRIVATE KEY-----
</key>
# Diffie hellman parameters.
# Generate your own with:
# openssl dhparam -out dh2048.pem 2048
# dh dh2048.pem
<dh>
-----BEGIN DH PARAMETERS-----
MIIBCAKCAQEAos0ITHmDf0W7uowvg774AkCmbZi1K8uPUC7gUhTF+wfusA3z3kgl
RBYctN8JJxZ9B5NEAIYBZ7SjOXck1ntQIwIBAg==
-----END DH PARAMETERS-----
</dh>
# Maintain a record of client <-> virtual IP address
# associations in this file. If OpenVPN goes down or
# is restarted, reconnecting clients can be assigned
# the same virtual IP address from the pool that was
# previously assigned.
ifconfig-pool-persist ipp.txt
# This allows clients to have network line of sight
# with other clients. I'm using this so that I can ssh
# from my phone, to my desktop.
client-to-client
# This tells the server that only one connection is allowed
# per CNAME. On each public certificate/private key pair,
# there is a unique CNAME associated with it. If the same
# cert/key pair is used twice, the two clients could get the same
# ip address and you'll get undefined behaviour. You ALWAYS
# want this on. If you allow yourself to use this in testing,
# it's likely you'll forget to uncomment it when you go to
# production.
duplicate-cn
# keepalive send_interval fail_threshold
# send every 10 seconds,
# declare dead if they failed for 120 seconds
keepalive 10 120
# For extra security beyond that provided
# by SSL/TLS, create an "HMAC firewall"
# to help block DoS attacks and UDP port flooding.
#
# Generate with:
# openvpn --genkey --secret hmac-firewall.key
#
# The server and each client must have
# a copy of this key.
# The second parameter should be '0'
# on the server and '1' on the clients.
# tls-auth hmac-firewall.key 0 # This file is secret
key-direction 0
<tls-auth>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
a706a52299dac30b2a0ee95ef1895654
d39ab66e3bad892ed827a6980b77081a
-----END OpenVPN Static key V1-----
</tls-auth>
# Select a cryptographic cipher.
# This config item must be copied to
# the client config file as well.
# Note that v2.4 client/server will automatically
# negotiate AES-256-GCM in TLS mode.
# See also the ncp-cipher option in the manpage
cipher AES-256-CBC
# The maximum number of concurrently connected
# clients we want to allow. Use this number and
# check the NIC on the host. Since I'm using a t3.nano,
# and I only expect myself to connect to it, I'm setting a low
# upper bound to prevent bad actors.
max-clients 10
# The persist options will try to avoid
# accessing certain resources on restart
# that may no longer be accessible because
# of the privilege downgrade.
persist-key
persist-tun
# Output a short status file showing
# current connections, truncated
# and rewritten every minute.
status openvpn-status.log
# Notify the client that when the server restarts so it
# can automatically reconnect.
explicit-exit-notify 1
To deploy it, I put a systemd on a server in AWS with the following security group. If I were at work, and a company were paying my AWS bill, I’d put an NLB in front of my VPN server, but that would cost roughly $18 USD a month.
resource "aws_security_group" "vpn_server" {
name = "vpn-server"
description = "VPN Server"
ingress {
description = "Ingressing VPN Traffic"
from_port = 1194
to_port = 1194
protocol = "udp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "Allow SSH from trusted places"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = data.terraform_remote_state.networking.outputs.trusted_ips
}
ingress {
description = "Allow MOSH from trusted places"
from_port = 22
to_port = 22
protocol = "udp"
cidr_blocks = data.terraform_remote_state.networking.outputs.trusted_ips
}
egress {
description = "Allows for initating requests. Usefull for installing deps"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
The Client
Provisioning the client looks like this:
# This is the first and most important line that says if we're the
# client or server.
client
# When you start the server or client, a new
# bridgle will get created. There are two options
# here, tun, and tap.
# Once the client process is running, you'll see
# tun0: flags=4305<UP,POINTOPOINT,RUNNING,NOARP,MULTICAST> mtu 1500
# inet 10.121.69.10 netmask 255.255.255.255 destination 10.121.69.9
# inet6 fe80::3971:ed85:d2d3:66b6 prefixlen 64 scopeid 0x20<link>
# unspec 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00 txqueuelen 100 (UNSPEC)
# RX packets 2 bytes 96 (96.0 B)
# RX errors 0 dropped 0 overruns 0 frame 0
# TX packets 433 bytes 56060 (56.0 KB)
# TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
dev tun
# proto is short for protocol
proto udp
# You can specify multiple servers (remote's) and
# the client will try several
remote ec2-1-1-1-1.us-west-2.compute.amazonaws.com 1194
;remote my-server-2 1194
# Keep trying indefinitely to resolve the
# host name of the OpenVPN server. Very useful
# on machines which are not permanently connected
# to the internet such as laptops.
resolv-retry infinite
# Most clients don't need to bind to
# a specific local port number.
nobind
# This will presever the tunnel and key across process restarts
persist-key
persist-tun
# The openvpn config file usually has filenames and is configured like
# ca something-chain.cert.pem. I don't really like this because when you
# copy files over, you have copy all of them over. I look at every file
# copied as another place you can make a mistake, so I prefer to just
# put them into one .ovpn file. This is also much easier for other users to
# reason about. If you just tell a user to call `openvpn config-file.ovpn`
# then they don't have to worry about relative locations and where to find
# the cert, key, chain, and tls-auth file.
# This is the CA chain. The first certificate is the root of the
# chain, the second is the intermediary, and the third is the leaf
<ca>
-----BEGIN CERTIFICATE-----
MIIFajCCA1KgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwOTELMAkGA1UEBhMCVVMx
aidLXA8FoSzQMg2662QP1L0vx2jFtxHqnUuqy0FfT7cREtdT/sFq
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFajCCA1KgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwOTELMAkGA1UEBhMCVVMx
DzVx1E4auMadsfasdfasdfasdfasdfasdfasdfasdfasdfGkYRBhJ7ebEwWekA==
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIF2DCCA8CgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwOTELMAkGA1UEBhMCVVMx
8qadhabntfT+x9Y1
-----END CERTIFICATE-----
</ca>
# This is the leaf certificate
<cert>
-----BEGIN CERTIFICATE-----
MIIF2DCCA8CgAwIBAgICEAIwDQYJKoZIhvcNAQELBQAwOTELMAkGA1UEBhMCVVMx
8qadhabntfT+x9Y1
-----END CERTIFICATE-----
</cert>
# key leaf.key.pem # Keep this secret
<key>
-----BEGIN PRIVATE KEY-----
MIICWNqUlQ6zJRSsJCDokCOIAqwpDwnrYjeKjgX5OmPV5WeYeeDOiiNrnHtmZbOR
KR5Xw+v07RJ+qLF5lH5gMwW5emtVSQ==
-----END PRIVATE KEY-----
</key>
# Verify server certificate by checking that the
# certificate has the correct key usage set.
# This is an important precaution to protect against
# a potential attack discussed here:
# http://openvpn.net/howto.html#mitm
#
# To use this feature, you will need to generate
# your server certificates with the keyUsage set to
# digitalSignature, keyEncipherment
# and the extendedKeyUsage to
# serverAuth
# EasyRSA can do this for you.
remote-cert-tls server
# To avoid a possible Man-in-the-Middle attack where an authorized
# client tries to connect to another client by impersonating the
# server, make sure to enforce some kind of server certificate
# verification by clients. There are currently four different ways
# of accomplishing this, listed in the order of preference:
# (1) Build your server certificates with the build-key-server
# script. This will designate the certificate as a
# server-only certificate by setting nsCertType=server.
# Now add the following line to your client configuration:
# This will block clients from connecting to any
# server which lacks the nsCertType=server designation
# in its certificate, even if the certificate has been
# signed by the CA which is cited in the OpenVPN configuration
# file (--ca directive).
# https://openvpn.net/community-resources/rsa-key-management/
ns-cert-type server
# If a tls-auth key is used on the server
# then every client must also have the key.
# This firewall key is placed on the server.
# That same file also needs to be on the client.
# tls-auth hmac-firewall.key 1
# When the openvpn client tries to make the initial handshake
# it'll assert that this file is the exact same as the servers.
# They just do a direct byte by byte comparison. Then they're allowed
# to start their Diffie Hellman dance. This is to protect from DOS'ing
# the server. It's really cheap to do just check the contents of this file
# vs doing a whole cryptographic handshake.
key-direction 1
<tls-auth>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
a706a52299dac30b2a0ee95ef1895654
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
-----END OpenVPN Static key V1-----
</tls-auth>
# Select a cryptographic cipher.
# If the cipher option is used on the server
# then you must also specify it here.
# Note that v2.4 client/server will automatically
# negotiate AES-256-GCM in TLS mode.
# See also the data-ciphers option in the manpage
cipher AES-256-CBC
# Enable compression on the VPN link.
# Don't enable this unless it is also
# enabled in the server config file.
#comp-lzo
# Set log file verbosity.
verb 3
# Silence repeating messages
;mute 20
I put this file in /etc/vpn-client/client.ovpn and wrote a systemd for it. Here’s my /etc/systemd/system/vpn-client.service.
[Unit]
Description="OpenVPN for personal use"
Documentation=https://openvpn.net/community-resources/how-to/
Requires=network-online.target
After=network-online.target
[Service]
User=root
WorkingDirectory=/etc/vpn
ExecStart=/usr/sbin/openvpn /etc/vpn/server.ovpn
ExecReload=/bin/kill --signal HUP $MAINPID
KillMode=process
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Then
$ sudo systemctl enable vpn-client
$ sudo systemctl daemon-reload
$ sudo systemctl start vpn-client
will start this whenever the computer starts.
To provision my phone, I created another leaf, and created another ovpn file. Then I took that ovpn file (with its nested keys, certs, chain, hmac-firewall) and uploaded it to my Google drive and downloaded it on my phone. Then I used the OpenVPN app on the Play store and started my client. This was the primary reason that I wanted all my secrets in one file. Provisioning my phone with relative paths would have been really unintuitive.
