Nginx As Reverse Proxy In Front Of Keycloak
By: Martina Ivaničová
Tags: Front End Development, Nginx, Infrastructure, Authentication, Proxy

1. Nginx serving static content

Let’s start by creating nginx.conf configuration file, which defines how the HTTP server behaves.

In the server section, we define that incoming traffic coming to port 80 and any path should be routed to the data/www folder.

The try_files directive as it is written instructs the Nginx to search for the $uri in the root and use index.html as a fallback.

Now create locally a folder named www and put some valid index.html file in it, in the next step we will map this file to the server’s data/www folder.

We have the Nginx configuration ready, but no Nginx server yet. Luckily to spin up one is a matter of seconds. Create the following docker-compose.yml file:

Test: Run it in the command line in the root directory by typing docker-compose up. Navigate to http://localhost/ to see your index.html.

2. Adding Keycloak

The easiest way to spin up Keycloak is to add JBoss Keycloak image to our docker-compose.yml file

Locally, in the root directory, create a file named .env and write the Keycloak admin password in it, like this KEYCLOAK_PASSWORD=mysecret!

Keycloak service is by default available on the path /auth. Thus we need to instruct Nginx that traffic coming to the endpoint /auth should be redirected to the Keycloak server and all the other traffic should be served with the static content. To achieve this, nginx.conf needs to be extended by the additional location section as follows:

Note, that if the client requests the endpoint with the path /auth Nginx passes the request to the internal network endpoint mykeycloak:8080/auth. This internal network endpoint should match the name of the service we defined earlier in the docker-compose file. Port 8080 is the default port exposed by the JBoss Keycloak image.

Last, but not least by setting proxy_set_header we pass to Keycloak information about the original host and client IP. This enables Keycloak to generate correct in-app navigation links. If we haven’t done this when a user clicks on the login button, the Keycloak page would send him to incorrect https://mykeycloak:8080/auth/admin/master/console, instead of the correct https://example.com/auth/admin/master/console . We also want to pass the client IP to Keycloak to support proper access logging.

Test: Run docker-compose up and browse to http://localhost/auth. You see the Keycloak landing page and when navigating to Administration console you can log in with credentials you provided earlier in the .env file. When navigating to any other path, you see the index.html

3. Fine-tuning Keycloak

Connecting to database

So far our Keycloak instance uses the in-memory H2 database to store settings like realms, roles, or clients. To make these settings persistent, Keycloak needs to connect to a real database. JBoss Keycloak docker image comes with support for a couple of databases like Postgress, MariaDB, MySQL, Oracle. Let’s assume we have a MySQL database available at my-sql-host endpoint (you can extend the docker-compose with MySQL container or connect to the existing DB). First, when you are connected to a database instance with an admin account create a database and the user used by Keycloak.

Then we are ready to modify the docker-compose.yml file by appending the following environment variables to the mykeycloak service.

Don’t forget to edit the .env file by adding a new line DB_KEYCLOAK_PASSWORD=mytopsecret

Test: Run docker-compose up. Log in to the Keycloak admin console, create a new realm, roles, or clients. Tear down everything by docker-compose down and then start again by docker-compose up. Log in to the Admin console and see your realm, roles, or clients are still there.

Allocating memory

Once we have a database in place we might want to play around with other settings. For instance, even if the Keycloak specification says that the minimum memory requirement is 512MB, with low memory container tends to crash. Memory and other configuration can be passed by the JAVA_OPTS params environment variable in the docker-compose.yml file

-JAVA_OPTS=-Xms1024m -Xmx2048m

Importing realms

It may come in handy to export Keycloak settings and then import them when deploying to somewhere else.

In Keycloak Administration Console we can export the realm and store it as a local file (realm-export.json). We can mount it as a volume and then navigate the Keycloak container to load this configuration.

4. Keycloak with Managed Databases

I tested the above setup with a couple of managed databases.

For Amazon Aurora Serverless v1 with MySQL engine, I had to specify TLSProtocol by setting to Keycloak an environment variable JDBC_PARAMS=enabledTLSProtocols=TLSv1.2

I failed to test this set up with Managed MySQL database on Digital Ocean because Keycloak during initializing attempts to create a table without a primary key Unable to create or change a table without a primary key CREATE TABLE keycloak.DATABASECHANGELOG . My first thought was to run: SET GLOBAL sql_require_primary_key=OFF , however, Digital Ocean does not allow changing this setting on their managed database.

. My first thought was to run: , however, Digital Ocean does not allow changing this setting on their managed database. AWS RDS MySQL worked just fine

5. Configuring TLS protocol

For just playing around it’s perfectly fine to rely on HTTP, but for the production setup, secured communication is a must.

We will now make the communication from the Internet to Nginx over HTTPS, while the internal communication behind the Nginx, for example when our backend service will talk to Keycloak will be over HTTP. This corresponds to Keycloak’s default settings for “sslRequired”: “external”.

Note: Keycloak’s documentation suggests having in production the sslRequired set to “all”. I’m sure many stacks running in production do not have TLS behind the load balancer or reverse proxy, thus relying on the network separation. Consider security aspects carefully based on your project specifics.

First, we obtain a valid TLS certificate for our domain. We can get one for free from the Let’s encrypt or just for purpose of this tutorial to generate a self-signed certificate (example here).

Let’s assume we have now fullchain.pem and privkey.pem stored in the local directory /etc/letsencrypt.

Let’s mount them as volumes in the nginx_rproxy section of docker-compose.yml

- /etc/letsencrypt/:/etc/letsencrypt/

Make final adjustments to nginx.conf file:

modify the server to specify the path to the certificate and private key

modify the server to listen only on port 443 to serve HTTPS traffic

add a server that listens on port 80 and redirects all the HTTP traffic to the HTTPS endpoint

6. Security fine tunning

The infrastructure is ready, we developed and deployed a fancy web application, everybody is happy except for Mozilla Observatory (or your preferred security scanning tool). Luckily we can fix some of the most common issues by adding to nginx.conf security-related response headers. See the example below for inspiration:

Summary

In this blog, we configured the Nginx server to serve static content and pass authorization requests to Keycloak. Similarly, we can pass requests to any other internal servers e.g. /api to myservice:5000 as shown in the complete nginx.conf :

We defined running containers and services by the docker-compose.yml file (usable for development purposes or small projects). For making this example complete, let’s assume, that the myservice is a container running the application that exposes port 5000 and which is built from the Dockerfile in your root directory:

The docker-compose.yml file assumes that you have .env file with secrets ready:

Thanks for reading!