From ccfa52cb5bce88a14e36a9967c5803570321c325 Mon Sep 17 00:00:00 2001 From: Tom Hughes Date: Fri, 3 Mar 2023 18:07:47 +0000 Subject: [PATCH] Add support for using an nftables based firewall --- .../templates/default/jail.default.erb | 5 + cookbooks/networking/attributes/default.rb | 4 + cookbooks/networking/recipes/default.rb | 433 ++++++++++-------- .../networking/resources/firewall_rule.rb | 83 ++++ .../templates/default/nftables.conf.erb | 118 +++++ 5 files changed, 456 insertions(+), 187 deletions(-) create mode 100644 cookbooks/networking/templates/default/nftables.conf.erb diff --git a/cookbooks/fail2ban/templates/default/jail.default.erb b/cookbooks/fail2ban/templates/default/jail.default.erb index fc0f8bdc8..890e35117 100644 --- a/cookbooks/fail2ban/templates/default/jail.default.erb +++ b/cookbooks/fail2ban/templates/default/jail.default.erb @@ -2,5 +2,10 @@ [DEFAULT] destemail = admins@openstreetmap.org +<%- if node[:networking][:firewall][:engine] == "shorewall" %> banaction = shorewall +<%- elsif node[:networking][:firewall][:engine] == "nftables" %> +banaction = nftables[type=multiport] +banaction_allports = nftables[type=allports] +<%- end %> bantime = 14400 diff --git a/cookbooks/networking/attributes/default.rb b/cookbooks/networking/attributes/default.rb index aef019dd5..84ccd170d 100644 --- a/cookbooks/networking/attributes/default.rb +++ b/cookbooks/networking/attributes/default.rb @@ -1,6 +1,10 @@ +default[:networking][:firewall][:engine] = "shorewall" default[:networking][:firewall][:enabled] = true default[:networking][:firewall][:inet] = [] default[:networking][:firewall][:inet6] = [] +default[:networking][:firewall][:sets] = [] +default[:networking][:firewall][:incoming] = [] +default[:networking][:firewall][:outgoing] = [] default[:networking][:firewall][:http_rate_limit] = "-" default[:networking][:firewall][:http_connection_limit] = "-" default[:networking][:firewall][:log] = true diff --git a/cookbooks/networking/recipes/default.rb b/cookbooks/networking/recipes/default.rb index 8ec499d13..cef11a071 100644 --- a/cookbooks/networking/recipes/default.rb +++ b/cookbooks/networking/recipes/default.rb @@ -383,6 +383,7 @@ link "/etc/resolv.conf" do to "../run/systemd/resolve/stub-resolv.conf" end +hosts = { :inet => [], :inet6 => [] } zones = {} search(:node, "networking:interfaces").collect do |n| @@ -391,275 +392,333 @@ search(:node, "networking:interfaces").collect do |n| n.interfaces.each do |interface| next unless interface[:role] == "external" && interface[:zone] + hosts[interface[:family]] << interface[:address] + zones[interface[:zone]] ||= {} zones[interface[:zone]][interface[:family]] ||= [] zones[interface[:zone]][interface[:family]] << interface[:address] end end -package "shorewall" - -systemd_service "shorewall-docker" do - service "shorewall" - dropin "docker" - exec_stop "/sbin/shorewall $OPTIONS stop" - notifies :restart, "service[shorewall]" -end +hosts[:inet] << "127.0.0.1" if hosts[:inet].empty? +hosts[:inet6] << "::1" if hosts[:inet6].empty? -template "/etc/default/shorewall" do - source "shorewall-default.erb" - owner "root" - group "root" - mode "644" - notifies :restart, "service[shorewall]" -end +if node[:networking][:firewall][:engine] == "shorewall" + package "shorewall" -template "/etc/shorewall/shorewall.conf" do - source "shorewall.conf.erb" - owner "root" - group "root" - mode "644" - notifies :restart, "service[shorewall]" -end - -template "/etc/shorewall/zones" do - source "shorewall-zones.erb" - owner "root" - group "root" - mode "644" - variables :type => "ipv4" - notifies :restart, "service[shorewall]" -end - -template "/etc/shorewall/interfaces" do - source "shorewall-interfaces.erb" - owner "root" - group "root" - mode "644" - notifies :restart, "service[shorewall]" -end - -template "/etc/shorewall/hosts" do - source "shorewall-hosts.erb" - owner "root" - group "root" - mode "644" - variables :zones => zones - notifies :restart, "service[shorewall]" -end - -template "/etc/shorewall/conntrack" do - source "shorewall-conntrack.erb" - owner "root" - group "root" - mode "644" - notifies :restart, "service[shorewall]" - only_if { node[:networking][:firewall][:raw] } -end - -template "/etc/shorewall/policy" do - source "shorewall-policy.erb" - owner "root" - group "root" - mode "644" - notifies :restart, "service[shorewall]" -end - -template "/etc/shorewall/rules" do - source "shorewall-rules.erb" - owner "root" - group "root" - mode "644" - variables :family => "inet" - notifies :restart, "service[shorewall]" -end - -template "/etc/shorewall/stoppedrules" do - source "shorewall-stoppedrules.erb" - owner "root" - group "root" - mode "644" - notifies :restart, "service[shorewall]" -end - -if node[:networking][:firewall][:enabled] - service "shorewall" do - action [:enable, :start] - supports :restart => true - status_command "shorewall status" - ignore_failure true - end -else - service "shorewall" do - action [:disable, :stop] - supports :restart => true - status_command "shorewall status" - ignore_failure true - end -end - -template "/etc/logrotate.d/shorewall" do - source "logrotate.shorewall.erb" - owner "root" - group "root" - mode "644" - variables :name => "shorewall" -end - -firewall_rule "limit-icmp-echo" do - action :accept - family :inet - source "net" - dest "fw" - proto "icmp" - dest_ports "echo-request" - rate_limit "s:1/sec:5" -end - -if node[:networking][:wireguard][:enabled] - wireguard_source = if node[:roles].include?("gateway") - "net" - else - "osm" - end - - firewall_rule "accept-wireguard" do - action :accept - source wireguard_source - dest "fw" - proto "udp" - dest_ports "51820" - source_ports "51820" - end -end - -file "/etc/shorewall/masq" do - action :delete -end - -file "/etc/shorewall/masq.bak" do - action :delete -end - -if node[:roles].include?("gateway") - template "/etc/shorewall/snat" do - source "shorewall-snat.erb" - owner "root" - group "root" - mode "644" - notifies :restart, "service[shorewall]" - end -else - file "/etc/shorewall/snat" do - action :delete + systemd_service "shorewall-docker" do + service "shorewall" + dropin "docker" + exec_stop "/sbin/shorewall $OPTIONS stop" notifies :restart, "service[shorewall]" end -end -unless node.interfaces(:family => :inet6).empty? - package "shorewall6" - - template "/etc/default/shorewall6" do + template "/etc/default/shorewall" do source "shorewall-default.erb" owner "root" group "root" mode "644" - notifies :restart, "service[shorewall6]" + notifies :restart, "service[shorewall]" end - template "/etc/shorewall6/shorewall6.conf" do - source "shorewall6.conf.erb" + template "/etc/shorewall/shorewall.conf" do + source "shorewall.conf.erb" owner "root" group "root" mode "644" - notifies :restart, "service[shorewall6]" + notifies :restart, "service[shorewall]" end - template "/etc/shorewall6/zones" do + template "/etc/shorewall/zones" do source "shorewall-zones.erb" owner "root" group "root" mode "644" - variables :type => "ipv6" - notifies :restart, "service[shorewall6]" + variables :type => "ipv4" + notifies :restart, "service[shorewall]" end - template "/etc/shorewall6/interfaces" do - source "shorewall6-interfaces.erb" + template "/etc/shorewall/interfaces" do + source "shorewall-interfaces.erb" owner "root" group "root" mode "644" - notifies :restart, "service[shorewall6]" + notifies :restart, "service[shorewall]" end - template "/etc/shorewall6/hosts" do - source "shorewall6-hosts.erb" + template "/etc/shorewall/hosts" do + source "shorewall-hosts.erb" owner "root" group "root" mode "644" variables :zones => zones - notifies :restart, "service[shorewall6]" + notifies :restart, "service[shorewall]" end - template "/etc/shorewall6/conntrack" do + template "/etc/shorewall/conntrack" do source "shorewall-conntrack.erb" owner "root" group "root" mode "644" - notifies :restart, "service[shorewall6]" + notifies :restart, "service[shorewall]" only_if { node[:networking][:firewall][:raw] } end - template "/etc/shorewall6/policy" do + template "/etc/shorewall/policy" do source "shorewall-policy.erb" owner "root" group "root" mode "644" - notifies :restart, "service[shorewall6]" + notifies :restart, "service[shorewall]" end - template "/etc/shorewall6/rules" do + template "/etc/shorewall/rules" do source "shorewall-rules.erb" owner "root" group "root" mode "644" - variables :family => "inet6" - notifies :restart, "service[shorewall6]" + variables :family => "inet" + notifies :restart, "service[shorewall]" + end + + template "/etc/shorewall/stoppedrules" do + source "shorewall-stoppedrules.erb" + owner "root" + group "root" + mode "644" + notifies :restart, "service[shorewall]" end if node[:networking][:firewall][:enabled] - service "shorewall6" do + service "shorewall" do action [:enable, :start] supports :restart => true - status_command "shorewall6 status" + status_command "shorewall status" ignore_failure true end else - service "shorewall6" do + service "shorewall" do action [:disable, :stop] supports :restart => true - status_command "shorewall6 status" + status_command "shorewall status" ignore_failure true end end - template "/etc/logrotate.d/shorewall6" do + template "/etc/logrotate.d/shorewall" do source "logrotate.shorewall.erb" owner "root" group "root" mode "644" - variables :name => "shorewall6" + variables :name => "shorewall" end - firewall_rule "limit-icmp6-echo" do + firewall_rule "limit-icmp-echo" do action :accept - family :inet6 + family :inet source "net" dest "fw" - proto "ipv6-icmp" + proto "icmp" dest_ports "echo-request" rate_limit "s:1/sec:5" end + + file "/etc/shorewall/masq" do + action :delete + end + + file "/etc/shorewall/masq.bak" do + action :delete + end + + if node[:roles].include?("gateway") + template "/etc/shorewall/snat" do + source "shorewall-snat.erb" + owner "root" + group "root" + mode "644" + notifies :restart, "service[shorewall]" + end + else + file "/etc/shorewall/snat" do + action :delete + notifies :restart, "service[shorewall]" + end + end + + unless node.interfaces(:family => :inet6).empty? + package "shorewall6" + + template "/etc/default/shorewall6" do + source "shorewall-default.erb" + owner "root" + group "root" + mode "644" + notifies :restart, "service[shorewall6]" + end + + template "/etc/shorewall6/shorewall6.conf" do + source "shorewall6.conf.erb" + owner "root" + group "root" + mode "644" + notifies :restart, "service[shorewall6]" + end + + template "/etc/shorewall6/zones" do + source "shorewall-zones.erb" + owner "root" + group "root" + mode "644" + variables :type => "ipv6" + notifies :restart, "service[shorewall6]" + end + + template "/etc/shorewall6/interfaces" do + source "shorewall6-interfaces.erb" + owner "root" + group "root" + mode "644" + notifies :restart, "service[shorewall6]" + end + + template "/etc/shorewall6/hosts" do + source "shorewall6-hosts.erb" + owner "root" + group "root" + mode "644" + variables :zones => zones + notifies :restart, "service[shorewall6]" + end + + template "/etc/shorewall6/conntrack" do + source "shorewall-conntrack.erb" + owner "root" + group "root" + mode "644" + notifies :restart, "service[shorewall6]" + only_if { node[:networking][:firewall][:raw] } + end + + template "/etc/shorewall6/policy" do + source "shorewall-policy.erb" + owner "root" + group "root" + mode "644" + notifies :restart, "service[shorewall6]" + end + + template "/etc/shorewall6/rules" do + source "shorewall-rules.erb" + owner "root" + group "root" + mode "644" + variables :family => "inet6" + notifies :restart, "service[shorewall6]" + end + + if node[:networking][:firewall][:enabled] + service "shorewall6" do + action [:enable, :start] + supports :restart => true + status_command "shorewall6 status" + ignore_failure true + end + else + service "shorewall6" do + action [:disable, :stop] + supports :restart => true + status_command "shorewall6 status" + ignore_failure true + end + end + + template "/etc/logrotate.d/shorewall6" do + source "logrotate.shorewall.erb" + owner "root" + group "root" + mode "644" + variables :name => "shorewall6" + end + + firewall_rule "limit-icmp6-echo" do + action :accept + family :inet6 + source "net" + dest "fw" + proto "ipv6-icmp" + dest_ports "echo-request" + rate_limit "s:1/sec:5" + end + end +elsif node[:networking][:firewall][:engine] == "nftables" + service "shorewall6" do + action [:disable, :stop] + end + + package "shorewall6" do + action :purge + end + + service "shorewall" do + action [:disable, :stop] + end + + systemd_service "shorewall-docker" do + action :delete + service "shorewall" + dropin "docker" + end + + package "shorewall" do + action :purge + end + + package "nftables" + + interfaces = [] + + node.interfaces(:role => :external).each do |interface| + interfaces << interface[:interface] + end + + interfaces << "eth0" if kitchen? && interfaces.empty? + + template "/etc/nftables.conf" do + source "nftables.conf.erb" + owner "root" + group "root" + mode "755" + variables :interfaces => interfaces, :hosts => hosts + notifies :restart, "service[nftables]" + end + + if node[:networking][:firewall][:enabled] + service "nftables" do + action [:enable, :start] + end + else + service "nftables" do + action [:disable, :stop] + end + end +end + +if node[:networking][:wireguard][:enabled] + wireguard_source = if node[:roles].include?("gateway") + "net" + else + "osm" + end + + firewall_rule "accept-wireguard" do + action :accept + source wireguard_source + dest "fw" + proto "udp" + dest_ports "51820" + source_ports "51820" + end end firewall_rule "accept-http" do diff --git a/cookbooks/networking/resources/firewall_rule.rb b/cookbooks/networking/resources/firewall_rule.rb index a3e2e7eb0..0dd9a488a 100644 --- a/cookbooks/networking/resources/firewall_rule.rb +++ b/cookbooks/networking/resources/firewall_rule.rb @@ -51,6 +51,21 @@ end action_class do def add_rule(action) + if node[:networking][:firewall][:engine] == "shorewall" + add_shorewall_rule(action) + elsif node[:networking][:firewall][:engine] == "nftables" + if new_resource.family.nil? + add_nftables_rule(action, "inet") + add_nftables_rule(action, "inet6") + elsif new_resource.family.to_s == "inet" + add_nftables_rule(action, "inet") + elsif new_resource.family.to_s == "inet6" + add_nftables_rule(action, "inet6") + end + end + end + + def add_shorewall_rule(action) rule = { :action => action.to_s.upcase, :source => new_resource.source, @@ -76,4 +91,72 @@ action_class do end end end + + def add_nftables_rule(action, family) + rule = [] + + ip = case family + when "inet" then "ip" + when "inet6" then "ip6" + end + + proto = case new_resource.proto + when "udp" then "udp" + when "tcp", "tcp:syn" then "tcp" + end + + if new_resource.source_ports != "-" + rule << "#{proto} sport { #{new_resource.source_ports} }" + end + + if new_resource.dest_ports != "-" + rule << "#{proto} dport { #{new_resource.dest_ports} }" + end + + if new_resource.source == "osm" + rule << "#{ip} saddr { $#{ip}-osm-addresses }" + elsif new_resource.source =~ /^net:(.*)$/ + addresses = Regexp.last_match(1).split(",").join(", ") + + rule << "#{ip} saddr { #{addresses} }" + end + + if new_resource.dest == "osm" + rule << "#{ip} daddr $#{ip}-osm-addresses" + elsif new_resource.dest =~ /^net:(.*)$/ + addresses = Regexp.last_match(1).split(",").join(", ") + + rule << "#{ip} daddr { #{addresses} }" + end + + if new_resource.proto == "tcp:syn" + rule << "ct state new" + end + + if new_resource.connection_limit != "-" + rule << "ct count #{new_resource.connection_limit}" + end + + if new_resource.rate_limit =~ %r{^s:(\d+)/sec:(\d+)$} + set = "#{new_resource.rule}-#{ip}" + rate = Regexp.last_match(1) + burst = Regexp.last_match(2) + + node.default[:networking][:firewall][:sets] << set + + rule << "add @#{set} { #{ip} saddr limit rate #{rate}/second burst #{burst} packets }" + end + + rule << case action + when :accept then "accept" + when :drop then "jump log-and-drop" + when :reject then "jump log-and-reject" + end + + if new_resource.source == "fw" + node.default[:networking][:firewall][:outcoming] << rule.join(" ") + elsif new_resource.dest == "fw" + node.default[:networking][:firewall][:incoming] << rule.join(" ") + end + end end diff --git a/cookbooks/networking/templates/default/nftables.conf.erb b/cookbooks/networking/templates/default/nftables.conf.erb new file mode 100644 index 000000000..cd48c0b7f --- /dev/null +++ b/cookbooks/networking/templates/default/nftables.conf.erb @@ -0,0 +1,118 @@ +#!/usr/sbin/nft -f + +define external-interfaces = { <%= @interfaces.sort.uniq.join(", ") %> } + +define ip-private-addresses = { 10.0.0.0/8, 127.0.0.0/8, 169.254.0.0/16, 172.16.0.0/12, 192.0.2.0/24, 192.168.0.0/16 } +define ip6-private-addresses = { 2001:db8::/32, fc00::/7 } + +define ip-osm-addresses = { <%= Array(@hosts[:inet]).sort.join(", ") %> } +define ip6-osm-addresses = { <%= Array(@hosts[:inet6]).sort.join(", ") %> } + +flush ruleset + +table inet filter { + set ip-blacklist { + type ipv4_addr + flags dynamic + } + + set ip6-blacklist { + type ipv6_addr + flags dynamic + } + +<%- node[:networking][:firewall][:sets].each do |set| %> + set <%= set %> { +<%- if set.end_with?("-ip") %> + type ipv4_addr +<%- elsif set.end_with?("-ip6") %> + type ipv6_addr +<%- end %> + flags dynamic + } + +<%- end %> + chain log-and-drop { + limit rate 1/second log + drop + } + + chain log-and-reject { + limit rate 1/second log + reject + } + + chain incoming { + ip saddr { $ip-private-addresses } jump log-and-drop + ip6 saddr { $ip6-private-addresses } jump log-and-drop + + ip saddr @ip-blacklist jump log-and-drop + ip6 saddr @ip6-blacklist jump log-and-drop + + ct state { established, related } accept + + icmp type { destination-unreachable } accept + icmpv6 type { nd-neighbor-solicit, nd-neighbor-advert, nd-router-solicit, nd-router-advert } accept + + icmp type { echo-request } limit rate 1/second accept + icmpv6 type { echo-request } limit rate 1/second accept + + meta l4proto { icmp, icmpv6 } jump log-and-drop + +<%- node[:networking][:firewall][:incoming].uniq.each do |rule| %> + <%= rule %> +<%- end %> + + jump log-and-drop + } + + chain outgoing { + ip daddr { $ip-private-addresses } jump log-and-drop + ip6 daddr { $ip6-private-addresses } jump log-and-drop + +<%- node[:networking][:firewall][:outgoing].each do |rule| %> + <%= rule %> +<%- end %> + + accept + } + + chain input { + type filter hook input priority filter; + + iif { $external-interfaces } jump incoming + + accept + } + + chain forward { + type filter hook forward priority filter; + + iif { $external-interfaces } jump incoming + oif { $external-interfaces } jump outgoing + + accept + } + + chain output { + type filter hook output priority filter; + + oif { $external-interfaces } jump outgoing + + accept + } +} +<%- if node[:roles].include?("gateway") %> + +table ip nat { + chain postrouting { + type nat hook postrouting priority srcnat; + +<%- node.interfaces(:role => :external).each do |external| %> +<%- node.interfaces(:role => :internal).each do |internal| %> + oif { < %= external[:interface] %> } ip saddr { <%= internal[:network] %>/<%= internal[:prefix] %> } snat <%= external[:address] %> +<%- end %> +<%- end %> + } +} +<%- end %> -- 2.43.2