How to Use KongCustomEntity for JWT Signer Plugin With Kong Ingress Controller

In my previous post, I demonstrated how to persist private keys for the JWT Signer plugin when Kong is deployed in DB-less mode by manually writing the keys in Kong declarative config. But if you’re using the Kong Ingress Controller (KIC), which generates Kong configurations dynamically, what’s the best approach?

There are two options:

  1. Host the keys in a separate service within the same cluster, as shown in my previous post.
  2. Use the new KongCustomEntity CRD for seamless key management. This CRD was introduced a few months back and made GA in KIC 3.4 version.

In this post, I’ll show you how to generate keys and use the KongCustomEntity CRD to create the jwt_signer_jwks entity and integrate it with the JWT signer plugin.

Let’s get started.

Prerequisites:

  • A Kubernetes cluster (e.g., KinD)
  • CLIs
    • kubectl
    • helm
    • yq
    • envsubst
  • Python
  • (Optional) nix

Install Kong

Add Helm Repository

1
2
helm repo add kong https://charts.konghq.com
helm repo update

Deploy Kong

We will use the kong/ingress chart to deploy Kong in DB-less mode with the Kong Ingress Controller.

1
2
3
4
5
6
7
8
helm upgrade -i kong kong/ingress \
--namespace kong \
--set gateway.image.repository=kong/kong-gateway \
--set gateway.image.tag=3.8.1.0 \
--set gateway.env.router_flavor=expressions \
--set gateway.manager.enabled=false \
--set controller.ingressController.tag=3.4.1 \
--create-namespace

Apply the Kong License

You need an enterprise license to use the JWT signer plugin. You can apply the license using the KongLicense CRD. Assuming your license is stored in a license.json file, you can use the following command to set it to an environment variable:

1
export KONG_LICENSE_DATA=$(cat license.json)

Next, apply the license by running the following command:

1
2
3
4
5
6
7
8
envsubst << EOF | kubectl apply -f -
apiVersion: configuration.konghq.com/v1alpha1
kind: KongLicense
metadata:
name: kong-license
namespace: kong
rawLicenseString: '$KONG_LICENSE_DATA'
EOF

With Kong set up, let’s proceed to generate the JWKs.

Deploy demo app

For this demo, I will create a deployment with go-httpbin image and expose it with an Ingress.

1
2
kubectl create deployment httpbin --image=mccutchen/go-httpbin
kubectl expose deployment httpbin --name httpbin-svc --port 8080

We also need an ingress to route requests to the httpbin service.

1
2
kubectl create ingress httpbin-route --class kong --rule '/test*=httpbin-svc:8080'
kubectl annotate ingress httpbin-route konghq.com/strip-path=true

Once it is done, we should be able to

Generate keys

Here is a simple Python script to help us with the demo. Let’s store it to jwk.py and run it.

We’ll use Python to generate our RSA and ECC keys. Create a file named jwk.py with the following content:

You need to install the following Python libraries:

  • joserfc
  • pyyaml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
from joserfc.jwk import RSAKey, ECKey
import json
import os
import yaml

def quoted_presenter(dumper, data):
if data in ['y', 'n']:
return dumper.represent_scalar('tag:yaml.org,2002:str', data, style='"')
return dumper.represent_scalar('tag:yaml.org,2002:str', data)

yaml.add_representer(str, quoted_presenter)

def generate_rsa_key(key_size, algorithm):
rsa_key = RSAKey.generate_key(key_size, algorithm)
rsa_key.ensure_kid()
return rsa_key

def generate_ecc_key(curve, algorithm):
ecc_key = ECKey.generate_key(curve, algorithm)
ecc_key.ensure_kid()
return ecc_key

def export_public_key(key):
return key.as_dict(private=False)

def export_private_key(key):
return key.as_dict(private=True)

def save_private_keys_to_yaml(private_keys, directory):
jwk_private_keys = {"keys": private_keys}
yaml_private_key_path = os.path.join(directory, "private-jwk.yaml")

with open(yaml_private_key_path, "w") as f:
yaml.dump(jwk_private_keys, f, default_flow_style=False)

def save_private_key_to_pem(key, filename):
pem_data = key.as_pem(private=True)
with open(filename, "wb") as f:
f.write(pem_data)

def save_public_keys_to_json(public_keys, directory):
json_public_key_path = os.path.join(directory, "public-jwk.json")
with open(json_public_key_path, "w") as f:
json.dump({"keys": public_keys}, f, indent=2)

def main():
current_dir = os.path.dirname(os.path.abspath(__file__))

rsa_key = generate_rsa_key(2048, {"alg": "RS256"})
ecc_key = generate_ecc_key("P-256", {"alg": "ES256"})

rsa_public_key = export_public_key(rsa_key)
rsa_private_key = export_private_key(rsa_key)
ecc_public_key = export_public_key(ecc_key)
ecc_private_key = export_private_key(ecc_key)

public_keys = [rsa_public_key, ecc_public_key]
private_keys = [rsa_private_key, ecc_private_key]

save_private_keys_to_yaml(private_keys, current_dir)
save_private_key_to_pem(rsa_key, os.path.join(current_dir, "rsa-private.pem"))
save_private_key_to_pem(ecc_key, os.path.join(current_dir, "ecc-private.pem"))
save_public_keys_to_json(public_keys, current_dir)

if __name__ == "__main__":
main()

Running this script will generate several files:

  • public-jwk.json: Contains the public keys for token verification
  • private-jwk.yaml: Contains the private keys for signing
  • rsa-private.pem and ecc-private.pem: PEM format private keys
1
2
3
4
5
6
7
➜ tre
.
├── public-jwk.json
├── private-jwk.yaml
├── jwk.py
├── ecc-private.pem
└── rsa-private.pem

To view the algorithms and key IDs for your generated keys:

1
(echo "alg kid"; yq e '.keys[] | .alg + " " + .kid' private-jwk.yaml) | column -t

Example output:

1
2
3
alg    kid
RS256 SO8p08_VOqh4OGJDh53LZx8prLz83av37vqgJVMqPe8
ES256 _9NQ0RKVjdMaovuNlTtzcgQKFLd9SBLPNKUKWib_Ni8

(Optional) Dev with Nix

If you have nix installed, you can use my flake-template to prepare the python devevlopment environment. For mroe information, please check this post.

Configuring Kong

Now that we have the keys generated, let’s see how we can use it with JWT signer plugin.

Create a KongCustomEntity

Create a template template.yaml with below content.

1
2
3
4
5
6
7
8
9
10
11
apiVersion: configuration.konghq.com/v1alpha1
kind: KongCustomEntity
metadata:
namespace: default
name: jwt-signer-demo-keyset
spec:
type: jwt_signer_jwks
controllerName: kong
fields:
name: demo
keys: []

We will use yq to populate the keys field from private-jwk.yaml to the template and applies it to our cluster:

1
yq eval '.spec.fields.keys = (load("private-jwk.yaml") | .keys)' template.yaml | kubectl apply -f -

In the example above, we have created a KongCustomEntity object named jwt-signer-demo-keyset in the default namespace. The type is jwt_signer_jwks, and the keyset name is demo.

Create a KongPlugin

Now, let’s create a KongPlugin resource that will use the keyset demo we just created. In this case, I’m specifying ES256 as the signing algorithm.

1
2
3
4
5
6
7
8
9
10
11
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
name: jwt-signer-demo
namespace: default
plugin: jwt-signer
config:
access_token_jwks_uri: https://<keycloak>/auth/realms/demo/protocol/openid-connect/certs
access_token_keyset: demo
channel_token_optional: true
access_token_signing_algorithm: ES256

Apply plugin

Attach the plugin to the ingress we created earlier:

1
kubectl annotate ingress httpbin-route konghq.com/plugins=jwt-signer-demo

Verify the set up

Now that the plugin is applied, when we send a request to <kong_proxy>/test/anything, we should receive a 401 response. After making the request with a valid token, the upstream httpbin service should return a response that includes a token signed by the JWT Signer plugin.

We can verify the token header kid to confirm it was signed with the key we generated earlier.

For example:

1
2
3
4
5
6
curl -s http://proxy.li.k8s/test/anything \
--header "authorization:bearer $k_token" \
| jq -r '.headers.Authorization[0]' \
| awk '{print $2}' \
| jwt decode -j - \
| jq -r .header.kid

This should return _9NQ0RKVjdMaovuNlTtzcgQKFLd9SBLPNKUKWib_Ni8 which is the ECC kid we saw earlier.

Key rotation

To rotate the keys, simply regenerate private-jwk.yaml and apply the KongCustomEntity will do the trick.

That’s all I want to show you today, see you in the next one.