OpenVPN and Keycloak : Link your VPN Infrastructure with your SSO


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.

| See more

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.



OpenVPN

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/


A Keycloak realm

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 :

  • A confidential client with password grant enabled
  • Some users (imported, from your LDAP ??)

Client

Now we create a client "VPN" with only :

  • access type : confidential
  • standard flow : off
  • direct access grant : on
The "credentials" tab lets us retrieve a client_secret.

Users

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.


TIP : how password grant work

Password Grant is the simpliest oauth2 authentication flow.

3 parameters needed :

  • Client identification (for public client) or authentication (for confidential) with client_id and client_secret
  • a grant_type at "password" value
  • user credentials (username and password)

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'


PAM Oauth2 module

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

Fill parameters for PAM Oauth2

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: {
   },
}

Activate module

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 !

Autocreate unknown accounts

https://www.howtoforge.com/community/threads/force-pam-to-create-user-home-folder-if-it-already-not-exists.54868/

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.


OpenVPN usage

With PAM as primary authentication in OpenVPN, now any login will be delegated to our oauth2 PAM module we installed.

Register user

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.

Client

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 :-)


Do more...

Login with Google : way to retrieve a token

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.

Web app

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/

Token verification

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.

PAM module

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.

Connect

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 !


Sources

Let's Get In Touch!


Any question ? Want more information ? Follow us on twitter or you can reach out to us via email.