Open source security for Kibana — Node.js reverse proxy with Kerberos

We use the Elastic stack extensively to collect, analyze and serve data — with Kibana being a major component of that stack thanks to its powerful visualization and data exploration tools.
However, Kibana comes without any security controls — these become available only if you purchase an Elastic subscription, with SSO capabilities being available only in the most expensive “Enterprise” subscription (which is licensed per node and could end up costing a fortune).

One of the most common “free” solutions for securing Kibana is putting it behind a reverse proxy, such as NGINX, and delegate security functions (authentication, authorization, SSL termination, etc.) to that proxy.
You can find multiple tutorials online on how to set up such a proxy, so this article will not cover it, but I will point out some of the drawbacks, which are what led us to look for other, better solutions:

  1. While NGINX is a very powerful and popular reverse proxy, it does not come with strong security capabilities out of the box. In fact, by default it only supports basic authentication with a password file, which is a poor solution for an enterprise environment. If you want to authenticate users against LDAP, for example, you have to rely on other modules such as this one (simple to use, but project is abandoned) or this one (official, but depends on external python daemons that only work with Python 2 (??)).
  2. Customizing NGINX is difficult — it is written in C and requires good low level understanding of how NGINX (and indeed, HTTP) works to be able to extend it efficiently. Acquiring such skills in a Java/JavaScript oriented development team is a very expensive endeavor if done solely to be able to customize a reverse proxy solution.
  3. Basic authentication is not good enough — in an enterprise environment where SSO is available, users expect to log in once, and not be forced to send their credentials over and over again when accessing resources.
    While there exists a module that adds Kerberos support to NGINX, it is of very limited use since it only provides authentication, not authorization (it can authorize against an explicitly specified list of users, not groups or any other attribute).

Long story short — NGINX is great if you want to get basic security, but if you need a more intricate flow, with SSO and group-based authorization, it is not enough. And that was what we needed.

Enter Express

In order to implement a smart reverse proxy, that would meet our demands, we decided to leverage Express, the leading web framework for Node.js applications. A few npm modules and the Express middleware model is all that is required to create a proxy that would not only perform SSO authentication and authorization but would also allow us to easily add custom routing and business logic.

The minimum required modules are:

  • express
  • express-http-proxy
  • express-kerberos
  • activedirectory

We will use a few additional modules for convenience, but the core functionality is provided by these four modules.

Kerberos Pre-Requisites

While the actual Kerberos setup is out of scope for this article, below are the basic settings you will need to have in place. This article will not cover them in depth.

  • A Service Principal Name (SPN) registered in Active Directory, on behalf of a service account, that matches the URL by which the proxy is accessible.
    For example, if the proxy is being access by URL http://kibana-proxy.domain.com, and the service account is “DOMAIN\service_acct”, you would need to register the SPN “HTTP/kibana-proxy.domain.com” under that account in Active Directory.
  • A keytab file with an entry for the same SPN, generated using the credentials of the service account. See additional details below.

The most basic usage of the proxy looks pretty much like this:

const express = require(‘express’)
const kerberos = require(‘express-kerberos’)
const proxy = require(‘express-http-proxy’)
const app = express()
app.use(kerberos.default())
app.use('/', proxy('localhost:5601'));
app.listen(80)

This tiny piece of code is all you need to perform Kerberos authentication and proxy all requests to localhost:5601 (default Kibana port), assuming your Kerberos setup is correct.

However, since we also want to support group-based authorization, dynamic routing and sessions we should probably add some logic to our proxy — you will find example code in this GitHub repository.

So what’s going on there? Let’s take a look.

First, we have added a couple of files to our project — proxyConfig.js, which contains some configurations for our reverse proxy, and auth.js — which contains the authorization logic.

The app.js file looks almost identical to the above example, with a couple of changes:

  1. We’ve added several middlewares — sessions and a custom “auth” function that will authorize our users. We will store the authorization information in the session to avoid repeating the process for every user interaction.
  2. The proxy middleware now dynamically looks up the upstream URL based on the request hostname, and also injects an Authorization header.
    express-http-proxy is a very robust library, and you can read more about its configuration on NPM.

All of the authorization logic is encapsulated as a single function inside auth.js — the Kerberos middleware provides us with the username, which we can use to lookup group membership in Active Directory. If the user is a member of any of the allowed groups — he is allowed through. Otherwise, he is rejected with a 403 Unauthorized error.

You can see that we’ve made our proxy a bit smarter — based on the URL that was used to access it, we determine which backend service is being requested, and proxy the request accordingly. 
Since this example is based on a Kibana use-case, we “enrich” the proxy with information about the Active Directory groups that are allowed to access each instance of Kibana (assuming we have several), and even inject an Authorization header in case there is another proxy along the way that requires Basic authorization.

Some caveats:

  • The kerberos middleware looks for a keytab file. By default, that file should be in /etc/krb5.keytab, but you may override that location by setting the KRB5_KTNAME environment variable
  • The “express-kerberos” package only supports versions ≤0.0.24 of the “kerberos”. This dependency should be manually specified in your package.json file.
  • The provided GitHub repository should be used as a reference only — it cannot be executed as-is since it depends on multiple settings which your application must provide (Kerberos, LDAP, etc.). It also lacks fundamentals such as persistent session storage (never use the in-memory store!), logging, error handling, etc.

Summary

We’ve demonstrated in this article how we can easily create a highly customizable reverse proxy with Node.js that can replace NGINX as a free, open source security solution for Kibana, including easy integration with enterprise SSO.
Building on these fundamentals, you can create an extremely configurable and robust reverse proxy solution, for almost any kind of application and use-case — once you’ve gotten the authentication part out of the way, you can leverage Express and the Node.js ecosystem to implement any kind of authorization logic that you want.

It goes without saying that NGINX is an extremely powerful reverse proxy, and we rely on it heavily, but for some use cases it is easier to use other solutions, that provide greater flexibility — instead, or in combination with, NGINX.