Skip to content


详细讲解如何在OpenBSD 6.1 上配置OpenVPN服务。鉴于文章在墙外,我就全部搬过来吧.

作者: Guillaume Kaddouch


I did a previous article in 2015 about this very subject, and explained how to build your VPN server on a Virtual Private Server (VPS). It was based on OpenBSD 5.6 and Vultr VPS provider. Since then, VPN has remained a hot subject of interest for a lot of people, especially after learning about all of the spying around (NSA’s leaks, Wikileaks, etc…). Around me I have seen more people starting to use a VPN, and I received some questions since my last article. The main need remains the same however: if you neither trust your ISP nor a dedicated VPN provider (log or security wise), your best option is to be your own VPN provider.

This new article to build your own VPN server is an upgrade of the previous one. At a glance, the upgrades are:

  • Full Disk Encryption (FDE)
  • OpenBSD 6.1 and new syspatch utility
  • Comparison of OpenVPN and IPSEC
  • Stricter firewall rules marking all inbound traffic to be blacklisted
  • Scheduled Python script (instead of bash) to blacklist the intruders
  • Separate CA/signing machine (optional)
  • Multiple DNSCrypt proxy instances for failover
  • OpenVPN: Certificate Revocation List/CRL (optional)
  • OpenVPN: TLS 1.2 only
  • OpenVPN: TLS cipher based on AES-256-GCM only
  • OpenVPN: HMAC-SHA512 instead of HMAC-SHA1
  • OpenVPN: TLS encryption of control channel (makes it harder to identify OpenVPN traffic)

The others OpenVPN’s encryption parameters are identical: Diffie Helman 4096 bits, server’s private RSA key of 4096 bits, TLS key of 2048 bits for HMAC, AES-256-CBC used for encrypting the traffic, and Perfect Forward Secrecy (PFS).

The steps described in this article have been made on a VPS from Vultr provider, with which I have no affiliation of any kind, I merely use it because it fits my needs: possibility to boot a custom ISO of any OS, console access, Two Factor Authentication with Yubikey, snapshots, and an affordable starting plan of 2.5$/month (512Mo RAM, 1 CPU, 20GB SSD, and 500GB of traffic). You are free to follow this guide and installation steps on any VPS provider of course! 🙂

In what follows, I expect that you were able to boot on an OpenBSD 6.1 amd64 ISO, ready to install the OS. Also, as I did it on Vultr on a SSD VPS, drive letters will be specific to it. If you install your server on a mechanical HDD, instead of /dev/sd0a you will have /dev/wd0a, and instead of a RAID /dev/sd1 you will have /dev/sd0. Therefore you must adapt the commands from this article to your particular environement.



    8.1 SERVER
    8.2 CLIENT



I have been asked which was better, the most secure, which one should we choose to make our VPN server. I do not intend to start a war as to which is best, and to make the suspens short, both are good. Both can be used to encrypt traffic with AES-256, they both can use certificates as well, and can work for remote “road warrior” nomade users, or mobile phones. Both use HMAC authentication for incomming packets, allowing them to drop “valid” IPSEC/OpenVPN packet which are however not signed. Both can be weaken by choosing weak encryption algorithm and/or weak passwords/keys. Fact is, I’m using both for professional use, it all depends on the context.

That being said, we can still find differences between both, which may help you choose.

IPSEC: has two easily identifiables UDP ports, 500 by default, and 4500 for NAT Traversal (NAT-T), and uses AH/ESP protocols. This means it can potentially be blocked voluntarily or unvoluntarily by an ISP or by a WIFI or hotel network. Also, my personal experience with IPSEC is that it is “sensitive” and can have packet loss when SA rekey timeout is reached, or when there is packet fragmentation. It can be difficult to diagnose when unexpected issues appear, such as packets not coming back despite VPN tunnel still being up, no timeout is reached, and no error messages from iked. On the other hand, OpenIKED on OpenBSD has native commands to create a CA and certificates, which is very handy. It enables us to choose from a wide selection of authentication and encryption algorithm, and the iked.conf syntax is rather easy. I find it generaly more suited for site to site VPN. Obviously IPSEC can be made perfectly stable and secure, however in my experience it requires more troubleshooting when something goes wrong.

OpenVPN: CA and certificates management require an external tool, either OpenSSL or a package such as Easy-RSA. About ports, a single one needs to be used, and we are free to choose the one we want, as well as the TCP or UDP protocol. OpenVPN is a SSL VPN and does not use a separate VPN protocol. This point allows you to circumvent potential network restrictions, as at worst you will always have at least ports 80, 443, and 53 available. OpenVPN has no problem with NAT and firewalls, and works flawlesly. In my experience, I found OpenVPN perfectly stable after 2 years of usage.

To be honest, when I started my new VPN server to make this article, I wanted to do it with IPSEC as iked was native to OpenBSD. I did manage to have a functional tunnel, however it never was stable no matter what I tried. It is very likely a configuration error on my side, but still it was pretty frustrating. When I finally went back to OpenVPN, I had it working at first try with a perfect stability. I personaly prefer OpenVPN, and it can also be configured to be pretty secure as well.


In my previous article, I did set up a scenario where everything was in your hands: the server, the encryption algorithm, the keys and certificates, the firewall rules. It was pretty secure, in part because of running on OpenBSD which enables a lot of security features by default. However, as some people mentioned it to me, if the VPS provider is either malicious, or hacked, or for any reason has to give access to your server or give a copy of it, all of your security falls flat. Indeed, your certificates, keys, and may be even the Certificate Authority, are all stored on the server, which is not encrypted. There is therefore in this scenario a required trust toward the VPS provider.

By doing a Full Disk Encryption (FDE), you are further hardening your VPN server from unauthorized access. Fortunately, FDE is natively supported on OpenBSD. From the official FAQ, OpenBSD uses AES in XTS mode. It is not specified if it is AES-128 or AES-256 though at this link. However I found a detailed analysis of OpenBSD encryption source code, and it clearly shows that AES-256 XTS is used.

Please note however that while it is an improvement to setup a FDE, and an absolute security requirement in my opinion nowadays, it is still not 100% secure as the VPS provider can do a snapshot of the running system and extract information from memory.


When you first boot onto the ISO to install OpenBSD, you end up on this screen:

root on rd0a swap on rd0b dump on rd0b
erase ^?, werase ^W, kill ^U, intr ^C, status ^T

Welcome to the OpenBSD/amd64 X.X installation program.
(I)nstall, (U)pgrade, (A)utoinstall or (S)hell?

We need to create an encrypted volume before proceeding with the installation. For that, we must escape to a Shell first (last option). The OpenBSD FAQ linked above advises to write random data first to the whole drive. Indeed, if we do not, it may be possible for an adversary to tell the difference between used and unused space. It costs nothing to do it, except some patience, and if we encrypt the whole drive it’s better to do it right. As a reminder, drive paths from now on are based on a SSD of Vultr VPS, if you use a classical HDD, you will have to adjust the commands.

# dd if=/dev/random of=/dev/rsd0c bs=1m

This command reads data from /dev/random and writes it to the whole /dev/rds0c device. It takes 5 minutes on my server’s 20GB SSD, but it can take a lot more time on bigger drives, especially on HDD. Now we need to initialise the disk and create label for it:

# fdisk -iy sd0
# disklabel -E sd0

Here, choose to create a partition using the entire disk, this way is easier. Notice we choose a “RAID” filesystem here, as it is what will allow us to enable encryption afterwards:

a a
offset: [64]
size: [41929586] *
FS type: [4.2BSD] RAID
No label changes.

Now we will create an encrypted volume. By default, from the bioctl man page, there is 16 rounds of the KDF algorithm used when converting your passphrase into key. This is a defense against bruteforce, as it requires, for every passphrase to try, to make 16 rounds of it. It increases required CPU power, and will increase the time necessary to crack the passphrase. However, 16 rounds, althought conservative and being able to run on even less powerful systems, is not that much against someone having great CPU power to attack your passphrase. On a decent and recent system with fast SSD, upping it to 8192 rounds is transparent with no noticable delay after having typed the passphrase to boot the server (on my VPS server). However if I test it on a local VM on my computer, boot is delayed by 55 seconds, so it really depends on your hardware.

# bioctl -c C -r 8192 -l /dev/sd0a softraid0
New passphrase:
Re-type passphrase:
sd1 at scsibus2 targ 1 lun 0:  <OPENBSD, SR CRYPTO, 006>  SCSI2 0/direct fixed
sd1: 20473MB, 512 bytes/sector, 41929058 sectors
softraid0: CRYPTO volume attached as sd1

Choose here a strong passphrase! Having a high number of rounds with a weak passphrase is useless. If in the future you want to modify your passphrase, from a user shell you will have to type this command:

$ doas bioctl -P sd1

Before coming back to the setup, as we created a new sd1 device, we have to make sure the nodes are created for it:

# cd /dev
# sh MAKEDEV sd1

Last step, also from the official FAQ, is to zeroing the first chunk of the new device, where should normally be a Master Boot Record and disklabel instead of some garbage:

# dd if=/dev/zero of=/dev/rsd1c bs=1m count=1
# exit

Now you come back to the setup prompt, with an sd0 device filled with an sd1 encrypted volume. You just need to answer the questions, essentially:

Initial prompt : I (Install)
Keyboard layout: fr (for instance)
Hostname : MY-SERVER
Network : keep defaults and dhcp
Root password : choose a strong password, different than the boot passphrase
Start sshd : yes
Run X Window : no
Setup a user : no
Allow root ssh login : no
Timezone : choose your own (e.g Europe/Paris)

We do not create another user for now, and we deny root from connecting to SSH. This implies that for the begining of the setup which follows, you have to have an access to the server’s console, that Vultr for instance provides.

Be carefull when the disk to install to is asked for, remember to change the default from the unencrypted device to the encrypted volume:

Which disk is the root disk? (’?’ for details) [sd0] sd1

Then, for the following questions:

Use Whole disk : whole
Use Auto layout : a
Location of sets : http
Package selection : -g* [enter]
Package selection : done
Location of sets : done

# reboot


When you want to upgrade an existing system, always first read the official upgrade guide pertaining to your upgrade (here from 6.0 to 6.1). For instance, before upgrading from OpenBSD 6.0 to 6.1, some cleanup have to be made first, before booting onto the new version ISO.

If you already have an encrypted server, or if you want to know how you will update it when the next OpenBSD will be released, the install steps are different. The obvious way seems to boot onto the ISO, at the setup prompt drop to a shell, mount the encrypted volume, and continue with the setup. That is in fact correct, but the bioctl manpage, althought having a parameter to “detach” a volume, does not mention how to “attach” or “mount” an existing one. The parameter to use, althought not explicitely written in the manpage, is the “-c” parameter we used to “create” our volume. If the volume already exists, bioctl will just mount it, not overwrite it:

Welcome to the OpenBSD/amd64 X.X installation program.
(I)nstall, (U)pgrade, (A)utoinstall or (S)hell? S

# bioctl -c C -r 8192 -l /dev/sd0a softraid0
sd1 at scsibus2 targ 1 lun 0:  <OPENBSD,   SR CRYPTO,   006>  SCSI2 0/direct fixed
sd1: 20473MB, 512 bytes/sector, 41929058 sectors
softraid0: CRYPTO volume attached as sd1

Once the volume is attached as sd1, as before we need to create nodes for it:

# cd /dev
# sh MAKEDEV sd1

Then quit shell with CTRL+D, and at the setup prompt select “(U)pgrade”. Once done and your upgraded server is rebooted, follow the post-upgrade steps given at the official upgrade guide.


OpenBSD default settings are very secure, and no services are listening on the outside except SSH. However SSH is listening on the default 22 port, accepting password authentication. Before configuring our server, it is best to block any inbound access except from our computer public IP, and then take our time to lock down SSH. Let’s start by a basic pf ruleset:

# vi /etc/pf.conf

block in quick from ! x.x.x.x # your public IP address
pass out quick

Replace “x.x.x.x” by your computer public IP address you are connecting from. You can check on if you don’t know it. Of course this ruleset is temporary. Now apply it:

# pfctl -f /etc/pf.conf

Now we will create a regular user, that we will use afterwards. I’m creating a user named “guillaume” just for the example, but pick the one you want:

# adduser

First time this command is ran, some general questions are asked, keep the defaults. Then when a username is asked to create a user, enter yours, and keep the defaults for all other questions regarding your user. When that is done, we want to be able to run commands as root, in same way we used “sudo” in previous OpenBSD versions. Since 5.8, we have to use “doas” instead, which has an easier configuration syntax:

# vi /etc/doas.conf

permit persist guillaume as root

The “persist” option makes doas command request the password the first time, and then not asking it again for a period of time. If you do not use this option, a password is requested everytime doas command is used (which will makes you crazy while configuring a server!). Now, logout from root, and try your fresh new user and enjoy the doas command from now on 🙂 Of course this configuration is only temporary, afterwards you should restrict the commands your user is allowed to do. You can check the doas.conf manpage for more information.

We now have few things to do: create an SSH key, disable root login and password base authentication, and make SSH listening on another port as a bonus (to avoid automated scans adding “noise” to our logs). For Linux/BSD clients you can create a key with -t ed25519, but for Windows your SSH client may only be compatible with keys created using -t rsa. In the following I will suppose you have a Windows client.

$ cd /home/guillaume
$ ssh-keygen -t rsa

Then copy your public key to ~/.ssh/authorized_keys :

$ cd .ssh
$ cp authorized_keys

Copy the content of your private key “~/.ssh/id_rsa” (not on the remote computer you will use to connect to your router, and set strict permissions on it. If on Linux or BSD, do a “chmod 600”, and you will be able to connect later with “ssh -i your_private_key your_server_ip”. On Windows, I advise you to run a recent client such as Royal TS to SSH into your server. This software is free under 10 connections, but a licence is needed if you have more. A regular Putty could do the trick, but last time I tried it would not connect (with the regular ppk file converted with puttygen), and RoyalTS has some additional interesting features such as encrypting the database, multi-tab window, custom macro, and the absolutely vital possibility to select custom icons 🙂

Now modify your SSH server with this temporary configuration, the TCP port 21598 being an example, choose the port you want. You can choose a higher port to avoid scans, or choose a port such as 443 to be able to connect to your server from everywhere, when outbound TCP port 22 could be blocked:

$ doas vi /etc/ssh/sshd_config

# Modify default listening port
Port 21598

# Authentication
PasswordAuthentication yes # temporary
PermitRootLogin no
AllowUsers YOUR_USER
AuthorizedKeysFile .ssh/authorized_keys
AllowTcpForwarding no
UsePrivilegeSeparation sandbox # Default for new installations.
Subsystem sftp /usr/libexec/sftp-server

Restart sshd :

$ doas rcctl restart sshd

Now you can connect by SSH with your user and its password, and then retrieve your id_rsa private key file. You will then need to convert it to a “.ppk” file to be able to use it to connect from Windows. One easy way to do it is to use puttygen. Just run this utility, open your id_rsa file from it, and convert it into id_rsa.ppk.

Connect from your remote computer with the private key, check that connecting with your regular user and with the SSH key fully works. Once it’s working, modify the following line in /etc/ssh/sshd_config :

PasswordAuthentication no

Restart sshd :
$ doas rcctl restart sshd

On the client side, if you are using a Linux/BSD client, you can enable SSH key fingerprint visual display in /etc/ssh/ssh_config which displays your SSH key in hex format and an ACSCII graphic everytime you connect. Once you get used to the ASCII graphic of your server, you should notice if all of a sudden it is completely different (probably a man-in-the-middle):

$ doas vi /etc/ssh/ssh_config

# Display fingerprint in hex and ASCII graphic when connecting
VisualHostKey yes

You now have a SSH listening on a non default port, root denied from connecting in, password authentication disabled, and authencation based on SSH keys and passphrase. For further SSH hardening, read the following excellent article describing which protocols and ciphers to use for optimum security. I may in the future update the SSH settings given in this article to follow some of the advices from this page.

Finally, to update the system and packages easily, I really liked to use “openup” from M:Tier. However, starting with OpenBSD 6.1, a new “syspatch” command appears to easily update the base system. It won’t take care of the additinal packages however, which have to be taken care of with the ports tree if you want to rely only on OpenBSD official commands and repositories. You can learn more details at the patches FAQ. “pkg_add -u” only upgrades packages if you are running the -current flavor, not the -release flavor we are running in our example. If you want the easiest way you can use openup from M:Tier, which is a third party not related to OpenBSD project, so you have to decide if you wish to trust their repository or not. If you want to use “openup”:

$ ftp
$ chmod +x openup
$ doas openup
===> Checking for openup update
===> Installing/updating binpatch(es)
===> Updating package(s)

If however you want to use syspatch instead as I do, start it this way, and then update the external packages using the ports tree:

$ doas syspatch

To fetch the ports tree, if you don’t use openup, you can follow the steps outlined in the patches FAQ link given above. Basically fetch the ports tree with CVS, run the “out-of-date” script to know which package needs an update, and run a “make update” in the affected port directories. I’ll try to detail these steps later.

Here we go, a fully updated system. If you use the syspatch option “-c” it will list available updates. You can read syspatch manpage for more information. Let’s continue to the next part.


Before going further, as “vi” is not my favorite text editor, let’s install “nano” instead:

$ doas pkg_add nano

A few tweaks we can make before configuring further our server. As Vultr VPS are hosted on SSD, it is a good idea to add in the fstab file the mount options “softdep” and “noatime”. The first one increase disk performance, while the second will prevent the “last access time” file properties to be written:

$ sed 's/rw/rw,softdep,noatime/g' /etc/fstab > ./fstab_tmp
$ doas mv fstab_tmp /etc/fstab
$ cat /etc/fstab

Now, remember we are running on a fully encrypted volume. However OpenBSD also encrypt the swap by default, therefore you can disable it as it is not necessary in our context:

$ doas nano /etc/sysctl.conf

# Disable swap encryption (whole disk is already encrypted)

Your server has booted with DHCP and has acquired its network configuration from your VPS provider. As the server IP address is fixed, I prefer setting manually the network configuration to avoid relying on another server (VPS provider’s DHCP and DNS) I do not control. Modify the following files according to your server public ip address, mask, and gateway:
$ doas nano /etc/hostname.vio0

inet server_public_ip server_netmask

Add the gateway :

$ doas nano /etc/mygate


Our server will forward traffic between its VPN interface and its default network interface. We have to enable forwarding:
$ doas sysctl net.inet.ip.forwarding=1

$ doas nano /etc/sysctl.conf

# Enable forwarding

At this point you still rely on your provider DNS server in /etc/resolv.conf, we will take care of that later.


The pf ruleset below does many things:

  • denies all inbound, except SSH and OpenVPN on non default ports
  • protects SSH from SYN flood, and bruteforce
  • detects accesses to all other ports and blacklist the miscreants for 24H (you cannot expect less from a Blowfish mascot!)
  • allows VPN clients to make DNS requests to unbound on localhost (which uses dnscrypt, as it will be setup in the next part)
  • does not log blocked network traffic from the provider’s DHCP

You should modify this ruleset according to your chosen SSH and VPN listening ports (we will setup OpenVPN later, but you can choose a random port now). The scan detection and blacklisting concept can theoretically backfire, if someone sends spoofed packets with trusted IPs. However, as it is implemented, only incoming traffic will match the blacklist table. Therefore, if a spoofed packet is sent with a website IP you trust, it will not prevent you to access that website at all as your traffic will be outbound. Also, as we will write a script to parse pf logs and add IPs into the blacklist, we will add an exception for our own trusted client computer public IP address to avoid being locked out. As a last resort the remote console access from the VPS provider could allow us to login, if we were totally locked out for any reason. This adaptative behavior is not mandatory for your VPN server to be operational, but it is useful to block automatic scans on the Internet from querying your SSH and VPN ports once they hit your server on another port.

$ doas cp /etc/pf.conf /etc/pf.allow.conf
$ doas nano /etc/pf.conf

# Title: “Being your own VPN provider with OpenBSD v2”
# Author: Guillaume Kaddouch:
# Date: ruleset last modified: 2017 April 17 for OpenBSD 6.1
# Github:

# —————————————————————————————
ssh_port=“21598” # just a random example, modify to match your chosen SSH port
vpn_port=“21599” # just a random example, modify to match your chosen OpenVPN port

bad_ports=”{ 1:66, 69:21597, 21600:65535 }” # adjust according to your SSH and VPN ports (+ DHCP)

table <internet> const { $all_networks, !self, !$private_networks }
table <myself> const { self }
table <bruteforce> persist
table <badguys> persist

# —————————————————————————————
set block-policy drop
set loginterface egress
set skip on lo
block log all
match in all scrub (no-df max-mss 1440 random-id)
block in log quick from <bruteforce> label “bruteforce”
block in log quick from <badguys> label “old_guys”

# ——————————————————————————–
match in on egress proto tcp from <internet> to port $ssh_port
match in on egress proto { udp tcp } from <internet> to port $bad_ports
match in on egress proto udp to port $vpn_port
match in on $vpn proto { icmp udp tcp }
match in on $vpn proto { udp tcp } to $vpn_ip port domain
match out on egress tagged VPN_TUN_IN
match out on egress proto tcp from <myself> to port { http https }
match out on egress proto { udp tcp } from <myself> to port domain
match out on egress proto udp from <myself> to port https
match out on egress proto udp from <myself> to port ntp
match in on egress from { no-route urpf-failed } to any
match out on egress from any to no-route
match inet6 all
tag SSH_IN
tag IPV6

# —————————————————————————————
match in tagged VPN_EGRESS_IN set tos lowdelay set prio 6
match out tagged VPN_FORWARD nat-to (egress) set prio 6

# Blocking spoofed or malformed packets, IPv6, and some bad traffic
antispoof log quick for egress label “antispoof”
block quick log tagged BAD_PACKET label “noroute_urpf”
block quick log tagged IPV6 label “ipv6”
block quick log tagged BAD_GUYS label “new_guy”

# Standard rules
# protect SSH from SYN flood and bruteforce
pass in quick tagged SSH_IN synproxy state
(max-src-conn 10, max-src-conn-rate 5/5, overload <bruteforce> flush global)

# Redirect VPN clients DNS requests to unbound
pass in quick inet tagged VPN_DNS_IN rdr-to port domain

pass in quick tagged VPN_EGRESS_IN
pass in quick tagged VPN_TUN_IN

pass out quick tagged HTTP_OUT
pass out quick tagged DNS_OUT
pass out quick tagged VPN_FORWARD modulate state
pass out quick tagged NTP_OUT

# no log for
block in quick proto udp from port bootpc to port bootps
block in quick proto udp from any port bootps to port bootpc

If you prefer, you can now fetch this ruleset from this [url=”“]Github link[/url].

Check the ruleset syntax, with the first command, and then apply it if no error returned:

$ doas pfctl -nf /etc/pf.conf
$ doas pfctl -f /etc/pf.conf

Now we have to make our script which will look for IPs blocked, because they dare approaching our server, labeled “new_guy”. Script will be in /home/user/scripts:

$ doas pkg_add python3.5
$ cd ~
$ mkdir scripts
$ cd scripts

$ doas nano ./

# File:
# Version: 1.0
# Date: 2017/04/21
# Blog:
import subprocess

# Modify below your home path, and your trusted public IP address you connect from
badguys_file    = '/home/guillaume/badguys.txt'
pf_file         = '/etc/pf.conf'
rule_tag        = 'new_guy'
rule_id         = 0
get_ruleset     = 'pfctl -sr'
get_blocked_ips = 'tcpdump -enr /var/log/pflog'
block_badguys   = 'pfctl -t badguys -T add -f ' + badguys_file
counter         = 0
trusted         = ['YOUR_PUBLIC_IP_HERE']

def execute(cmd):
   "Execute the provided commmand and return its output "
   output = subprocess.getoutput(cmd)
   return output.split('\n')

# Retrieve loaded rules with 'pfctl -sr'
ruleset = execute(get_ruleset)

# Locate our blacklisting rule ID
for rule in ruleset:
   counter += 1
   if rule_tag in rule:
       rule_id = counter - 1

# Retrieve all blocked IPs from /var/log/pflog
blocked_ips = execute(get_blocked_ips)

# Match only IPs blocked by our blacklisting rule
badguys = []
for logline in range(len(blocked_ips)):
   if ('rule ' + str(rule_id) + '/(match) block') in blocked_ips[logline]:
       ip = blocked_ips[logline].split()[7] # retrieve ip.port
       ip = ip.split('.')[0] + '.' + ip.split('.')[1] + '.' + ip.split('.')[2] + '.' + ip.split('.')[3]

# Remove duplicate IPs
badguys = list(set(badguys))

# Remove your trusted IPs from the list!
for good_ip in trusted:
   for bad_ip in badguys:
       if bad_ip == good_ip:
# Writing the list of bad IPs to a file
with open(badguys_file, 'w') as file:
   for ip in badguys:
       file.write(ip + '\n')

# Finally blocking the badguys ;-(

If you do not see the embedded source code frame above (you are blocking scripts from, you can access with this direct link the script source code.

Edit the crontab to schedule execution of this script, as well as check every hour to expire table entries older than 24 hours. Be sure to make the script readable, executable, and writable only by root, as it will run in root’s crontab. Replace “/home/your_user” below with your username:

$ doas chown root:wheel
$ doas chmod 700
$ doas crontab -e

# add badguys to the pf table to be blocked
*/5 * * * * python3.5 /home/guillaume/scripts/

# Clear pf tables
0 * * * * pfctl -t bruteforce -T expire 86400
0 * * * * pfctl -t badguys -T expire 86400

You can check with another script the state of your blacklist/badguys table, ports blocked to them once blacklisted , and number of blocked IP in your bruteforce table. Here is the script example, you can freely modify:

$ doas nano ./

# File:
# Version: 1.0
# Date: 2017/04/21
# Blog:
import subprocess
import re
from collections import defaultdict, OrderedDict

pf_file         = '/etc/pf.conf'
rule_tag_new    = 'new_guy'
rule_tag_old    = 'old_guy'
rule_id         = 0
pf_ruleset      = 'pfctl -sr'
pf_badguys      = 'pfctl -t badguys -T show'
pf_bruteforce   = 'pfctl -t bruteforce -T show'
pf_stats        = 'pfctl -sl'

get_blocked_ips = 'tcpdump -enr /var/log/pflog'
counter         = 0

def execute(cmd):
    "Execute the provided commmand and return its output "
    output = subprocess.getoutput(cmd)
    return output.split('\n')

def getKey(item):
    "Return the item to base the sort on, used by sorted() function"
    return item[1]

# Retrieve all blocked IPs from /var/log/pflog
blocked_ips = execute(get_blocked_ips)

# Parse blocked IPs and ports and sort them out
blocked_list = defaultdict(list)
stats_ip     = defaultdict(list)
stats_ports  = defaultdict(list)
ip           = ''

total_block = 0
for logline in range(len(blocked_ips)):
    if ('/(match) block in on') in blocked_ips[logline]: # if an inbound block was logged
        ip = blocked_ips[logline].split()[7]             # retrieve ip.port
        total_block += 1

        if not 'icmp' in blocked_ips[logline]:
            port = ip.split('.')[4]
            port = 'icmp'

        ip = ip.split('.')[0] + '.' + ip.split('.')[1] + '.' + ip.split('.')[2] + '.' + ip.split('.')[3]

        # Keep track of IP:ports tried, e.g: : 21, 80, 443, 443, 443, 8080, ...

        # Keep track of how many deny per IP, e.g: : 5, : 1, ...
        if stats_ip.get(ip, 'NA') == 'NA':
            stats_ip[ip] = 1
            stats_ip[ip] = stats_ip[ip] + 1

        # Keep track of how many deny per port, e.g: 80 : 5, 443 : 20, ...
        if stats_ports.get(port, 'NA') == 'NA':
            stats_ports[port] = 1
            stats_ports[port] = stats_ports[port] + 1

# Final ordered lists, about top blocked IPs and ports
top_ports = OrderedDict(sorted(stats_ports.items(), key=getKey, reverse=True))
top_ips   = OrderedDict(sorted(stats_ip.items(), key=getKey, reverse=True))

print('Blacklisted IPs: %d' % len(execute(pf_badguys)))
print('Blocks : %d' % total_block)

print('\nTOP blocked IPs:')
count = 0
max   = 5
for ip in top_ips:
    if top_ips[ip] > 1:
        print('%s : %d times ' % (ip, top_ips[ip])) : 5, : 1, ...
        count +=1
        if count == max: break

print('\nTOP blocked ports:')
count = 0
for port in top_ports:
    if top_ports[port] > 1:
            print('%s : %d times ' % (port, top_ports[port]))
            count +=1
if count == max: break

If you do not see the embedded source code frame above (you are blocking scripts from, you can access with this direct link the script source code.

Below is also an example of a possible output from the second script:

$ doas chown root:wheel
$ doas chmod 700
$ doas ./


You now have a nice adaptative firewall, however as warned above, be careful as it can backfire if trusted IPs are not excluded and become blacklisted. It is a working configuration I had no trouble with, in my context, but in your situation it may have to be modified to fit your case.

6. DNS

We will use DNSCrypt to make our DNS requests encrypted, and Unbound to have a local DNS cache. This will allow us to avoid using our VPS provider DNS servers, and will also be useful to your future VPN clients which will be able to use your VPN server as their DNS server too, if they wish too (e.g mobile phones). Both dnscrypt and unbound will listen on the localhost only, not to the outside. They will be reachable nonetheless later to your VPN clients trough the VPN tunnel, using a firewall redirection.

$ doas pkg_add dnscrypt-proxy
$ doas rcctl enable dnscrypt_proxy
$ doas rcctl set dnscrypt_proxy flags -a -l /dev/null -R

You can choose your dnscrypt enabled DNS server at the following list (choose a logless DNSSEC enabled one).
Now let’s start it:

$ doas rcctl start dnscrypt_proxy

Before configuring Unbound, which is the local DNS cache which will make requests to dnscrypt_proxy, we can configure an additional dnscrypt instance, as explained in the pkg readme. Indeed, dnscrypt DNS servers being public ones, they often goes into maintenance, become offline or temporarily unreachable. To address this issue, it is possible to setup multiple dnscrypt instances. Below are the steps to follow to add one, but you can add more if you wish. Notice the different local port 41 and different DNS server:

$ doas ln -s dnscrypt_proxy /etc/rc.d/dnscrypt_proxy2
$ doas rcctl enable dnscrypt_proxy2
$ doas rcctl set dnscrypt_proxy2 flags -a -l /dev/null -R
$ doas rcctl start dnscrypt_proxy2
$ ps aux | grep dnscrypt

You should see now two dnscrypt processes running, listening on ports 40 and 41, and linked to a different remote DNS server. This setup will be reflected in Unbound configuration below.

We now configure and enable unbound, already included in the base system. Unbound will drop privileges to user _unbound and will be chrooted in /var/unbound by default. Also by default, only localhost is allowed and everything else is refused. This is why it is unnecessary to specify the username/directory/chroot options below, or to define a default access-control. We just add what we need:

$ doas nano /var/unbound/etc/unbound.conf

do-not-query-localhost: no
access-control: allow
hide-identity: yes
hide-version: yes
auto-trust-anchor-file: “/var/unbound/db/root.key”

name: “.” # use for ALL queries
forward-addr: # dnscrypt-proxy
forward-addr: # dnscrypt-proxy failover

Do not forget to modify your /etc/resolv.conf:
$ doas nano /etc/resolv.conf

nameserver # unbound is listening there at port 53

To prevent dhclient from overwriting our nameserver in resolv.conf, add this line to dhclient.conf:
$ doas nano /etc/dhclient.conf

supersede domain-name-servers

Run Unbound, and enable it to launch at startup :

$ doas rcctl enable unbound
$ doas rcctl start unbound

Now that the outbound DNS requests are only made using dnscrypt on UDP port 443, we can comment/disable/remove the rule that allows outbound DNS requests on UDP/TCP 53:

$ doas nano /etc/pf.conf

#match out on egress proto { udp tcp } from <myself> to port domain tag DNS_OUT

$ doas pfctl -nf /etc/pf.conf
$ doas pfctl -f /etc/pf.conf

Test that your DNS chain is working:

$ host has address mail is handled by 6 mail is handled by 10

Unbound is listening on locahost port 53, and when contacted is forwarding to dnscrypt listening on locahost port 40, itself contacting an external dnscrypt enabled DNS server.


We need to create a Certificate Authority (CA) which will enable us to create certificates for our VPN server, our home router, and any other client we may have. A Certificate Authority will also be able to revoke certificates, which will prevent unused or lost ones to be accepted by our VPN server.

This CA, as you can see, has a critical role. A breach of the CA would means an attacker could steal certificates and private keys and spy on our communications, and could create certificates for himself. Therefore, I strongly advise you to create a dedicated CA server, that you can store and run on your local computer as a virtual machine for instance that you can start on demand. It is possible though to follow the steps below directly on your VPN server, but I do not recommend it.

Ideally, from a dedicated OpenBSD 6.1 CA host:

$ doas pkg_add easy-rsa
$ cd /usr/local/share/easy-rsa/
$ doas cp vars.example vars

$ doas nano vars

# 2048 – > 4096
set_var EASYRSA_KEY_SIZE 4096
# sha256 -> sha512
set_var EASYRSA_DIGEST “sha512”

Now it’s time to create our new Public Key Infrastructure (PKI) and CA :

$ doas easyrsa init-pki
$ doas easyrsa build-ca

Choose your CA’s passphrase wisely, preferably a strong one, it will be requested for every subsequent certificates you will want to create.

Then, from easy rsa readme, the recommanded steps to create certificates is to install easy-rsa on every requesting hosts, server and clients, generate a request with “./easyrsa init-pki” and “./easy-rsa gen-req”, import it on the CA server, and sign them. Then transport the newly created certs to each hosts. However this is not always doable, for instance if the client is a mobile phone.

If you prefer to do it all at once on the CA signing server directly:

$ doas easyrsa build-server-full your-server-name nopass
$ doas easyrsa build-client-full your-home-router-name nopass

The “nopass” option creates certificates without password, to allow openvpn to start automatically with the certificate without additionally prompting for a password. Then, we will create a Certificate Revocation List (CRL). This is optional, but I highly recommend it, as it will enable you to revoke certificates in the future if you need it. Without it, if you loose a certificate (stolen phone) or if you want to blacklist a certificate for any reason, the only possibility will be to create a whole new PKI and CA, and recreate all certificates. Better be safe than sorry and create a CRL:

$ doas easyrsa gen-crl

This will create the file /usr/local/share/easy-rsa/pki/crl.pem

Now we have to build 4096 bits Diffie-Hellman parameters, be warned it can take a while!

$ doas easyrsa gen-dh
$ doas mv ./pki/dh.pem ./pki/dh4096.pem

You can additionally generate an OpenVPN Pre-Shared Key (PSK) that will have to be copied to all server and clients:

$ doas pkg_add openvpn
$ doas mkdir -p /usr/local/etc/openvpn/secret
$ cd /usr/local/etc/openvpn/secret
$ doas openvpn --genkey --secret ta.key

On server, we will need to copy :

  • ca.crt : Root CA certificate […/easy-rsa/pki/]
  • crl.pem : Certificate Revocation List […/easy-rsa/pki/] (optional)
  • dh4096.pem : Diffie Hellman parameters […/easy-rsa/pki/]
  • server.crt : Server Certificate […/easy-rsa/pki/issued/]
  • server.key : Server Key (private) […/easy-rsa/pki/private/]
  • ta.key : OpenVPN TLS PSK [/usr/local/etc/openvpn/secret]

On clients, we will need to copy:

  • ca.crt : Root CA certificate […/easy-rsa/pki/]
  • client.crt : Client Certificate […/easy-rsa/pki/issued/]
  • client.key : Client Key (private) […/easy-rsa/pki/private/]
  • ta.key : OpenVPN TLS PSK [/usr/local/etc/openvpn/secret]

This way, your CA private key stays on your signing host, if you choosed a separate host as advised.

To transfer the required files to the aformentioned hosts, server and clients, an encrypted channel must be used. If you have network access from your signing host to your server and clients, you can do it directly with SCP. Or if you prefer doing it from an intermediate Windows machine, you can use WinSCP to connect simultaneously to the signing machine, server, and client, and transfer the files this way.


8.1 – SERVER

Now that our server is up and running, and our CA and certificates are created, we can finally setup OpenVPN on our server. Let’s download our packages and create the required directories:

$ doas pkg_add openvpn
$ doas mkdir -p /usr/local/etc/openvpn/{public,secret}

Do not forget to copy your certificates and keys, by changing directory to the folder you copied them to earlier:

$ cd folder_where_certs_and_keys_are
$ doas mv *.crt /usr/local/etc/openvpn/public/
$ doas mv *.key /usr/local/etc/openvpn/secret/

Making sure these secret files are only accessible to root:

$ doas chmod -Rf 600 /usr/local/etc/openvpn/secret/
$ doas chown -Rf root:wheel /usr/local/etc/openvpn/secret/

Let’s create the main OpenVPN server configuration file:

$ doas nano /usr/local/etc/openvpn/server.conf
# Server configuration
# SSL/TLS certificate and keys, PFS enabled by default

# Since OpenVPN 2.4, "tls-crypt" can be used instead of "tls-auth", as it does authentication and
# encryption of all control channel packets. Purpose of encrypting these packets: provides more
# privacy, makes it harder to identify OpenVPN traffic, and masks the pre-shared key
# (if I understand the official OpenVPN 2.4 manpage)

# ** WARNING ** as of 28 May 2017, "OpenVPN Connect" v1.1.1 app on iOS/iPhone does not
# yet support "tls-crypt" option. Consequently, if you have mobile phone clients,
# you should use "tls-auth /your_path/ta.key 0" instead on the server.

ca "/usr/local/etc/openvpn/public/ca.crt"
crl-verify "public/crl.pem" # Certificate Revocation List (optional)
cert "/usr/local/etc/openvpn/public/server.crt"
dh "/usr/local/etc/openvpn/public/dh4096.pem" # Diffie Helman 4096 bits
key "/usr/local/etc/openvpn/secret/server.key" # RSA 4096 bits
tls-crypt "/usr/local/etc/openvpn/secret/ta.key" # TLS 2048 bits for HMAC and encryption

# Protocols and ciphers
cipher AES-256-CBC # AES 256 bits
tls-version-min 1.2 # Only allow TLS 1.2
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
auth SHA512 # HMAC-SHA512 (default is SHA1)

# Network parameters
port 21600 # as an example, pick your own port
proto udp
dev tun
ifconfig-pool-persist ipp.txt
push "redirect-gateway def1 bypass-dhcp"

# push our DNS server to clients accepting it (will not override a home router DNS configuration
# with fixed DNS settings). Usefull for mobile phones for instance, where installing
# dnscrypt requires a rooted phone
push "dhcp-option DNS"

keepalive 10 120
comp-lzo yes

# Limits
max-clients 5 # change this value if you plan on connecting from more clients

# Privileges, chroot
chroot /var/openvpn
user _openvpn
group _openvpn

verb 4
mute 20


Now we should prepare the chroot environnement, as OpenVPN will chroot itself after being started. We must copy the CRL file in the chroot folder as it is checked after OpenVPN has chrooted its process, making the initial “/usr/local/etc/openvpn/” directory not accessible. That’s also why the CRL path in the previous configuration file is not a full path, as the root folder changes after initilization:

$ doas mkdir -p /var/openvpn/{tmp, public}
$ doas cp public/crl.pem /var/openvpn/public

It is time to start our server and to make it start at boot time:

$ doas /usr/local/sbin/openvpn --config /usr/local/etc/openvpn/server.conf --daemon

$ doas nano /etc/rc.local

# OpenVPN
/usr/local/sbin/openvpn –config /usr/local/etc/openvpn/server.conf –daemon

You should check with tail -f /var/log/messages that OpenVPN started successfully. If something went wrong, check your files and folders ownership and rights. Make also sure that both client and server time are close enough, to avoid any trouble with your certficates.

8.2 – CLIENT

In my OpenBSD router article there is a chapter about an OpenVPN client installation and configuration. Below I will just provide the OpenVPN configuration file to use on the client side:

$ doas nano /usr/local/etc/openvpn/client.conf

# Client configuration (router, computer)
# SSL/TLS certificate and keys
ca “/usr/local/etc/openvpn/ca.crt” # public
cert “/usr/local/etc/openvpn/myhome.crt” # public
key “/usr/local/etc/openvpn/myhome.key” # secret
tls-crypt “/usr/local/etc/openvpn/ta.key” # secret (OpenVPN 2.4 or higher)

# Protocols and ciphers
cipher AES-256-CBC # AES 256 bits
tls-version-min 1.2 # Only allow TLS 1.2
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
auth SHA512 # HMAC-SHA512 (default is SHA1)

remote-cert-tls server
dev tun
proto udp
resolv-retry infinite
comp-lzo yes

remote YOUR_SERVER_IP 21600

# Privileges, chroot
user _openvpn
group _openvpn
chroot /var/empty

verb 3
explicit-exit-notify 5

Client keys must be copied from the CA server to the client over a secure channel, for instance by using SCP/WinSCP to transfer them trough SSH, as advised earlier.


A smartphone such as Android or iOS can download and install “OpenVPN Connect”. Then you will have to transfer on the phone an OpenVPN configuration file, which will require to have the whole configuration in it, including certificates and keys. The configuration file has the certificates and keys inside it. Basically you just copy/paste the content of the required files and insert them between tags like below (as of 28 May 2017, “OpenVPN Connect” v1.1.1 app on iOS/iPhone does not yet support “tls-crypt”. We should use tls-auth on both client and server) :

# Protocols and ciphers
cipher AES-256-CBC # AES 256 bits
tls-version-min 1.2 # Only allow TLS 1.2
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
auth SHA512 # HMAC-SHA512 (default is SHA1)

# Global options
remote-cert-tls server
dev tun
proto udp
resolv-retry infinite
comp-lzo yes

remote YOUR_SERVER_IP 21600

key-direction 1

verb 3
explicit-exit-notify 5
# ca.crt below, just a random example. Full extract is above 35 lines

# myphone.crt below, just a random example. Full extract is above 35 lines
Version: 3 (0x2)
Serial Number: 4 (0x4)
Signature Algorithm: sha512WithRSAEncryption

# myphone.key below, just a random example. Full extract is above 35 lines

# ta.key below, just a random example. Full extract is above 20 lines
# 2048 bit OpenVPN static key
—–BEGIN OpenVPN Static key V1—–
—–END OpenVPN Static key V1—–

This configuration file, you can name “myserver.ovpn” for instance, must be transfered into the phone via a secure channel. As I’m using SpiderOak I used the Hive folder (which is a synchronized folder among many clients/devices) to retrieve the file. However, once you copied with SCP the phone keys and certificate on your computer, I guess a classical USB transfer will do the job as well 🙂

Your VPN server is now finished!


We have seen how to build an OpenVPN server based on OpenBSD with Full Disk Encryption, to benefit from OpenBSD’s memory protection, randomness implementation, LibreSSL, and secure by default philosophy. I find the VPS server to be a cost effective way to build our own VPN server, with many benefits such as snapshot before an upgrade, full access to boot the wanted ISO, remote console access, datacenter country location choice, and Two-Factor authentication with Yubikey. It is a way to be in full control of your computer, your home router, and your VPN server at the other end.

I used previously systrace which was removed starting from OpenBSD 6.0, because an application could circumvent it. Systrace was replaced by Pledge which is aimed at being more secure, but it works differently. Now with pledge, programmers have to include pledge API calls inside their code to be able to benefits from Pledge. Basically an application can request a mode allowing it to open files, or manage memory, but as soon as it will try for instance a network action not previously requested, the process will be killed. Theo de Raadt himself presented this new security feature at Hackfest 2015, you can find in the following links the slides and the video. A lot of base programs are already pledge protected: cat, chmod, mkdir, tar, gzip, ping, ftp, doas, nc, openssl, tcpdump, ntpd, httpd, smtpd, etc… From this 2015 slide, more than 400 programs were converted in 6 months. Apparently however, if I’m not mistaken, OpenVPN is not yet in the list unfortunately.

Also, in my previous article I talked about Security vs Anonymity, I won’t include it here but you can jump to this link if you never read it.

In the end, there is still benefits of using an external VPN provider such as AirVPN. You may spend less money yearly depending on your required traffic volume, and spend less time as you do not have to maintain a server or upgrade it.

By using an external VPN provider you choose convenience, whereas by managing your own server you prioritize control and trust. Make your choice!