Kong API Security in Kubernetes with Api Key and JWT

Kong API Access Control using JWT and ACL

Kong API Security in Kubernetes with Api Key and JWT

Previously we set up Kong and Konga in Kubernetes. We then deployed the vadal-echo service to K8s. Now we look at securing access to this service.

Key Auth

One mechanism afforded to us by Kong is the Key-Auth plugin.

In the following we add the plugin (key-auth), a consumer and a secret key "secretkey" for that consumer.

echo "
apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: http-auth
plugin: key-auth

---
apiVersion: configuration.konghq.com/v1
kind: KongConsumer
metadata:
  name: vadal
username: vadal

---
apiVersion: configuration.konghq.com/v1
kind: KongCredential
metadata:
  name: vadalcred
consumerRef: vadal
type: key-auth
config:
  key: secretkey
" | kubectl apply -f -

Run the above.

Then we add the key-auth plugin to the ingres for vadal-echo, referenced by the metaname http-auth.

Add your host IP to /etc/host for a dummy host eg.

192.168.0.111 vadal.cluster.local

As in the following:

echo "
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: vadal-cluster
  annotations:
    konghq.com/strip-path: "true"
    konghq.com/plugins: http-auth
spec:
  rules:
    - host: vadal.cluster.local
      http:
        paths:
          - path: /echo
            backend:
              serviceName: vadal-echo
              servicePort: 80
" | kubectl apply -f -

Try to access the service

curl -i http://vadal.cluster.local/echo
HTTP/1.1 401 Unauthorized
Date: Mon, 06 Jul 2020 20:51:27 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
WWW-Authenticate: Key realm="kong"
Content-Length: 41
X-Kong-Response-Latency: 1
Server: kong/2.0.4
{"message":"No API key found in request"}

So the access is now blocked. Add the key to the request.

curl -i http://vadal.cluster.local/echo?apikey=secretkey

Returns the expected response. Nice.

JWT Auth

Another method is using JSON web tokens (JWT), one that can later make use of a common identity provider. For this we use the site https://jwt.io.

Create the JWT (RS256)

https://jwt.io/#debugger-io?token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InZhZGFsIiwiaXNzIjoidmFkYWwtaXNzdWVyIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.DJ0n_25h2kJN1oZZ8ObgXhv-yMZDfL6EtOMLVVKMC160egQpuPV943MxaliWEkfZ3yyrf5kOdkcbk2_daIyaKS_gBzX7AKSiCd44OGMhP7PeoovkYncND25W7mKyRBmTCRG_5VQAwtKPS2jpF0t3IqOVfG5klB9V_oAO-aIF8TOSlxE7WW_xgYoDnW0JSzh4V-VW9uFbmVnx86SeaBlVjH_cPcEDanwlUbIz1zY4_vyP4adSrEfH-chRxW6ewpjfIXTMfVj2sdtx3uabufgvVmFQ4QSQH6NKPcUWSUK58KmYJ0g-7G5BfyKCA1Lj7qLrmAHCCm-oC8OO7qdLrqcJTg&publicKey=-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv vkTtwlvBsaJq7S5wA%2BkzeVOVpVWwkWdVha4s38XM%2Fpa%2Fyr47av7%2Bz3VTmvDRyAHc aT92whREFpLv9cj5lTeJSibyr%2FMrm%2FYtjCZVWgaOYIhwrXwKLqPr%2F11inWsAkfIy tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0 e%2Blf4s4OxQawWD79J9%2F5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb V6L11BWkpzGXSW4Hv43qa%2BGSYOD2QU68Mb59oSk2OB%2BBtOLpJofmbGEGgvmwyCI9 MwIDAQAB -----END PUBLIC KEY-----

Helpfully, jwt.io will also create the key pairs for us. We do not need the private key unless we use it in Konga to create the JWT authorisation from there.

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
MwIDAQAB
-----END PUBLIC KEY-----


Create a file jwt.yml with the contents below and apply it to k8s.

The first section applies the JWT plugin. The second section creates a secret in K8s, which includes the public key above. In the third section we have the Consumer that references the secret. The fourth ingress section sets the JWT restriction app-jwt on the service.

apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: app-jwt
plugin: jwt

---
apiVersion: v1
kind: Secret
metadata:
  name: vadal-jwt-k8s
type: Opaque
stringData:
  kongCredType: jwt
  key: vadal-issuer
  algorithm: RS256
  rsa_public_key: |-
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnzyis1ZjfNB0bBgKFMSv
    vkTtwlvBsaJq7S5wA+kzeVOVpVWwkWdVha4s38XM/pa/yr47av7+z3VTmvDRyAHc
    aT92whREFpLv9cj5lTeJSibyr/Mrm/YtjCZVWgaOYIhwrXwKLqPr/11inWsAkfIy
    tvHWTxZYEcXLgAXFuUuaS3uF9gEiNQwzGTU1v0FqkqTBr4B8nW3HCN47XUu0t8Y0
    e+lf4s4OxQawWD79J9/5d3Ry0vbV3Am1FtGJiJvOwRsIfVChDpYStTcHTCMqtvWb
    V6L11BWkpzGXSW4Hv43qa+GSYOD2QU68Mb59oSk2OB+BtOLpJofmbGEGgvmwyCI9
    MwIDAQAB
    -----END PUBLIC KEY-----

---
apiVersion: configuration.konghq.com/v1
kind: KongConsumer
metadata:
  name: vadal-jwt-k8s
username: vadal-jwt-k8s
credentials:
  - vadal-jwt-k8s

---
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: vadal-cluster
  annotations:
    konghq.com/strip-path: "true"
    konghq.com/plugins: app-jwt
spec:
  rules:
    - host: vadal.cluster.local
      http:
        paths:
          - path: /echo
            backend:
              serviceName: vadal-echo
              servicePort: 80


kubectl apply -f jwt.yml

Now when we call it we are not authorised (we have removed http-auth from the earlier api-key section, the only plugin is now app-jwt).

curl -i http://vadal.cluster.local/echo
HTTP/1.1 401 Unauthorized
Date: Mon, 06 Jul 2020 21:33:58 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Content-Length: 26
X-Kong-Response-Latency: 0
Server: kong/2.0.4
{"message":"Unauthorized"}

Export the JWT token to a variable.

export VADAL_JWT=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InZhZGFsIiwiaXNzIjoidmFkYWwtaXNzdWVyIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.DJ0n_25h2kJN1oZZ8ObgXhv-yMZDfL6EtOMLVVKMC160egQpuPV943MxaliWEkfZ3yyrf5kOdkcbk2_daIyaKS_gBzX7AKSiCd44OGMhP7PeoovkYncND25W7mKyRBmTCRG_5VQAwtKPS2jpF0t3IqOVfG5klB9V_oAO-aIF8TOSlxE7WW_xgYoDnW0JSzh4V-VW9uFbmVnx86SeaBlVjH_cPcEDanwlUbIz1zY4_vyP4adSrEfH-chRxW6ewpjfIXTMfVj2sdtx3uabufgvVmFQ4QSQH6NKPcUWSUK58KmYJ0g-7G5BfyKCA1Lj7qLrmAHCCm-oC8OO7qdLrqcJTg

Call the service and pass it to the Authorization header.

curl -i -H "Authorization: Bearer ${VADAL_JWT}" http://vadal.cluster.local/echo

The result also shows the consumer and credential headers.


{   "timestamp":"2020-07-07T00:36:08.874",
  "headers":{      
     "host":"vadal.cluster.local",
     "connection":"keep-alive",
     "kong-request-id":"17b1274c-0e9a-4c9e-bd21-42895b9d5702#26",
     "x-forwarded-proto":"http",
     "x-forwarded-host":"vadal.cluster.local",
     "x-forwarded-port":"8000",
     "x-real-ip":"192.168.65.3",
     "user-agent":"curl/7.64.1",
     "accept":"*/*",
     "authorization":"Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6InZhZGFsIiwiaXNzIjoidmFkYWwtaXNzdWVyIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.DJ0n_25h2kJN1oZZ8ObgXhv-yMZDfL6EtOMLVVKMC160egQpuPV943MxaliWEkfZ3yyrf5kOdkcbk2_daIyaKS_gBzX7AKSiCd44OGMhP7PeoovkYncND25W7mKyRBmTCRG_5VQAwtKPS2jpF0t3IqOVfG5klB9V_oAO-aIF8TOSlxE7WW_xgYoDnW0JSzh4V-VW9uFbmVnx86SeaBlVjH_cPcEDanwlUbIz1zY4_vyP4adSrEfH-chRxW6ewpjfIXTMfVj2sdtx3uabufgvVmFQ4QSQH6NKPcUWSUK58KmYJ0g-7G5BfyKCA1Lj7qLrmAHCCm-oC8OO7qdLrqcJTg",
     "x-consumer-id":"74e2e3b6-36f9-44cf-aa7d-f82767c37672",
     "x-consumer-username":"vadal-jwt-k8s",
     "x-credential-identifier":"vadal-issuer"

  }
}

You can view the JWT setting in Konga. Notice the details have been obtained from K8s secret stored in vadal-jwt-k8s, where you can easily manage the secret and change the key (vadal-issuer) for example, within K8s. That's cool.

You can also create JWT tokens from the GUI, although the details will not be in k8s as they are stored in Kong.

The Kong API also shows the JWTs,

http://localhost:31492/jwts

Or the consumers:

http://localhost:31492/consumers

JWT via the Kong API and HS256

A  JWT symmetric key algorithm. Create it like so:

curl -X POST http://localhost:31492/consumers/vadal-jwt-k8s/jwt

{"rsa_public_key":null,"created_at":1594169412,"consumer":{"id":"74e2e3b6-36f9-44cf-aa7d-f82767c37672"},"id":"367a5e08-a904-4ab1-a1da-7915ec9bf7a8","tags":null,"key":"Y1rKZX1oZx746pf8u88NBw4RNKs9ZGjg","secret":"EpGbTPaLkW4G3pKpNqYaY5MeP1eD0kkq","algorithm":"HS256"}

Take the snippet below and apply it at  http://jwt.io

{
"typ": "JWT",
"alg": "HS256"
}
{
"iss": "Y1rKZX1oZx746pf8u88NBw4RNKs9ZGjg"
}

Add the secret in the verify block (EpGbTPaLkW4G3pKpNqYaY5MeP1eD0kkq)

You'll get a token like so:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJZMXJLWlgxb1p4NzQ2cGY4dTg4TkJ3NFJOS3M5WkdqZyIsImlhdCI6bnVsbCwiZXhwIjpudWxsLCJhdWQiOiIiLCJzdWIiOiIifQ.COQJfjBfXieh0OxIh5Kg4_NqDEqULrhBQyIcQTMETyU

Check it:

curl -i http://vadal.cluster.local/echo?jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJZMXJLWlgxb1p4NzQ2cGY4dTg4TkJ3NFJOS3M5WkdqZyIsImlhdCI6bnVsbCwiZXhwIjpudWxsLCJhdWQiOiIiLCJzdWIiOiIifQ.COQJfjBfXieh0OxIh5Kg4_NqDEqULrhBQyIcQTMETyU

Should get access on the HS256 token as well.

HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Date: Wed, 08 Jul 2020 15:33:05 GMT
X-Kong-Upstream-Latency: 9
X-Kong-Proxy-Latency: 1
Via: kong/2.0.4
{"timestamp":"2020-07-08T15:33:05.483","joined":["host: localhost","connection: keep-alive","kong-request-id: 55d28927-75c5-44a7-b656-3e218054ac04#2","x-forwarded-proto: http","x-forwarded-host: localhost","x-forwarded-port: 8000","x-real-ip: 192.168.65.3","user-agent: curl/7.64.1","accept: /"],"headers":{"host":"localhost","connection":"keep-alive","kong-request-id":"55d28927-75c5-44a7-b656-3e218054ac04#2","x-forwarded-proto":"http","x-forwarded-host":"localhost","x-forwarded-port":"8000","x-real-ip":"192.168.65.3","user-agent":"curl/7.64.1","accept":"/"}}

Using ACLs

Access Control Lists in Kong are governed by groups within consumers. A consumer will have credentials with some authorisation method. An ACL plugin can then be attached to a route or a service, and a group defined in either it's whitelist or blacklist.

The plugins enable the feature. The consumer credentials define the authorisation details to be used.

In this way the ACL allows/disallows the consumers access to a route or service. Only the correct consumers credentials can access the defined resource.

Let's create another deployment from the same image but restrict access to it via an ACL.

kubectl create deploy vadal-echo-admin --image=vadal-echo:0.0.1-SNAPSHOT
kubectl expose deploy vadal-echo-admin --port=80 --target-port=8080

Define a new consumer and add the plugin and ingress, using the following content.


apiVersion: configuration.konghq.com/v1
kind: KongPlugin
metadata:
  name: vadal-acl
plugin: acl
config:
  whitelist: ['vadal-admin']

---
apiVersion: v1
kind: Secret
metadata:
  name: vadal-acl-secret
type: Opaque
stringData:
  kongCredType: acl
  group: vadal-admin

---
apiVersion: configuration.konghq.com/v1
kind: KongConsumer
metadata:
  name: vadal-admin
username: vadal-admin
credentials:
  - vadal-acl-secret

---
kind: Ingress
apiVersion: extensions/v1beta1
metadata:
  name: vadal-cluster
  annotations:
    kubernetes.io/ingress.class: "kong"
    konghq.com/strip-path: "true"
    konghq.com/plugins: app-jwt, vadal-acl
spec:
  rules:
    - host: vadal.cluster.local
      http:
        paths:
          - path: /echo-admin
            backend:
              serviceName: vadal-echo-admin
              servicePort: 80
          - path: /echo
            backend:
              serviceName: vadal-echo
              servicePort: 80

The above sets an ACL (vadal-acl) against the route path /echo-admin. This acl has a group vadal-admin whitelisted. The consumer vadal-admin consists of the group vadal-admin.

Call the service using the previously defined JWT.

curl -i -H "Authorization: Bearer $VADAL_JWT" vadal.cluster.local/echo-admin

HTTP/1.1 403 Forbidden
Date: Wed, 08 Jul 2020 00:00:15 GMT
Content-Type: application/json; charset=utf-8
Connection: keep-alive
Content-Length: 45
X-Kong-Response-Latency: 1
Server: kong/2.0.4
{"message":"You cannot consume this service"}

The ACL disallows access. Nice.

Now we define a JWT token against this new consumer, and only this consumers credentials ought to be accepted.

curl -X POST http://localhost:31492/consumers/vadal-admin/jwt

{"rsa_public_key":null,"created_at":1594221350,"consumer":{"id":"45b8094b-5a30-4285-9c84-90b106f4131b"},"id":"0bffd6f3-cedb-475e-9958-9ea005699d17","tags":null,"key":"po3Of3TmRIpDv2txV3GTSpwk0F6NYvA0","secret":"S6MPaRLXihetsns5MlMHu599B3C58I1f","algorithm":"HS256"}

You need both the key (iss) and secret to create your token.

https://jwt.io/#debugger-io?token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwbzNPZjNUbVJJcER2MnR4VjNHVFNwd2swRjZOWXZBMCJ9.1HrgMwzpGRlzy0mqcv1WN-enfiJcDNnISOM7LhQSEGg

We now have a legitimate token for this consumer/group.

export VADAL_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwbzNPZjNUbVJJcER2MnR4VjNHVFNwd2swRjZOWXZBMCJ9.1HrgMwzpGRlzy0mqcv1WN-enfiJcDNnISOM7LhQSEGg
curl -i -H "Authorization: Bearer $VADAL_JWT" vadal.cluster.local/echo-admin
HTTP/1.1 200
Content-Type: application/json
Transfer-Encoding: chunked
Connection: keep-alive
Date: Wed, 08 Jul 2020 15:24:33 GMT
X-Kong-Upstream-Latency: 279
X-Kong-Proxy-Latency: 3
Via: kong/2.0.4
{"timestamp":"2020-07-08T15:24:33.707","joined":["host: vadal.cluster.local","connection: keep-alive","x-consumer-groups: vadal-admin","x-forwarded-proto: http","x-forwarded-host: vadal.cluster.local","x-forwarded-port: 8000","x-real-ip: 192.168.65.3","user-agent: curl/7.64.1","accept: /","authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwbzNPZjNUbVJJcER2MnR4VjNHVFNwd2swRjZOWXZBMCJ9.1HrgMwzpGRlzy0mqcv1WN-enfiJcDNnISOM7LhQSEGg","x-consumer-id: 45b8094b-5a30-4285-9c84-90b106f4131b","x-consumer-username: vadal-admin","x-credential-identifier: po3Of3TmRIpDv2txV3GTSpwk0F6NYvA0"],"headers":{"host":"vadal.cluster.local","connection":"keep-alive","x-consumer-groups":"vadal-admin","x-forwarded-proto":"http","x-forwarded-host":"vadal.cluster.local","x-forwarded-port":"8000","x-real-ip":"192.168.65.3","user-agent":"curl/7.64.1","accept":"/","authorization":"Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwbzNPZjNUbVJJcER2MnR4VjNHVFNwd2swRjZOWXZBMCJ9.1HrgMwzpGRlzy0mqcv1WN-enfiJcDNnISOM7LhQSEGg","x-consumer-id":"45b8094b-5a30-4285-9c84-90b106f4131b","x-consumer-username":"vadal-admin","x-credential-identifier":"po3Of3TmRIpDv2txV3GTSpwk0F6NYvA0"}}

Cooking with gas. Note the bearer, consumer username and credential details. A service can further decrypt this to get any stateful information, but does not need to worry about verifying this as it's all taken care of by Kong. Nice.

Conclusion

We have added authorised access to our service, using api keys and JWTs. The same service can have different access endpoints controlled by ACLs. The authorisation concerns have been nicely separated out from the service itself, leaving it to concentrate on providing the business value. Kong nicely sits in front to protect and manage access. All within a resilient scaleable K8s architecture.

Neat.

Related Article