Blogg
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
Här finns tekniska artiklar, presentationer och nyheter om arkitektur och systemutveckling. Håll dig uppdaterad, följ oss på LinkedIn
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.
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.
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 serviceupstream_cert_fingerprint_header
specifies a header in which the client certificate fingerprint will be made availableupstream_cert_serial_header
specifies a header in which the client certificate serial number will be made availableupstream_cert_i_dn_header
specifies a header in which the ssuer DN will be made availableupstream_cert_s_dn_header
specifies a header in which the subject DN will be made availableupstream_cert_cn_header
specifies a header in which the Common Name will be made availableupstream_cert_org_header
specifies a header in which the Organization will be made availableLet’s see how a service route can be added which exposes our API secured with Mutual TLS!
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
.
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"
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
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.
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
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!
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
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.
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
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.
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!
The following Kong plugin has been a very useful inspiration when writing the mtls-auth plugin:
https://github.com/emersonqueiroz/kong-plugin-mtls-validate