Настройка Rate Limiter#
Synapse Rate Limiter требует предварительной установки и не входит в поставку Keycloak.SE. Компонент Keycloak.SE предусматривает механизм настройки лимитов по ограничению (квотированию) входящих запросов. Возможность использования Rate Limiter доступна только в связке с SSM (Synapse Service Mesh)
Для настройки требуется:
В файле 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
Описание конфигурационных параметров на странице Настройка конфигурационных параметров
Установка лимитов средствами 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; |
kcse_rate_limits.endpoints[].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
Описание алгоритма передачи 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
Обновление лимитов средствами 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)"