Skip to content

Commit 8e60efa

Browse files
committed
Add jwks-with-sni example
1 parent 769389d commit 8e60efa

File tree

8 files changed

+439
-0
lines changed

8 files changed

+439
-0
lines changed
Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# JWKS
2+
3+
In this example we deploy a web application, configure load balancing with a VirtualServer, and apply a JWT policy.
4+
Instead of using a local secret to verify the client request such as in the
5+
[jwt](https://github.com/nginx/kubernetes-ingress/tree/main/examples/custom-resources/jwt) example, we will define an
6+
external Identity Provider (IdP) using the `JwksURI` field.
7+
8+
We will be using a deployment of [KeyCloak](https://www.keycloak.org/) to work as our IdP in this example. In this
9+
example, KeyCloak is deployed as a single container for the purpose of exposing it with an Ingress Controller.
10+
11+
## Prerequisites
12+
13+
1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/)
14+
instructions to deploy the Ingress Controller.
15+
16+
2. Save the public IP address of the Ingress Controller into `/etc/hosts` of your machine:
17+
18+
```text
19+
...
20+
21+
XXX.YYY.ZZZ.III webapp.example.com
22+
XXX.YYY.ZZZ.III keycloak.example.com
23+
```
24+
25+
Here `webapp.example.com` is the domain for the web application and `keycloak.example.com` is the domain for
26+
Keycloak.
27+
28+
## Step 1 - Deploy a TLS Secret
29+
30+
Create a secret with the TLS certificate and key that will be used for TLS termination of the Keycloak application:
31+
32+
```console
33+
kubectl apply -f tls-secret.yaml
34+
```
35+
36+
## Step 2 - Deploy a Web Application
37+
38+
Create the application deployment and service:
39+
40+
```console
41+
kubectl apply -f webapp.yaml
42+
```
43+
44+
## Step 3 - Deploy Keycloak
45+
46+
1. Create the Keycloak deployment and service:
47+
48+
```console
49+
kubectl apply -f keycloak.yaml
50+
```
51+
52+
1. Create a VirtualServer resource for Keycloak:
53+
54+
```console
55+
kubectl apply -f virtual-server-idp.yaml
56+
```
57+
58+
## Step 4 - Configure Keycloak
59+
60+
To set up Keycloak:
61+
62+
1. To connect to Keycloak, use `https://keycloak.example.com`.
63+
64+
2. Create a new Realm. We will use `jwks-example` for this example. This can be done by selecting the dropdown menu on
65+
the left and selecting `Create Realm`
66+
67+
3. Create a new Client called `jwks-client`. This can be done by selecting the `Client`s tab on the left and then
68+
selecting `Create client`.
69+
- When creating the Client, ensure both `Client authentication` and `Authorization` are enabled.
70+
71+
4. Once the client is created, navigate to the `Credentials` tab for that client and copy the client secret.
72+
- This can be saved in the `SECRET` shell variable for later:
73+
74+
```console
75+
export SECRET=<client secret>
76+
```
77+
78+
5. Create a new User called `jwks-user` by selecting the Users tab on the left and then selecting Create client.
79+
80+
6. Once the user is created, navigate to the `Credentials` tab for that user and select `Set password`. For this example
81+
the password can be whatever you want.
82+
- This can be saved in the `PASSWORD` shell variable for later:
83+
84+
```console
85+
export PASSWORD=<user password>
86+
```
87+
88+
## Step 5 - Deploy the JWT Policy
89+
90+
Create a policy with the name `jwt-policy` and configure the `JwksURI` field so that it only permits requests to our
91+
web application that contain a valid JWT. In the example policy below, replace `<your_realm>` with the realm created in
92+
Step 4. We used `jwks-example` as our realm name. The value of `spec.jwt.token` is set to `$http_token` in this example
93+
as we are sending the client token in an HTTP header.
94+
95+
```yaml
96+
apiVersion: k8s.nginx.org/v1
97+
kind: Policy
98+
metadata:
99+
name: jwt-policy
100+
spec:
101+
jwt:
102+
realm: MyProductAPI
103+
token: $http_token
104+
jwksURI: http://keycloak.default.svc.cluster.local:8080/realms/<your_realm>/protocol/openid-connect/certs
105+
keyCache: 1h
106+
```
107+
108+
Deploy the policy:
109+
110+
```console
111+
kubectl apply -f jwks.yaml
112+
```
113+
114+
## Step 6 - Deploy a config map with a resolver
115+
116+
If the value of `jwksURI` uses a hostname, the Ingress Controller will need to reference a resolver. This can be done by
117+
deploying a ConfigMap with the `resolver-addresses` data field
118+
119+
```yaml
120+
kind: ConfigMap
121+
apiVersion: v1
122+
metadata:
123+
name: nginx-config
124+
namespace: nginx-ingress
125+
data:
126+
resolver-addresses: <resolver-address>
127+
```
128+
129+
In this example, we create a ConfigMap using Kubernetes' default DNS `kube-dns.kube-system.svc.cluster.local` for the
130+
resolver address. For more information on `resolver-addresses` and other related ConfigMap keys, please refer to our
131+
documentation [ConfigMap
132+
Resource](https://docs.nginx.com/nginx-ingress-controller/configuration/global-configuration/configmap-resource/#summary-of-configmap-keys)
133+
and our blog post [Using DNS for Service Discovery with NGINX and NGINX
134+
Plus](https://www.nginx.com/blog/dns-service-discovery-nginx-plus)
135+
136+
NOTE: When setting the value of `jwksURI` in Step 5, the response will differ depending on the IDP used. In some cases
137+
the response will be too large for NGINX to properly handle. If this occurs you will need to configure the
138+
[subrequest_output_buffer_size](https://nginx.org/en/docs/http/ngx_http_core_module.html#subrequest_output_buffer_size)
139+
directive in the http context. This can currently be done using `http-snippets`. Please refer to our document on
140+
[snippets and custom
141+
templates](https://docs.nginx.com/nginx-ingress-controller/configuration/global-configuration/configmap-resource/#snippets-and-custom-templates)
142+
for details on how to configure this directive.
143+
144+
The code block below is an example of the updated configmap which adds `subrequest_output_buffer_size` under the http
145+
context in the nginx.conf.
146+
147+
NOTE: The value of `subrequest_output_buffer_size` is only an example value and should be changed to suit your
148+
environment.
149+
150+
```yaml
151+
kind: ConfigMap
152+
apiVersion: v1
153+
metadata:
154+
name: nginx-config
155+
namespace: nginx-ingress
156+
data:
157+
resolver-addresses: <resolver-address>
158+
http-snippets: |
159+
subrequest_output_buffer_size 64k;
160+
```
161+
162+
```console
163+
kubectl apply -f nginx-config.yaml
164+
```
165+
166+
## Step 7 - Configure Load Balancing
167+
168+
Create a VirtualServer resource for the web application:
169+
170+
```console
171+
kubectl apply -f virtual-server.yaml
172+
```
173+
174+
Note that the VirtualServer references the policy `jwt-policy` created in Step 5.
175+
176+
## Step 8 - Get the client token
177+
178+
For the client to have permission to send requests to the web application they must send a Bearer token to the
179+
application. To get this token, run the following `curl` command:
180+
181+
```console
182+
export TOKEN=$(curl -k -L -X POST 'https://keycloak.example.com/realms/jwks-example/protocol/openid-connect/token' \
183+
-H 'Content-Type: application/x-www-form-urlencoded' \
184+
--data-urlencode grant_type=password \
185+
--data-urlencode scope=openid \
186+
--data-urlencode client_id=jwks-client \
187+
--data-urlencode client_secret=$SECRET \
188+
--data-urlencode username=jwks-user \
189+
--data-urlencode password=$PASSWORD \
190+
| jq -r .access_token)
191+
```
192+
193+
This command will save the token in the `TOKEN` shell variable.
194+
195+
## Step 9 - Test the Configuration
196+
197+
If you attempt to access the application without providing the bearer token, NGINX will reject your requests for that
198+
VirtualServer:
199+
200+
```console
201+
curl -H 'Accept: application/json' webapp.example.com
202+
```
203+
204+
```text
205+
<html>
206+
<head><title>401 Authorization Required</title></head>
207+
<body>
208+
<center><h1>401 Authorization Required</h1></center>
209+
</body>
210+
</html>
211+
```
212+
213+
If a valid bearer token is provided, the request will succeed:
214+
215+
```console
216+
curl -H 'Accept: application/json' -H "token: ${TOKEN}" webapp.example.com
217+
```
218+
219+
```text
220+
Server address: 10.42.0.7:8080
221+
Server name: webapp-5c6fdbcbf9-pt9tp
222+
Date: 13/Dec/2022:14:50:33 +0000
223+
URI: /
224+
Request ID: f1241390ac51318afa4fcc39d2341359
225+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: Policy
3+
metadata:
4+
name: jwt-policy
5+
spec:
6+
jwt:
7+
realm: jwks-example
8+
token: $http_token
9+
jwksURI: http://keycloak.nginx-ingress.svc.cluster.local:8080/realms/jwks-example/protocol/openid-connect/certs
10+
keyCache: 1h
11+
sniName: keycloak.example.com
12+
sniEnabled: true
13+
---
14+
apiVersion: k8s.nginx.org/v1
15+
kind: Policy
16+
metadata:
17+
name: jwt-other-policy
18+
spec:
19+
jwt:
20+
realm: jwks-other-example
21+
token: $http_token
22+
jwksURI: http://otherkeycloak.nginx-ingress.svc.cluster.local:8080/realms/jwks-example/protocol/openid-connect/certs
23+
keyCache: 1h
24+
sniName: otherkeycloak.example.com
25+
sniEnabled: true
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
apiVersion: v1
2+
kind: Service
3+
metadata:
4+
name: keycloak
5+
labels:
6+
app: keycloak
7+
spec:
8+
ports:
9+
- name: http
10+
port: 8080
11+
targetPort: 8080
12+
selector:
13+
app: keycloak
14+
---
15+
apiVersion: apps/v1
16+
kind: Deployment
17+
metadata:
18+
name: keycloak
19+
namespace: nginx-ingress
20+
labels:
21+
app: keycloak
22+
spec:
23+
replicas: 1
24+
selector:
25+
matchLabels:
26+
app: keycloak
27+
template:
28+
metadata:
29+
labels:
30+
app: keycloak
31+
spec:
32+
containers:
33+
- name: keycloak
34+
image: quay.io/keycloak/keycloak:20.0.1
35+
args: ["start-dev"]
36+
env:
37+
- name: KEYCLOAK_ADMIN
38+
value: "admin"
39+
- name: KEYCLOAK_ADMIN_PASSWORD
40+
value: "admin"
41+
- name: KC_PROXY
42+
value: "edge"
43+
ports:
44+
- name: http
45+
containerPort: 8080
46+
- name: https
47+
containerPort: 8443
48+
readinessProbe:
49+
httpGet:
50+
path: /realms/master
51+
port: 8080
52+
---
53+
apiVersion: v1
54+
kind: Service
55+
metadata:
56+
name: otherkeycloak
57+
labels:
58+
app: otherkeycloak
59+
spec:
60+
ports:
61+
- name: http
62+
port: 8080
63+
targetPort: 8080
64+
selector:
65+
app: otherkeycloak
66+
---
67+
apiVersion: apps/v1
68+
kind: Deployment
69+
metadata:
70+
name: otherkeycloak
71+
namespace: nginx-ingress
72+
labels:
73+
app: otherkeycloak
74+
spec:
75+
replicas: 1
76+
selector:
77+
matchLabels:
78+
app: otherkeycloak
79+
template:
80+
metadata:
81+
labels:
82+
app: otherkeycloak
83+
spec:
84+
containers:
85+
- name: otherkeycloak
86+
image: quay.io/keycloak/keycloak:20.0.1
87+
args: ["start-dev"]
88+
env:
89+
- name: KEYCLOAK_ADMIN
90+
value: "admin"
91+
- name: KEYCLOAK_ADMIN_PASSWORD
92+
value: "admin"
93+
- name: KC_PROXY
94+
value: "edge"
95+
ports:
96+
- name: http
97+
containerPort: 8080
98+
- name: https
99+
containerPort: 8443
100+
readinessProbe:
101+
httpGet:
102+
path: /realms/master
103+
port: 8080
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
kind: ConfigMap
2+
apiVersion: v1
3+
metadata:
4+
name: nginx-config
5+
namespace: nginx-ingress
6+
data:
7+
resolver-addresses: "kube-dns.kube-system.svc.cluster.local"
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: v1
2+
kind: Secret
3+
metadata:
4+
name: tls-secret
5+
type: kubernetes.io/tls
6+
data:
7+
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZtZ0F3SUJBZ0lVS2hTQzBBcnhUblYrbjBhVnNENkFVTE5VQWhZd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0dERVdNQlFHQTFVRUF3d05LaTVsZUdGdGNHeGxMbU52YlRBZUZ3MHlNVEF4TVRZd01qSXpNekZhRncwegpNVEF4TVRRd01qSXpNekZhTUJneEZqQVVCZ05WQkFNTURTb3VaWGhoYlhCc1pTNWpiMjB3Z2dFaU1BMEdDU3FHClNJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURGeU1DSlhlSm9tMTdhcUVQc01NbTNlVzlpQzFHdlI4YW8KaDJhNmgvZWRXTUFndEtWSERmR2tPQ2V5NDBEdGtXTDN3U0NvZE1McnhPcnN2Lzhuc1VablFwQmNBekxBbzBJVgptYnhoS21WaS9EMkJpb2pBcDlqVXlsMjNma2RWMFdYM3NYV0JQekhSa3RyK0ozaW83YVcvNUl0WVBNWWFYM3dmCkZYRWFXVmQ4QmJDQ0hyVlZ3ckMvem9aTEF3dFE0d1I5NUI2NHdtd2d4TEhNZDlWZDRSZ1l2U0ppc1QzWi9IRkkKTGpaTGdMa0FlMGlDci9xdmFsdnVhU3BNVmJUd1lQZ2l6YWhXSVFTYjVyd29JeUhnYXFBWnRYSEhjNSsydDVoZQpMMDc2RjgrOE84b0hpdDR6WGpsR1V4TFNjTWFPTnI2ZHI0Q256NmlXZzJNTGlJcno0VnR4QWdNQkFBR2pVekJSCk1CMEdBMVVkRGdRV0JCUTdCSGpyZHlicnpWNHIwVkRrc2k3TXFPNWRKREFmQmdOVkhTTUVHREFXZ0JRN0JIanIKZHlicnpWNHIwVkRrc2k3TXFPNWRKREFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFDdm5TdUY4dUFUWFl2VHVjVGhEcG9jKzI5RU1LVFp2VDBmSmJrNWZMaWQzYjhFTDQxdk5tTjRwUTUrCmJtSFh1bkhLL29aSm43bWVNTngwc0ZQMW1Pa1U5MXBqZVJLWmoxOXVNQjlvTVBreXdXRENuQ1BHYWtFUHpxOS8KWjFwcERKQ0FJc2cvME8wZ1BCMDdFSm9RcU0wdDlZc3BuMlJ4djMwUGdBZ3ZuSXduUlNzUWpvOEpxQ1VuemZJLwpPdXovNVl1UkhJRHQzY0RpdTdzWG1DTW01cFJ5eUd2WGZiWEsrSVFWOHZDRTZlZS9FTlNFcnB0NUdzeVNURjZKCk5LdDhXM1VwNkUvL2dwMkRvTXBxS0tGQkE0aG5OQXVzQVphTkNQdi9EY0xueG9xQUp4S0V5cmpxelJBeTlCRXkKRzBhSTJ5bitKWW5yVW8wMmc1OWFXalZMTzg4RwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
8+
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRREZ5TUNKWGVKb20xN2EKcUVQc01NbTNlVzlpQzFHdlI4YW9oMmE2aC9lZFdNQWd0S1ZIRGZHa09DZXk0MER0a1dMM3dTQ29kTUxyeE9ycwp2Lzhuc1VablFwQmNBekxBbzBJVm1ieGhLbVZpL0QyQmlvakFwOWpVeWwyM2ZrZFYwV1gzc1hXQlB6SFJrdHIrCkozaW83YVcvNUl0WVBNWWFYM3dmRlhFYVdWZDhCYkNDSHJWVndyQy96b1pMQXd0UTR3Ujk1QjY0d213Z3hMSE0KZDlWZDRSZ1l2U0ppc1QzWi9IRklMalpMZ0xrQWUwaUNyL3F2YWx2dWFTcE1WYlR3WVBnaXphaFdJUVNiNXJ3bwpJeUhnYXFBWnRYSEhjNSsydDVoZUwwNzZGOCs4TzhvSGl0NHpYamxHVXhMU2NNYU9OcjZkcjRDbno2aVdnMk1MCmlJcno0VnR4QWdNQkFBRUNnZ0VBQXhBcjR6VEFCK3k0R0Z6WXlIU3MreGwzWHlaYnVvSTdFbXNlYlM4ajU1enoKUk01bmJPVkxZOGEyM3E5a1Z3bVVaYy9vNkpMK1hkWnI2UVRFTitJbisvdHM3dS9odmxnSTh2cXhqek92NUV1Ugp6RXJQK1dQZ0dOT1ZoZnovcjlXUlpiZXE0VGlRVmZXWFRLNWgwUVAxT0RhYTdkL3JGWWQ3RGFRd1h6OFkrc080CnhqV0dNNFprOW1oWm1PbG9nZjNtYyszUFhYTWV6RFRMY2kzRWNpZVlaTkhTeXIzWkg2NU8rSkdsOFZ2bkZUWS8KQytQZi9tYmJKL282dlNWWDNWQUVIM29BY05qd1dqMkdBNUhiRk5RTnV0ckhRcnNkR0ZqUVB5aHNBYjNOV1h2bwo2M3hoS1NNbHpxSWd2WXZMbENOS0VjZmJsVjRuelJ4NVhhM0dzZjJkUFFLQmdRRDlYeEs4ekhpN2g4WjlQV2sxCktDZFlvZDFVa2ViWktYUVQvOUtNcmhrOE9abG1oV2hFK1lBY3lJRElVeFZuZ2xkR0d3RVViTFcyWEVnVStQVmEKM1ZlaUNCTlRWM3FwV3lYWXdIdG9yYm5WbGtlMGh4eE9WakhvSmpZWitmV0h6MDU0algvYkdsdWp5bVJGMWpoWApuMnhNUW5RUkV0S2FGN0R2d2FGK083dGExd0tCZ1FESDFndWRlVCsvQ3M1R3g3eEkwUnhwRUt4c0FtcUV3blBECklsaHoxZHJqbGZFaTRPZ25wK0ZOK05acGJiMHRaWmUyTTM2QXpMVENIUURmQVNJTlBDMkxzOHEvTjAyR2xzcG8KalVTd3M4cWc2N2ZjcG1UN1FVVTVMZmZuaDE3S1A5ZEdCdlRuK3Vza1MwVjRFZ2M0Ti9lS2pUQi9xcjYzYWRHUwp4dmRaYzdnNjl3S0JnRE9CQWdRUzVHL3FkN1M1cVFzL01GQmFCdTNNQXNzZUhCUjhxa1lpbGNxaVFzYU9VOVhCCmlnTlAxcTNpQmJYV3p2clhQbTd5Y2pXeHFJMXExaVUwWFQzNHVrVDB3V0J2d00vQXdOVlVpelFacWxYT0tUamIKV0tYQ0xyazFFRzRjKyt5Umh1MzQrNnZkMW1oRDFZd3FRZzkyYXJXVngrMis1eDYxazZoZmFBUmRBb0dCQU1Kcgp0QmM4VE5IQVlKb3FYenYwL3BBVm9icmZ5dVJwRHhsdFErTkd6OVFXSUduUHFPNVQvZmJQUDBPSmVjRStFeEU0CkhqNlBhdGxrUUdHMmgzdWE3YkQ2ZGluOVV4YTdoQ2VlTVpNOUNNbnhLNHVuODUwampvYW4rNFd0aFlKK0JDSmsKU0VlZUxzRzczZFdJcks5OGZBQzNodFRldVBoWElvZUx2a0N3UGpCWEFvR0JBUFBteVJJRGs5bUF5M2ZINnBtVwplRWlqYlBWbFdDd3FjalI5ZjQ0L3duVEpha0h4cVVxRk04cTVLNnJJejdPMmMvcDdmTm83andrVHc0R0hIVWcrCjQyVkpGOXRrdnRDbEhOZ3l6cXNjT3FjN0p2ZDNyYnBFbGVpNGgyTHo4Z0RDNFo4WldqWDBBKzVTaTlQd3RMaFEKN3pBZEJUMHk5WjZuNGYxMVg0UWhKSkR1Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
apiVersion: k8s.nginx.org/v1
2+
kind: VirtualServer
3+
metadata:
4+
name: keycloak
5+
spec:
6+
host: keycloak.example.com
7+
tls:
8+
secret: tls-secret
9+
redirect:
10+
enable: true
11+
upstreams:
12+
- name: keycloak
13+
service: keycloak
14+
port: 8080
15+
routes:
16+
- path: /
17+
action:
18+
pass: keycloak

0 commit comments

Comments
 (0)