Skip to content

Commit 8e7c5b6

Browse files
committed
frontend: introduce request rate limter to frontend
Motivation: protect the system from DoS attacks. Modification: Enable rate-limiter aware jetty handler list. Update rate-limiter handler list to log with warn only on too many auth errors. New properties are introduced: frontend.limits.max-blocked-clients frontend.limits.rate.overall frontend.limits.rate.per-client.fractions frontend.limits.error.max-allowed frontend.limits.error.block.window.time frontend.limits.error.block.window.time.units frontend.limits.rate.per-client.block.window.time frontend.limits.rate.per-client.block.window.time.units frontend.limits.blocked-clients.idle-time frontend.limits.blocked-clients.idle-time.units Result: frontend now can be protected against DoS attacks. Ticket: #10371 Acked-by: Dmitry Litvintsev Target: master Require-book: no Require-notes: yes
1 parent fbeba80 commit 8e7c5b6

File tree

4 files changed

+73
-4
lines changed

4 files changed

+73
-4
lines changed

modules/dcache-frontend/src/main/resources/org/dcache/frontend/frontend.xml

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -378,8 +378,23 @@
378378
<description>Loads the factories for producers of well-known endpoint content.</description>
379379
</bean>
380380

381-
<bean id="handlers" class="org.eclipse.jetty.server.handler.HandlerList">
381+
<bean id="rate-limiter-config" class="org.dcache.util.jetty.RateLimitedHandlerList.Configuration">
382+
<description>Configuration of requests rate limiter</description>
383+
<property name="maxClientsToTrack" value="${frontend.limits.max-blocked-clients}"/>
384+
<property name="globalRequestsPerSecond" value="${frontend.limits.rate.overall}"/>
385+
<property name="limitPercentagePerClient" value="${frontend.limits.rate.per-client.fractions}"/>
386+
<property name="clientBlockingTime" value="${frontend.limits.rate.per-client.block.window.time}"/>
387+
<property name="clientBlockingTimeUnit" value="${frontend.limits.rate.per-client.block.window.time.units}"/>
388+
<property name="clientIdleTime" value="${frontend.limits.blocked-clients.idle-time}"/>
389+
<property name="clientIdleTimeUnit" value="${frontend.limits.blocked-clients.idle-time.units}"/>
390+
<property name="numErrorsBeforeBlocking" value="${frontend.limits.error.max-allowed}"/>
391+
<property name="errorCountingWindow" value="${frontend.limits.error.block.window.time}"/>
392+
<property name="errorCountingWindowUnit" value="${frontend.limits.error.block.window.time.units}"/>
393+
</bean>
394+
395+
<bean id="handlers" class="org.dcache.util.jetty.RateLimitedHandlerList">
382396
<description>List of handlers for HTTP requests</description>
397+
<constructor-arg ref="rate-limiter-config"/>
383398
<property name="handlers">
384399
<list>
385400
<bean class="org.eclipse.jetty.server.handler.ContextHandler">

modules/dcache-webdav/src/main/resources/org/dcache/webdav/webdav.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@
271271

272272

273273
<bean id="rate-limiter-config" class="org.dcache.util.jetty.RateLimitedHandlerList.Configuration">
274+
<description>Configuration of requests rate limiter</description>
274275
<property name="maxClientsToTrack" value="${webdav.limits.max-blocked-clients}"/>
275276
<property name="globalRequestsPerSecond" value="${webdav.limits.rate.overall}"/>
276277
<property name="limitPercentagePerClient" value="${webdav.limits.rate.per-client.fractions}"/>

modules/dcache/src/main/java/org/dcache/util/jetty/RateLimitedHandlerList.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,23 +217,23 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
217217

218218
boolean blocked = blockedClients.getIfPresent(client) != null;
219219
if (blocked) {
220-
LOGGER.warn("Blocking client with too many auth errors {}", client);
220+
LOGGER.debug("Blocking client with too many auth errors {}", client);
221221
response.setStatus(HttpStatus.TOO_MANY_REQUESTS_429);
222222
response.getWriter().write("Server is busy. Please try again later.");
223223
baseRequest.setHandled(true);
224224
return;
225225
}
226226

227227
if (!getClientRateLimiter(client).tryAcquire()) {
228-
LOGGER.warn("Blocking client with too many requests {}", client);
228+
LOGGER.debug("Blocking client with too many requests {}", client);
229229
response.setStatus(HttpStatus.TOO_MANY_REQUESTS_429);
230230
response.getWriter().write("Server is busy. Please try again later.");
231231
baseRequest.setHandled(true);
232232
return;
233233
}
234234

235235
if (!globalRateLimiter.tryAcquire()) {
236-
LOGGER.warn("Blocking client due to globally too many requests {}", client);
236+
LOGGER.debug("Blocking client due to globally too many requests {}", client);
237237
response.setStatus(HttpStatus.TOO_MANY_REQUESTS_429);
238238
response.getWriter().write("Server is busy. Please try again later.");
239239
baseRequest.setHandled(true);
@@ -249,6 +249,7 @@ public void handle(String target, Request baseRequest, HttpServletRequest reques
249249
if (response.getStatus() >= 400 && response.getStatus() <= 407) {
250250
int errors = getClientErrorRateLimiter(client).incrementAndGet();
251251
if (errors >= maxErrorsPerClient) {
252+
LOGGER.warn("Blocking client due to too many auth errors or bad requests: {}", client);
252253
blockedClients.put(client, BLOCK);
253254
// as client blocked, no reason to keep track of further errors
254255
perClientErrorCount.invalidate(client);

skel/share/defaults/frontend.properties

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,58 @@ frontend.srr.shares =
655655
#
656656
frontend.stage.supported-sitenames=
657657

658+
659+
660+
# ---- Rate limiting and blocking of clients
661+
#
662+
# The rate limiting and blocking of clients is a mechanism to protect dCache from misbehaving clients.
663+
#
664+
# There are three limits that apply to clients (in defined order):
665+
#
666+
# - The maximum number of auth or permission errors a client may issue within a defined time window.
667+
# - The maximum number of the requests that a single client may issue.
668+
# - The maximum number of overall requests per second from all clients.
669+
#
670+
# If any of these limits is exceeded then the client will get 429 (Too Many Requests) responses for all subsequent requests
671+
#
672+
# The maximum number of requests from a single client is defined as a percentage of the overall maximum number of requests.
673+
# For example, if the overall maximum number of requests is 1000 request per second and the per-client fraction is 25%,
674+
# then a single client may issue up to 250 requests per second.
675+
#
676+
# Clients that exceed the rate limit, will be unblocked after webdav.limits.per-client.block.window.time.
677+
# Clients that exceed the error limit, will be unblocked after webdav.limits.block.window.time.
678+
# If clients blocked due to global rate limit, they will be unblocked as soon as the overall request rate drops below the limit.
679+
#
680+
681+
# The maximum number of clients that can be blocked at any one time. Preferably a power of 2.
682+
frontend.limits.max-blocked-clients = 1048576
683+
684+
# The maximum number of requests per second allowed from all clients.
685+
frontend.limits.rate.overall = 1000
686+
687+
# The maximum share of requests a single client is allowed, expressed as a percentage of the total requests across all clients.
688+
# For example, if this value is 25, and limit of overall requests is 1000 rps, then a single client may issue up to 25% of the
689+
# overall requests, which is 250 requests per second.
690+
frontend.limits.rate.per-client.fractions = 25
691+
692+
# The maximum number of errors allowed within a time window before client is blocked. If more than this number of auth
693+
# or permission errors are received from a client within the time window, the client is blocked.
694+
frontend.limits.error.max-allowed = 3
695+
frontend.limits.error.block.window.time = 20
696+
(one-of?MILLISECONDS|SECONDS|MINUTES|HOURS|DAYS)\
697+
frontend.limits.error.block.window.time.units = SECONDS
698+
699+
# The time window to block clients that exceed their request rate.
700+
frontend.limits.rate.per-client.block.window.time = 10
701+
(one-of?MILLISECONDS|SECONDS|MINUTES|HOURS|DAYS)\
702+
frontend.limits.rate.per-client.block.window.time.units = SECONDS
703+
704+
# The idle time window to reset limit or error counters. If a client does not issue
705+
# any requests within this time window, the counters are reset.
706+
frontend.limits.blocked-clients.idle-time = 10
707+
(one-of?MILLISECONDS|SECONDS|MINUTES|HOURS|DAYS)\
708+
frontend.limits.blocked-clients.idle-time.units = SECONDS
709+
658710
(deprecated)frontend.wellknown!wlcg-tape-rest-api.path = Use dcache.wellknown!wlcg-tape-rest-api.path instead
659711

660712
(obsolete)frontend.dcache-view.endpoints.webapi = Use frontend.static!dcache-view.endpoints.webapi instead

0 commit comments

Comments
 (0)