Настройка Rate Limiter#

Synapse Rate Limiter требует предварительной установки и не входит в поставку Keycloak.SE. Компонент Keycloak.SE предусматривает механизм настройки лимитов по ограничению (квотированию) входящих запросов. Возможность использования Rate Limiter доступна только в связке с SSM (Synapse Service Mesh)

Для настройки требуется:

  1. В файле kcse.istio.all.conf установить следующие параметры:

# Включить использование RateLimiter
kcse.k8s.rls.enabled=true

# URL RateLimiter сервиса:
kcse.k8s.rls.service.url=rate-limiter-headless-service.some-namespace.svc.cluster.local

# Порт RateLimiter:
kcse.k8s.rls.service.port=8081

Описание конфигурационных параметров на странице Настройка конфигурационных параметров

  1. Установка лимитов средствами DOT.

Для развертывания средствами DOT, в репозитории с конфигурационными параметрами стенда, в файле custom_property.conf.yaml требуется определить структуру для генерации манифеста типа GlobalRateLimit.

Параметр

Описание

kcse_rate_limits.envoyVersion

Версия используемого envoy

kcse_rate_limits.endpoints[]

Определяет точки подключения

kcse_rate_limits.endpoints[].name

Название точки подключения

kcse_rate_limits.endpoints[].shorname

Обязательный параметр, используется в работе, для каждого endpoint значение shortname должно быть уникальным

kcse_rate_limits.endpoints[].endpoint

Определяет точку подключения и порт

kcse_rate_limits.endpoints[].overall_limit

Максимальное ограничение квоты для endpoint;
- 0 — блокировка всех запросов
- отрицательное значение — нет ограничения;
- если значение больше 0, дополнительно создается отдельный счетчик всех;

в случае превышения запрос будет отклонен, даже если потребитель еще не исчерпал свою квоту; overall_limit считается общим для всех префиксов

kcse_rate_limits.endpoints[].soft

Опциональный блок, если задан, создается метрика, показывающая приближение к лимиту.

soft.value — значение квоты, после которой с шагом кратным soft.step будет инкрементирована метрика мониторинга превышения уровня soft квоты для конкретного потребителя вплоть до превышения основного лимита;

kcse_rate_limits.endpoints[].url_prefixes

Список сервисов на которые требуется установить лимиты

kcse_rate_limits.endpoints[].url_prefixes[].prefix

Префикс сервиса

kcse_rate_limits.endpoints[].url_prefixes[].unit

Единица. Может принимать те же значения, что и endpoints[].unit

kcse_rate_limits.endpoints[].url_prefixes[].value

Опциональное поле, по умолчанию 1, позволяет задать квоту для запросов со значениями о клиенте, содержащемся в jwt-токене

kcse_rate_limits.endpoints[].url_prefixes[].anon_value

Опциональное поле, позволяет задать квоту для неидентифицированных (анонимных) потребителей, т.е. если jwt-токен не передается или в jwt-токене отсутсвует информация о клиенте

kcse_rate_limits.endpoints[].url_prefixes[].invokers[]

Если требуется установить квоты на конкретного клиента, отличные от квот опредделенных в общем блоке endpoints[].url_prefixes[].value

kcse_rate_limits.endpoints[].url_prefixes[].invokers[].client

Идентификатор клиента. Значение "azp", содержащееся в jwt-токене.

kcse_rate_limits.endpoints[].url_prefixes[].invokers[].unit

Если требуется установить единицу квотирования на конкретного клиента, отличную от того, что определено в блоке блоке endpoints[].url_prefixes[].unit

kcse_rate_limits.endpoints[].url_prefixes[].invokers[].value

Значение квоты, если требуется переопределить то, что определено в блоке endpoints[].url_prefixes[].value

Существует несколько вариантов назначения лимитов

2.1 Определение лимитов посервисно

В этом примере лимит устанавливается через префикс сервиса /auth/admin/realms/API-TEST-URI-ONLY/. В текущем примере на каждого клиента, для всех запросов имеющих в jwt-токен информацию о клиенте накладывается ограничение 10 запросов в минуту.

Пример:

kcse_rate_limits.endpoints

kcse_rate_limits:
    endpoints:
      - name: kcse-endpoint
        shortname: kcserl
        endpoint: kcse-pub2-tribe-sc-kcse-ift-1.apps.ocp.devpub.solution.mycompany:5314
        overall_limit: -1
        uri_prefixes:
          - prefix: /auth/admin/realms/API-TEST-URI-ONLY/
            unit: minute
            value: 10

2.2 Определение лимитов посервисно, с указанием своих лимитов для клиента

В этом примере лимит устанавливается через префикс сервиса /auth/admin/realms/API-TEST-URI-ONLY/ и определяются свои лимиты для клиента "test_client3". Таким образов для всех запросов, содержащих в jwt-токен информацию о клиенте, будет действовать лимит 10 запросов в минуту. Для клиента "test_client3" определены свои лимиты - 50 запросов в минуту.

kcse_rate_limits:
    endpoints:
      - name: kcse-endpoint
        shortname: kcserl
        endpoint: kcse-pub2-tribe-sc-kcse-ift-1.apps.ocp.devpub.solution.mycompany:5314
        overall_limit: -1
        uri_prefixes:
          - prefix: /auth/admin/realms/API-TEST-URI-ONLY/
            unit: minute
            anon_value: 5
            value: 10
            invokers:
               - client: test_client3
                 unit: minute
                 value: 50

2.3 Определение лимитов в разрезе клиентов

В этом примере устанавливается ограничение на префикс /auth. Если в jwt-токене присутствует информация о client_id, лимит задается в размере 200000 запросов в минуту. Все запросы, в которых отсутствует информация о клиенте, попадают в ограничение anon_value=1000000. Для клиента "test_client2", будет действовать ограничения 10 запросов в минуту.

Пример:

kcse_rate_limits:
    endpoints:
      - name: kcse-endpoint
        shortname: kcserl
        endpoint: kcse-pub2-tribe-sc-kcse-ift-1.apps.ocp.devpub.solution.mycompany:5314
        overall_limit: -1
        uri_prefixes:
           - prefix: /auth
             unit: minute
             value: 200000
             anon_value: 1000000
             invokers:
                - client: test_client2
                  unit: minute
                  value: 10

Стоит обратить внимание на то, что неидентифицированные запросы т.е. те запросы которые не содержат в jwt-токене информацию о клиенте или запросы без jwt-токена, попадают в лимит annon_value. Примерами таких запросов могут быть ui-запросы или например запросы к ресурсам /auth/resources/*.{css,js,...}. Лимиты к таким запросам требуют отдельной настройки и делятся между всеми потребителями.

Например:

kcse_rate_limits:
    endpoints:
      - name: kcse-endpoint
        shortname: kcserl
        endpoint: kcse-pub2-tribe-sc-kcse-ift-1.apps.ocp.devpub.solution.mycompany:5314
        overall_limit: -1
        uri_prefixes:
          ...
          - prefix: /auth/resources/
            unit: second
            anon_value: 1000000

Полный пример конфигурации RLS:

kcse_rate_limits:
    endpoints:
      - name: kcse-endpoint
        shortname: kcserl
        endpoint: kcse-pub2-tribe-sc-kcse-ift-1.apps.ocp.devpub.solution.mycompany:5314
        overall_limit: 1000000
        uri_prefixes:
          # Общее ограничение на все области 
          - prefix: /auth
            unit: minute
            value: 200000
            invokers:
              - client: test_client2
                unit: minute
                value: 10
                anon_value: 1000000
          # Ограничение на реалм API-TEST
          - prefix: /auth/admin/realms/API-TEST/
            unit: minute
            value: 10
            anon_value: 100
            invokers:
              - client: test_client
                value: 20
                anon_value: 200
                unit: minute
          # Ограничение на endpoint запроса токена, т.к. запрос не содержит информацию о клиенте,
          # значение лимита определяется через параметр anon_value
          - prefix: /auth/realms/API-TEST/protocol/openid-connect/token
            unit: minute
            anon_value: 10
          # Ограничение на запросы ресурсов css-, js- файлы. Задается через anon_value
          # т.к. в таких запросах не передается jwt-токен, лимит делится между всеми клиентами
          - prefix: /auth/resources/
            anon_value: 2000000
  1. Описание алгоритма передачи client_id в SRLS

Средствами SRLS можно выполнить ограничение нагрузки в разрезе потребителя. SRLS выполняет ограничение нагрузки на основе информации о клиенте, которая приходит в заголовке synapse-consumerid. Т.к. SRLS по умолчанию не представляет механизмов по parsing(s) значения synapse-consumerid из query-параметров, тела запроса или заголовка Authorization, в составе конфигурационной части дистрибутива Keycloak.SE поставляется файл kcse-ef-srls-ingress.yaml. Этот фильтр извлекает информацию о клиенте и передает в заголовок synapse-consumerid, по которому SRLS анализирует информацию по лимитам того или иного клиента.

Полный пример реализации envoy-фильтра:

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
  name: kcse-ef-jwt-ingress
spec:
  configPatches:
    - applyTo: HTTP_FILTER
      match:
        context: GATEWAY
#        listener:
#          filterChain:
#            filter:
#              name: envoy.http_connection_manager
#              subFilter:
#                name: envoy.router
      patch:
        operation: INSERT_BEFORE
        value:
          name: envoy.lua
          typed_config:
            '@type': type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua
            inlineCode: |
              local a='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'local function b(c)if c>64 then error("Bad number "..c.." to convert to binary")end;local d=tonumber(c)local e=''for f=5,0,-1 do local g=2^f;if d>=g then e=e..'1'd=d-g else e=e..'0'end end;return e end;local function h(e)return tonumber(e,2)end;local function i(j)local k=''local l=''for f=1,string.len(j)do local m,n=string.find(a,string.sub(j,f,f))if m==nil then error("Bad base64 character "..string.sub(j,f,f))end;k=k..b(m-1)end;for f=1,string.len(k),8 do l=l..string.char(h(string.sub(k,f,f+7)))end;return l end;local o={}local function p(q,r,s,t)r=r+#q:match('^%s*',r)if q:sub(r,r)~=s then if t then error('Expected '..s..' near position '..r)end;return r,false end;return r+1,true end;local function u(q,r,v)v=v or''local w='End of input found while parsing string.'if r>#q then error(w)end;local x=q:sub(r,r)if x=='"'then return v,r+1 end;if x~='\\'then return u(q,r+1,v..x)end;local y={b='\b',f='\f',n='\n',r='\r',t='\t'}local z=q:sub(r+1,r+1)if not z then error(w)end;return u(q,r+2,v..(y[z]or z))end;local function A(q,r)local B=q:match('^-?%d+%.?%d*[eE]?[+-]?%d*',r)local v=tonumber(B)if not v then error('Error parsing number at position '..r..'.')end;return v,r+#B end;o.null={}function o.parse(q,r,C)r=r or 1;if r>#q then error('Reached unexpected end of input.')end;local r=r+#q:match('^%s*',r)local D=q:sub(r,r)if D=='{'then local E,F,G={},true,true;r=r+1;while true do F,r=o.parse(q,r,'}')if F==nil then return E,r end;if not G then error('Comma missing between object items.')end;r=p(q,r,':',true)E[F],r=o.parse(q,r)r,G=p(q,r,',')end elseif D=='['then local H,v,G={},true,true;r=r+1;while true do v,r=o.parse(q,r,']')if v==nil then return H,r end;if not G then error('Comma missing between array items.')end;H[#H+1]=v;r,G=p(q,r,',')end elseif D=='"'then return u(q,r+1)elseif D=='-'or D:match('%d')then return A(q,r)elseif D==C then return nil,r+1 else local I={['true']=true,['false']=false,['null']=o.null}for J,K in pairs(I)do local L=r+#J-1;if q:sub(r,L)==J then return K,L+1 end end;local M='position '..r..': '..q:sub(r,r+10)error('Invalid json syntax starting at '..M)end end;local function decode_jwt(O)local f=0;local P={}for Q in(O..'.'):gmatch("(.-)%.")do P[f]=i(Q)f=f+1 end;local R=o.parse(P[0])local S=o.parse(P[1])return{head=R,claims=S}end
              local alpha='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'

              function base64dec(data)
                  data = string.gsub(data, '[^'..alpha..'=]', '')
                  return (data:gsub('.', function(x)
                      if (x == '=') then return '' end
                      local r,f='',(alpha:find(x)-1)
                      for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end
                      return r;
                  end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x)
                      if (#x ~= 8) then return '' end
                      local c=0
                      for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end
                          return string.char(c)
                  end))
              end

              function getClientId(str)
                  if not str then return "" end
                  return str:match("client_id=([^&]+)")
              end
              
              function envoy_on_request(request_handle)
                  local headers = request_handle:headers()
                  local location = headers:get(":path") or ""
                  local content_type = headers:get("content-type") or ""
                  local auth_header = headers:get("authorization") or ""
                  local auth_schema, auth_payload = auth_header:match('^(%a+)%s(.*)')

                  local client_id = nil;

                  if auth_header ~= nil and auth_schema == 'Basic' then
                      local tmp = base64dec(auth_payload)
                      client_id = tmp:match('(.*):.*')
                  end

                  if not client_id then
                      client_id = getClientId(location)
                  end

                  if not client_id and content_type == 'application/x-www-form-urlencoded' then
                      local body_buffer = request_handle:body(true)
                      local body = body_buffer:getBytes(0, body_buffer:length())
                      client_id = getClientId(body)
                  end
              
                  if not client_id and auth_schema == 'Bearer' then
                      local content = decode_jwt(auth_payload)
                      if content["claims"] ~= nil then
                        client_id = content["claims"]["azp"]
                      end
                  end

                  if client_id ~= nil then
                    headers:add("synapse-consumerid", client_id)
                  end
              end
  workloadSelector:
    labels:
      istio: {{ KCSE_IGW_SELECTOR }}

Приоритет получения client_id следующий:

  • Сначала анализируется информация о client_id из заголовка Authorization по схеме Basic

  • Затем, если заголовк отсутствует, анализируются query-параметры запроса на наличие параметра client_id

  • Затем, если такой query-параметр отсутствует, анализируется тело запроса на наличие параметра client_id

  • Затем, если такой параметр отсутствует в теле запроса, анализируется заголовок Authorization по схеме Bearer на наличие в jwt-токене поля apz

Пример 1. За получение client_id из заголовка Authorization по схеме Basic отвечает:

if auth_header ~= nil and auth_schema == 'Basic' then
  local tmp = base64dec(auth_payload)
  client_id = tmp:match('(.*):.*')
end

В этом случае заголовок Authorization может быть представлен в двух видах:

  • с секретом "client_id:secret"

  • без секрета "client_id:"

В обоих случаях client_id будет передан в SRLS.

Пример 2. За получение client_id из query-параметров запроса отвечает:

if not client_id then
  client_id = getClientId(location)
end

client_id будет получен в не зависимости от месторасположения query-параметра.

Пример 3. За получение client_id из тела запроса в формате form-urlencoded отвечает:

if not client_id and content_type == 'application/x-www-form-urlencoded' then
  local body_buffer = request_handle:body(true)
  local body = body_buffer:getBytes(0, body_buffer:length())
  client_id = getClientId(body)
end

Пример 4. За получение client_id из заголовка "Authorization" по схеме Bearer

if not client_id and auth_schema == 'Bearer' then
  local content = decode_jwt(auth_payload)
  if content["claims"] ~= nil then
    client_id = content["claims"]["azp"]
  end
end
  1. Обновление лимитов средствами DOT

Обновление конфигурации GlobalRateLimit-манифеста выполняется в момент развертывания основного приложения. Но также инструмент DOT позволяет выполнить обновление только манифеста GlobalRateLimit. Для использования этой возможности требуется выполнить дополнительные настройки инструмента CDJE DEPLOY.

В environment.json в секции "playbooks_fpi" требуется добавить playbook(s) OPENSHIFT_DEPLOY_CR для развертывания манифестов из директории customresources:

"playbooks_fpi": {
    ...
    "MIGRATION_FP_CONF": {
      "id": 19
    },
    ...
    "OPENSHIFT_DEPLOY": {
      "id": 23
    },
    ...
    "OPENSHIFT_INGRESS_EGRESS_DEPLOY": {
       "id": 26
    },
    ...
    "OPENSHIFT_DEPLOY_CR": {
      "id": 30
    }
}

После реконфигурации CDJE DEPLOY появится новый playbook(s) "Деплой custom resourses (не istio)"