JWKS#

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

  • развертывается веб-приложение;

  • настраивается балансировка нагрузки с помощью VirtualServer;

  • применяется политика JWT.

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

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

  1. Установите ANIC.

  2. Добавьте в файл /etc/hosts записи:

    XXX.YYY.ZZZ.III webapp.example.com
    XXX.YYY.ZZZ.III keycloak.example.com
    

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

Настройка JWKS#

  1. Создайте Secret с TLS-сертификатом и ключом для терминального TLS-шифрования в Keycloak:

    apiVersion: v1
    kind: Secret
    metadata:
      name: tls-secret
    type: kubernetes.io/tls
    data:
      tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURFVENDQWZtZ0F3SUJBZ0lVS2hTQzBBcnhUblYrbjBhVnNENkFVTE5VQWhZd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0dERVdNQlFHQTFVRUF3d05LaTVsZUdGdGNHeGxMbU52YlRBZUZ3MHlNVEF4TVRZd01qSXpNekZhRncwegpNVEF4TVRRd01qSXpNekZhTUJneEZqQVVCZ05WQkFNTURTb3VaWGhoYlhCc1pTNWpiMjB3Z2dFaU1BMEdDU3FHClNJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURGeU1DSlhlSm9tMTdhcUVQc01NbTNlVzlpQzFHdlI4YW8KaDJhNmgvZWRXTUFndEtWSERmR2tPQ2V5NDBEdGtXTDN3U0NvZE1McnhPcnN2Lzhuc1VablFwQmNBekxBbzBJVgptYnhoS21WaS9EMkJpb2pBcDlqVXlsMjNma2RWMFdYM3NYV0JQekhSa3RyK0ozaW83YVcvNUl0WVBNWWFYM3dmCkZYRWFXVmQ4QmJDQ0hyVlZ3ckMvem9aTEF3dFE0d1I5NUI2NHdtd2d4TEhNZDlWZDRSZ1l2U0ppc1QzWi9IRkkKTGpaTGdMa0FlMGlDci9xdmFsdnVhU3BNVmJUd1lQZ2l6YWhXSVFTYjVyd29JeUhnYXFBWnRYSEhjNSsydDVoZQpMMDc2RjgrOE84b0hpdDR6WGpsR1V4TFNjTWFPTnI2ZHI0Q256NmlXZzJNTGlJcno0VnR4QWdNQkFBR2pVekJSCk1CMEdBMVVkRGdRV0JCUTdCSGpyZHlicnpWNHIwVkRrc2k3TXFPNWRKREFmQmdOVkhTTUVHREFXZ0JRN0JIanIKZHlicnpWNHIwVkRrc2k3TXFPNWRKREFQQmdOVkhSTUJBZjhFQlRBREFRSC9NQTBHQ1NxR1NJYjNEUUVCQ3dVQQpBNElCQVFDdm5TdUY4dUFUWFl2VHVjVGhEcG9jKzI5RU1LVFp2VDBmSmJrNWZMaWQzYjhFTDQxdk5tTjRwUTUrCmJtSFh1bkhLL29aSm43bWVNTngwc0ZQMW1Pa1U5MXBqZVJLWmoxOXVNQjlvTVBreXdXRENuQ1BHYWtFUHpxOS8KWjFwcERKQ0FJc2cvME8wZ1BCMDdFSm9RcU0wdDlZc3BuMlJ4djMwUGdBZ3ZuSXduUlNzUWpvOEpxQ1VuemZJLwpPdXovNVl1UkhJRHQzY0RpdTdzWG1DTW01cFJ5eUd2WGZiWEsrSVFWOHZDRTZlZS9FTlNFcnB0NUdzeVNURjZKCk5LdDhXM1VwNkUvL2dwMkRvTXBxS0tGQkE0aG5OQXVzQVphTkNQdi9EY0xueG9xQUp4S0V5cmpxelJBeTlCRXkKRzBhSTJ5bitKWW5yVW8wMmc1OWFXalZMTzg4RwotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
      tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRREZ5TUNKWGVKb20xN2EKcUVQc01NbTNlVzlpQzFHdlI4YW9oMmE2aC9lZFdNQWd0S1ZIRGZHa09DZXk0MER0a1dMM3dTQ29kTUxyeE9ycwp2Lzhuc1VablFwQmNBekxBbzBJVm1ieGhLbVZpL0QyQmlvakFwOWpVeWwyM2ZrZFYwV1gzc1hXQlB6SFJrdHIrCkozaW83YVcvNUl0WVBNWWFYM3dmRlhFYVdWZDhCYkNDSHJWVndyQy96b1pMQXd0UTR3Ujk1QjY0d213Z3hMSE0KZDlWZDRSZ1l2U0ppc1QzWi9IRklMalpMZ0xrQWUwaUNyL3F2YWx2dWFTcE1WYlR3WVBnaXphaFdJUVNiNXJ3bwpJeUhnYXFBWnRYSEhjNSsydDVoZUwwNzZGOCs4TzhvSGl0NHpYamxHVXhMU2NNYU9OcjZkcjRDbno2aVdnMk1MCmlJcno0VnR4QWdNQkFBRUNnZ0VBQXhBcjR6VEFCK3k0R0Z6WXlIU3MreGwzWHlaYnVvSTdFbXNlYlM4ajU1enoKUk01bmJPVkxZOGEyM3E5a1Z3bVVaYy9vNkpMK1hkWnI2UVRFTitJbisvdHM3dS9odmxnSTh2cXhqek92NUV1Ugp6RXJQK1dQZ0dOT1ZoZnovcjlXUlpiZXE0VGlRVmZXWFRLNWgwUVAxT0RhYTdkL3JGWWQ3RGFRd1h6OFkrc080CnhqV0dNNFprOW1oWm1PbG9nZjNtYyszUFhYTWV6RFRMY2kzRWNpZVlaTkhTeXIzWkg2NU8rSkdsOFZ2bkZUWS8KQytQZi9tYmJKL282dlNWWDNWQUVIM29BY05qd1dqMkdBNUhiRk5RTnV0ckhRcnNkR0ZqUVB5aHNBYjNOV1h2bwo2M3hoS1NNbHpxSWd2WXZMbENOS0VjZmJsVjRuelJ4NVhhM0dzZjJkUFFLQmdRRDlYeEs4ekhpN2g4WjlQV2sxCktDZFlvZDFVa2ViWktYUVQvOUtNcmhrOE9abG1oV2hFK1lBY3lJRElVeFZuZ2xkR0d3RVViTFcyWEVnVStQVmEKM1ZlaUNCTlRWM3FwV3lYWXdIdG9yYm5WbGtlMGh4eE9WakhvSmpZWitmV0h6MDU0algvYkdsdWp5bVJGMWpoWApuMnhNUW5RUkV0S2FGN0R2d2FGK083dGExd0tCZ1FESDFndWRlVCsvQ3M1R3g3eEkwUnhwRUt4c0FtcUV3blBECklsaHoxZHJqbGZFaTRPZ25wK0ZOK05acGJiMHRaWmUyTTM2QXpMVENIUURmQVNJTlBDMkxzOHEvTjAyR2xzcG8KalVTd3M4cWc2N2ZjcG1UN1FVVTVMZmZuaDE3S1A5ZEdCdlRuK3Vza1MwVjRFZ2M0Ti9lS2pUQi9xcjYzYWRHUwp4dmRaYzdnNjl3S0JnRE9CQWdRUzVHL3FkN1M1cVFzL01GQmFCdTNNQXNzZUhCUjhxa1lpbGNxaVFzYU9VOVhCCmlnTlAxcTNpQmJYV3p2clhQbTd5Y2pXeHFJMXExaVUwWFQzNHVrVDB3V0J2d00vQXdOVlVpelFacWxYT0tUamIKV0tYQ0xyazFFRzRjKyt5Umh1MzQrNnZkMW1oRDFZd3FRZzkyYXJXVngrMis1eDYxazZoZmFBUmRBb0dCQU1Kcgp0QmM4VE5IQVlKb3FYenYwL3BBVm9icmZ5dVJwRHhsdFErTkd6OVFXSUduUHFPNVQvZmJQUDBPSmVjRStFeEU0CkhqNlBhdGxrUUdHMmgzdWE3YkQ2ZGluOVV4YTdoQ2VlTVpNOUNNbnhLNHVuODUwampvYW4rNFd0aFlKK0JDSmsKU0VlZUxzRzczZFdJcks5OGZBQzNodFRldVBoWElvZUx2a0N3UGpCWEFvR0JBUFBteVJJRGs5bUF5M2ZINnBtVwplRWlqYlBWbFdDd3FjalI5ZjQ0L3duVEpha0h4cVVxRk04cTVLNnJJejdPMmMvcDdmTm83andrVHc0R0hIVWcrCjQyVkpGOXRrdnRDbEhOZ3l6cXNjT3FjN0p2ZDNyYnBFbGVpNGgyTHo4Z0RDNFo4WldqWDBBKzVTaTlQd3RMaFEKN3pBZEJUMHk5WjZuNGYxMVg0UWhKSkR1Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
    

    Примените настройки:

    $ kubectl apply -f tls-secret.yaml
    
  2. Создайте Deployment и Service для веб-приложения:

    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
    

    Примените настройки:

    $ kubectl apply -f webapp.yaml
    
  3. Создайте Deployment и Service для Keycloak:

    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
    

    Примените настройки:

    $ kubectl apply -f keycloak.yaml
    
  4. Создайте VirtualServer для Keycloak:

    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
    

    Примените настройки:

    $ kubectl apply -f virtual-server-idp.yaml
    
  5. Настройте Keycloak:

    • Перейдите в Keycloak: https://keycloak.example.com.

    • Создайте новую область Realm с именем jwks-example.

    • Перейдите во вкладку Client, создайте новый клиент с именем jwks-client и включите для него Client authentication и Authorization.

    • Перейдите во вкладку Credentials и скопируйте секрет клиента.

      Сохраните секрет:

      export SECRET=<client secret>
      
    • Перейдите во вкладку Users и создайте пользователя jwks-user.

    • Перейдите во вкладку Credentials этого пользователя и установите пароль. Для примера подойдет любой пароль.

      Сохраните пароль:

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

    В приведенной ниже политике замените область jwks-example на область (realm), созданную вами. Значение spec.jwt.token в примере установлено в $http_token, так как токен клиента передается в заголовке HTTP.

    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
    

    Примените политику:

    $ kubectl apply -f jwks.yaml
    
  7. Разверните ConfigMap с резолвером.

    Если в jwksURI используется имя хоста, необходимо настроить resolver. Для этого необходимо добавить поле resolver-addresses.

    kind: ConfigMap
    apiVersion: v1
    metadata:
      name: angie-config
      namespace: angie-ingress
    data:
      resolver-addresses: <resolver-address>
    

    В этом примере мы создаем ConfigMap, используя стандартный DNS Kubernetes kube-dns.kube-system.svc.cluster.local в качестве адреса резолвера. Дополнительную информацию о resolver-addresses и других связанных ключах ConfigMap можно посмотреть в разделе ConfigMap.

    Примечание

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

    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;
    

    Примените конфигурацию:

    $ kubectl apply -f angie-config.yaml
    
  8. Настройте балансировку нагрузки. Создайте VirtualServer для веб-приложения:

    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
    

    Примените настройки:

    $ kubectl apply -f virtual-server.yaml
    

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

  9. Получите токен клиента.

    Для доступа к веб-приложению клиент должен передавать токен-носитель. Чтобы получить токен, выполните команду:

    $ 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. Протестируйте конфигурацию.

    • Попытка запроса без токена-носителя:

      $ curl -H 'Accept: application/json' webapp.example.com
      

      Ответ:

      <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>
      
    • Запрос с корректным токеном-носителем:

      $ curl -H 'Accept: application/json' -H "token: ${TOKEN}" webapp.example.com
      

      Ответ сервера:

      Server address: 10.42.0.7:8080
      Server name: webapp-5c6fdbcbf9-pt9tp
      Date: 13/Dec/2022:14:50:33 +0000
      URI: /
      Request ID: f1241390ac51318afa4fcc39d2341359