OpenVPN

Das EPEL-Repo stellt für gewöhnlich eine sehr aktuelle OpenVPN-Version bereit.

OpenVPN ist nicht multi-threaded. Damit ist die maximale Bandbreite durch die Leistungsfähigkeit eines CPU-Cores begrenzt.

Die Systemd-Unit-Files erwarten, dass die (Server-)Konfigurationsdateien auf .conf statt auf .ovpn enden. Beispielkonfigurationen für OpenVPN finden sich in /usr/share/doc/openvpn*.

OpenVPN-Client installieren und konfigurieren

Eine zum hier gezeigten Server passende Konfigurationsdatei:

/etc/openvpn/client.ovpn
auth SHA384
auth-nocache
client
data-ciphers-fallback AES-256-GCM
dev tun98
keepalive 10 60
nobind
persist-key
persist-tun
pkcs12 /etc/openvpn/myself.p12 # use double slashes on Windows clients, for example c://path//to//myself.p12
proto udp
remote vpn.example.com 12345
remote-cert-tls server
tls-cipher TLS-RSA-WITH-AES-256-GCM-SHA384:TLS-RSA-WITH-AES-256-CBC-SHA256:TLS-RSA-WITH-AES-256-CBC-SHA
tls-version-min 1.2
verb 1
verify-x509-name vpn.example.com name

Client unter RHEL installieren und starten:

# from EPEL-Repository
dnf -y install openvpn
openvpn --config /etc/openvpn/client.ovpn &

Client unter Windows:

Client unter Apple macOS:

OpenVPN-Server installieren und konfigurieren

IP-Forwarding aktivieren:

echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf
sysctl -p

Dann:

# from EPEL-Repository
dnf -y install openvpn

Diffie Hellmann-Parameter erstellen:

openssl dhparam -out /etc/openvpn/dh4096.pem 4096
chmod 0400 /etc/openvpn/server/server.p12

Auf gehärteten Maschinen (z.B. CIS), oder bei Erscheinen der Fehlermeldung „RTNETLINK answers: Operation not permitted“ im openvpn.log:

cat > /etc/sudoers.d/openvpn << EOF
openvpn ALL=(ALL) NOPASSWD: /sbin/ip
Defaults:openvpn !requiretty
EOF

Konfiguration eines OpenVPN-Servers, der auf Port 12345 hört, ein VPN-Netz „192.0.2.0“ anbietet, und dieses ins interne Netz „10.80.109.0“ routet:

/etc/openvpn/server/server.conf
auth SHA384
cipher AES-256-GCM
# crl file (or directory) is read every time a peer connects,
# and it has to contain at least one item
#crl-verify /etc/openvpn/server/crl.pem
#client-config-dir /etc/openvpn/ccd
daemon
dev tun
dh /etc/openvpn/dh4096.pem
#duplicate-cn
group openvpn
keepalive 10 60
log-append /var/log/openvpn.log
persist-key
persist-tun
pkcs12 /etc/openvpn/server/server.p12
port 12345
proto udp

push "route 10.80.109.0 255.255.255.0"
server 192.0.2.0 255.255.255.0

status /var/log/openvpn-status.log
syslog
tls-cipher TLS-RSA-WITH-AES-256-GCM-SHA384:TLS-RSA-WITH-AES-256-CBC-SHA256:TLS-RSA-WITH-AES-256-CBC-SHA
tls-server
tls-version-min 1.2
topology subnet
user openvpn
verb 4

Tipp

Der OpenVPN-Server liest bei jedem Verbindungsaufbau eines Clients die CRL-Datei. Das schafft er aber nur, wenn sie im Verzeichnis /etc/openvpn liegt. Liegt sie woanders, liest er sie zwar beim Start des VPN-Servers ein (CRL: loaded 1 CRLs from file ...), schafft das aber bei den einzelnen Verbindungsanfragen nicht mehr und quittiert dies jedes Mal mit dem Fehler WARNING: Failed to stat CRL file, not reloading CRL. - egal, ob die Datei dem OpenVPN-Benutzer gehört und ob sie die entsprechenden Rechte aufweist. Wird also eine CRL ausserhalb des Verzeichnisses /etc/openvpn aktualisiert, muss der OpenVPN-Server neu gestartet werden.

semanage port -a -t openvpn_port_t -p udp 12345

OpenVPN-Server starten:

# if config file is named "linuxfabrik.conf", run "...@linuxfabrik":
systemctl enable --now openvpn-server@server

Eine Firewall muss, wenn sie Regeln auf das VPN-Interface anwenden möchte, auf dieses beim Hochfahren warten. Ausserhalb von Systemd lässt sich das so umsetzen:

### Wait for OpenVPN interface ###
device="tun0"
t=60

while [[ $t -gt 0 ]]; do
    if [[ $(ip link show $device 2> /dev/null) ]]; then
        break
    else
        ((t-=1));
        sleep 1;
    fi
done

if [[ $t -le 0 ]]; then
    echo "Cannot find $device."
    logger "Cannot find $device."
    exit 1;
fi

echo "$device found."
logger "$device found."

Logrotate:

cat >/etc/logrotate.d/openvpn << EOF
/var/log/openvpn.log {
    # The log is kept open *even if* I send a
    # SIGUSR1 or SIGHUP signal to the OpenVPN process.
    # Also, if I move away the logfile it is never
    # recreated, just because the
    # daemon keeps writing on the renamed file.
    # The copytruncate fixes this.
    compress
    copytruncate
    daily
    dateext
    missingok
    rotate 14
    size 1
}
EOF

Clients fixe IP zuweisen (CCD)

Auf einem Server mit gesetzter Client Config Dir-Option (CCD) hat es sich bewährt, die CCDs von hinten anfangen zu lassen, damit der IP-Adresspool (Angabe server) sich nicht so schnell mit den statischen IPs aus ccd überschneidet. Die Dateinamen im ccd-Verzeichnis müssen denen des CommonName im Client-Zertifkat entsprechen.

/etc/openvpn/server/server.conf
client-config-dir /etc/openvpn/ccd

Im Beispiel erhält der Client mit dem Zertifikat „myhost“ bei der Einwahl die statische IP „192.0.2.47“:

mkdir -p /etc/openvpn/ccd

cat > /etc/openvpn/ccd/myhost << EOF
ifconfig-push 192.0.2.47 255.255.255.0
EOF

TUN vs. TAP

  • TUN = Punkt-zu-Punkt Netzwerkgerät, sendet und empfängt IP-Pakete auf Layer 3. Routed Network.

  • TAP = virtuelle Ethernet-Schnittstelle, sendet und empfängt Ethernet-Frames auf Layer 2, ist also unabhängig vom verwendeten Protokoll (IP, IPX, etc.). In erster Linie Bridged Network (Routing wäre auch möglich). TAP benötigt Netzwerkkarten im Promiscuous Mode.

Schlüsselstärken

Liste verfügbarer Chiffren anzeigen:

openvpn --show-ciphers

Liste möglicher TLS-Ciphers:

openvpn --show-tls

Liste unterstützter Message Authentications (HMACs):

openvpn --show-digests

OpenVPN und NetworkManager - PKCS#12-Zertifikate ohne Passwort

Der NetworkManager unterstützt generell OpenVPN-Verbindungen. Dazu werden zwei Pakete benötigt:

sudo dnf -y install NetworkManager-openvpn NetworkManager-openvpn-gnome

Der NetworkManager hat jedoch Probleme mit dem Import von vorgefertigten OpenVPN-Konfigurationen, die PKCS#12 SSL-Zertifikate ohne Passwort verwenden.

Damit das am Ende doch funktioniert, muss die OpenVPN-Konfiguration zunächst in das NetworkManager-Format konvertiert werden. Für die Konvertierung gibt es ein lua-Script auf NetworkManager github contrib/scripts page.

cd ~/.local/bin
wget https://raw.githubusercontent.com/NetworkManager/NetworkManager/main/contrib/scripts/nm-import-openvpn
chmod +x ./nm-import-openvpn
sudo ./nm-import-openvpn openvpn-config.conf /etc/NetworkManager/system-connections/openvpn-config

Anschliessend das Passwort für das Zertifikat auf any setzen:

am Ende von /etc/NetworkManager/system-connections/openvpn-config hinzufügen
[vpn-secrets]
cert-pass=any

Dann importieren und aktivieren:

sudo chmod 600 /etc/NetworkManager/system-connections/*
sudo nmcli con load /etc/NetworkManager/system-connections/*

Verbindungsaufbau über das GUI oder per:

nmcli con up openvpn-config.nmconnection  # name from "id="

OpenVPN und 2FA (TOTP)

Variante mit Shell-Skript

So wird der OpenVPN-Server konfiguriert, falls 2FA umgesetzt werden soll (hier mit TOTP, aber andere Verfahren funktionieren analog).

Auf dem OpenVPN-Server zusätzlich installieren:

dnf -y install oathtool
dnf -y install qrencode

OpenVPN server.conf ergänzen:

# https://openvpn.net/community-resources/reference-manual-for-openvpn-2-4/
script-security 2
auth-user-pass-verify ./oath-user-pass.sh via-file

Auf dem OpenVPN-Server zwei Skripte ablegen:

/etc/openvpn/server/oath-user-pass.sh
#!/usr/bin/env bash

# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# Script to verify OTP using oathtool.
# Will be called from openvpn server like so:
# `$0 /tmp/openvpn_up_53ebbfc3ee15299f67dcff30b05d87ad.tmp`
# (where openvpn will write the username and password to the first two lines of the temporary file)

# Get the user/pass from the openvpn tmp passfile
openvpn_passfile=$1
openvpn_username=$(head -1 "$openvpn_passfile")
openvpn_secret=$(tail -1 "$openvpn_passfile")  # expecting "password:otp"

# Find the entry in our oath.secrets file, ignore case
pwd_stored=$(grep -i -m 1 "$openvpn_username:" oath.secrets | cut -d: -f2)

# Calculate the code we should expect
otp_calculated=$(oathtool --totp "$pwd_stored")

if [ "$otp_calculated" = "$openvpn_secret" ];
then
    # we already got a hex encoded secret key
    exit 0
fi

# See if we have password and OTP, or just OTP
echo -n "$openvpn_secret" | grep -q -i :
if [ $? -eq 0 ];
then
    pwd_given=$(echo -n "$openvpn_secret" | cut -d: -f1)
    otp_given=$(echo -n "$openvpn_secret" | cut -d: -f2)
    hashed_pwd_given=$(echo -n "$pwd_given" | sha256sum | cut -b 1-30)
    if [ "$pwd_stored" = "$hashed_pwd_given" ] && [ "$otp_calculated" = "$otp_given" ];
    then
        # credentials match
        exit 0
    fi
fi

# If we make it here, auth hasn't succeeded, don't grant access
exit 1
/etc/openvpn/server/oath-secret-gen.sh
#!/usr/bin/env bash

# Author:  Linuxfabrik GmbH, Zurich, Switzerland
# Contact: info (at) linuxfabrik (dot) ch
#          https://www.linuxfabrik.ch/
# License: The Unlicense, see LICENSE file.

# Set your URL-encoded issuer string here
issuer='Linuxfabrik%20OpenVPN'

user=$1
pwd=$2

hex_pwd=$(echo -n "$pwd" | sha256sum | cut -b 1-30)
base32_pwd=$(/usr/bin/oathtool --totp -v "$hex_pwd" | grep Base32 | awk '{print $3}')

echo "$user:$hex_pwd" >> /etc/openvpn/server/oath.secrets
qrencode --type ansi "otpauth://totp/$issuer:$user?secret=$base32_pwd"
echo "User Credentials: $user / $pwd"
echo "User String: otpauth://totp/$issuer:$user?secret=$base32_pwd"

Benutzer anlegen:

# /etc/openvpn/server/oath-secret-gen.sh $USERNAME $PASSWORD
/etc/openvpn/server/oath-secret-gen.sh alice linuxfabrik

QR-Code für Apps wie FreeOTP und andere wird im Terminal geprintet. QR-Code, Benutzername und Passwort den Benutzern auf sicheren Kanälen zukommen lassen.

OpenVPN Client-Config um den Eintrag ergänzen:

/path/to/openvpn-client.ovpn
auth-user-pass

Der Benutzer kann sich wie folgt einloggen:

  1. sudo openvpn --config /path/to/my.ovpn

  2. Enter Auth Username: alice

  3. 🔐 Enter Auth Password: linuxfabrik:123456 (Eingabe also Passwort, gefolgt von Doppelpunkt, gefolgt von OTP-Code)

Und so funktioniert das ganze:

  1. Benutzernamen und gehashte Passwörter finden sich in /etc/openvpn/server/oath.secret

  2. Benutzer verbindet sich inkl. Client-Zertifikat (und unter Umständen einem Zertifikats-Passwort). Soweit bekannt.

  3. Wegen auth-user-pass wird der Benutzer nach seinen Credentials gefragt (Benutzername und „Passwort:OTP“)

  4. Durch die Direktive auth-user-pass-verify ./oath-user-pass.sh legt der OpenVPN-Server die Credentials in einer temporären Datei ab und ruft das angegebene Skript auf.

  5. Das Skript …

    • liest die übergebenen Credentials aus

    • liest das gespeicherte Passwort des Benutzers aus /etc/openvpn/server/oath.secret

    • kalkuliert basierend auf dem gespeicherten Passwort das theoretische OTP

    • prüft, ob das eingegebene mit dem gespeicherten Passwort sowie das eingegebene OTP mit dem kalkulierten OTP übereinstimmen

  6. Nach dem Skript-Run wird die temporäre Datei gelöscht.

Variante mit EasyRSA und viel Komfort

Der Unterschied zur obigen Lösung ist, dass hier PAM zum Einsatz kommt, welches den Google-Authenticator nutzt (Benutzer können die OTP-App frei wählen, z.B. FreeOTP). Es werden also echte Benutzer auf dem System eingerichtet, denen jedoch ein SSH-Zugang per /bin/nolgin verunmöglicht wird. Für die Zertifikatsverwaltung kommt Easy-RSA direkt auf der Maschine zum Einsatz. Die Anleitung geht von einem RHEL 9 aus.

Inspiriert von https://perfecto25.medium.com/openvpn-community-2fa-with-google-authenticator-4f2a7cb08128, aktualisiert, und mit weniger Abhängigkeiten. Am Ende springt viel Komfort für den Admin raus. Dieser muss nur folgende Kommandos aufrufen:

sudo vpn-user-create linus
sudo vpn-user-send linus linus@example.com

So richtet man den OpenVPN-Server dafür ein:

dnf -y install easy-rsa google-authenticator pwgen qrencode s-nail

mkdir -p /opt/lfvpn/{client,easy-rsa}
cd /opt/lfvpn/easy-rsa
/usr/share/easy-rsa/3/easyrsa init-pki

Folgende Dateien anlegen:

/etc/pam.d/openvpn (chmod 0644, root:root)
# OpenVPN 2FA PAMaccount required pam_unix.so
# This file is managed by Ansible - do not edit

# Here we are telling the VPN server to parse an incoming user auth request through the
# standard Linux auth pam stack (user + password) which will check to see if the incoming
# user exists on the OS itself, and that the user’s password matches the OS user password

# then it checks the pam_google_authenticator.so library (this should be installed by
# install.sh script (yum install google-authenticator). Check the path to this .so file on
# your OS, it should match /usr/lib64/security path

# the secret= parameter tells the plugin to check the incoming OTP token against the
# Google Authenticator file that is generated every time you create a new VPN user
# (see below). If the OTP token matches the Google secret code, it authenticates.

# user=root tells the PAM module to look into the /opt/openvpn/google-auth/$USER file as
# "root" user, so you dont have any permission errors reading that file

# authtok_prompt=pin, checks the incoming OTP token from the user’s Authy app

# the sequence of the PAM stack is important, so make sure the google_auth PAM line is
# last in the stack, otherwise it wont work

auth required pam_unix.so
auth       substack     password-auth
auth       include      postlogin
account    required     pam_sepermit.so
account    required     pam_nologin.so
account    include      password-auth
password   include      password-auth
auth requisite /usr/lib64/security/pam_google_authenticator.so secret=/opt/lfvpn/client/${USER}/otp user=root authtok_prompt=pin
/opt/lfvpn/client/template.conf (chmod0644, root:openvpn)
# powered by Linuxfabrik
auth SHA384
auth-nocache

# for PAM and TOTP
auth-user-pass
static-challenge "2FA Authenticator Code:" 1

tls-cipher TLS-RSA-WITH-AES-256-GCM-SHA384:TLS-RSA-WITH-AES-256-CBC-SHA256:TLS-RSA-WITH-AES-256-CBC-SHA
tls-version-min 1.2
remote-cert-tls server
data-ciphers-fallback AES-256-GCM

verify-x509-name <my-short-hostname> name

dev tun
client
nobind
proto udp
persist-key
persist-tun
keepalive 10 60

remote 192.0.2.74 1194

# 0 is silent, except for fatal errors
# 4 is reasonable for general usage
# 5 and 6 can help to debug connection problems
# 9 is extremely verbose
verb 1
/opt/lfvpn/easy-rsa/pki/vars (chmod 0600, root:root)
# This file is managed by Ansible - do not edit
set_var EASYRSA_ALGO                ec
set_var EASYRSA_CA_EXPIRE           3650
set_var EASYRSA_CERT_EXPIRE         380
set_var EASYRSA_CRL_DAYS            1000
set_var EASYRSA_CURVE               prime256v1
set_var EASYRSA_DIGEST              sha256
set_var EASYRSA_DN                  cn_only
set_var EASYRSA_KDC_REALM           "CHANGEME.EXAMPLE.COM"
set_var EASYRSA_KEY_SIZE            2048
set_var EASYRSA_NO_PASS             1
set_var EASYRSA_NS_COMMENT          "Powered by Linuxfabrik"
set_var EASYRSA_NS_SUPPORT          no
set_var EASYRSA_OPENSSL             openssl
set_var EASYRSA_PKI                 "$PWD/pki"
set_var EASYRSA_PRE_EXPIRY_WINDOW   14
set_var EASYRSA_PRESERVE_DN         1
set_var EASYRSA_RAND_SN             yes
set_var EASYRSA_SSL_CONF            "$EASYRSA_PKI/openssl-easyrsa.cnf"
set_var EASYRSA_TEMP_DIR            "$EASYRSA_PKI"
/tmp/lf-chkpwd
module lf-chkpwd 1.0;

require {
    type chkpwd_t;
    class capability dac_override;
}

#============= chkpwd_t ==============
allow chkpwd_t self:capability dac_override;

Alle weiteren Dateien finden sich auf https://github.com/Linuxfabrik/openvpn-2fa-easyrsa und sollten in /usr/local/bin abgelegt werden.

Easy-RSA vorbereiten:

cd /opt/lfvpn/easy-rsa

# create CA
EASYRSA_REQ_CN="CA powered by Linuxfabrik" /usr/share/easy-rsa/3/easyrsa --batch build-ca nopass

# create server certificates:
/usr/share/easy-rsa/3/easyrsa --batch build-server-full $(hostname --short) nopass
/usr/share/easy-rsa/3/easyrsa --batch export-p12 $(hostname --short)
/usr/share/easy-rsa/3/easyrsa --batch gen-crl
\cp /opt/lfvpn/easy-rsa/pki/crl.pem /etc/openvpn/server/crl.pem
chmod 0600 /etc/openvpn/server/crl.pem
chown openvpn:openvpn /etc/openvpn/server/crl.pem

CA und Server-Zertifikate sind eingerichtet. OpenVPN-Server installieren. Was in der OpenVPN-Konfiguration unbedingt angegeben werden muss:

/etc/openvpn/server/server.conf
crl-verify /etc/openvpn/server/crl.pem
pkcs12 /opt/lfvpn/easy-rsa/pki/private/<my-short-hostname>.p12
plugin /usr/lib64/openvpn/plugins/openvpn-plugin-auth-pam.so "openvpn login USERNAME password PASSWORD pin OTP"

Bemerkung

Die OpenVPN-Client-Konfiguration wird folgende Anweisungen für den System-Login (damit PAM greift) als auch für TOTP enthalten:

caption:

client.conf

# for PAM and TOTP - ask for User, Password and TOTP auth-user-pass static-challenge „Enter 2FA Authenticator Code:“ 1

Diesen Job - die Erstellung der Client-Konfigurationsdatei - erledigt nachher vpn-user-create auf dem OpenVPN-Server.

SELinux:

semanage fcontext --add --type openvpn_tmp_t "/opt/lfvpn/client(/.*)?"
restorecon -Fvr /opt

cd /tmp
audit2allow --all --module-package=lf-chkpwd
semodule --install lf-chkpwd.pp

Fertig.

Benutzer-Zertifkat erstellen (diese gelten ein Jahr):

vpn-user-create linus

Credentials an den Benutzer senden (E-Mail-Adresse erforderlich):

vpn-user-send linus linus@example.com

Zugang für den Benutzer entziehen:

vpn-user-delete linus

Welche Benutzer gibt es?

vpn-user-list

Beispielhafter, erfolgreicher Login (Linux-Kommandozeile):

sudo openvpn --config /tmp/walle.conf
Ausgabe etwas gekürzt
Enter Auth Username: linus
Enter Auth Password: ••••••••••••••••••••••••••••••••••••••••••••••••••••••••••••
CHALLENGE: Enter 2FA Authenticator Code: 411039
2024-06-28 17:09:38 Initialization Sequence Completed

Troubleshooting

VPN-Client: „Authenticate/Decrypt packet error: missing authentication info“

Liegt an einer der folgenden fehlenden Client-Settings: nobind, remote-cert-tls server - oder meist an der veralteten Einstellung „ns-cert-type server“ statt „remote-type-tls“. Wird letztere durch „remote-cert-tls server“ ersetzt, ist der Client auch in der Lage, „extended key usage“ der Zertifikate zu validieren.

VPN-Client: Server-Maschine hinter dem VPN-Server ist nicht erreichbar, VPN-Server aber schon

Rückroute auf der Server-Maschine hinter dem OpenVPN-Server fehlt. Entweder auf dem zentralen Gateway die Route hin zum OpenVPN-Server eintragen, oder jedem per VPN zu erreichenden System einzeln.

Built on 2024-11-18