Migrated FreeRADIUS with Google Authenticator to a Docker container


The number of virtual machines in my two-node ESXi cluster is growing and my 32GB RAM Intel NUC ESXi build will be out of memory soon if I don’t make changes. That said, I had to find a way to cut down my RAM usage to squeeze more out of this build. I don’t want to spend a couple of thousand dollars on another build. The ESXi build that I am looking at has Supermicro X10SDV-TLN4F-O motherboard with Intel Xeon D-1541 and 128GB ECC RDIMM RAM, an expensive ESXi build. Yes, I could buy a used server on eBay for less, but I don’t want those servers because they are too bulky and loud.

Update: FreeRADIUS 3.0 with Two-Factor Authentication using Google Authenticator

Enter Docker

What is Docker? Docker is an open-source software platform that allows users to package software into containers, allowing them to be portable among different operating systems (Windows, Linux, and macOS).

Here’s a ten-minute video that further explains what Docker is. If you want to watch an hour long video, here’s one from Docker, Inc.

Currently, I have three VMs running Ubuntu server edition for FreeRADIUS and tac_plus (TACACS+ daemon). On top of that, I was planning to set up another VM for Pi-hole (running it as a Docker container now). Sure, I could easily install it on one of the existing VMs that I have, but I want separation. With Docker, I could easily have all the separation I want but with fewer system resources and efficient use of it.

My base VM for Ubuntu server has the following assigned resources: 256MB RAM, 1 x vCPU, and 8GB of disk space. Depending on what I want to do with the new VM, I could change the resources assigned to it. If I assign it with too little of RAM, then the VM will use the disk for additional memory. Swapping is not ideal, so I usually add more RAM, which means there will be some free RAM just waiting to get used.

With Docker, I could assign 1GB of RAM to my Ubuntu VM with Docker installed and not worry about the efficient use of it. I know that eventually, the system will use the resources as I continue to add more containers. If the VM begins to swap, I could easily add more RAM.

Another advantage of Docker is the speed of spinning up new containers. With VMs, I need to clone the base image, create the VMX file, turn it on, etc. These processes would take several minutes. With Docker, I could write a one-line Dockerfile to create the Docker image and start the container. Starting up the container takes less than one second compared to the minutes spent on processes that I have to do when creating VMs.

Docker Installation

If you’re a returning visitor, you probably already know that I use Ubuntu. With that said, the Docker image will use Ubuntu as the OS. The installation could be a one-liner, but I wanted to install the newest version. Docker has the how-to guide, so just follow that if you want. Though, I will still list all of the things I did since I skipped a step or so.

$ sudo apt-get update
$ sudo apt-get install apt-transport-https ca-certificates curl software-properties-common -y
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
$ sudo apt-get update
$ sudo apt-get install docker-ce -y
$ docker --version
Docker version 17.06.0-ce, build 02c1d87

Docker Compose Installation

In this next section, we will install Docker Compose. While this is optional, I like the Docker compose because it makes it easier for me to run multiple containers in one command.

$ sudo -i
# curl -L https://github.com/docker/compose/releases/download/1.14.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
# chmod +x /usr/local/bin/docker-compose
# exit
$ docker-compose --version
docker-compose version 1.14.0, build c7bdf9e

FreeRADIUS Docker Image

There are plenty of FreeRADIUS Docker images on Docker Hub, but I wanted to learn how to create one on my own. Having said that, I read several websites, including Docker’s documentation page, to get an idea on how to create my own image. It took me several tries to get my FreeRADIUS Docker image working, since I am, after all, a Docker newbie.

Writing Dockerfile

When I was ready to write my Docker file, one question that I had was where do I put it. For testing, I decided to create a directory in my home directory and put any files related to this Docker image. I then switched to the new directory and created my Dockerfile there. In the future, I will still create a new directory just for the sake of separation. Though, it doesn’t really matter where you put the Dockerfile.

$ mkdir radius
$ cd radius
$ vi Dockerfile

Once I have VIM running, I started writing my Dockerfile. At first, I only started with few lines since I haven’t used the Ubuntu Docker image. I wanted to make sure that I could run all the commands I need to put in the Dockerfile. Once everything worked in the container, I started to write the rest. Here’s the complete Dockerfile that I wrote.

# Use Base Ubuntu image
FROM ubuntu:16.04

# Author of this Dockerfile
MAINTAINER NetworkJutsu <networkjutsu.com>

# Update & upgrades
RUN apt-get update && apt-get dist-upgrade -y

# Install FreeRADIUS and Google Authenticator
RUN apt-get install freeradius libpam-google-authenticator -y

# Add user to container with home directory
RUN useradd -m -d /home/networkjutsu -s /bin/bash networkjutsu

# Add password to networkjutsu account.
# Obviously, you wouldn't want to do this in production.
# Go to the container and add the password there then commit the changes to the container.
RUN echo "networkjutsu:letsmakemypasswordgreatagain" | chpasswd

# Edit /etc/pam.d/radiusd file
RUN sed -i 's/@include/#@include/g' /etc/pam.d/radiusd
RUN echo "auth requisite pam_google_authenticator.so forward_pass secret=/etc/freeradius/networkjutsu/.google_authenticator user=freerad" >> /etc/pam.d/radiusd
RUN echo "auth required pam_unix.so use_first_pass" >> /etc/pam.d/radiusd

# Edit /etc/freeradius/users file
RUN sed -i '1s/^/# Instruct FreeRADIUS to use PAM to authenticate users\n/' /etc/freeradius/users
RUN sed -i '2s/^/DEFAULT Auth-Type := PAM\n/' /etc/freeradius/users

# Copy existing /etc/freeradius/sites-enabled/default file to container
COPY default /etc/freeradius/sites-enabled/default

# Copy existing /etc/freeradius/clients.conf file to container
COPY clients.conf /etc/freeradius/clients.conf

# Copy existing .google_authenticator file to container
COPY .google_authenticator /home/networkjutsu

# Create a folder in /etc/freeradius equal to the user name
RUN mkdir /etc/freeradius/networkjutsu

# Copy .google_authenticator file to /etc/freeradius/networkjutsu
RUN cp /home/networkjutsu/.google_authenticator /etc/freeradius/networkjutsu

# Change owner to freerad
RUN chown freerad:freerad /etc/freeradius/networkjutsu && chown freerad:freerad /etc/freeradius/networkjutsu/.google_authenticator

# Expose the port
EXPOSE 1812/udp 1813/udp 18120/udp

# Run FreeRADIUS
CMD freeradius -f

The lines where I instructed Docker engine to copy existing FreeRADIUS files, those files are based on the configuration covered in my past blog posts. If you’re curious about the config files, please check this post and this one. I could’ve copied everything from my existing RADIUS server, but I wanted to show other ways of writing the Dockerfile and how to edit the config files.

Building Docker Image

Once done with the Dockerfile, the next step is to build the Docker image. Creating the Docker image is pretty straightforward. We just need to issue the build command, and it will create the Docker image based on the Dockerfile that we wrote. The -t flag allows us to tag the Docker image with a friendly name.

$ sudo docker build -t radius1 .

If we don’t tag the image with a friendly name, it would look like this.

$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS                        PORTS               NAMES
bb3ad23725a6        9ea220058853        "/bin/sh -c 'apt-g..."   32 minutes ago      Exited (100) 32 minutes ago                       adoring_poitras

If there are errors in the Dockerfile, the image will not get built. The Docker engine is good about telling the user where it failed. Here’s an example where I took some lines out of the Dockerfile.

[email protected]:~$ sudo docker build -t radius1 .
Sending build context to Docker daemon  27.65kB
Step 1/10 : RUN apt-get install freeradius libpam-google-authenticator -y
Please provide a source image with `from` prior to run

[email protected]:~$ sudo docker build -t radius .
Sending build context to Docker daemon  27.65kB
Step 1/12 : FROM ubuntu:16.04
<-- Output omitted for brevity -->
Step 2/12 : MAINTAINER Network Jutsu <networkjutsu.com>
<-- Output omitted for brevity -->
Step 3/12 : RUN apt-get install freeradius libpam-google-authenticator -y
<-- Output omitted for brevity -->
E: Unable to locate package freeradius
E: Unable to locate package libpam-google-authenticator
The command '/bin/sh -c apt-get install freeradius libpam-google-authenticator -y' returned a non-zero code: 100
[email protected]:~$

If there are no errors, it should look something like below. The image created here is from a modified Dockerfile where I excluded the COPY commands because I do not have those files in this VM.

[email protected]:~$ sudo docker build -t radius1 .
Sending build context to Docker daemon  27.65kB
Step 1/13 : FROM ubuntu:16.04
<-- Output omitted for brevity -->
Step 2/13 : MAINTAINER Network Jutsu <networkjutsu.com>
<-- Output omitted for brevity -->
Step 3/13 : RUN apt-get update && apt-get dist-upgrade -y
<-- Output omitted for brevity -->
Step 4/13 : RUN apt-get install freeradius libpam-google-authenticator -y
<-- Output omitted for brevity -->
Step 5/13 : RUN useradd -m -d /home/networkjutsu -s /bin/bash networkjutsu
<-- Output omitted for brevity -->
Step 6/13 : RUN echo "networkjutsu:letsmakemypasswordgreatagain" | chpasswd
<-- Output omitted for brevity -->Removing intermediate container 356f78c72b6c
Step 7/13 : RUN sed -i 's/@include/#@include/g' /etc/pam.d/radiusd
<-- Output omitted for brevity -->
Step 8/13 : RUN echo "auth requisite pam_google_authenticator.so forward_pass secret=/etc/freeradius/networkjutsu/.google_authenticator user=freerad" >> /etc/pam.d/radiusd
<-- Output omitted for brevity -->Removing intermediate container a1baec16ec31
Step 9/13 : RUN echo "auth required pam_unix.so use_first_pass" >> /etc/pam.d/radiusd
<-- Output omitted for brevity -->
Step 10/13 : RUN sed -i '1s/^/# Instruct FreeRADIUS to use PAM to authenticate users\n/' /etc/freeradius/users
<-- Output omitted for brevity -->
Step 11/13 : RUN sed -i '2s/^/DEFAULT Auth-Type := PAM\n/' /etc/freeradius/users
<-- Output omitted for brevity -->
Step 12/13 : EXPOSE 1812/udp 1813/udp 18120/udp
<-- Output omitted for brevity -->
Step 13/13 : CMD freeradius -f
<-- Output omitted for brevity -->
Successfully built c227ded15dcd
Successfully tagged radius:latest
[email protected]:~$

Verification

To see the Docker images in our system, issue the command below. Notice that there is an Ubuntu image. This is the result of the FROM ubuntu:16.04 line from our Dockerfile.

$ sudo docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
radius1             latest              c227ded15dcd        27 minutes ago      246MB
ubuntu              16.04               d355ed3537e9        3 weeks ago         119MB

Docker Compose

We’re now ready to run the Docker image. We could issue the docker run command, however, I like running the Docker image using Docker Compose. In this section, we’re going to write what we need for our docker-compose.yml file to run the Docker image. The YML file doesn’t need to be in the radius directory. In my case, I just put my file in my home directory. Remember though, when you run the docker-compose command, it will look for the YML file. Be sure to run the command in the directory where you stored the YML file.

$ vi docker-compose.yml

version: "3"
services:
  radius1:
    container_name: radius1
    image: radius1
    ports:
    - "192.168.0.100:1812:1812/udp"
    - "192.168.0.100:1813:1813/udp"
    - "192.168.0.100:18120:18120/udp"
    environment:
    - VIRTUAL_HOST=radius1.networkjutsu.lan
    restart: always
    volumes:
    - /etc/timezone:/etc/timezone:ro
    - /etc/localtime:/etc/localtime:ro
  radius2:
    container_name: radius2
    image: radius2
    ports:
    - "192.168.0.101:1812:1812/udp"
    - "192.168.0.101:1813:1813/udp"
    - "192.168.0.101:18120:18120/udp"
    environment:
    - VIRTUAL_HOST=radius2.networkjutsu.lan
    restart: always
    volumes:
    - /etc/timezone:/etc/timezone:ro
    - /etc/localtime:/etc/localtime:ro

Ports

For this section, I included an IP address. By default, the container will use the host’s IP address. If one wants to use the default IP address, then the line would look like “1812:1812/udp”. In this case, I wanted to use a different IP address than the host. This will allow me to create another Docker container using the same ports. Since the host doesn’t have this particular IP address assigned to it, we need to set up the host machine to have another IP address (IP aliasing). Editing the network interface config means that we want the changes to be persistent even after a reboot. I think I don’t need the other lines in the alias section since it will use the main interface configs. However, I still included it in the config just in case.

$ sudo vi /etc/network/interfaces

# Host machine
auto ens160
iface ens160 inet static
address 192.168.0.200
netmask 255.255.255.0
network 192.168.0.200
broadcast 192.168.0.255
gateway 192.168.0.1
dns-nameserver 192.168.200.53
dns-search networkjutsu.lan

# First IP alias
# IP address for radius1
auto ens160:0
iface ens160:0 inet static
address 192.168.0.100
netmask 255.255.255.0
network 192.168.0.0
broadcast 192.168.0.255
gateway 192.168.0.1
dns-nameserver 192.168.200.53
dns-search networkjutsu.lan

# Second IP alias
# IP address for raduius2
auto ens160:1
iface ens160:1 inet static
address 192.168.0.101
netmask 255.255.255.0
network 192.168.0.0
broadcast 192.168.0.255
gateway 192.168.0.1
dns-nameserver 192.168.200.53
dns-search networkjutsu.lan

$ sudo service networking restart

Volumes

As I was verifying everything in my FreeRADIUS Docker container, I noticed that the time was incorrect even after setting the environment with the right time zone. During my search, I came across a thread that talks about how to set the time zone correctly. There are several ways of doing it, but I settled on this way. With this config, the container syncs with the host machine’s time. Having said that, the host needs to sync with NTP servers. Having correct time is important because my Google Authenticator is TOTP-based.

Starting the container

Once done with the YML file, we’re now ready to run our image with docker-compose command. The -d flag instructs Docker engine to run the container(s) as a daemon.

$ sudo docker-compose up -d
$ sudo docker ps
CONTAINER ID        IMAGE               COMMAND                  CREATED             STATUS              PORTS                                                        NAMES
2edac8f51819        radius              "/bin/sh -c 'freer..."   39 seconds ago      Up 38 seconds       192.168.0.100:1812-1813->1812-1813/udp, 192.168.0.100:18120->18120/udp   radius

Stopping the container

If for whatever reason, you want to stop the container, you can issue the docker stop command.

$ sudo docker stop 2edac8f51819
2edac8f51819

Final Words

My FreeRADIUS Docker image is by no means perfect. I am still a Docker newbie, so I am pretty sure if a Docker expert looks at my Dockerfile there will be some comments. But, I’ve tested this already with my PA-200, and it worked perfectly.

With Docker, it will allow me to turn off multiple VMs running on my ESXi host. On top of that, I will now be able to spin up new services quickly without going through the processes that I use to create a new VM with Ubuntu as the OS.

You might also like to read

FreeRADIUS 3.0 with Two-Factor Authentication (2FA)
Adding Two-Factor Authentication to FreeRADIUS
Securing SSH with Google Authenticator
Adding Two-Factor Authentication to TACACS+

Disclosure

NetworkJutsu.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to Amazon.com.


About Andrew Roderos

As an IT professional, I have a strong passion for technology and a desire to learn more about it. Technologies that I am mostly interested in are computer networking, network security, virtualization, and programming. Outside of the information technology world, I enjoy traveling, reading science fiction books and manga, watching movies, and photography.