Add support to Octavia ACLs

Since Train, Octavia has a new API to restrict lbs access on
listeners. This is important when enforcing Network Policies
on services.

Before this patch, Kuryr required either admin priviledges to
change the security group rules associated to the loadbalancer,
or use the ovn-octavia loadbalancer that does not require those
rules as the source IP is not changed when passing through the
LoadBalancer VIP.

By adopting the new Octavia ACL API, there is no need for admin
priviledges to limit the access to the loadbalancers.

Implements: blueprint octavia-acls
Change-Id: I8f6bae00413aa181e9c2cac72c87bd93161796bc
This commit is contained in:
Luis Tomas Bolivar 2020-01-02 18:21:21 +01:00
parent c213d926d3
commit 0e581caa2d
2 changed files with 108 additions and 4 deletions

View File

@ -55,6 +55,7 @@ _L7_POLICY_ACT_REDIRECT_TO_POOL = 'REDIRECT_TO_POOL'
_LB_STS_POLL_FAST_INTERVAL = 1 _LB_STS_POLL_FAST_INTERVAL = 1
_LB_STS_POLL_SLOW_INTERVAL = 3 _LB_STS_POLL_SLOW_INTERVAL = 3
_OCTAVIA_TAGGING_VERSION = 2, 5 _OCTAVIA_TAGGING_VERSION = 2, 5
_OCTAVIA_ACL_VERSION = 2, 12
class LBaaSv2Driver(base.LBaaSDriver): class LBaaSv2Driver(base.LBaaSDriver):
@ -64,12 +65,16 @@ class LBaaSv2Driver(base.LBaaSDriver):
super(LBaaSv2Driver, self).__init__() super(LBaaSv2Driver, self).__init__()
self._octavia_tags = False self._octavia_tags = False
self._octavia_acls = False
# Check if Octavia API supports tagging. # Check if Octavia API supports tagging.
# TODO(dulek): *Maybe* this can be replaced with # TODO(dulek): *Maybe* this can be replaced with
# lbaas.get_api_major_version(version=_OCTAVIA_TAGGING_VERSION) # lbaas.get_api_major_version(version=_OCTAVIA_TAGGING_VERSION)
# if bug https://storyboard.openstack.org/#!/story/2007040 gets # if bug https://storyboard.openstack.org/#!/story/2007040 gets
# fixed one day. # fixed one day.
v = self.get_octavia_version() v = self.get_octavia_version()
if v >= _OCTAVIA_ACL_VERSION:
self._octavia_acls = True
LOG.info('Octavia supports ACLs for Amphora provider.')
if v >= _OCTAVIA_TAGGING_VERSION: if v >= _OCTAVIA_TAGGING_VERSION:
LOG.info('Octavia supports resource tags.') LOG.info('Octavia supports resource tags.')
self._octavia_tags = True self._octavia_tags = True
@ -194,8 +199,58 @@ class LBaaSv2Driver(base.LBaaSDriver):
LOG.exception('Failed when creating security group rule ' LOG.exception('Failed when creating security group rule '
'for listener %s.', listener.name) 'for listener %s.', listener.name)
def _create_listeners_acls(self, loadbalancer, port, target_port,
protocol, lb_sg, new_sgs, listener_id):
all_pod_rules = []
add_default_rules = False
neutron = clients.get_neutron_client()
if new_sgs:
sgs = new_sgs
else:
sgs = loadbalancer.security_groups
# Check if Network Policy allows listener on the pods
for sg in sgs:
if sg != lb_sg:
if sg in config.CONF.neutron_defaults.pod_security_groups:
# If default sg is set, this means there is no NP
# associated to the service, thus falling back to the
# default listener rules
add_default_rules = True
break
rules = neutron.list_security_group_rules(
security_group_id=sg)
for rule in rules['security_group_rules']:
# NOTE(ltomasbo): NP sg can only have rules with
# or without remote_ip_prefix. Rules with remote_group_id
# are not possible, therefore only applying the ones
# with or without remote_ip_prefix.
if rule.get('remote_group_id'):
continue
if (rule['protocol'] == protocol.lower() and
rule['direction'] == 'ingress'):
# If listener port not in allowed range, skip
min_port = rule.get('port_range_min')
max_port = rule.get('port_range_max')
if (min_port and target_port not in range(min_port,
max_port+1)):
continue
if rule.get('remote_ip_prefix'):
all_pod_rules.append(rule['remote_ip_prefix'])
else:
add_default_rules = True
if add_default_rules:
# update the listener without allowed-cidr
self._update_listener_acls(loadbalancer, listener_id, None)
else:
self._update_listener_acls(loadbalancer, listener_id,
all_pod_rules)
def _apply_members_security_groups(self, loadbalancer, port, target_port, def _apply_members_security_groups(self, loadbalancer, port, target_port,
protocol, sg_rule_name, new_sgs=None): protocol, sg_rule_name, listener_id,
new_sgs=None):
LOG.debug("Applying members security groups.") LOG.debug("Applying members security groups.")
neutron = clients.get_neutron_client() neutron = clients.get_neutron_client()
lb_sg = None lb_sg = None
@ -216,6 +271,11 @@ class LBaaSv2Driver(base.LBaaSDriver):
if not lb_sg: if not lb_sg:
return return
if self._octavia_acls:
self._create_listeners_acls(loadbalancer, port, target_port,
protocol, lb_sg, new_sgs, listener_id)
return
lbaas_sg_rules = neutron.list_security_group_rules( lbaas_sg_rules = neutron.list_security_group_rules(
security_group_id=lb_sg) security_group_id=lb_sg)
all_pod_rules = [] all_pod_rules = []
@ -529,8 +589,10 @@ class LBaaSv2Driver(base.LBaaSDriver):
if network_policy and listener_port: if network_policy and listener_port:
protocol = pool.protocol protocol = pool.protocol
sg_rule_name = pool.name sg_rule_name = pool.name
listener_id = pool.listener_id
self._apply_members_security_groups(loadbalancer, listener_port, self._apply_members_security_groups(loadbalancer, listener_port,
port, protocol, sg_rule_name) port, protocol, sg_rule_name,
listener_id)
return result return result
def release_member(self, loadbalancer, member): def release_member(self, loadbalancer, member):
@ -632,6 +694,32 @@ class LBaaSv2Driver(base.LBaaSDriver):
listener.id = response.id listener.id = response.id
return listener return listener
def _update_listener_acls(self, loadbalancer, listener_id, allowed_cidrs):
admin_state_up = True
if allowed_cidrs is None:
# World accessible, no restriction on the listeners
pass
elif len(allowed_cidrs) == 0:
# Prevent any traffic as no CIDR is allowed
admin_state_up = False
request = {
'allowed_cidrs': allowed_cidrs,
'admin_state_up': admin_state_up,
}
# Wait for the loadbalancer to be ACTIVE
self._wait_for_provisioning(loadbalancer, _ACTIVATION_TIMEOUT,
_LB_STS_POLL_FAST_INTERVAL)
lbaas = clients.get_loadbalancer_client()
response = lbaas.put(o_lis.Listener.base_path + '/' + listener_id,
json={o_lis.Listener.resource_key: request})
if not response.ok:
LOG.error('Error when updating %s: %s',
o_lis.Listener.resource_key, response.text)
raise k_exc.ResourceNotReady(listener_id)
def _find_listener(self, listener): def _find_listener(self, listener):
lbaas = clients.get_loadbalancer_client() lbaas = clients.get_loadbalancer_client()
response = lbaas.listeners( response = lbaas.listeners(
@ -1003,12 +1091,19 @@ class LBaaSv2Driver(base.LBaaSDriver):
utils.set_lbaas_state(endpoint, lbaas) utils.set_lbaas_state(endpoint, lbaas)
lsnr_ids = {(l.protocol, l.port): l.id for l in lbaas.listeners}
for port in svc_ports: for port in svc_ports:
port_protocol = port['protocol'] port_protocol = port['protocol']
lbaas_port = port['port'] lbaas_port = port['port']
target_port = port['targetPort'] target_port = port['targetPort']
sg_rule_name = "%s:%s:%s" % (lbaas_name, port_protocol, lbaas_port) sg_rule_name = "%s:%s:%s" % (lbaas_name, port_protocol, lbaas_port)
listener_id = lsnr_ids.get((port_protocol, target_port))
if listener_id is None:
LOG.warning("There is no listener associated to the protocol "
"%s and port %s. Skipping", port_protocol,
lbaas_port)
continue
self._apply_members_security_groups(lbaas_obj, lbaas_port, self._apply_members_security_groups(lbaas_obj, lbaas_port,
target_port, port_protocol, target_port, port_protocol,
sg_rule_name, sgs) sg_rule_name, listener_id, sgs)

View File

@ -0,0 +1,9 @@
---
features:
- |
Added support for Octavia VIP access control list. This new Octavia API
allows users to limit incomming traffic to a set of allowed CIDRs. Kuryr
uses this to enforce Network Policies on services, changing the security
group associated to the Load Balancer through this new API instead of
directly. Thanks to it, Kuryr no longer needs admin priviledges to
restrict the access to the loadbalancers VIPs some details.