This project is everything you need to turn a new Fedora server into an OpenVPN server, including administrative and integration tools. If you're using other than Fedora, the software will work fine but you may need to tweak the Ansible playbook for installation.
Special thanks to Playground Global, LLC for open-sourcing this software. See LICENSE
for details.
git clone --recursive https://github.com/morrildl/bifrost
Note that this software relies on git submodules, so don't overlook the --recursive
flag. (If you
did overlook it, try git submodule update --init --recursive
.)
Though this is Go software, I use git submodules to manually mount libraries at specific places in
my tree, as the standard go get
behavior of conflating hosting site with source package name is
poor software engineering practice. Specifically, the playground/*
libraries are mirrored (or
have been mirrored in the course of their development) on multiple sites, so I use git submodules
to manage them.
This project consists of 3 key components.
OpenVPN is, of course, doing all the heavy lifting. This project is essentially a constellation of tools to help deploy an OpenVPN with a decently secure configuration, with decent usability.
The key moving pieces are:
- a SQLite3 database with a simple schema tracking certificate validity, and audit logs
ovpn-tls-verify.py
- a script for OpenVPN'stls-verify
hook that handles certificate validity and revocations via the databaseovpn-auth-user-pass-verify.py
- a script for theauth-user-pass-verify
hook that implements TOTP authentication (not passwords) suitable for use with Google Authenticator or Authyovpn-client-logger.py
- a script for theclient-(dis)?connect
hook that logs usage by IP
The model is multi-factor authentication with a minimum of integration or overhead, in particular avoiding dependencies on other systems, especially password databases.
Essentially this is three-factor authentication. To access the VPN you must have:
- The client certificate on the device (i.e. laptop) wanting to use VPN (ideally stored in a hardware TPM, but beyond the scope of this project)
- The device (i.e. phone) where the TOTP app is installed
- The OS passwords/lock codes to those devices
That is, if an attacker wants to get onto the VPN, he must steal your phone, and your laptop, and know your screensaver password and your phone unlock code.
Naturally the actual security of this model depends on the OS and user behavior, so sensible policies must also be used. Specifically, the device used for TOTP must not itself have a VPN client certificate (because then you lose a factor). And of course suitable OS-level screen locks must be used.
Note that this implementation (currently) does not use the OpenVPN administrative runtime hooks to disconnect a device with an extant connection, if that device's cert is revoked. Since the expectation is that the web UI runs behind the VPN, and not necessarily on the public internet, VPN access is required to refresh device certificates. Thus we cannot revoke clients immediately via OpenVPN admin hooks: it would kick users off instantly as soon as they click the disconnect button but before they can generate a new certificate for their device. Certainly a dedicated "re-up this device" UI flow for this case is possible, but it would be more complicated, and the current UI is specifically intended to be dirt simple. All of which is to say, this is a conscious usability vs. security tradeoff.
Heimdall is an API server to front the SQLite3 database. The client authentication runtime scripts use the database to read certificate status (i.e. for validity and revocations), and write logs to it. The API server provides REST endpoints to manage certificates -- create users, reset TOTP seeds, issue and revoke certificates, etc.
The web UI is simply a front-end to Heimdall. A command-line front-end is also provided, but generally it's expected that most operations will be done via the web UI.
Heimdall authenticates its client via certificate pinning. The intention is that the Heimdall process itself runs on the OpenVPN server, where the SQLite3 database is located. The web UI can be run anywhere, using Heimdall as its back-end.
The specific configuration encoded in the Ansible playbook has Heimdall and Bifröst running on the same machine. This is also fine, though with a reduced security posture; but the two were built separately to make it straightforward to split the two if desired.
The Bifröst web UI is where policy enforcement happens. This project is intended for use by a relatively small number of total users, perhaps up to a couple hundred. The UI is intended to be generally self-service.
Users can create and revoke certificates, up to a limit on number of extant certificates set by the administrator. For instance, the admin can set the limit to 1, allowing for only one machine at a time, intended to be a laptop. Or, the admin can set the limit to 3, perhaps allowing for a laptop, desktop, and tablet. If a user is at the limit, they must revoke a certificate to create a new one.
The administrator can opt to either have a manual whitelist of users, or allow unrestricted access to a particular domain via Google's OAuth2/OpenID Connect. In both cases, the certificate limits are enforced.
Gjallarhorn is a binary intended to be run as a cron job that scans the extant certificates in the database, and sends notification emails about impending expirations. That is, it notifies users when their certificates are set to expire in 30/7/1 days, so that they can log in to the web UI and re-issue new certificates before they lose VPN access.
The ./ansible/
directory contains config files and an Ansible playbook to configure a fresh Fedora
27 server as an OpenVPN server.
The ./src/
directory contains the Go source code for all three programs.
GOPATH=`pwd` go build src/bifrost/cmd/bifrost.go
GOPATH=`pwd` go build src/heimdall/cmd/heimdall.go
GOPATH=`pwd` go build src/gjallarhorn/cmd/gjallarhorn.go
GOPATH=`pwd` go build src/vendor/playground/ca/cmd/pgcert.go
mv pgcert bifrost heimdall gjallarhorn ansible/tmp
cd ansible/tmp
./pgcert \
-bits 4096 -days 3650 -pass something \
-cn "Temp Authority" -org "Sententious Heavy Industries" \
-locality "Mountain View" -province "CA" -country "US" \
rootca ca.key ca.crt
./pgcert \
-bits 4096 -days 365 -rootpass something -pass something \
-cn "vpn.domain.tld" \
server ca.key ca.crt
mv vpn.domain.tld.crt openvpn-server.crt
mv vpn.domain.tld.key openvpn-server-tmp.key
openssl rsa -in openvpn-server-tmp.key -out openvpn-server.key
rm openvpn-server-tmp.key
Note that the -cn
value must be the hostname of the server, or it will fail validation by the
clients.
./pgcert \
-bits 4096 -days 365 -rootpass something -pass something \
-cn "localhost" \
server ca.key ca.crt
mv localhost.crt heimdall-server.crt
mv localhost.key heimdall-server-tmp.key
openssl rsa -in heimdall-server-tmp.key -out heimdall-server.key
rm heimdall-server-tmp.key
The Bifröst UI web server calls into Heimdall for most operations; that is, Bifröst is the primary (usually only) client of Heimdall. The purpose is to allow the API server, which handles the root CA keymatter, to be separated onto a different machine, if desired. This would improve an organization's security posture.
Note that, again, the -cn
value must be the hostname of the server, or it will fail validation
by the client.
./pgcert \
-bits 4096 -days 365 -rootpass something -pass something \
-cn "Heimdall API Client" \
client ca.key ca.crt
mv "Heimdall API Client".crt heimdall-client.crt
mv "Heimdall API Client".key heimdall-client-tmp.key
openssl rsa -in heimdall-client-tmp.key -out heimdall-client.key
rm heimdall-client-tmp.key
This certificate identifies the front-end UI server (Bifröst) to the API server which manages the database and keys (Heimdall.) Heimdall will refuse to talk to any client except one which presents this certificate and private key.
Create an OpenVPN tls-auth
file, used to improve security during client connections:
openvpn --genkey --secret tls-auth.pem
Create Diffie-Hellman parameters for the TLS server:
openssl dhparam -out dh-4096.pem -outform PEM 4096
Place the password for the OpenVPN server private key into the relevant file:
echo something > openvpn-server-pw.txt
Note that this must match the value of the -pass
argument used above.
The certificates created above are signed by your custom root CA, created in the first step. As this root CA will not be recognized by browsers, it can't be used to sign certificates that browsers will accept by default.
So, you'll need to copy in standard TLS certificates, sourced from a commercial CA in the usual way. Note that certificates from Let's Encrypt are perfectly acceptable.
Copy these files into the tree as PEM-encoded X509 certificate and PKCS11 RSA private key files
as ansible/tmp/bifrost-server.crt
and ansible/tmp/bifrost-server.key
respectively.
cp etc/hosts-example.ini ansible/tmp/hosts.ini
vim ansible/tmp/hosts.ini
vpn_public_ip
- the public IP address of your VPN servervpn_public_port
- the public port of your VPN servervpn_bind_ip
- the machine-local IP address the OpenVPN process should listen on (possibly the same asvpn_public_ip
but different if you're behind a firewall)vpn_bind_port
- the machine-local port the OpenVPN process should bindvpn_uplink_interface
- the interface name of your machine's primary uplink (e.g.eth0
)vpn_client_domain
- the domain name to push to clients (i.e. via DHCP)vpn_client_dns_servers
- the list of DNS servers to push to clients (i.e. via DHCP)vpn_client_routes
- the list of routes to push to clients (i.e. via DHCP)oauth_client_id
- the Google Cloud OAuth client ID from developer consoleoauth_client_secret
- the Google Cloud OAuth client secret from developer consoleoauth_redirect_prefix
- the prefix (i.e. scheme+host+port) of the redirect target, configured in Google Cloud consoleca_key_password
- the password of the CA signing key (ca.key
)bifrost_admin_list
- the list of email addresses who shall have admin rights in the web UIbifrost_bind_address
- the IP address for the web UI to listen on (possibly but not necessarily the same asvpn_bind_ip
)
cd .. # i.e. up to $TREE/ansible
ansible-playbook -i tmp/hosts.ini bifrost.yml
You should now be able to visit the web UI at the port and address you configured above. If you log in (using Google OAuth2) as an administrator, you'll see the admin UI to manage users, change settings and policies, and view events. Admins are also users of the service, and can set TOTP password seeds and configure device certificates.
If you're not an admin, you'll only be able to use the self-service features.
Read on to get some tips on common tasks.
systemctl restart bifrost
systemctl restart heimdall
systemctl restart openvpn-server@main
sqlite3 /opt/bifrost/heimdall.sqlite3
vim /etc/sysconfig/iptables
systemctl restart iptables
- Firewall rules:
/etc/sysconfig/iptables
- OpenVPN server:
/etc/openvpn/server/main.conf
- Change the
.ovpn
config files generated by Heimdall:/opt/bifrost/etc/template.ovpn
- Change Bifröst web UI settings (e.g. to add/remove admins):
/opt/bifrost/etc/bifrost.json
- Change Heimdall API server settings:
/opt/bifrost/etc/heimdall.json