proxmox: set up firewall
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.
This commit is contained in:
parent
179547beff
commit
e7f9132571
|
@ -1,10 +1,19 @@
|
||||||
#!/usr/bin/python
|
#!/usr/bin/python
|
||||||
|
|
||||||
|
import os
|
||||||
|
import pynetbox
|
||||||
|
|
||||||
class FilterModule(object):
|
class FilterModule(object):
|
||||||
'''Various utilities for manipulating NetBox data'''
|
'''Various utilities for manipulating NetBox data'''
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.nb = pynetbox.api(os.getenv('NETBOX_API'), os.getenv('NETBOX_TOKEN'))
|
||||||
|
|
||||||
def filters(self):
|
def filters(self):
|
||||||
return {
|
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):
|
def device_address(self, device):
|
||||||
|
@ -13,3 +22,26 @@ class FilterModule(object):
|
||||||
for addr in iface['ip_addresses']:
|
for addr in iface['ip_addresses']:
|
||||||
if addr.get('role') and addr['role'].get('value') == 'loopback':
|
if addr.get('role') and addr['role'].get('value') == 'loopback':
|
||||||
yield addr
|
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)
|
||||||
|
|
12
roles/proxmox/tasks/firewall.yml
Normal file
12
roles/proxmox/tasks/firewall.yml
Normal file
|
@ -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
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
- name: Get all nodes in my cluster
|
||||||
|
set_fact:
|
||||||
|
nodes: "{{ groups['cluster_'+cluster] | map('extract', hostvars) }}"
|
||||||
|
|
||||||
- name: Disable enterprise repositories
|
- name: Disable enterprise repositories
|
||||||
apt_repository:
|
apt_repository:
|
||||||
repo: '{{ item }}'
|
repo: '{{ item }}'
|
||||||
|
@ -40,4 +44,6 @@
|
||||||
|
|
||||||
- include_tasks: mgmt.yml
|
- include_tasks: mgmt.yml
|
||||||
|
|
||||||
|
- include_tasks: firewall.yml
|
||||||
|
|
||||||
- include_tasks: frr.yml
|
- include_tasks: frr.yml
|
||||||
|
|
23
roles/proxmox/templates/cluster.fw.j2
Normal file
23
roles/proxmox/templates/cluster.fw.j2
Normal file
|
@ -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 %}
|
||||||
|
|
Loading…
Reference in a new issue