OpenVPN
Siehe auch
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:
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:
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.
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:
[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:
#!/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
#!/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:
auth-user-pass
Der Benutzer kann sich wie folgt einloggen:
sudo openvpn --config /path/to/my.ovpn
Enter Auth Username: alice
🔐 Enter Auth Password: linuxfabrik:123456 (Eingabe also Passwort, gefolgt von Doppelpunkt, gefolgt von OTP-Code)
Und so funktioniert das ganze:
Benutzernamen und gehashte Passwörter finden sich in
/etc/openvpn/server/oath.secret
Benutzer verbindet sich inkl. Client-Zertifikat (und unter Umständen einem Zertifikats-Passwort). Soweit bekannt.
Wegen
auth-user-pass
wird der Benutzer nach seinen Credentials gefragt (Benutzername und „Passwort:OTP“)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.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
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:
# 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
# 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
# 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"
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:
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
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-10-08