healthcheck: Limit source IP range

This change introduces the new [healthcheck] allowed_source_ranges
option, to restrict access to healthcheck endpoint within specific
network ranges. This parameter is useful to avoid access from external
network, because healthcheck endpoint has no authentication usually.

Change-Id: I2b88704c260edd1a4c49cfde9de4cee4b90be862
This commit is contained in:
Takashi Kajinami 2021-07-12 14:44:44 +09:00
parent 43ab17010a
commit 9ce08a6f0f
4 changed files with 44 additions and 3 deletions

View File

@ -16,6 +16,7 @@
import collections
import gc
import io
import ipaddress
import json
import platform
import socket
@ -391,6 +392,9 @@ Reason
group='healthcheck')
self._path = self._conf_get('path')
self._show_details = self._conf_get('detailed')
self._source_ranges = [
ipaddress.ip_network(r)
for r in self._conf_get('allowed_source_ranges')]
self._backends = stevedore.NamedExtensionManager(
self.NAMESPACE, self._conf_get('backends'),
name_order=True, invoke_on_load=True,
@ -550,6 +554,17 @@ Reason
def process_request(self, req):
if not self._ignore_path and req.path != self._path:
return None
if self._source_ranges:
remote_addr = ipaddress.ip_address(req.remote_addr)
for r in self._source_ranges:
if r.version == remote_addr.version and remote_addr in r:
break
else:
# Because source ip is not included in allowed ranges, ignore
# the request in this middleware.
return None
results = [ext.obj.healthcheck(req.server_port)
for ext in self._backends]
healthy = self._are_results_healthy(results)

View File

@ -29,6 +29,11 @@ HEALTHCHECK_OPTS = [
default=[],
help='Additional backends that can perform health checks and '
'report that information back as part of a request.'),
cfg.ListOpt('allowed_source_ranges',
default=[],
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.'),
]

View File

@ -63,20 +63,22 @@ class HealthcheckTests(test_base.BaseTestCase):
def _do_test_request(self, conf={}, path='/healthcheck',
accept='text/plain', method='GET',
server_port=80):
server_port=80, 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
req.remote_addr = remote_addr
res = req.get_response(self.app)
return res
def _do_test(self, conf={}, path='/healthcheck',
expected_code=webob.exc.HTTPOk.code,
expected_body=b'', accept='text/plain',
method='GET', server_port=80):
method='GET', server_port=80, remote_addr='127.0.0.1'):
res = self._do_test_request(conf=conf, path=path,
accept=accept, method=method,
server_port=server_port)
server_port=server_port,
remote_addr=remote_addr)
self.assertEqual(expected_code, res.status_int)
self.assertEqual(expected_body, res.body)
@ -200,3 +202,16 @@ class HealthcheckTests(test_base.BaseTestCase):
sort_keys=True).encode('utf-8')
self._do_test(expected_body=expected_body,
accept='application/json')
def test_source_within_allowed_ranges(self):
conf = {'allowed_source_ranges': ['192.168.0.0/24', '192.168.1.0/24']}
self._do_test(conf,
expected_code=webob.exc.HTTPOk.code,
remote_addr='192.168.0.1')
def test_source_out_of_allowed_ranges(self):
conf = {'allowed_source_ranges': ['192.168.0.0/24', '192.168.1.0/24']}
self._do_test(conf,
expected_code=webob.exc.HTTPOk.code,
expected_body=b'Hello, World!!!',
remote_addr='192.168.3.1')

View File

@ -0,0 +1,6 @@
---
features:
- |
The new ``[healthcheck] allowed_source_ranges`` parameter has been added.
This parameter defines a list of network ranges from which access to
``/healthcheck`` endpoint is allowed.