diff --git a/etc/octavia.conf b/etc/octavia.conf index dcc55ccb66..5361bdf9a8 100644 --- a/etc/octavia.conf +++ b/etc/octavia.conf @@ -161,6 +161,9 @@ # List of network_ids that are valid for VIP creation. # If this field empty, no validation is performed. # valid_vip_networks = +# List of reserved IP addresses that cannot be used for member addresses +# The default is the nova metadata service address +# reserved_ips = ['169.254.169.254'] [haproxy_amphora] # base_path = /var/lib/octavia diff --git a/octavia/api/v2/controllers/member.py b/octavia/api/v2/controllers/member.py index 0e6b72d133..4252038be2 100644 --- a/octavia/api/v2/controllers/member.py +++ b/octavia/api/v2/controllers/member.py @@ -135,6 +135,9 @@ class MemberController(base.BaseController): """Creates a pool member on a pool.""" member = member_.member context = pecan.request.context.get('octavia_context') + + validate.ip_not_reserved(member.address) + # Validate member subnet if member.subnet_id and not validate.subnet_exists(member.subnet_id): raise exceptions.NotFound(resource='Subnet', diff --git a/octavia/common/config.py b/octavia/common/config.py index 67884ff929..af3d2b0b22 100644 --- a/octavia/common/config.py +++ b/octavia/common/config.py @@ -165,6 +165,12 @@ networking_opts = [ help=_('List of network_ids that are valid for VIP ' 'creation. If this field is empty, no validation ' 'is performed.')), + cfg.ListOpt('reserved_ips', + default=['169.254.169.254'], + item_type=cfg.types.IPAddress(), + help=_('List of IP addresses reserved from being used for ' + 'member addresses. IPv6 addresses should be in ' + 'expanded, uppercase form.')), ] healthmanager_opts = [ diff --git a/octavia/common/validate.py b/octavia/common/validate.py index 51aec677be..7db6d3a0e3 100644 --- a/octavia/common/validate.py +++ b/octavia/common/validate.py @@ -19,11 +19,13 @@ Defined here so these can also be used at deeper levels than the API. """ +import ipaddress import re import netaddr from oslo_config import cfg import rfc3986 +import six from octavia.common import constants from octavia.common import exceptions @@ -322,3 +324,11 @@ def check_session_persistence(SP_dict): except Exception: raise exceptions.ValidationException(detail=_( 'Invalid session_persistence provided.')) + + +def ip_not_reserved(ip_address): + ip_address = ( + ipaddress.ip_address(six.text_type(ip_address)).exploded.upper()) + if ip_address in CONF.networking.reserved_ips: + raise exceptions.InvalidOption(value=ip_address, + option='member address') diff --git a/octavia/tests/unit/common/test_validations.py b/octavia/tests/unit/common/test_validations.py index a80330ea92..c1a8204ca8 100644 --- a/octavia/tests/unit/common/test_validations.py +++ b/octavia/tests/unit/common/test_validations.py @@ -414,3 +414,31 @@ class TestValidations(base.TestCase): self.assertRaises(exceptions.ValidationException, validate.check_session_persistence, valid_cookie_name_dict) + + def test_ip_not_reserved(self): + self.conf.config(group="networking", reserved_ips=['198.51.100.4']) + + # Test good address + validate.ip_not_reserved('203.0.113.5') + + # Test IPv4 reserved address + self.assertRaises(exceptions.InvalidOption, + validate.ip_not_reserved, + '198.51.100.4') + + self.conf.config( + group="networking", + reserved_ips=['2001:0DB8:0000:0000:0000:0000:0000:0005']) + + # Test good IPv6 address + validate.ip_not_reserved('2001:0DB8::9') + + # Test reserved IPv6 expanded + self.assertRaises(exceptions.InvalidOption, + validate.ip_not_reserved, + '2001:0DB8:0000:0000:0000:0000:0000:0005') + + # Test reserved IPv6 short hand notation + self.assertRaises(exceptions.InvalidOption, + validate.ip_not_reserved, + '2001:0DB8::5') diff --git a/releasenotes/notes/reserved-ips-7ef3a63ab0b6b28a.yaml b/releasenotes/notes/reserved-ips-7ef3a63ab0b6b28a.yaml new file mode 100644 index 0000000000..c75d02e7ff --- /dev/null +++ b/releasenotes/notes/reserved-ips-7ef3a63ab0b6b28a.yaml @@ -0,0 +1,6 @@ +--- +security: + - | + Adds a configuration option, "reserved_ips" that allows the operator to + block addresses from being used in load balancer members. The default + setting blocks the nova metadata service address.