Blogg

Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn

Callista medarbetare Björn Beskow

Managing APIs using Kong Gateway Part 2: Securing an API using Mutual TLS and Access Control Lists

// Björn Beskow

In the last part, we implemented access control to a sample API using Kong plugins for OIDC and OAuth 2.0. In this post, we will instead use Mutual TLS, where an x509 client certificate is used to authenticate the client. We will combine that with an Access Control List, where information from the client certificate is used to grant access.

Blog Series Parts

Mutual TLS

For machine-to-machine communication, token-based authorization mechanisms such as OAuth 2.0 is not always the best solution. In IoT scenarios, where devices communicate with APIs, authentication and authorization based on x509 certificates using Mutual TLS is often used. In such a scenario, each client possess an individual x509 client certificate, issued by a Certificate Authority (CA) trusted by the API. Correspondingly, the API holds a server certificate issued by a CA trusted by the clients. When an https/SSL session is established, both the server and client certificates are validated.

Lock Photo by Nicholas Githiri on Pexels

Kong provides an kong-mtls-auth plugin for Mutual TLS, but it is only avaliable in the Enterprise edition. All the actual mTLS functionality is however provided by the underlying Nginx on which Kong is based. Hence it is quite easy to build a custom Kong plugin which exposes this mTLS capability. Several such plugins can be found in the LuaRocks Repository and on GitHub, but none of them fitted my needs. If additional access control decisions are needed based on properties from the client’s certificate, the relevant metadata must be made available to additional plugins or the underlying API. Custom http headers are normally used for such purposes.

Hence I wrote a simple mtls-auth plugin in Lua (which is available as Open Source under the Apache license). It enables the mTLS mechanism of the underlying Nginx server, and extracts the following information from the client’s certificate:

  • upstream_cert_header specifies an HTTP header in which, if provided, the client certificate in PEM format (urlencoded) will be made available to the upstream service
  • upstream_cert_fingerprint_header specifies a header in which the client certificate fingerprint will be made available
  • upstream_cert_serial_header specifies a header in which the client certificate serial number will be made available
  • upstream_cert_i_dn_header specifies a header in which the ssuer DN will be made available
  • upstream_cert_s_dn_header specifies a header in which the subject DN will be made available
  • upstream_cert_cn_header specifies a header in which the Common Name will be made available
  • upstream_cert_org_header specifies a header in which the Organization will be made available

Let’s see how a service route can be added which exposes our API secured with Mutual TLS!

Preliminary setup: Create a self-signed PKI

First off, we need a Public Key Infrastructure with a server certificate and a couple of client certificates issued by a common Certificate Authority. The steps to generate such certificates are detailed in another blog: Creating a Public Key Infrastructure for development. I have used those steps to create the required certificates: certs/CA/localCA.crt contains the root CA certificate, certs/server/ contains the server certificate (localhost.key and localhost.crt) while certs/clients/ contains 3 client certificates: client1 and client2 belonging to the organization Callista Enterprise AB, and other belonging to the organization Other Corp.

Configuring Kong to use https with mutual TLS

We then need to update kong.conf to configure Kong to use https. We configure additional listening ports for https/ssl:

# listening ports
proxy_listen = 0.0.0.0:8080, 0.0.0.0:8443 ssl
admin_listen = 0.0.0.0:8081, 0.0.0.0:8444 ssl

We configure the server certificate to use for the ssl handshake:

# ssl configuration
ssl_cert = /etc/kong/ssl/server/localhost.crt
ssl_cert_key = /etc/kong/ssl/server/localhost.key

We configure the underlying nginx server to use optional mutual TLS (that is, verify a client certificate if it is provided) as well as the CA certificate to use as trust anchor when verifying client certificates:

# mTLS configuration
nginx_proxy_ssl_client_certificate = /etc/kong/ssl/CA/localCA.crt
nginx_proxy_ssl_verify_client = optional

We also need to update the Kong gateway service in our docker-compose.yml file, to add the KONG_SSL=on environment variable, expose the https ports and map the certs folder as /etc/kong/ssl:

...

  kong:
    image: kong-with-plugins
    build:
      context: ./kong-with-plugins
    user: root
    volumes:
      - ./kong.yml:/etc/kong/kong.yml
      - ./kong.conf:/etc/kong/kong.conf
      - ./certs:/etc/kong/ssl
    environment:
      KONG_DATABASE: "off"
      KONG_DECLARATIVE_CONFIG: "/etc/kong/kong.yml"
      KONG_SSL: "on"
    ports:
      - "8080:8080"
      - "8081:8081"
      - "8443:8443"
      - "8444:8444"

Update the Docker image to include mtls-auth

We then need to update the Dockerfile to bundle the mtls-auth plugin:

...

COPY ./plugins/kong-plugin-mtls-auth /custom-plugins/kong-plugin-mtls-auth
WORKDIR /custom-plugins/kong-plugin-mtls-auth
RUN luarocks make

...

and update kong.conf to load it:

plugins = bundled, oidc, access-token-introspection, mtls-auth

Configuring Kong to expose the upstream api with mTLS protection

We finally add configuration in kong.yml to expose our sample API beneath the path /mtls/, with the mtls-auth plugin applied:

services:
...
- host:  upstream
  name: mtls
  port: 80
  protocol: http
  plugins: 
  - name: mtls-auth
    config:
      upstream_cert_s_dn_header: "X-Client-Cert-Dn"
      upstream_cert_cn_header: "X-Client-Cert-San"
      upstream_cert_org_header: "X-Client-Cert-Organization"
  routes:
  - name: mtls-route
    paths:
    - /mtls/
    strip_path: true

The plugin is configured to extract the client certificate’s full Distinguished Name as X-Client-Cert-Dn, the Common Name as X-Client-Cert-San and the Organization as X-Client-Cert-Organization headers.

Testing the mTLS flow

We are now ready to test the Mutual TLS flow. Rebuild the docker image, and start up the containers using docker compose in a terminal window:

> docker compose build
> docker compose up -d

Then call the api, providing a client certificate to the TLS handshake:

> curl --cacert certs/CA/localCA.crt --key certs/clients/client1.key --cert certs/clients/client1.crt \
    https://localhost:8443/mtls/api/ | jq .request.headers

In the echo’ed result, we can see the headers retrieved from the certificate:

{
  "host": "upstream",
  ...
  "x-client-cert-dn": "CN=client1,O=Callista Enterprise AB,ST=Sweden,C=SE",
  "x-client-cert-san": "client1",
  "x-client-cert-organization": "Callista Enterprise AB"
}

If we try to call the api without a valid client certificate, access is denied:

> curl -v --cacert certs/CA/localCA.crt https://localhost:8443/mtls/api/ | jq .
...
< HTTP/1.1 401 Unauthorized
...
{
  "error": "invalid_request",
  "error_description": "mTLS client not provided or invalid"
}

Finally, shut down the containers:

> docker compose down

Adding an Access Control List

It is a common requirement to be able to further restrict access to an API based on attributes of the caller. In the case of mTLS authentication, the meta data from the certficate such as serial number, Subject CN, Common Name or Organization may be such attributes. An Access Control List is an explicit list of allowed (or disallowed) clients, based on some client attribute(s).

Kong provides an acl plugin, but it is unfortunately rather coupled to other Kong plugins (such as the kong-mtls-auth Enterprise-only plugin) and not very well documented. It does not allow ACLs to operate on arbitrary headers, only indirectly using the Kong consumer concept. It thus doesn’t fit our purpose very well.

Hence I wrote a simple mtls-acl plugin (again in Lua), provided as Open Source under the Apache license. It allows the discriminating property for the Access Control List to be any artitrary header. Let’s add an access control list that specified which organizations (provided in the x-client-cert-organization header) that are allowed to call the API!

Update the Docker image to include mtls-acl

We need to update the Dockerfile to bundle the mtls-acl plugin:

...

COPY ./plugins/kong-plugin-mtls-acl /custom-plugins/kong-plugin-mtls-acl
WORKDIR /custom-plugins/kong-plugin-mtls-acl
RUN luarocks make

...

and update kong.conf to load it:

plugins = bundled, oidc, access-token-introspection, mtls-auth, mtls-acl

Configuring Kong to with the added ACL plugin

We then update the configuration in kong.yml for the path /mtls/ to also apply the mtls-acl plugin:

services:
...
- host:  upstream
  name: mtls
  port: 80
  protocol: http
  plugins: 
  - name: mtls-auth
    config:
      upstream_cert_s_dn_header: "X-Client-Cert-Dn"
      upstream_cert_cn_header: "X-Client-Cert-San"
      upstream_cert_org_header: "X-Client-Cert-Organization"
  - name: mtls-acl
    config:
      certificate_header_name: "X-Client-Cert-Organization" 
      allow:
      - "Callista Enterprise AB"
  routes:
  - name: mtls-route
    paths:
    - /mtls/
    strip_path: true

The plugin is configured to only allow clients belonging to the Callista Enterprise AB organization, taken from the X-Client-Cert-Organization header.

Testing the ACL flow

We are now ready to test the ACL flow. Rebuild the docker image, and start up the containers using docker compose in a terminal window:

> docker compose build
> docker compose up -d

When we call the api, client1 and client2 are allowed, since they both belong to the Callista Enterprise AB organization:

> curl --cacert certs/CA/localCA.crt --key certs/clients/client1.key --cert certs/clients/client1.crt \
    https://localhost:8443/mtls/api/ | jq .request.headers

{
  "host": "upstream",
  ...
  "x-client-cert-dn": "CN=client1,O=Callista Enterprise AB,ST=Sweden,C=SE",
  "x-client-cert-san": "client1",
  "x-client-cert-organization": "Callista Enterprise AB"
}

> curl --cacert certs/CA/localCA.crt --key certs/clients/client2.key --cert certs/clients/client2.crt \ 
    https://localhost:8443/mtls/api/ | jq .request.headers

{
  "host": "upstream",
  ...
  "x-client-cert-dn": "CN=client2,O=Callista Enterprise AB,ST=Sweden,C=SE",
  "x-client-cert-san": "client2",
  "x-client-cert-organization": "Callista Enterprise AB"
}

If we try to call the api with a client certificate from another organization, access is denied:

> curl -v --cacert certs/CA/localCA.crt --key certs/clients/other.key --cert certs/clients/other.crt \
    https://localhost:8443/mtls/api/ | jq .

...
< HTTP/1.1 403 Forbidden
...
{
  "message": "You cannot consume this service"
}

Finally, shut down the containers:

> docker compose down

Summing up

In this post, we have added autentication and authorization based on x509 client certificates to our sample API using Kong Gateway plugins. Just as in the previous example, the result is simple, elegant and well encapsuled.

A fully working, minimalistic example can be found in the Github repository.

What’s next?

In the next part, we’ll conclude the Access Control examples by combining authorization using Mutual TLS with OAuth 2.0 Client Credentials. Stay tuned!

References

The following Kong plugin has been a very useful inspiration when writing the mtls-auth plugin:

https://github.com/emersonqueiroz/kong-plugin-mtls-validate

Tack för att du läser Callistas blogg.
Hjälp oss att nå ut med information genom att dela nyheter och artiklar i ditt nätverk.

Kommentarer