nftables: do some fancy stuff with sets
- Fix the incorrect use of rate limit on ICMP rule ('over' keyword matched over the rate limit) - Use dynamic sets to limit connections on opened ports - Naively whitelist all libvirt bridges. This includes the whole 192.168.0.0/16 subnet, so it probably will clash with the internal LAN network. I control my own router :) so I don't mind (just use a different private IPv4 address space).
This commit is contained in:
parent
4d1dd6cd7a
commit
385332e312
|
@ -13,7 +13,7 @@ This is an Ansible playbook to deploy my system configurations for desktop usage
|
|||
- Install needed external modules (e.g. `apk`, `pamd`, `mount`):
|
||||
|
||||
```bash
|
||||
ansible-galaxy collection install -r requirements/collections.yml
|
||||
ansible-galaxy install -r requirements/collections.yml
|
||||
```
|
||||
|
||||
- Create an encrypted file to store your user password:
|
||||
|
|
2
TODO.md
2
TODO.md
|
@ -13,7 +13,7 @@ Stuff that are planned to be changed.
|
|||
- [ ] Option for other bootloaders (refind / efistub / limine / zfsbootmenu)
|
||||
- [ ] Refactor grub role
|
||||
- [ ] Option for other dhcp clients (connman / networkmanager)
|
||||
- [ ] Option for other initramfs generators (booster)
|
||||
- [ ] Option for other initramfs generators (booster, dracut)
|
||||
|
||||
## Cosmetic
|
||||
|
||||
|
|
|
@ -103,13 +103,8 @@ libvirt_daemons:
|
|||
- virtqemud
|
||||
- virtstoraged
|
||||
|
||||
# For libvirt's NAT firewall rules
|
||||
# IPv6 is optional (https://wiki.gentoo.org/wiki/QEMU/KVM_IPv6_Support)
|
||||
libvirt_bridges:
|
||||
- name: virbr0
|
||||
ip4: 192.168.122.0/24
|
||||
|
||||
# Public facing network interfaces
|
||||
# Public facing network interfaces to configured
|
||||
# (don't include wireless interfaces here as they should use dhcp with iwctl)
|
||||
# https://wiki.alpinelinux.org/wiki/Configure_Networking
|
||||
network_interfaces:
|
||||
- name: eth0
|
||||
|
|
|
@ -55,6 +55,7 @@ crond_provider:
|
|||
syslog_provider:
|
||||
- busybox
|
||||
- rsyslog
|
||||
- sysklogd
|
||||
|
||||
ntp_client:
|
||||
- ntpsec
|
||||
|
|
|
@ -1,16 +1,34 @@
|
|||
{% set all_public_network_interfaces = ( ansible_interfaces | select('match', '^(eth|wlan|(en|wl|ww)[osxpP])[0-9]+') | list ) -%}
|
||||
#!/usr/sbin/nft -f
|
||||
# You can find examples in /usr/share/nftables/.
|
||||
|
||||
# NOTE:
|
||||
# Dynamic blacklisting:
|
||||
# - https://wiki.nftables.org/wiki-nftables/index.php/Meters
|
||||
# - https://wiki.archlinux.org/title/Nftables#Dynamic_blackhole
|
||||
# - https://unix.stackexchange.com/questions/581964/create-dynamic-blacklist-with-nftables
|
||||
#
|
||||
# Early broken packages dropping in netdev:
|
||||
# - https://wiki.gentoo.org/wiki/Nftables#Family_netdev_and_ingress_hook
|
||||
# - https://serverfault.com/questions/772195/drop-fragmented-packets-in-nftables
|
||||
#
|
||||
# udev's network interface naming scheme:
|
||||
# - https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/8/html/configuring_and_managing_networking/consistent-network-interface-device-naming_configuring-and-managing-networking
|
||||
# - https://systemd.io/PREDICTABLE_INTERFACE_NAMES/
|
||||
#
|
||||
# UDP is stateless, but the kernel can still mark a few conntrack states on the packets:
|
||||
# - https://blog.cloudflare.com/everything-you-ever-wanted-to-know-about-udp-sockets-but-were-afraid-to-ask-part-1/
|
||||
# - https://www.rigacci.org/wiki/lib/exe/fetch.php/doc/appunti/linux/sa/iptables/conntrack.html
|
||||
# - https://www.frozentux.net/iptables-tutorial/chunkyhtml/x1555.html
|
||||
# - https://github.com/torvalds/linux/blob/master/net/ipv4/udp.c
|
||||
|
||||
# Clear all prior state
|
||||
flush ruleset
|
||||
|
||||
# https://wiki.gentoo.org/wiki/Nftables#Family_netdev_and_ingress_hook
|
||||
table netdev filter {
|
||||
chain ingress {
|
||||
# https://serverfault.com/questions/772195/drop-fragmented-packets-in-nftables
|
||||
# Hook priority list: https://wiki.nftables.org/wiki-nftables/index.php/Netfilter_hooks
|
||||
# The priority needs to be lower than -400 (NF_IP_PRI_CONNTRACK_DEFRAG) to see fragments
|
||||
type filter hook ingress devices = { {{ network_interfaces | map(attribute='name') | join(', ') }} } priority -500; policy accept;
|
||||
type filter hook ingress devices = { {{ all_public_network_interfaces | join(', ') }} } priority -500; policy accept;
|
||||
|
||||
# Drop all fragments.
|
||||
ip frag-off & 0x1fff != 0 drop
|
||||
|
@ -28,6 +46,12 @@ table netdev filter {
|
|||
|
||||
# Basic IPv4/IPv6 stateful firewall for server/workstation.
|
||||
table inet filter {
|
||||
set blackhole { type ipv4_addr; flags dynamic, timeout; size 65535; }
|
||||
set blackhole6 { type ipv6_addr; flags dynamic, timeout; size 65535; }
|
||||
# NOTE: connlimit shouldn't be used with timeout flag (same goes for "update" set statement)
|
||||
set connlimit { type ipv4_addr; flags dynamic; size 65535; }
|
||||
set connlimit6 { type ipv6_addr; flags dynamic; size 65535; }
|
||||
|
||||
chain input {
|
||||
type filter hook input priority 0; policy drop;
|
||||
|
||||
|
@ -50,21 +74,42 @@ table inet filter {
|
|||
comment "Block spoofing as localhost (IPv4)"
|
||||
iif != lo ip6 daddr ::1/128 drop \
|
||||
comment "Block spoofing as localhost (IPv6)"
|
||||
{% for interface in network_interfaces %}
|
||||
{% if opened_ports.tcp is sequence and opened_ports.tcp | length > 0 %}
|
||||
|
||||
iif {{ interface. name }} tcp dport { {{ opened_ports.tcp | join(', ') }} } ct state new accept \
|
||||
comment "Open specified TCP ports"
|
||||
{% endif %}
|
||||
{% if opened_ports.udp is sequence and opened_ports.udp | length > 0 %}
|
||||
|
||||
iif {{ interface.name }} udp dport { {{ opened_ports.udp | join(', ') }} } ct state new accept \
|
||||
comment "Open specified UDP ports"
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
# Allow stuff first before the dynamic blacklisting
|
||||
jump input_icmp
|
||||
jump input_libvirt
|
||||
|
||||
# Blacklisting should be done before stateful accept rules
|
||||
ip saddr @blackhole counter drop
|
||||
ip6 saddr @blackhole6 counter drop
|
||||
|
||||
# Drop future attempts on opened ports if there are already 3 established connections
|
||||
{% if opened_ports.tcp is sequence and opened_ports.tcp | length > 0 -%}
|
||||
tcp dport { {{ opened_ports.tcp | join(', ') }} } ct state new \
|
||||
add @connlimit { ip saddr ct count over 3 } drop
|
||||
tcp dport { {{ opened_ports.tcp | join(', ') }} } ct state new \
|
||||
add @connlimit6 { ip6 saddr ct count over 3 } drop
|
||||
{% endif -%}
|
||||
{% if opened_ports.udp is sequence and opened_ports.udp | length > 0 -%}
|
||||
udp dport { {{ opened_ports.udp | join(', ') }} } ct state new \
|
||||
add @connlimit { ip saddr ct count over 3 } drop
|
||||
udp dport { {{ opened_ports.udp | join(', ') }} } ct state new \
|
||||
add @connlimit6 { ip6 saddr ct count over 3 } drop
|
||||
{% endif -%}
|
||||
|
||||
# Allow opened ports but also dynamically add them to the blacklist
|
||||
{% if opened_ports.tcp is sequence and opened_ports.tcp | length > 0 -%}
|
||||
tcp dport { {{ opened_ports.tcp | join(', ') }} } ct state new \
|
||||
add @blackhole { ip saddr timeout 60s limit rate 10/second } accept
|
||||
tcp dport { {{ opened_ports.tcp | join(', ') }} } ct state new \
|
||||
add @blackhole6 { ip6 saddr timeout 60s limit rate 10/second } accept
|
||||
{% endif -%}
|
||||
{% if opened_ports.udp is sequence and opened_ports.udp | length > 0 -%}
|
||||
udp dport { {{ opened_ports.udp | join(', ') }} } ct state new \
|
||||
add @blackhole { ip saddr timeout 60s limit rate 10/second } accept
|
||||
udp dport { {{ opened_ports.udp | join(', ') }} } ct state new \
|
||||
add @blackhole6 { ip6 saddr timeout 60s limit rate 10/second } accept
|
||||
{% endif -%}
|
||||
}
|
||||
|
||||
chain forward {
|
||||
|
@ -85,7 +130,7 @@ table inet filter {
|
|||
echo-request, # type 8
|
||||
time-exceeded, # type 11
|
||||
parameter-problem, # type 12
|
||||
} limit rate over 1/second burst 4 packets accept \
|
||||
} limit rate 10/second burst 4 packets accept \
|
||||
comment "Accept ICMP"
|
||||
|
||||
# ICMPv6
|
||||
|
@ -97,7 +142,7 @@ table inet filter {
|
|||
parameter-problem, # type 4
|
||||
echo-request, # type 128
|
||||
echo-reply, # type 129
|
||||
} limit rate over 1/second burst 4 packets accept \
|
||||
} limit rate 10/second burst 4 packets accept \
|
||||
comment "Accept basic IPv6 functionality"
|
||||
|
||||
ip6 nexthdr icmpv6 icmpv6 type {
|
||||
|
@ -121,26 +166,19 @@ table inet filter {
|
|||
}
|
||||
|
||||
chain input_libvirt {
|
||||
{% for bridge in libvirt_bridges %}
|
||||
# Open DHCP and DNS on {{ bridge.name }}
|
||||
iifname "{{ bridge.name }}" udp dport { 53, 67 } accept
|
||||
iifname "{{ bridge.name }}" tcp dport { 53, 67 } accept
|
||||
{% endfor %}
|
||||
# Open DHCP and DNS on libvirt's bridges
|
||||
iifname "virbr*" udp dport { 53, 67 } accept
|
||||
iifname "virbr*" tcp dport { 53, 67 } accept
|
||||
}
|
||||
|
||||
chain forward_libvirt {
|
||||
{% for bridge in libvirt_bridges %}
|
||||
# Allow forwarding on {{ bridge.name }}
|
||||
oifname "{{ bridge.name }}" ip daddr {{ bridge.ip4 }} ct state { established, related } accept
|
||||
iifname "{{ bridge.name }}" ip saddr {{ bridge.ip4 }} accept
|
||||
{% if bridge.ip6 is defined %}
|
||||
oifname "{{ bridge.name }}" ip6 daddr {{ bridge.ip6 }} ct state { established, related } accept
|
||||
iifname "{{ bridge.name }}" ip6 saddr {{ bridge.ip6 }} accept
|
||||
{% endif %}
|
||||
iifname "{{ bridge.name }}" oifname "{{ bridge.name }}" accept
|
||||
oifname "{{ bridge.name }}" reject with icmpx type port-unreachable
|
||||
iifname "{{ bridge.name }}" reject with icmpx type port-unreachable
|
||||
{% endfor %}
|
||||
# NOTE: use 10.0.0.0/8 or 172.16.0.0/12 subnets for the LAN network instead
|
||||
# These rules naively defeat the purpose of having multiple separated subnets but I'm fine with it being on my desktops
|
||||
oifname "virbr*" ip daddr 192.168.0.0/16 ct state { established, related } accept
|
||||
iifname "virbr*" ip saddr 192.168.0.0/16 accept
|
||||
iifname "virbr*" oifname "virbr*" accept
|
||||
oifname "virbr*" reject with icmpx type port-unreachable
|
||||
iifname "virbr*" reject with icmpx type port-unreachable
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -151,8 +189,7 @@ table inet nat {
|
|||
}
|
||||
|
||||
chain postrouting_libvirt {
|
||||
{% for bridge in libvirt_bridges %}
|
||||
ip saddr {{ bridge.ip4 }} ip daddr != {{ bridge.ip4 }} masquerade
|
||||
{% endfor %}
|
||||
# IPv6 shouldn't be NAT
|
||||
meta nfproto ipv4 iifname "virbr*" oif { {{ all_public_network_interfaces | join(', ') }} } masquerade
|
||||
}
|
||||
}
|
||||
|
|
Reference in New Issue