Because of the COVID-19 pandemic, many companies are trying to set up remote work. A VPN is a secure solution,
creating a kind of "network extension".
Let's try to see how to set up an authentication using oauth2 (Keycloak and Google) as identity provider on an OpenVPN server.
TL;DR
OpenVPN allows usage of PAM modules. By using an oauth2 client PAM module and password grant, we can
use our own SSO (Keycloak) to authenticate users.
For Oauth2 providers which do not allow Password Grant, we will use a "token authentication" by providing a
valid token instead of a password. Code and demo with Google as authentication provider.
https://openvpn.net/ is a flexible VPN solution, for professionnal or personnal use. It has great client software for all operating systems, that makes it easy to deploy. The server part is well documented and supported.
Check out their pricing matching your use case. Note that we have no trade agreement with this company.
For this test, we use a virtual appliance. We encourage you to use this solution before deploying on your infrastructure. After downloaded an HyperV appliance, we create a virual machine on Windows 10. This machine has ip address 172.17.166.138.
https://openvpn.net/download-open-vpn/
First, connect to your machine with user "root" and default password "openvpnas". Follow the console wizard.
Change the openvpn password :
$ passwd openvpn
Open your browser, open url https://172.17.166.138:943/admin, a great admin web ui for openvpn.
The interesting part for this post is in "authentication". We can activate some others authentication methods, such as RADIUS, LDAP or PAM. We will use PAM with a module we already wrote about here : https://www.mathieupassenaud.fr/ssh-oauth2/
In our case, we got a Keycloak Realm on our platform. Check it out on https://realms.please-open.it... We have a free tier, useful for testing purposes.
This realm has a name, it's only a "display name". The ID is forced on our platform for security reasons. This ID is called "realm_name" or "realm_id" in some cases.
We need :
Now we create a client "VPN" with only :
In users section, create a new user. We call him "user1" :
After creation, we can set a password for this user. Go to "credentials" tab, and set a password. Do not set it as "temporary" :
There are no roles yet, we do not use them for this demo.
Password Grant is the simpliest oauth2 authentication flow.
3 parameters needed :
A simple POST request to /token
endpoint will give you an access token :
curl --location --request POST 'https://app.please-open.it/auth/realms/122aa842-0cf0-48e6-a5bc-cca00254a9bb/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=VPN' \
--data-urlencode 'client_secret=a5fa7094-67ba-4fd9-8dea-0d6c249680d1' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'username=user1' \
--data-urlencode 'password=user1'
We will use a PAM oauth2 exec module written in golang. This module is available here : https://github.com/shimt/pam-exec-oauth2
install golang :
cd /opt
wget https://dl.google.com/go/go1.14.1.linux-amd64.tar.gz
tar xvf go1.14.1.linux-amd64.tar.gz
PATH=$PATH:/opt/go/bin
export PATH
GOROOT=/opt/go
export GOROOT
GOPATH=/root
export GOPATH
Then, get the project and compile it (follow the official documentation) :
apt-get install git
cd /root
go get github.com/shimt/pam-exec-oauth2
PREFIX=/opt/pam-exec-oauth2
mkdir $PREFIX
cp go/bin/pam-exec-oauth2 $PREFIX/pam-exec-oauth2
touch $PREFIX/pam-exec-oauth2.yaml
chmod 755 $PREFIX/pam-exec-oauth2
chmod 600 $PREFIX/pam-exec-oauth2.yaml
In the file /opt/pam-exec-oauth2/pam-exec-oauth2.yaml
we have to write :
{
client-id: OUR CLIENT,
client-secret: SECRET IN "CREDENTIALS" TAB,
scopes: ["profile"],
endpoint-token-url: TOKEN ENDPOINT,
extra-parameters: {
},
}
To find the /token
URI, go to your Keycloak Realm console, and click on "OpenID Endpoint
Configuration". This URI comes from OpenId Connect
specs and exposes all URLs in a JSON document. Find "token" url, and paste it in "endpoint-token-url" field
in configuration file.
By using this template, we can fill with all parameters like this :
{
client-id: "VPN",
client-secret: "a5fa7094-67ba-4fd9-8dea-0d6c249680d1",
scopes: ["profile"],
endpoint-token-url: "https://app.please-open.it/auth/realms/122aa842-0cf0-48e6-a5bc-cca00254a9bb/protocol/openid-connect/token",
extra-parameters: {
},
}
As said in the documentation, we add in file /etc/pam.d/common-auth
:
auth sufficient pam_exec.so expose_authtok /opt/pam-exec-oauth2/pam-exec-oauth2
This module has a limitation, you can not log in with a user that does not exists on the system. There is no truth about that, it depends on your needs. Next part will explain how to create a user automatically.
In our case, we added the user : useradd user1
Then try to log in with a console (IE : SSH) with the Keycloak account. It works !
In the previous part, we saw that we need to create each account by hand on the system. We can make it smoother. By using libpam-script, we can add a custom script on login.
apt-get install libpam-script
We add it in /etc/pam.d/common-auth
this new module :
auth optional pam_script.so
Then, the script itself. In the file /usr/share/libpam-script/pam_script_auth
:
#!/bin/bash
adduser $PAM_USER --disabled-password --quiet --gecos ""
In PAM modules, username is given in "$PAM_USER" variable.
Make this script executable :
chmod +x /usr/share/libpam-script/pam_script_auth
And it is done.
With PAM as primary authentication in OpenVPN, now any login will be delegated to our oauth2 PAM module we installed.
In "USER MANAGEMENT", go to "User Permissions" and add our "user1" with the settings you need.
That's it, no password is needed on the VPN and also on the host. Keycloak will be used to authenticate.
We downloaded the client for our Windows 10 desktop machine : https://openvpn.net/client-connect-vpn-for-windows/
Just add the IP of the server, then user infos :
VPN is working fine :-)
With Google, we can not do password grant. Reminder : never type your Google password in a form not hosted on Google servers.
So we need a workaround.
authorization code flow is designed for web applications.
With a simple web application, we can retrieve a token, an access_token to be truth. This access_token is used to authenticate a user session on a backend. Access_token is sent using a "authorization" header.
For this use case, we will use a single web app just for authentication. Then, we will give the token to the VPN server in order to authenticate user.
We made a very simple web app for authentication, for a previous project : https://www.mathieupassenaud.fr/please-open-it/
It's easy to integrate login with Google, by just following https://developers.google.com/identity/sign-in/web/sign-in
If you want a geeky way, we encourage using https://developers.google.com/oauthplayground/
Now with a token, we can call "token" endpoint. For Google this endpoint is
https://www.googleapis.com/oauth2/v3/tokeninfo
Anwser looks like this :
{
"azp": "947227895516-68tp60nti613r42u41bch5vesr5iqpbi.apps.googleusercontent.com",
"aud": "947227895516-68tp60nti613r42u41bch5vesr5iqpbi.apps.googleusercontent.com",
"sub": "107227635759936----",
"scope": "openid https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email",
"exp": "1585681976",
"expires_in": "3479",
"email": "mathieu.passenaud@----.com",
"email_verified": "true",
"access_type": "online"
}
Instead of giving a password, we will ask user to give a valid token. After token verification, we log user.
A PAM module needs to retrieve an email (login) and a token (password). With pam_exec.so, we can have our own script on login.
Register it in /etc/pam.d/common-auth
as :
auth sufficient pam_exec.so expose_authtok /root/auth.sh
In a bash script, we can get username on $PAM_USER variable and the password (thanks to expose_authtok parameter) from standard input.
We have 2 dependancies, curl for requests and jq for json :
apt-get install curl jq
Our script looks like :
#!/bin/bash
TOKEN=`cat -`
res=$(curl -s -w "%{http_code}" 'https://www.googleapis.com/oauth2/v3/tokeninfo' --header "Authorization: Bearer $TOKEN")
body=${res::-4}
status=$(echo $res | tail -c 4)
if [ "$status" -ne "200" ]; then
echo "Error: HTTP repsonse is $status"
exit 1
fi
# get user from json
user=$(echo $body | jq -r .email)
if [ $user != $PAM_USER ]; then
echo "error on username"
exit 1
fi
exit 0
It verifies if the username is the same as the given session. If not, the exit code 1 will block the login process.
First, create the user on the server (or activate auto create using a script)
useradd ...........@gmail.com
Create an account on openvpn :
Then connect using your email. Paste a token from https://developers.google.com/oauthplayground/ instead of your password.
Works great !
Any question ? Want more information ? Follow us on twitter or you can reach out to us via email.