Introduce source IP based rate limiting

Since we are running haproxy in L4, we are tracking the incoming
byte rate from client IPs and rejecting TCP connections in a
sliding window.

This approach limits the incoming HTTP requests however image uploading
through the horizon web app is unaffected.

Change-Id: Ie40d28acb2dc2983fc9edbbeacfd671b380a8f6d
Closes-Bug: #1836514
Signed-off-by: Mert Kırpıcı <mert.kirpici@canonical.com>
This commit is contained in:
Mert Kırpıcı 2022-07-20 13:55:38 +00:00
parent e47ecb9359
commit c0f8708761
4 changed files with 54 additions and 0 deletions

View File

@ -291,6 +291,24 @@ options:
default: False
description: |
If True, exposes stats interface externally.
haproxy-rate-limiting-enabled:
type: boolean
default: False
description: |
If True, imposes source IP based rate limits on accessing the dashboard.
The actual limits are controlled through the configuration options:
haproxy-limit-period and haproxy-max-bytes-in-rate.
haproxy-max-bytes-in-rate:
type: int
default: 500000
description: |
The number of bytes the client is allowed to send through the connection
during one limit period.
haproxy-limit-period:
type: int
default: 10
description: |
The number of seconds over the number of bytes are counted.
enforce-ssl:
type: boolean
default: False

View File

@ -87,6 +87,14 @@ class HorizonHAProxyContext(OSContextGenerator):
'prefer_ipv6': config('prefer-ipv6'),
'haproxy_expose_stats': config('haproxy-expose-stats')
}
if config('haproxy-rate-limiting-enabled'):
ctxt['haproxy_rate_limiting_enabled'] = \
config('haproxy-rate-limiting-enabled')
ctxt['haproxy_max_bytes_in_rate'] = \
config('haproxy-max-bytes-in-rate')
ctxt['haproxy_limit_period'] = config('haproxy-limit-period')
return ctxt

View File

@ -51,6 +51,11 @@ listen {{ service }}
{%- endif %}
balance source
option tcplog
{% if haproxy_rate_limiting_enabled -%}
stick-table type ip size 100k store bytes_in_rate({{ haproxy_limit_period }}s)
tcp-request connection track-sc0 src
tcp-request connection reject if { sc_bytes_in_rate(0) gt {{ haproxy_max_bytes_in_rate }} }
{% endif -%}
{% for unit, address in units.items() -%}
server {{ unit }} {{ address }}:{{ ports[1] }} check
{% endfor %}

View File

@ -1239,6 +1239,29 @@ class TestHorizonContexts(CharmTestCase):
_open.assert_called_with('/etc/default/haproxy', 'w')
self.assertTrue(_file.write.called)
def test_HorizonHAProxyContext_rate_limiting(self):
limiting = True
max_bytes_in = 100000
limit_period = 42
self.test_config.set('haproxy-rate-limiting-enabled', limiting)
self.test_config.set('haproxy-max-bytes-in-rate', max_bytes_in)
self.test_config.set('haproxy-limit-period', limit_period)
self.relation_ids.return_value = []
self.local_unit.return_value = 'openstack-dashboard/0'
self.get_relation_ip.return_value = "10.5.0.1"
with patch_open() as (_open, _file):
self.assertEquals(horizon_contexts.HorizonHAProxyContext()(),
{'units': {'openstack-dashboard-0': '10.5.0.1'},
'service_ports': {'dash_insecure': [80, 70],
'dash_secure': [443, 433]},
'prefer_ipv6': False,
'haproxy_expose_stats': False,
'haproxy_rate_limiting_enabled': limiting,
'haproxy_max_bytes_in_rate': max_bytes_in,
'haproxy_limit_period': limit_period})
_open.assert_called_with('/etc/default/haproxy', 'w')
self.assertTrue(_file.write.called)
def test_RouterSettingContext(self):
self.test_config.set('profile', 'cisco')
self.assertEqual(horizon_contexts.RouterSettingContext()(),