diff --git a/Dockerfile b/Dockerfile index a573392..2cac8e2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,13 @@ # Original credit: https://github.com/jpetazzo/dockvpn -# Leaner build then Ubuntu +# Smallest base image FROM alpine:3.2 MAINTAINER Kyle Manna RUN echo "http://dl-4.alpinelinux.org/alpine/edge/community/" >> /etc/apk/repositories && \ - apk add --update openvpn iptables bash easy-rsa && \ + echo "http://dl-4.alpinelinux.org/alpine/edge/testing/" >> /etc/apk/repositories && \ + apk add --update openvpn iptables bash easy-rsa openvpn-auth-pam google-authenticator pamtester && \ ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \ rm -rf /tmp/* /var/tmp/* /var/cache/apk/* @@ -26,3 +27,6 @@ CMD ["ovpn_run"] ADD ./bin /usr/local/bin RUN chmod a+x /usr/local/bin/* + +# Add support for OTP authentication using a PAM module +ADD ./otp/openvpn /etc/pam.d/ diff --git a/README.md b/README.md index 604b9c7..6ec0fb3 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,7 @@ Conveniently, `kylemanna/openvpn` comes with a script called `ovpn_getclient`, which dumps an inline OpenVPN client configuration file. This single file can then be given to a client for access to the VPN. +To enable Two Factor Authentication for clients (a.k.a. OTP) see [this document](/docs/otp.md). ## OpenVPN Details diff --git a/bin/ovpn_genconfig b/bin/ovpn_genconfig index a809260..d039930 100755 --- a/bin/ovpn_genconfig +++ b/bin/ovpn_genconfig @@ -50,6 +50,7 @@ usage() { echo " -C A list of allowable TLS ciphers delimited by a colon (cipher)." echo " -a Authenticate packets with HMAC using the given message digest algorithm (auth)." echo " -z Enable comp-lzo compression." + echo " -2 Enable two factor authentication using Google Authenticator." } if [ "$DEBUG" == "1" ]; then @@ -79,7 +80,7 @@ OVPN_AUTH='' [ -r "$OVPN_ENV" ] && source "$OVPN_ENV" # Parse arguments -while getopts ":a:C:T:r:s:du:cp:n:DNm:tz" opt; do +while getopts ":a:C:T:r:s:du:cp:n:DNm:tz2" opt; do case $opt in a) OVPN_AUTH="$OPTARG" @@ -126,6 +127,9 @@ while getopts ":a:C:T:r:s:du:cp:n:DNm:tz" opt; do z) OVPN_COMP_LZO=1 ;; + 2) + OVPN_OTP_AUTH=1 + ;; \?) set +x echo "Invalid option: -$OPTARG" >&2 @@ -172,6 +176,7 @@ export OVPN_SERVER_URL OVPN_ENV OVPN_PROTO OVPN_CN OVPN_PORT export OVPN_CLIENT_TO_CLIENT OVPN_PUSH OVPN_NAT OVPN_DNS OVPN_MTU OVPN_DEVICE export OVPN_TLS_CIPHER OVPN_CIPHER OVPN_AUTH export OVPN_COMP_LZO +export OVPN_OTP_AUTH # Preserve config if [ -f "$OVPN_ENV" ]; then @@ -233,6 +238,12 @@ for i in "${OVPN_PUSH[@]}"; do echo push \"$i\" >> "$conf" done +# Optional OTP authentication support +if [ -n "$OVPN_OTP_AUTH" ]; then + echo -e "\n\n# Enable OTP+PAM for user authentication" >> "$conf" + echo "plugin /usr/lib/openvpn/plugins/openvpn-plugin-auth-pam.so openvpn" >> "$conf" +fi + set +e # Clean-up duplicate configs diff --git a/bin/ovpn_getclient b/bin/ovpn_getclient index 1c029c9..d456441 100755 --- a/bin/ovpn_getclient +++ b/bin/ovpn_getclient @@ -85,6 +85,11 @@ $OVPN_ADDITIONAL_CLIENT_CONFIG echo "auth $OVPN_AUTH" fi + if [ -n "$OVPN_OTP_AUTH" ]; then + echo "auth-user-pass" + echo "auth-nocache" + fi + if [ -n "$OVPN_COMP_LZO" ]; then echo "comp-lzo" fi diff --git a/bin/ovpn_otp_user b/bin/ovpn_otp_user new file mode 100755 index 0000000..7af9c1e --- /dev/null +++ b/bin/ovpn_otp_user @@ -0,0 +1,33 @@ +#!/bin/bash + +# +# Generate OpenVPN users via google authenticator +# + +if ! source "$OPENVPN/ovpn_env.sh"; then + echo "Could not source $OPENVPN/ovpn_env.sh." + exit 1 +fi + +if [ "x$OVPN_OTP_AUTH" != "x1" ]; then + echo "OTP authentication not enabled, please regenerate configuration using -2 flag" + exit 1 +fi + +if [ -z $1 ]; then + echo "Usage: ovpn_otp_user USERNAME" + exit 1 +fi + +# Ensure the otp folder is present +[ -d /etc/openvpn/otp ] || mkdir -p /etc/openvpn/otp + +# Binary is present in image, save an $user.google_authenticator file in /etc/openvpn/otp +if [ "$2" == "interactive" ]; then + # Authenticator will ask for other parameters. User can choose rate limit, token reuse policy and time window policy + # Always use time base OTP otherwise storage for counters must be configured somewhere in volume + google-authenticator --time-based --force -l "${1}@${OVPN_CN}" -s /etc/openvpn/otp/${1}.google_authenticator +else + google-authenticator --time-based --disallow-reuse --force --rate-limit=3 --rate-time=30 --window-size=3 \ + -l "${1}@${OVPN_CN}" -s /etc/openvpn/otp/${1}.google_authenticator +fi \ No newline at end of file diff --git a/docs/otp.md b/docs/otp.md new file mode 100644 index 0000000..22d6353 --- /dev/null +++ b/docs/otp.md @@ -0,0 +1,72 @@ +# Using two factor authentication for users + +Instead of relying on complex passwords for client certificates (that usually get written somewhere) this image +provides support for two factor authentication with OTP devices. + +The most common app that provides OTP generation is Google Authenticator ([iOS](https://itunes.apple.com/it/app/google-authenticator/id388497605?mt=8) and +[Android](https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=it)) you can download it +and use this image to generate user configuration. + +## Usage + +In order to enable two factor authentication the following steps are required. + +* Generate server configuration with `-2` option + + docker run --volumes-from $OVPN_DATA --rm fabn/openvpn ovpn_genconfig -u udp://vpn.example.com -2 + +* Generate your client certificate (possibly without a password since you're using OTP) + + docker run --volumes-from $OVPN_DATA --rm -it fabn/openvpn easyrsa build-client-full nopass + +* Generate authentication configuration for your client. -t is needed to show QR code, -i is optional for interactive usage + + docker run --volumes-from $OVPN_DATA --rm -t fabn/openvpn ovpn_otp_user + +The last step will generate OTP configuration for the provided user with the following options + +``` +google-authenticator --time-based --disallow-reuse --force --rate-limit=3 --rate-time=30 --window-size=3 \ + -l "${1}@${OVPN_CN}" -s /etc/openvpn/otp/${1}.google_authenticator +``` + +It will also show a shell QR code in terminal you can scan with the Google Authenticator application. It also provides +a link to a google chart url that will display a QR code for the authentication. + +**Do not share QR code (or generated url) with anyone but final user, that is your second factor for authentication + that is used to generate OTP codes** + +Here's an example QR code generated for an hypotetical user@example.com user. + +![Example QR Code](https://www.google.com/chart?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/user@example.com%3Fsecret%3DKEYZ66YEXMXDHPH5) + +Generate client configuration for `` and import it in OpenVPN client. On connection it will prompt for user and password. + Enter your username and a 6 digit code generated by Authenticator app and you're logged in. + +## TL;DR + +Under the hood this configuration will setup an `openvpn` PAM service configuration (`/etc/pam.d/openvpn`) +that relies on the awesome [Google Authenticator PAM module](https://github.com/google/google-authenticator). +In this configuration the `auth` part of PAM flow is managed by OTP codes and the `account` part is not enforced + because you're likely dealing with virtual users and you do not want to create a system account for every VPN user. + +`ovpn_otp_user` script will store OTP credentials under `/etc/openvpn/otp/.google_authentication`. In this + way when you take a backup OTP users are included as well. + +Finally it will enable the openvpn plugin `openvpn-plugin-auth-pam.so` in server configuration and append the +`auth-user-pass` directive in client configuration. + +## Debug + +If something is not working you can verify your PAM setup with these commands + +``` +# Start a shell in container +docker run --volumes-from $OVPN_DATA --rm -it fabn/openvpn bash +# Then in container install pamtester utility +apt-get update && apt-get install -y pamtester +# To check authentication use this command that will prompt for a valid code from Authenticator APP +pamtester -v openvpn authenticate +``` + +If you configured everything correctly you should get authenticated by entering a OTP code from the app. diff --git a/otp/openvpn b/otp/openvpn new file mode 100644 index 0000000..5179efc --- /dev/null +++ b/otp/openvpn @@ -0,0 +1,7 @@ +# Uses google authenticator library as PAM module using a single folder for all users tokens +# User root is required to stick with an hardcoded user when trying to determine user id and allow unexisting system users +# See https://github.com/google/google-authenticator/tree/master/libpam#secretpathtosecretfile--usersome-user +auth required pam_google_authenticator.so secret=/etc/openvpn/otp/${USER}.google_authenticator user=root + +# Accept any user since we're dealing with virtual users there's no need to have a system account (pam_unix.so) +account sufficient pam_permit.so diff --git a/tests/otp.sh b/tests/otp.sh new file mode 100755 index 0000000..9b8a031 --- /dev/null +++ b/tests/otp.sh @@ -0,0 +1,81 @@ +#!/bin/bash +set -ex +OVPN_DATA=basic-data-otp +CLIENT=travis-client +IMG=kylemanna/openvpn +OTP_USER=otp +# Function to fail +abort() { cat <<< "$@" 1>&2; exit 1; } + +# +# Create a docker container with the config data +# +docker run --name $OVPN_DATA -v /etc/openvpn busybox + +ip addr ls +SERV_IP=$(ip -4 -o addr show scope global | awk '{print $4}' | sed -e 's:/.*::' | head -n1) +# Configure server with two factor authentication +docker run --volumes-from $OVPN_DATA --rm $IMG ovpn_genconfig -u udp://$SERV_IP -2 + +# nopass is insecure +docker run --volumes-from $OVPN_DATA --rm -it -e "EASYRSA_BATCH=1" -e "EASYRSA_REQ_CN=Travis-CI Test CA" $IMG ovpn_initpki nopass + +docker run --volumes-from $OVPN_DATA --rm -it $IMG easyrsa build-client-full $CLIENT nopass + +# Generate OTP credentials for user named test, should return QR code for test user +docker run --volumes-from $OVPN_DATA --rm -it $IMG ovpn_otp_user $OTP_USER | tee client/qrcode.txt +# Ensure a chart link is printed in client OTP configuration +grep 'https://www.google.com/chart' client/qrcode.txt || abort 'Link to chart not generated' +grep 'Your new secret key is:' client/qrcode.txt || abort 'Secret key is missing' +# Extract an emergency code from textual output, grepping for line and trimming spaces +OTP_TOKEN=$(grep -A1 'Your emergency scratch codes are' client/qrcode.txt | tail -1 | tr -d '[[:space:]]') +# Token should be present +if [ -z $OTP_TOKEN ]; then + abort "QR Emergency Code not detected" +fi + +# Store authentication credentials in config file and tell openvpn to use them +echo -e "$OTP_USER\n$OTP_TOKEN" > client/credentials.txt + +# Override the auth-user-pass directive to use a credentials file +docker run --volumes-from $OVPN_DATA --rm $IMG ovpn_getclient $CLIENT | sed 's/auth-user-pass/auth-user-pass \/client\/credentials.txt/' | tee client/config.ovpn + +# +# Fire up the server +# +sudo iptables -N DOCKER || echo 'Firewall already configured' +sudo iptables -I FORWARD -j DOCKER || echo 'Forward already configured' +# run in shell bg to get logs +docker run --name "ovpn-test" --volumes-from $OVPN_DATA --rm -p 1194:1194/udp --privileged $IMG & + +#for i in $(seq 10); do +# SERV_IP=$(docker inspect --format '{{ .NetworkSettings.IPAddress }}') +# test -n "$SERV_IP" && break +#done +#sed -ie s:SERV_IP:$SERV_IP:g client/config.ovpn + +# +# Fire up a client in a container since openvpn is disallowed by Travis-CI, don't NAT +# the host as it confuses itself: +# "Incoming packet rejected from [AF_INET]172.17.42.1:1194[2], expected peer address: [AF_INET]10.240.118.86:1194" +# +docker run --rm --net=host --privileged --volume $PWD/client:/client $IMG /client/wait-for-connect.sh + +# +# Client either connected or timed out, kill server +# +kill %1 + +# +# Celebrate +# +cat < + ----------- + \ ^__^ + \ (oo)\_______ + (__)\ )\/\\ + ||----w | + || || +EOF