Merge pull request #102 from fabn/otp
Two factor authentication using a token application
This commit is contained in:
commit
eb22992a2f
@ -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/
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
33
bin/ovpn_otp_user
Executable 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
72
docs/otp.md
Normal 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
7
otp/openvpn
Normal 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
81
tests/otp.sh
Executable 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
|
Loading…
Reference in New Issue
Block a user