This repository documents how to allow RADIUS-based clients to authenticate against a Microsoft Entra ID (formerly Azure AD) tenant with the help of FreeRADIUS.
The configuration described here also adds support for account lockout based on repeatedly failed login attempts and the secure caching of authentication credentials (password hashes).
-
FreeRADIUS as a RADIUS server (with the FreeRADIUS Docker image used as base);
-
freeradius-oauth2-perl
module for authenticating users against a Microsoft Entra ID tenant; -
PostgreSQL database for automatically locking out users after a series of failed attempts;
-
Redis for caching credentials (in the form of salted, secure hashes);
-
NGINX to serve as a reverse proxy and load balancer;
-
Follow the official getting started guide to install FreeRADIUS on your server.
You can check that the basic setup works by authenticating as an user with a hardcoded password, as described in the Initial tests section of the guide. See the
clients.conf
and theauthorize
files for an example config.The
test-connection.sh
script can also be used for this purpose; it is a thin wrapper around theradtest
command. Example uses:- Try to authenticate using the username
user
and the passwordpass
(default credentials for the hardcoded user defined inauthorize
):
./test-connection.sh
- Try to authenticate using the given username and password:
./test-connection.sh username password
- Try to authenticate using the username
-
Follow the instructions from the
freeradius-oauth2-perl
repo to allow FreeRADIUS to authenticate requests against Microsoft Entra ID.At this point, you should be able to test that authentication works with the help of the
radtest
command or thetest-connection.sh
script:./test-connection.sh user@tenant.onmicrosoft.com <password>
-
Install PostgreSQL and Redis on your server. Both services should be configured to start automatically (since FreeRADIUS will depend on them).
It is imperative to set up a firewall in order to block external connections to PostgreSQL or Redis.
-
You might also need to install some additional dependencies:
-
If you're using the official FreeRADIUS Docker image, it already comes bundled with all the possible modules.
-
On Ubuntu Server, you can install the packages required for PostgreSQL and Redis support using:
sudo apt install freeradius-postgresql freeradius-redis
-
-
Create a PostgreSQL user and database for FreeRADIUS.
With Docker, this can be done automatically by configuring the
POSTGRES_USER
,POSTGRES_PASSWORD
andPOSTGRES_DB
environment variables (seecompose.yaml
for an example).Otherwise, you can do it easily using the command line tool
psql
. Start an interactive PostgreSQL session by usingsudo -u postgres psql
and then run
CREATE USER freeradius WITH PASSWORD '<some secure password>'; CREATE DATABASE freeradius; GRANT ALL PRIVILEGES ON DATABASE freeradius TO freeradius;
to create a new user called
freeradius
, with a secure password choosen by you, with complete access to the newly createdfreeradius
database. -
Initialize the required tables in the newly-created database.
If using Docker, you can define a database initialization script to be run when the container is first created. See the
create-database.sh
script and the corresponding lines incompose.yaml
.Otherwise, connect to the database by running
psql --host=localhost --dbname=freeradius --username=freeradius
and inputting the password you've defined above, then create the
failed_logins
table by running:CREATE TABLE failed_logins ( id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY, username text NOT NULL CHECK (username <> ''), time timestamptz NOT NULL ); CREATE INDEX failed_logins_username_index ON failed_logins (username);
-
Enable the SQL module for FreeRADIUS, by linking the corresponding file from the
mods-available
directory to themods-enabled
directory (e.g. something likeln -s /etc/freeradius/mods-available/sql /etc/freeradius/mods-enabled/
). -
Configure the SQL module (you can use the configuration from this repo as a guiding example).
You'll want to set
dialect = "postgresql"
,driver = "rlm_sql_${dialect}"
(the exact name of the driver depends on which distribution of FreeRADIUS you're using), as well as theserver
,port
,login
,password
andradius_db
attributes (use the values you've defined when setting up the PostgreSQL database).Since we're only interested in using PostgreSQL to track failed login attempts, we can disable other integrations by setting
read_groups = no
,read_profiles = no
andread_clients = no
. Also remember to comment out / remove any references to thesql
module in your default site's config file (see the config in this repo for an example). -
Set up the lockout policy. This is mostly based on the official guide for adding account lockout to FreeRADIUS.
Copy the
lockout
policy file to your/etc/freeradius/policy.d
directory and update the default site's config:authorize { lockout_check ... } post-auth { Post-Auth-Type REJECT { lockout_incr } ... }
The default lockout policy is configured to block all login attempts (even ones with correct credentials) after 5 failed attempts in the last 10 minutes. This is done to discourage brute force / password guessing attacks.
-
Verify that the newly configured lockout policy works as expected. Try to connect once with valid credentials:
./test-connection.sh user@tenant.onmicrosoft.com <password>
Then at least 5 times using the wrong password:
for i in {1..5} do ./test-connection.sh user@tenant.onmicrosoft.com bad-password done
At this point, even using the correct password should fail:
./test-connection.sh user@tenant.onmicrosoft.com <password>
You will have to wait 10 minutes or clear the database to be able to succesfully log in again.
-
It's a good idea to secure your Redis instance with a password (which is not done by default). See this SO answer for instructions, or adapt the
redis.conf
file from this repo. -
Configure the
freeradius-oauth-perl
module to use Redis as a cache, instead of the in-memory RB tree implementation.This can be done by updating the
module
file (usually located at/opt/freeradius-oauth2-perl/module
if you've followed the official installation instructions). Replace it with the variant of the file from this repo, or make the changes yourself:cache oauth2_cache { ... # Use Redis instead of `rlm_cache_rbtree` driver = "rlm_cache_redis" redis { server = 'redis' # Use `localhost` if your Redis instance is on the same machine port = 6379 query_timeout = 5 pool = redis } ... }
There is currently a bug with the way FreeRADIUS serializes dates stored in external caches. Until it gets fixed, you will also have to update the
dictionary
file:# ATTRIBUTE OAuth2-Password-Last-Modified 3000 date # This line has been commented out... ATTRIBUTE OAuth2-Password-Last-Modified 3000 string # ...with this line taking its place.
Depending on how you've set up the
freeradius-oauth-perl
module, you might also have to update the corresponding line in the/etc/freeradius/dictionary
file. -
Verify that the new caching config works correctly. Restart your FreeRADIUS instance, then try to connect twice using valid credentials:
./test-connection.sh user@tenant.onmicrosoft.com <password> ./test-connection.sh user@tenant.onmicrosoft.com <password>
The second time around, authentication should be nearly instant. You should find some similar log messages (if you're running FreeRADIUS in debug mode):
rlm_redis (redis): Reserved connection (0) (3) oauth2_cache: Found entry for "user@tenant.onmicrosoft.com" (3) oauth2_cache: Merging cache entry into request ... (3) pap: Login attempt with password (3) pap: Comparing with "known-good" SSHA2-512-Password (3) pap: User authenticated successfully
It is woth mentioning that Redis has support for data persistency and it will (by default) regularly dump its contents to disk. More information can be found in the Redis documentation.
-
One way of ensuring redundancy is to spin up multiple FreeRADIUS instances (with identical configurations) and place them behind a load balancer.
The
compose.yaml
file exemplifies the creation of a cluster of two FreeRADIUS instances, both sharing the same database and password cache. Either one can be the target of a RADIUS authentication request.We use NGINX with its UDP Load Balancing feature to ensure the RADIUS requests get distributed to the back end servers in a round-robin fashion. See the
nginx.conf
file for the required settings.
See the contributing instructions for information on how to set up your local development environment to work on this repo.
The code and configuration files in this repo are licensed under the GNU Affero General Public License, see the license file for details.