healthcheck: Ignore proxied requests

... so that operators can hide the healthcheck endpoint, which is
usually deployed without any auth mechanism, from users accessing
APIs through front-end load balancer or reverse proxy.

Note that this behavior is optional and can be enabled by
the new option.

Change-Id: Ib87da1b3d231dea44939686af544db101d68e179
This commit is contained in:
Takashi Kajinami 2023-11-17 13:32:07 +09:00
parent 81bd9ac7bb
commit 25f91a7b19
4 changed files with 57 additions and 2 deletions

View File

@ -395,6 +395,8 @@ Reason
self._source_ranges = [
ipaddress.ip_network(r)
for r in self._conf_get('allowed_source_ranges')]
self._ignore_proxied_requests = self._conf_get(
'ignore_proxied_requests')
self._backends = stevedore.NamedExtensionManager(
self.NAMESPACE, self._conf_get('backends'),
name_order=True, invoke_on_load=True,
@ -565,6 +567,13 @@ Reason
# the request in this middleware.
return None
if self._ignore_proxied_requests:
for hdr in [
'FORWARDED', 'FORWARDED_PROTO', 'FORWARDED_HOST',
'FORWARDED_FOR', 'FORWARDED_PREFIX']:
if req.environ.get("HTTP_X_%s" % hdr):
return None
results = [ext.obj.healthcheck(req.server_port)
for ext in self._backends]
healthy = self._are_results_healthy(results)

View File

@ -34,6 +34,9 @@ HEALTHCHECK_OPTS = [
help='A list of network addresses to limit source ip allowed '
'to access healthcheck information. Any request from ip '
'outside of these network addresses are ignored.'),
cfg.BoolOpt('ignore_proxied_requests',
default=False,
help='Ignore requests with proxy headers.')
]

View File

@ -63,10 +63,13 @@ class HealthcheckTests(test_base.BaseTestCase):
def _do_test_request(self, conf={}, path='/healthcheck',
accept='text/plain', method='GET',
server_port=80, remote_addr='127.0.0.1'):
server_port=80, headers=None,
remote_addr='127.0.0.1'):
self.app = healthcheck.Healthcheck(self.application, conf)
req = webob.Request.blank(path, accept=accept, method=method)
req.server_port = server_port
if headers:
req.headers = headers
req.remote_addr = remote_addr
res = req.get_response(self.app)
return res
@ -74,10 +77,12 @@ class HealthcheckTests(test_base.BaseTestCase):
def _do_test(self, conf={}, path='/healthcheck',
expected_code=webob.exc.HTTPOk.code,
expected_body=b'', accept='text/plain',
method='GET', server_port=80, remote_addr='127.0.0.1'):
method='GET', server_port=80, headers=None,
remote_addr='127.0.0.1'):
res = self._do_test_request(conf=conf, path=path,
accept=accept, method=method,
server_port=server_port,
headers=headers,
remote_addr=remote_addr)
self.assertEqual(expected_code, res.status_int)
self.assertEqual(expected_body, res.body)
@ -215,3 +220,28 @@ class HealthcheckTests(test_base.BaseTestCase):
expected_code=webob.exc.HTTPOk.code,
expected_body=b'Hello, World!!!',
remote_addr='192.168.3.1')
def test_proxied_not_ignored(self):
conf = {}
self._do_test(conf,
expected_code=webob.exc.HTTPOk.code,
headers={'Forwarded-For': 'http://localhost'})
def test_proxied_ignored(self):
conf = {'ignore_proxied_requests': True}
modern_headers = {
'x-forwarded': 'https://localhost'
}
self._do_test(conf,
expected_code=webob.exc.HTTPOk.code,
expected_body=b'Hello, World!!!',
headers=modern_headers)
legacy_headers = {
'x-forwarded-proto': 'https',
'x-forwarded-host': 'localhost',
'x-forwarded-for': '192.0.2.11',
}
self._do_test(conf,
expected_code=webob.exc.HTTPOk.code,
expected_body=b'Hello, World!!!',
headers=legacy_headers)

View File

@ -0,0 +1,13 @@
---
features:
- |
The new ``[healthcheck] ignore_proxied_requests`` option has been added.
When this option is set to true, the healthcheck middleware ignores
requests with any of the following headers, which indicates that
the requests came through a reverse proxy or a load balancer.
- ``x-forwarded``
- ``x-forwarded-proto``
- ``x-forwarded-host``
- ``x-forwarded-for``
- ``x-forwarded-prefix``