Merge pull request #102 from fabn/otp

Two factor authentication using a token application
This commit is contained in:
Kyle Manna 2016-03-14 07:42:30 -07:00
commit eb22992a2f
8 changed files with 217 additions and 3 deletions

View File

@ -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 <kyle@kylemanna.com>
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/

View File

@ -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

View File

@ -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

View File

@ -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

33
bin/ovpn_otp_user Executable file
View File

@ -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

72
docs/otp.md Normal file
View File

@ -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 <user> 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 <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 `<user>` 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/<user>.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 <user> authenticate
```
If you configured everything correctly you should get authenticated by entering a OTP code from the app.

7
otp/openvpn Normal file
View File

@ -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

81
tests/otp.sh Executable file
View File

@ -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 <<EOF
___________
< it worked >
-----------
\ ^__^
\ (oo)\_______
(__)\ )\/\\
||----w |
|| ||
EOF