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
|
# Original credit: https://github.com/jpetazzo/dockvpn
|
||||||
|
|
||||||
# Leaner build then Ubuntu
|
# Smallest base image
|
||||||
FROM alpine:3.2
|
FROM alpine:3.2
|
||||||
|
|
||||||
MAINTAINER Kyle Manna <kyle@kylemanna.com>
|
MAINTAINER Kyle Manna <kyle@kylemanna.com>
|
||||||
|
|
||||||
RUN echo "http://dl-4.alpinelinux.org/alpine/edge/community/" >> /etc/apk/repositories && \
|
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 && \
|
ln -s /usr/share/easy-rsa/easyrsa /usr/local/bin && \
|
||||||
rm -rf /tmp/* /var/tmp/* /var/cache/apk/*
|
rm -rf /tmp/* /var/tmp/* /var/cache/apk/*
|
||||||
|
|
||||||
@ -26,3 +27,6 @@ CMD ["ovpn_run"]
|
|||||||
|
|
||||||
ADD ./bin /usr/local/bin
|
ADD ./bin /usr/local/bin
|
||||||
RUN chmod a+x /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
|
which dumps an inline OpenVPN client configuration file. This single file can
|
||||||
then be given to a client for access to the VPN.
|
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
|
## OpenVPN Details
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ usage() {
|
|||||||
echo " -C A list of allowable TLS ciphers delimited by a colon (cipher)."
|
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 " -a Authenticate packets with HMAC using the given message digest algorithm (auth)."
|
||||||
echo " -z Enable comp-lzo compression."
|
echo " -z Enable comp-lzo compression."
|
||||||
|
echo " -2 Enable two factor authentication using Google Authenticator."
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ "$DEBUG" == "1" ]; then
|
if [ "$DEBUG" == "1" ]; then
|
||||||
@ -79,7 +80,7 @@ OVPN_AUTH=''
|
|||||||
[ -r "$OVPN_ENV" ] && source "$OVPN_ENV"
|
[ -r "$OVPN_ENV" ] && source "$OVPN_ENV"
|
||||||
|
|
||||||
# Parse arguments
|
# 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
|
case $opt in
|
||||||
a)
|
a)
|
||||||
OVPN_AUTH="$OPTARG"
|
OVPN_AUTH="$OPTARG"
|
||||||
@ -126,6 +127,9 @@ while getopts ":a:C:T:r:s:du:cp:n:DNm:tz" opt; do
|
|||||||
z)
|
z)
|
||||||
OVPN_COMP_LZO=1
|
OVPN_COMP_LZO=1
|
||||||
;;
|
;;
|
||||||
|
2)
|
||||||
|
OVPN_OTP_AUTH=1
|
||||||
|
;;
|
||||||
\?)
|
\?)
|
||||||
set +x
|
set +x
|
||||||
echo "Invalid option: -$OPTARG" >&2
|
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_CLIENT_TO_CLIENT OVPN_PUSH OVPN_NAT OVPN_DNS OVPN_MTU OVPN_DEVICE
|
||||||
export OVPN_TLS_CIPHER OVPN_CIPHER OVPN_AUTH
|
export OVPN_TLS_CIPHER OVPN_CIPHER OVPN_AUTH
|
||||||
export OVPN_COMP_LZO
|
export OVPN_COMP_LZO
|
||||||
|
export OVPN_OTP_AUTH
|
||||||
|
|
||||||
# Preserve config
|
# Preserve config
|
||||||
if [ -f "$OVPN_ENV" ]; then
|
if [ -f "$OVPN_ENV" ]; then
|
||||||
@ -233,6 +238,12 @@ for i in "${OVPN_PUSH[@]}"; do
|
|||||||
echo push \"$i\" >> "$conf"
|
echo push \"$i\" >> "$conf"
|
||||||
done
|
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
|
set +e
|
||||||
|
|
||||||
# Clean-up duplicate configs
|
# Clean-up duplicate configs
|
||||||
|
@ -85,6 +85,11 @@ $OVPN_ADDITIONAL_CLIENT_CONFIG
|
|||||||
echo "auth $OVPN_AUTH"
|
echo "auth $OVPN_AUTH"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [ -n "$OVPN_OTP_AUTH" ]; then
|
||||||
|
echo "auth-user-pass"
|
||||||
|
echo "auth-nocache"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "$OVPN_COMP_LZO" ]; then
|
if [ -n "$OVPN_COMP_LZO" ]; then
|
||||||
echo "comp-lzo"
|
echo "comp-lzo"
|
||||||
fi
|
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