Skip to content

Commit 296a02e

Browse files
committed
Different rate limits depending on HTTP method (#5555)
1 parent 69d94f4 commit 296a02e

File tree

2 files changed

+221
-1
lines changed

2 files changed

+221
-1
lines changed

src/azul/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1774,6 +1774,25 @@ def __attrs_post_init__(self):
17741774
name='rate_limit_alarm',
17751775
value=waf_rate_limit.value * 2)
17761776

1777+
#: The rate limit per IP for requests that trigger a manifest generation
1778+
#:
1779+
waf_rate_limit_manifests = RateLimit(name='rate_limit_manifests',
1780+
value=10,
1781+
period=10 * 60,
1782+
retry_after=30)
1783+
1784+
#: The rate limit for file download requests
1785+
#:
1786+
#: We aim for a global limit of 60 file downloads per 10 minutes. Based on
1787+
#: an observed average of 2.9 distinct IPs concurrently downloading files
1788+
#: in any 10-minute window, the maximum per-IP request rate we can allow is
1789+
#: 20/10min, or 10/5min.
1790+
#:
1791+
waf_rate_limit_files = RateLimit(name='rate_limit_files',
1792+
value=10,
1793+
period=5 * 60,
1794+
retry_after=30)
1795+
17771796
@property
17781797
def waf_bot_control(self) -> bool:
17791798
return self._boolean(self.environ['azul_waf_bot_control'])

terraform/api_gateway.tf.json.template.py

Lines changed: 202 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22
dataclass,
33
)
44
import importlib
5+
from itertools import (
6+
chain,
7+
)
58
import json
9+
import logging
610

711
from more_itertools import (
812
one,
@@ -20,6 +24,9 @@
2024
from azul.deployment import (
2125
aws,
2226
)
27+
from azul.http import (
28+
http_client,
29+
)
2330
from azul.modules import (
2431
load_app_module,
2532
)
@@ -35,6 +42,8 @@
3542
JSONs,
3643
)
3744

45+
log = logging.getLogger(__name__)
46+
3847

3948
@dataclass(frozen=True)
4049
class Application:
@@ -110,6 +119,21 @@ def check_waf_rules(rules: JSONs) -> JSONs:
110119
return rules
111120

112121

122+
def user_ip() -> str:
123+
"""
124+
Return the user's public IP address in CIDR notation.
125+
"""
126+
url = 'https://checkip.amazonaws.com'
127+
http = http_client(log)
128+
response = http.request('GET', url)
129+
if response.status != 200:
130+
raise RuntimeError('Unexpected response %s' % url, response.status)
131+
else:
132+
ip_address = response.data.decode().strip()
133+
log.info('Determined IP address: %s', ip_address)
134+
return ip_address + '/32'
135+
136+
113137
zones_by_domain = {
114138
domain: Zone.for_domain(domain)
115139
for app in apps
@@ -147,6 +171,17 @@ def check_waf_rules(rules: JSONs) -> JSONs:
147171
emit_tf({
148172
'data': [
149173
{
174+
'aws_nat_gateway': {
175+
**{
176+
f'gitlab_{zone}': {
177+
'filter': {
178+
'name': 'tag:Name',
179+
'values': [f'azul-gitlab_{zone}']
180+
},
181+
}
182+
for zone in range(vpc.num_zones)
183+
}
184+
},
150185
'aws_route53_zone': {
151186
zone.slug: {
152187
'name': zone.name,
@@ -223,6 +258,22 @@ def check_waf_rules(rules: JSONs) -> JSONs:
223258
],
224259
'resource': [
225260
{
261+
'aws_wafv2_ip_set': {
262+
'it_v4_ips': {
263+
'name': config.qualified_resource_name('it_v4_ips'),
264+
'scope': 'REGIONAL',
265+
'ip_address_version': 'IPV4',
266+
'addresses': list(chain(
267+
[
268+
f'${{data.aws_nat_gateway.gitlab_{zone}.public_ip}}/32'
269+
for zone in range(vpc.num_zones)
270+
],
271+
[
272+
user_ip(),
273+
]
274+
))
275+
}
276+
},
226277
'aws_wafv2_web_acl': {
227278
'api_gateway': {
228279
'name': config.qualified_resource_name('api_gateway'),
@@ -501,7 +552,157 @@ def check_waf_rules(rules: JSONs) -> JSONs:
501552
config.waf_rate_limit_alarm,
502553
config.waf_rate_limit,
503554
]
504-
]
555+
],
556+
{
557+
'name': 'allow_it_requests',
558+
'statement': {
559+
'and_statement': [
560+
{
561+
'statement': [
562+
{
563+
'ip_set_reference_statement': {
564+
'arn': '${aws_wafv2_ip_set.%s.arn}' % 'it_v4_ips'
565+
}
566+
},
567+
{
568+
'byte_match_statement': {
569+
'field_to_match': {
570+
'method': {}
571+
},
572+
'positional_constraint': 'EXACTLY',
573+
'search_string': 'PUT',
574+
'text_transformation': {
575+
'priority': 0,
576+
'type': 'NONE'
577+
}
578+
}
579+
},
580+
{
581+
'regex_match_statement': {
582+
'regex_string': '^(/fetch)?/manifest/files',
583+
'field_to_match': {
584+
'uri_path': {}
585+
},
586+
'text_transformation': {
587+
'priority': 0,
588+
'type': 'NONE'
589+
}
590+
}
591+
}
592+
]
593+
}
594+
]
595+
},
596+
'action': {
597+
'allow': {}
598+
},
599+
'visibility_config': {
600+
'metric_name': 'allow_it_requests',
601+
'sampled_requests_enabled': True,
602+
'cloudwatch_metrics_enabled': True
603+
}
604+
},
605+
{
606+
'name': config.waf_rate_limit_files.name,
607+
'statement': {
608+
'rate_based_statement': {
609+
'limit': config.waf_rate_limit_files.value,
610+
'evaluation_window_sec': config.waf_rate_limit_files.period,
611+
'aggregate_key_type': 'IP',
612+
'scope_down_statement': {
613+
'regex_match_statement': {
614+
'regex_string': '^(/fetch)?/repository/files',
615+
'field_to_match': {
616+
'uri_path': {}
617+
},
618+
'text_transformation': {
619+
'priority': 0,
620+
'type': 'NONE'
621+
}
622+
}
623+
}
624+
}
625+
},
626+
'action': {
627+
'block': {
628+
'custom_response': {
629+
'response_code': 429,
630+
'response_header': [
631+
{
632+
'name': 'Retry-After',
633+
'value': str(config.waf_rate_limit_files.retry_after)
634+
}
635+
]
636+
}
637+
}
638+
},
639+
'visibility_config': {
640+
'metric_name': config.waf_rate_limit_files.name,
641+
'sampled_requests_enabled': True,
642+
'cloudwatch_metrics_enabled': True
643+
}
644+
},
645+
{
646+
'name': config.waf_rate_limit_manifests.name,
647+
'statement': {
648+
'rate_based_statement': {
649+
'limit': config.waf_rate_limit_manifests.value,
650+
'evaluation_window_sec': config.waf_rate_limit_manifests.period,
651+
'aggregate_key_type': 'IP',
652+
'scope_down_statement': {
653+
'and_statement': [
654+
{
655+
'statement': [
656+
{
657+
'byte_match_statement': {
658+
'field_to_match': {
659+
'method': {}
660+
},
661+
'positional_constraint': 'EXACTLY',
662+
'search_string': 'PUT',
663+
'text_transformation': {
664+
'priority': 0,
665+
'type': 'NONE'
666+
}
667+
}
668+
},
669+
{
670+
'regex_match_statement': {
671+
'regex_string': '^(/fetch)?/manifest/files',
672+
'field_to_match': {
673+
'uri_path': {}
674+
},
675+
'text_transformation': {
676+
'priority': 0,
677+
'type': 'NONE'
678+
}
679+
}
680+
}
681+
]
682+
}
683+
]
684+
}
685+
}
686+
},
687+
'action': {
688+
'block': {
689+
'custom_response': {
690+
'response_code': 429,
691+
'response_header': [
692+
{
693+
'name': 'Retry-After',
694+
'value': str(config.waf_rate_limit_manifests.retry_after)
695+
}
696+
]
697+
}
698+
}
699+
},
700+
'visibility_config': {
701+
'metric_name': config.waf_rate_limit_manifests.name,
702+
'sampled_requests_enabled': True,
703+
'cloudwatch_metrics_enabled': True
704+
}
705+
}
505706
])
506707
]),
507708
'scope': 'REGIONAL',

0 commit comments

Comments
 (0)