How to Use Persistent Private Keys for JWT Signer Plugin in Kong DBless Mode
JWT validation has become increasingly important as security standards have improved and become more strictly enforced in the industry. I have written several posts about validating JWT with Kong in the past. One of these posts was about how to use the JWT signer plugin. In that post, I highlighted the benefits of the JWT signer plugin, including:
- Validating public keys from multiple IDPs
- Ensuring requests go through Kong by resigning a new token to the backend
- Introspecting tokens when they are opaque
- Modifying token claims arbitrarily based on your needs
Despite these benefits, there is a challenge when using this plugin with Kong in DB-less mode. The private key used to sign new tokens is not persistent, meaning Kong generates a new key to sign the new token for each request. As a result, your backend cannot maintain a consistent set of public keys to validate tokens from Kong. This issue arises because all Kong configurations are stored in a single declarative config file when Kong is running in DB-less mode. It is also impossible to rotate the keys since admin api is read-only in this mode.
In today’s post, I will present solutions to address this issue. These solutions depend on the value you use for config.access_token_keyset
or config.channel_token_keyset
in the JWT signer plugin, which determines where you put your private key for Kong to use.
Let’s get started.
Generate keys
Firstly, we need to generate JWKs. Here is a simple Python script that creates three files in the folder. I will demonstrate how to use these files in the next section.
- private.json: Contains RS256 and ES256 private keys in JSON format
- private.yaml: Contains RS256 and ES256 private keys in YAML format
- public.json: Contains RS256 and ES256 public keys in JSON format
Python library required:
- joserfc
- pyyaml
1 | from joserfc.jwk import RSAKey, ECKey |
Keys on server
Let’s start with the easier solution first. According to the official documentation:
If you specify config.access_token_keyset
or config.channel_token_keyset
with either an http://
or https://
prefix, the plugin loads the keys just like it does for config.access_token_jwks_uri
and config.channel_token_jwks_uri
.
This means the JWT signer plugin can discover keys from an external URL and use them to sign new tokens.
For security reasons, you should restrict access to the private key endpoint and only allow Kong to access it. This can be easily achieved with a service mesh, using traffic permission and OPA. I will discuss this use case further in my upcoming post on End-to-End JWT Validation with Kong Gateway and Kong Mesh.
Since we already have the keys generated in the previous section, let’s use a server to host these keys. For hosting static files, I will use Caddy here. However, you can use any server you prefer—you could even start another Kong instance as the upstream server and use the request termination plugin to return the keys.
Prepare server
Let’s store our keys in the keys/
folder and add the following to Caddyfile
and compose.yaml
respectively.
1 | { |
In this setup, the Caddy server is running within the same Docker network, kong-ee-net
, as my Kong container and is accessible on port 8080.
1 | services: |
Once configured, the folder structure should look as follows, and you can start the server with docker compose up -d
.
1 | . |
Config plugin
To configure the plugin to read the private key, our config can be as simple as below.
Please note,
config.access_token_keyset_client_username
andconfig.access_token_keyset_client_password
were introduced in Kong Gateway version 3.7.0.0. If you are running a lower version, the JWT signer plugin will need to access the URL directly without any authentication.
1 | _format_version: "3.0" |
Now, when we send a valid token to Kong, the upstream server will receive a new token signed by Kong, which can be validated using the public key we generated.
Keys in config
What if you are not allowed to host the private keys on a different server? Can you include the private key in the Kong config file instead? According to the official documentation:
If the prefix is not http://
or https://
(such as “my-company” or “kong”), Kong autogenerates JWKS for supported algorithms.
This suggests we might be able to include this keyset in the Kong config and reference the keyset name in our plugin config.
The solution involves using a custom entity named jwt_signer_jwks
. Let’s experiment with the generated private.yaml
key.
Prepare config
We’ll prepare our configuration again, naming the keyset demo
and referencing it in the plugin config. Since the keyset is listed in the same config file, we no longer need config.access_token_keyset_client_username
and config.access_token_keyset_client_password
.
Next, stop the Caddy server and push below configuration to Kong via the <ADMIN_API>/config
endpoint.
1 | _format_version: "3.0" |
Now, when we send valid tokens to Kong, the upstream server will see that Kong is using our generated key to sign the new tokens.
That’s all I want to share with you today. See you next time!