<a id="jwks"></a>

# JWKS

В этом примере:

- развертывается веб-приложение;
- настраивается балансировка нагрузки с помощью `VirtualServer`;
- применяется политика `JWT`.

В отличие от примера с `JWT`, здесь внешний провайдер идентификации (IdP) определен
с помощью поля `JwksURI`. В качестве провайдера используется `Keycloak`,
развернутый как контейнер и доступный с помощью ANIC.

## Предварительные действия

1. Установите ANIC.
2. Добавьте в файл `/etc/hosts` записи:
   ```text
   <ваш_IP-адрес> webapp.example.com
   <ваш_IP-адрес> keycloak.example.com
   ```

   Здесь `webapp.example.com` — домен веб-приложения,
   а `keycloak.example.com` — домен Keycloak.

## Настройка JWKS

1. Создайте Secret с TLS-сертификатом и ключом для терминального TLS-шифрования в `Keycloak`:
   ```yaml
   apiVersion: v1
   kind: Secret
   metadata:
     name: tls-secret
   type: kubernetes.io/tls
   data:
     tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZtZ0F3SUJBZ0lVS2hTQzBBcnhUblYrbjBhVnNENkFVTE5VQWhZd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0dERVdNQlFHQTFVRUF3d05LaTVsZUdGdGNHeGxMbU52YlRBZUZ3MHlNVEF4TVRZd01qSXpNekZhRncwegpNVEF4TVRRd01qSXpNekZhTUJneEZqQVVCZ05WQkFNTURTb3VaWGhoYlhCc1pTNWpiMjB3Z2dFaU1BMEdDU3FHClNJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURGeU1DSlhlSm9tMTdhcUVQc01NbTNlVzlpQzFHdlI4YW8KaDJhNmgvZWRXTUFndEtWSERmR2tPQ2V5NDBEdGtXTDN3U0NvZE1McnhPcnN2Lzhuc1VablFwQmNBekxBbzBJVgptYnhoS21WaS9EMkJpb2pBcDlqVXlsMjNma2RWMFdYM3NYV0JQekhSa3RyK0ozaW83YVcvNUl0WVBNWWFYM3dmCkZYRWFXVmQ4QmJDQ0hyVlZ3ckMvem9aTEF3dFE0d1I5NUI2NHdtd2d4TEhNZDlWZDRSZ1l2U0ppc1QzWi9IRkkKTGpaTGdMa0FlMGlDci9xdmFsdnVhU3BNVmJUd1lQZ2l6YWhXSVFTYjVyd29JeUhnYXFBWnRYSEhjNSsydDVoZQpMMDc2RjgrOE84b0hpdDR6WGpsR1V4TFNjTWFPTnI2ZHI0Q256NmlXZzJNTGlJcno0VnR4QWdNQkFBR2pVekJSCk1CMEdBMVVkRGdRV0JCUTdCSGpyZHlicnpWNHIwVkRrc2k3TXFPNWRKREFmQmdOVkhTTUVHREFXZ0JRN0JIanIKZHlicnpWNHIwVkRrc2k3TXFPNWRKREFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFDdm5TdUY4dUFUWFl2VHVjVGhEcG9jKzI5RU1LVFp2VDBmSmJrNWZMaWQzYjhFTDQxdk5tTjRwUTUrCmJtSFh1bkhLL29aSm43bWVNTngwc0ZQMW1Pa1U5MXBqZVJLWmoxOXVNQjlvTVBreXdXRENuQ1BHYWtFUHpxOS8KWjFwcERKQ0FJc2cvME8wZ1BCMDdFSm9RcU0wdDlZc3BuMlJ4djMwUGdBZ3ZuSXduUlNzUWpvOEpxQ1VuemZJLwpPdXovNVl1UkhJRHQzY0RpdTdzWG1DTW01cFJ5eUd2WGZiWEsrSVFWOHZDRTZlZS9FTlNFcnB0NUdzeVNURjZKCk5LdDhXM1VwNkUvL2dwMkRvTXBxS0tGQkE0aG5OQXVzQVphTkNQdi9EY0xueG9xQUp4S0V5cmpxelJBeTlCRXkKRzBhSTJ5bitKWW5yVW8wMmc1OWFXalZMTzg4RwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
     tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRREZ5TUNKWGVKb20xN2EKcUVQc01NbTNlVzlpQzFHdlI4YW9oMmE2aC9lZFdNQWd0S1ZIRGZHa09DZXk0MER0a1dMM3dTQ29kTUxyeE9ycwp2Lzhuc1VablFwQmNBekxBbzBJVm1ieGhLbVZpL0QyQmlvakFwOWpVeWwyM2ZrZFYwV1gzc1hXQlB6SFJrdHIrCkozaW83YVcvNUl0WVBNWWFYM3dmRlhFYVdWZDhCYkNDSHJWVndyQy96b1pMQXd0UTR3Ujk1QjY0d213Z3hMSE0KZDlWZDRSZ1l2U0ppc1QzWi9IRklMalpMZ0xrQWUwaUNyL3F2YWx2dWFTcE1WYlR3WVBnaXphaFdJUVNiNXJ3bwpJeUhnYXFBWnRYSEhjNSsydDVoZUwwNzZGOCs4TzhvSGl0NHpYamxHVXhMU2NNYU9OcjZkcjRDbno2aVdnMk1MCmlJcno0VnR4QWdNQkFBRUNnZ0VBQXhBcjR6VEFCK3k0R0Z6WXlIU3MreGwzWHlaYnVvSTdFbXNlYlM4ajU1enoKUk01bmJPVkxZOGEyM3E5a1Z3bVVaYy9vNkpMK1hkWnI2UVRFTitJbisvdHM3dS9odmxnSTh2cXhqek92NUV1Ugp6RXJQK1dQZ0dOT1ZoZnovcjlXUlpiZXE0VGlRVmZXWFRLNWgwUVAxT0RhYTdkL3JGWWQ3RGFRd1h6OFkrc080CnhqV0dNNFprOW1oWm1PbG9nZjNtYyszUFhYTWV6RFRMY2kzRWNpZVlaTkhTeXIzWkg2NU8rSkdsOFZ2bkZUWS8KQytQZi9tYmJKL282dlNWWDNWQUVIM29BY05qd1dqMkdBNUhiRk5RTnV0ckhRcnNkR0ZqUVB5aHNBYjNOV1h2bwo2M3hoS1NNbHpxSWd2WXZMbENOS0VjZmJsVjRuelJ4NVhhM0dzZjJkUFFLQmdRRDlYeEs4ekhpN2g4WjlQV2sxCktDZFlvZDFVa2ViWktYUVQvOUtNcmhrOE9abG1oV2hFK1lBY3lJRElVeFZuZ2xkR0d3RVViTFcyWEVnVStQVmEKM1ZlaUNCTlRWM3FwV3lYWXdIdG9yYm5WbGtlMGh4eE9WakhvSmpZWitmV0h6MDU0algvYkdsdWp5bVJGMWpoWApuMnhNUW5RUkV0S2FGN0R2d2FGK083dGExd0tCZ1FESDFndWRlVCsvQ3M1R3g3eEkwUnhwRUt4c0FtcUV3blBECklsaHoxZHJqbGZFaTRPZ25wK0ZOK05acGJiMHRaWmUyTTM2QXpMVENIUURmQVNJTlBDMkxzOHEvTjAyR2xzcG8KalVTd3M4cWc2N2ZjcG1UN1FVVTVMZmZuaDE3S1A5ZEdCdlRuK3Vza1MwVjRFZ2M0Ti9lS2pUQi9xcjYzYWRHUwp4dmRaYzdnNjl3S0JnRE9CQWdRUzVHL3FkN1M1cVFzL01GQmFCdTNNQXNzZUhCUjhxa1lpbGNxaVFzYU9VOVhCCmlnTlAxcTNpQmJYV3p2clhQbTd5Y2pXeHFJMXExaVUwWFQzNHVrVDB3V0J2d00vQXdOVlVpelFacWxYT0tUamIKV0tYQ0xyazFFRzRjKyt5Umh1MzQrNnZkMW1oRDFZd3FRZzkyYXJXVngrMis1eDYxazZoZmFBUmRBb0dCQU1Kcgp0QmM4VE5IQVlKb3FYenYwL3BBVm9icmZ5dVJwRHhsdFErTkd6OVFXSUduUHFPNVQvZmJQUDBPSmVjRStFeEU0CkhqNlBhdGxrUUdHMmgzdWE3YkQ2ZGluOVV4YTdoQ2VlTVpNOUNNbnhLNHVuODUwampvYW4rNFd0aFlKK0JDSmsKU0VlZUxzRzczZFdJcks5OGZBQzNodFRldVBoWElvZUx2a0N3UGpCWEFvR0JBUFBteVJJRGs5bUF5M2ZINnBtVwplRWlqYlBWbFdDd3FjalI5ZjQ0L3duVEpha0h4cVVxRk04cTVLNnJJejdPMmMvcDdmTm83andrVHc0R0hIVWcrCjQyVkpGOXRrdnRDbEhOZ3l6cXNjT3FjN0p2ZDNyYnBFbGVpNGgyTHo4Z0RDNFo4WldqWDBBKzVTaTlQd3RMaFEKN3pBZEJUMHk5WjZuNGYxMVg0UWhKSkR1Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
   ```

   Примените настройки:
   ```console
   $ kubectl apply -f tls-secret.yaml
   ```
2. Создайте Deployment и Service для веб-приложения:
   ```yaml
   apiVersion: apps/v1
   kind: Deployment
   metadata:
     name: webapp
   spec:
     replicas: 1
     selector:
       matchLabels:
         app: webapp
     template:
       metadata:
         labels:
           app: webapp
       spec:
         containers:
         - name: webapp
           image: angiesoftware/angie-hello:plain-text
           ports:
           - containerPort: 8080
   ---
   apiVersion: v1
   kind: Service
   metadata:
     name: webapp-svc
   spec:
     ports:
     - port: 80
       targetPort: 8080
       protocol: TCP
       name: http
     selector:
       app: webapp
   ```

   Примените настройки:
   ```console
   $ kubectl apply -f webapp.yaml
   ```
3. Создайте Deployment и Service для `Keycloak`:
   ```yaml
   apiVersion: v1
   kind: Service
   metadata:
     name: keycloak
     labels:
       app: keycloak
   spec:
     ports:
     - name: http
       port: 8080
       targetPort: 8080
     selector:
       app: keycloak
   ---
   apiVersion: apps/v1
   kind: Deployment
   metadata:
     name: keycloak
     namespace: default
     labels:
       app: keycloak
   spec:
     replicas: 1
     selector:
       matchLabels:
         app: keycloak
     template:
       metadata:
         labels:
           app: keycloak
       spec:
         containers:
         - name: keycloak
           image: quay.io/keycloak/keycloak:20.0.1
           args: ["start-dev"]
           env:
           - name: KEYCLOAK_ADMIN
             value: "admin"
           - name: KEYCLOAK_ADMIN_PASSWORD
             value: "admin"
           - name: KC_PROXY
             value: "edge"
           ports:
           - name: http
             containerPort: 8080
           - name: https
             containerPort: 8443
           readinessProbe:
             httpGet:
               path: /realms/master
               port: 8080
   ```

   Примените настройки:
   ```console
   $ kubectl apply -f keycloak.yaml
   ```
4. Создайте `VirtualServer` для Keycloak:
   ```yaml
   apiVersion: k8s.angie.software/v1
   kind: VirtualServer
   metadata:
     name: keycloak
   spec:
     host: keycloak.example.com
     tls:
       secret: tls-secret
       redirect:
         enable: true
     upstreams:
       - name: keycloak
         service: keycloak
         port: 8080
     routes:
       - path: /
         action:
           pass: keycloak
   ```

   Примените настройки:
   ```console
   $ kubectl apply -f virtual-server-idp.yaml
   ```
5. Настройте Keycloak:
   - Перейдите в Keycloak: `https://keycloak.example.com`.
   - Создайте новую область `Realm` с именем `jwks-example`.
   - Перейдите во вкладку `Client`, создайте новый клиент с именем `jwks-client` и
     включите для него `Client authentication` и `Authorization`.
   - Перейдите во вкладку `Credentials` и скопируйте секрет клиента.

     Сохраните секрет:
     ```console
     export SECRET=<client secret>
     ```
   - Перейдите во вкладку `Users` и создайте пользователя `jwks-user`.
   - Перейдите во вкладку `Credentials` этого пользователя и установите пароль. Для примера подойдет любой пароль.

     Сохраните пароль:
     ```console
     export PASSWORD=<user password>
     ```
6. Создайте политику `jwt-policy` с указанием `JwksURI` и настройте поле `JwksURI` так,
   чтобы оно разрешало только те запросы к веб-приложению, которые содержат действительный JWT.

   В приведенной ниже политике замените область `jwks-example` на область (realm), созданную вами.
   Значение `spec.jwt.token` в примере установлено в `$http_token`, так как токен клиента передается в заголовке HTTP.
   ```yaml
   apiVersion: k8s.angie.software/v1
   kind: Policy
   metadata:
     name: jwt-policy
   spec:
     jwt:
       realm: MyProductAPI
       token: $http_token
       jwksURI: http://keycloak.default.svc.cluster.local:8080/realms/jwks-example/protocol/openid-connect/certs
   ```

   Примените политику:
   ```console
   $ kubectl apply -f jwks.yaml
   ```
7. Разверните ConfigMap с резолвером.

   Если в `jwksURI` используется имя хоста, необходимо настроить `resolver`. Для этого необходимо добавить поле `resolver-addresses`.
   ```yaml
   kind: ConfigMap
   apiVersion: v1
   metadata:
     name: angie-config
     namespace: angie-ingress
   data:
     resolver-addresses: kube-dns.kube-system.svc.cluster.local
     http-snippets: |
       subrequest_output_buffer_size 64k;
   ```

   В этом примере мы создаем ConfigMap, используя стандартный DNS Kubernetes
   `kube-dns.kube-system.svc.cluster.local` в качестве адреса резолвера.
   Дополнительную информацию о `resolver-addresses` и других связанных
   ключах ConfigMap можно посмотреть в разделе [ConfigMap](https://angie.software//anic/docs/configuration/configmap-resource.md#configmap-resource).

   #### NOTE
   При установке значения `jwksURI` ответ может отличаться в зависимости от используемого IDP.
   В некоторых случаях ответ может быть слишком большим для корректной обработки Angie.
   Если это произойдет, необходимо настроить директиву `subrequest_output_buffer_size` в контексте `http`.
   Это можно сделать с помощью `http-snippets`.
   Значение `subrequest_output_buffer_size` указано только для примера
   и должно быть изменено в соответствии с вашей средой.

   Примените конфигурацию:
   ```console
   $ kubectl apply -f angie-config.yaml
   ```
8. Настройте балансировку нагрузки. Создайте `VirtualServer` для веб-приложения:
   ```yaml
   apiVersion: k8s.angie.software/v1
   kind: VirtualServer
   metadata:
     name: webapp
   spec:
     host: webapp.example.com
     policies:
     - name: jwt-policy
     upstreams:
     - name: webapp
       service: webapp-svc
       port: 80
     routes:
     - path: /
       action:
         pass: webapp
   ```

   Примените настройки:
   ```console
   $ kubectl apply -f virtual-server.yaml
   ```

   Обратите внимание, что `VirtualServer` ссылается на политику `jwt-policy`, созданную выше.
9. Получите токен клиента.

   Для доступа к веб-приложению клиент должен передавать токен-носитель.
   Чтобы получить токен, выполните команду:
   ```console
   $ export TOKEN=$(curl -k -L -X POST 'https://keycloak.example.com/realms/jwks-example/protocol/openid-connect/token' \
     -H 'Content-Type: application/x-www-form-urlencoded' \
     --data-urlencode grant_type=password \
     --data-urlencode scope=openid \
     --data-urlencode client_id=jwks-client \
     --data-urlencode client_secret=$SECRET \
     --data-urlencode username=jwks-user \
     --data-urlencode password=$PASSWORD \
     | jq -r .access_token)
   ```

   Эта команда сохранит токен в переменной окружения TOKEN.
10. Протестируйте конфигурацию.
    - Попытка запроса без токена-носителя:
      ```console
      $ curl -H 'Accept: application/json' webapp.example.com
      ```

      Ответ:
      ```html
      <html>
      <head><title>401 Authorization Required</title></head>
      <body>
      <center><h1>401 Authorization Required</h1></center>
      <hr><center>Angie/1.8.1</center>
      </body>
      </html>
      ```
    - Запрос с корректным токеном-носителем:
      ```console
      $ curl -H 'Accept: application/json' -H "token: ${TOKEN}" webapp.example.com
      ```

      Ответ сервера:
      ```text
      Server address: 10.42.0.7:8080
      Server name: webapp-5c6fdbcbf9-pt9tp
      Date: 13/Dec/2022:14:50:33 +0000
      URI: /
      Request ID: f1241390ac51318afa4fcc39d2341359
      ```
