From e7f9132571455707f7b44ce58e2445e46a20810f Mon Sep 17 00:00:00 2001 From: Timotej Lazar Date: Fri, 5 Apr 2024 06:00:50 +0200 Subject: [PATCH] proxmox: set up firewall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Firewall policy is set in NetBox as cluster services¹. For Proxmox we have to manually allow communication between nodes when using L3, since the default management ipset does not get populated correctly. We also need to open VTEP communication between nodes, which the default rules don’t. We allow all inter-node traffic, as SSH without passwords must be permitted anyway. This also adds some helper filters that are spectacularly annoying to implement purely in templates. ¹ There is actually no such thing as as a cluster service (yet?), so instead we create a fake VM for the cluster, define services for it, and then add the same services to a custom field on the cluster. Alternative would be to tie services to a specific node, but that could be problematic if that node is replaced. --- filter_plugins/netbox.py | 34 ++++++++++++++++++++++++++- roles/proxmox/tasks/firewall.yml | 12 ++++++++++ roles/proxmox/tasks/main.yml | 6 +++++ roles/proxmox/templates/cluster.fw.j2 | 23 ++++++++++++++++++ 4 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 roles/proxmox/tasks/firewall.yml create mode 100644 roles/proxmox/templates/cluster.fw.j2 diff --git a/filter_plugins/netbox.py b/filter_plugins/netbox.py index db0c629..b519b3f 100644 --- a/filter_plugins/netbox.py +++ b/filter_plugins/netbox.py @@ -1,10 +1,19 @@ #!/usr/bin/python +import os +import pynetbox + class FilterModule(object): '''Various utilities for manipulating NetBox data''' + + def __init__(self): + self.nb = pynetbox.api(os.getenv('NETBOX_API'), os.getenv('NETBOX_TOKEN')) + def filters(self): return { - 'device_address': self.device_address + 'device_address': self.device_address, + 'compact_numlist': self.compact_numlist, + 'allowed_prefixes': self.allowed_prefixes } def device_address(self, device): @@ -13,3 +22,26 @@ class FilterModule(object): for addr in iface['ip_addresses']: if addr.get('role') and addr['role'].get('value') == 'loopback': yield addr + + def compact_numlist(self, nums, delimiter=',', range_delimiter='-'): + '''Transform [1,2,3,5,7,8,9] into "1-3,5,7-9"''' + i = 0 + spans = [] + while i < len(nums): + j = i + 1 + while j < len(nums) and nums[j]-nums[i] == j-i: + j += 1 + spans += [f'{nums[i]}{range_delimiter}{nums[j-1]}' if j > i+1 else f'{nums[i]}'] + i = j + return delimiter.join(spans) + + def allowed_prefixes(self, service): + '''Return a list of allowed IP prefixes for the given service''' + service_data = self.nb.ipam.services.get(service['id']).custom_fields + if service_data['allowed_prefixes']: + yield from self.nb.ipam.prefixes.filter(id=[prefix['id'] for prefix in service_data['allowed_prefixes']]) + if service_data['allowed_vlans']: + yield from self.nb.ipam.prefixes.filter(vlan_id=[vlan['id'] for vlan in service_data['allowed_vlans']]) + if service_data['allowed_clusters']: + for device in self.nb.dcim.devices.filter(cluster_id=[cluster['id'] for cluster in service_data['allowed_clusters']]): + yield from self.nb.ipam.ip_addresses.filter(role='loopback', device_id=device.id) diff --git a/roles/proxmox/tasks/firewall.yml b/roles/proxmox/tasks/firewall.yml new file mode 100644 index 0000000..32c3328 --- /dev/null +++ b/roles/proxmox/tasks/firewall.yml @@ -0,0 +1,12 @@ +- name: Retrieve service list + set_fact: + services: '{{ query("netbox.netbox.nb_lookup", "clusters", raw_data=true, api_filter="name="+cluster) | map(attribute="custom_fields.services") | flatten }}' + +- name: Set up firewall + template: + dest: /etc/pve/firewall/cluster.fw + src: cluster.fw.j2 + mode: 0640 + owner: root + group: www-data + diff --git a/roles/proxmox/tasks/main.yml b/roles/proxmox/tasks/main.yml index 9007844..e2460ef 100644 --- a/roles/proxmox/tasks/main.yml +++ b/roles/proxmox/tasks/main.yml @@ -1,3 +1,7 @@ +- name: Get all nodes in my cluster + set_fact: + nodes: "{{ groups['cluster_'+cluster] | map('extract', hostvars) }}" + - name: Disable enterprise repositories apt_repository: repo: '{{ item }}' @@ -40,4 +44,6 @@ - include_tasks: mgmt.yml +- include_tasks: firewall.yml + - include_tasks: frr.yml diff --git a/roles/proxmox/templates/cluster.fw.j2 b/roles/proxmox/templates/cluster.fw.j2 new file mode 100644 index 0000000..5f98e79 --- /dev/null +++ b/roles/proxmox/templates/cluster.fw.j2 @@ -0,0 +1,23 @@ +[OPTIONS] + +enable: 1 + +[RULES] + +IN Ping(ACCEPT) -log nolog # don’t be rude +IN SSH(ACCEPT) -i mgmt # for ansible etc. +IN ACCEPT -source {{ nodes | map('device_address') | flatten | selectattr('family.value', '==', 4) | map(attribute='address') | join(',') }} # my cluster +IN ACCEPT -source {{ nodes | map('device_address') | flatten | selectattr('family.value', '==', 6) | map(attribute='address') | join(',') }} # my cluster +{% for service in services %} +{% set prefixes = service | allowed_prefixes %} +{% set prefixes4 = prefixes | selectattr('family.value', '==', 4) | map('string') %} +{% set prefixes6 = prefixes | selectattr('family.value', '==', 6) | map('string') %} +{% set ports = service.ports | compact_numlist(range_delimiter=':') %} +{% if prefixes4 %} +IN ACCEPT -source {{ prefixes4 | join(',') }} -p {{ service.protocol }} -dport {{ ports }} # {{ service.name }} +{% endif %} +{% if prefixes6 %} +IN ACCEPT -source {{ prefixes6 | join(',') }} -p {{ service.protocol }} -dport {{ ports }} # {{ service.name }} +{% endif %} +{% endfor %} +