Commit 9344abf6 authored by m.pausch's avatar m.pausch
Browse files

Merge branch 'nftables_default_ruleset' into 'master'

Nftables default ruleset

See merge request !45
parents 8e484f92 b604ddc1
......@@ -42,9 +42,9 @@ kitchen:
- banner
- chef
- ferm
- firewall
- linuxlogo
- mail
- nftables
- nsswitch
- ohai
- resolv
......
......@@ -183,14 +183,9 @@ suites:
- 'policy ACCEPT;'
FORWARD:
- 'policy DROP;'
- name: sys_firewall
- name: sys_nftables
run_list:
- recipe[firewall-test::default]
attributes:
sys:
firewall:
manage: true
disable: false
- recipe[nftables-test::default]
- name: sys_linuxlogo
run_list:
- recipe[sys::linuxlogo]
......
......@@ -10,5 +10,5 @@ cookbook 'fixtures', path: 'test/unit/fixtures', group: :chefspec
cookbook 'line', github: 'sous-chefs/line', tag: "v0.6.3"
group :integration do
cookbook 'firewall-test', path: 'test/fixtures/cookbooks/firewall-test'
cookbook 'nftables-test', path: 'test/fixtures/cookbooks/nftables-test'
end
......@@ -2,6 +2,14 @@
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [1.66.0] - 2022-05-05
### Changed
- Use a more modern approach for the `firewall` and `firewall_rule` resources.
- No attributes to configure the `firewall` or `firewall_rule` resources
- No default recipe
- Rename the resources to `nftables` and `nftables_rule`.
## [1.65.0] - 2022-04-29
### Added
......
default['sys']['firewall']['manage'] = false
default['sys']['firewall']['disable'] = true
default['sys']['firewall']['allow_ssh'] = true
default['sys']['firewall']['allow_loopback'] = true
default['sys']['firewall']['allow_icmp'] = true
default['sys']['firewall']['allow_established'] = true
default['sys']['firewall']['defaults']['policy'] = {
'input' => 'drop',
'forward' => 'drop',
'output' => 'accept',
}
default['sys']['firewall']['defaults']['ruleset'] = {
'add table inet filter' => 1,
"add chain inet filter input { type filter hook input priority 0 ; policy #{node['sys']['firewall']['defaults']['policy']['input']}; }" => 2,
"add chain inet filter output { type filter hook output priority 0 ; policy #{node['sys']['firewall']['defaults']['policy']['output']}; }" => 2,
"add chain inet filter forward { type filter hook forward priority 0 ; policy #{node['sys']['firewall']['defaults']['policy']['forward']}; }" => 2,
}
if node['sys']['firewall']['table_ip_nat']
default['sys']['firewall']['defaults']['ruleset']['add table ip nat'] = 1
default['sys']['firewall']['defaults']['ruleset']['add chain ip nat postrouting { type nat hook postrouting priority 100 ;}'] = 2
default['sys']['firewall']['defaults']['ruleset']['add chain ip nat prerouting { type nat hook prerouting priority -100 ;}'] = 2
end
if node['sys']['firewall']['table_ip6_nat']
default['sys']['firewall']['defaults']['ruleset']['add table ip6 nat'] = 1
default['sys']['firewall']['defaults']['ruleset']['add chain ip6 nat postrouting { type nat hook postrouting priority 100 ;}'] = 2
default['sys']['firewall']['defaults']['ruleset']['add chain ip6 nat prerouting { type nat hook prerouting priority -100 ;}'] = 2
end
# `sys::firewall`
Use the firewall recipe to configure nftables.
`attributes/firewall.rb`
`recipes/firewall.rb`
`resources/firewall.rb`
`resources/firewall_rule.rb`
`libraries/sys_helpers_firewall.rb`
`documents/firewall.rb`
`test/unit/recipies/firewall_spec.rb`
## Basic Usage
### Disable nftables
`node['sys']['firewall']['disable'] = true` will turn off *nftables*:
```ruby
node['sys']['firewall']['manage'] = true # To manage nftables at all
node['sys']['firewall']['disable'] = true # To disable nftables
```
### Enable nftables
The `sys::firewall` recipe **does nothing unless explicitly
activated**. To active the recipe, the following steps are required:
1. Include the recipe in your run list.
1. Set `node['sys']['firewall']['manage']` to `true`
1. Set `node['sys']['firewall']['disable']` to `false`
This will give you a rather permissive default-set of rules, since the
following attributes default to `true`. Adjust to your needs:
`node['sys']['firewall']['allow_established']`
`node['sys']['firewall']['allow_icmp']`
`node['sys']['firewall']['allow_loopback']`
`node['sys']['firewall']['allow_ssh']`
This will give you the following default rules:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
iif "lo" accept comment "allow loopback"
icmp type echo-request accept comment "allow icmp"
tcp dport ssh accept comment "allow world to ssh"
ct state established,related accept comment "established"
}
chain output {
type filter hook output priority 0; policy accept;
}
chain foward {
type filter hook forward priority 0; policy drop;
}
}
table ip6 nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
}
chain prerouting {
type nat hook prerouting priority -100; policy accept;
}
}
table ip nat {
chain postrouting {
type nat hook postrouting priority 100; policy accept;
}
chain prerouting {
type nat hook prerouting priority -100; policy accept;
}
}
### You are scared and just want to take a look
If you want to generate the nftables rule-set but not activate it, use
your own firewall-recipe, like so:
```RUBY
firewall 'default' # Default action is :install
firewall_rule 'example-ips from rfc5737' do
source ['192.0.2.0/24', '198.51.100.0/24', '203.0.113.0/24']
port 22
end
```
Make sure to [disable](#disable-nftables) the firewall. These steps
should disable nftables but still generate `/etc/nftables.conf`.
## Using `sys::firewall` from other recipes
Depend on the `sys`-cookbook in the `metadata.rb` and include
`sys::firewall` in the runlist. If access via ports `443` and `80`
should be possible, write a resource like this:
```ruby
firewall_rule 'allow http(s)' do
port [80,443]
end
```
If `sys::firewall` is not what you want, it is also sufficient to
define the resource `firewall['default']` instead of including
`sys::firewall` and build everything from scratch.
For further examples see the recipe
[sys::firewall](recipes/firewall.rb) and the recipe [firewall-test::default](test/fixtures/cookbooks/firewall-test/recipes/default.rb).
# `resource::nftables`
Use the nftables resource to configure nftables.
`resources/nftables.rb`
`resources/nftables_rule.rb`
`libraries/sys_helpers_nftables.rb`
`documents/nftables.rb`
`test/unit/recipies/nftables_spec.rb`
## Basic Usage
### Disable nftables
```ruby
nftables 'default' do
action :disable
end
```
### Enable nftables
```ruby
nftables 'default'
```
This will give you the following default rules:
table inet filter {
chain input {
type filter hook input priority 0; policy accept;
}
chain output {
type filter hook output priority 0; policy accept;
}
chain foward {
type filter hook forward priority 0; policy drop;
}
}
### You are scared and just want to take a look
If you want to generate the nftables rule-set but not activate it, use
your own nftables-recipe, like so:
```RUBY
nftables 'default' do
action [:rebuild, :disable]
end
nftables_rule 'example-ips from rfc5737' do
source ['192.0.2.0/24', '198.51.100.0/24', '203.0.113.0/24']
port 22
end
```
Setting `action` to [:rebuild, :disable] will disable nftables but
still generate `/etc/nftables.conf`.
## Using the `nftables`-resource
Depend on the `sys`-cookbook in the `metadata.rb`. Write a recipe to
configure nftables, e.g. to configure a ruleset which only allows
access via port `22`, write a recipe like this
```ruby
nftables 'default' do
input_policy 'drop'
end
nftables_rule 'allow http(s)' do
port [80,443]
end
```
For further examples see [nftables-test::default](test/fixtures/cookbooks/nftables-test/recipes/default.rb).
#
# Cookbook Name:: sys
# Library:: Helpers::Firewall
# Cookbook:: sys
# Library:: Helpers::Nftables
#
# Copyright 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH
# Copyright:: 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH
#
# Authors:
# Matthias Pausch (m.pausch@gsi.de)
......@@ -24,18 +24,10 @@
module Sys
module Helpers
module Firewall
module Nftables
require 'ipaddr'
include Chef::Mixin::ShellOut
def dport(new_resource)
new_resource.dest_port || new_resource.port
end
def sport(new_resource)
new_resource.source_port
end
def valid_ips?(ips)
Array(ips).inject(false) do |a, ip|
a || !!IPAddr.new(ip)
......@@ -55,61 +47,49 @@ module Sys
end
end
def port_to_s(p)
if p.is_a?(String)
p
elsif p && p.is_a?(Integer)
p.to_s
elsif p && p.is_a?(Array)
p_strings = p.map { |o| port_to_s(o) }
"{#{p_strings.sort.join(',')}}"
elsif p && p.is_a?(Range)
"#{p.first}-#{p.last}"
def port_to_s(port)
if port.is_a?(String)
port
elsif port && port.is_a?(Integer)
port.to_s
elsif port && port.is_a?(Array)
port_strings = port.map { |o| port_to_s(o) }
"{#{port_strings.sort.join(',')}}"
elsif port && port.is_a?(Range)
"#{port.first}-#{port.last}"
end
end
def disabled?
node['sys']['firewall']['disable']
end
def managed?
node['sys']['firewall']['manage']
end
def build_rule_file(rules)
contents = []
sorted_values = rules.values.sort.uniq
sorted_values.each do |sorted_value|
contents << "# position #{sorted_value}"
contents << rules.select { |_,v| v == sorted_value }.keys.join("\n")
contents << rules.select { |_, v| v == sorted_value }.keys.join("\n")
end
"#{contents.join("\n")}\n"
end
unless defined? CHAIN
CHAIN = {
in: 'input',
out: 'output',
pre: 'prerouting',
post: 'postrouting',
forward: 'forward',
}.freeze
end
unless defined? TARGET
TARGET = {
accept: 'accept',
allow: 'accept',
deny: 'drop',
drop: 'drop',
log: 'log prefix "nftables:" group 0',
masquerade: 'masquerade',
redirect: 'redirect',
reject: 'reject',
}.freeze
end
def build_firewall_rule(rule_resource)
CHAIN ||= {
in: 'input',
out: 'output',
pre: 'prerouting',
post: 'postrouting',
forward: 'forward',
}.freeze
TARGET ||= {
accept: 'accept',
allow: 'accept',
deny: 'drop',
drop: 'drop',
log: 'log prefix "nftables:" group 0',
masquerade: 'masquerade',
redirect: 'redirect',
reject: 'reject',
}.freeze
def build_nftables_rule(rule_resource)
return rule_resource.raw.strip if rule_resource.raw
ip_family = rule_resource.family
......@@ -118,64 +98,71 @@ module Sys
else
'filter'
end
firewall_rule = if table == 'nat'
nftables_rule = if table == 'nat'
"add rule #{ip_family} #{table} "
else
"add rule inet #{table} "
end
firewall_rule << CHAIN.fetch(rule_resource.direction.to_sym)
firewall_rule << ' '
firewall_rule << "iif #{rule_resource.interface} " if rule_resource.interface
firewall_rule << "oif #{rule_resource.dest_interface} " if rule_resource.dest_interface
nftables_rule << CHAIN.fetch(rule_resource.direction.to_sym)
nftables_rule << ' '
nftables_rule << "iif #{rule_resource.interface} " if rule_resource.interface
nftables_rule << "oif #{rule_resource.outerface} " if rule_resource.outerface
if rule_resource.source
source_set = build_set_of_ips(rule_resource.source)
firewall_rule << "#{ip_family} saddr #{source_set} "
nftables_rule << "#{ip_family} saddr #{source_set} "
end
if rule_resource.destination
destination_set = build_set_of_ips(rule_resource.destination)
firewall_rule << "#{ip_family} daddr #{destination_set} "
nftables_rule << "#{ip_family} daddr #{destination_set} "
end
case rule_resource.protocol
when :icmp
firewall_rule << 'icmp type echo-request '
nftables_rule << 'icmp type echo-request '
when :'ipv6-icmp', :icmpv6
firewall_rule << 'icmpv6 type { echo-request, nd-router-solicit, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } '
nftables_rule << 'icmpv6 type { echo-request, nd-router-solicit, nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } '
when :tcp, :udp
firewall_rule << "#{rule_resource.protocol} sport #{port_to_s(sport(rule_resource))} " if sport(rule_resource)
firewall_rule << "#{rule_resource.protocol} dport #{port_to_s(dport(rule_resource))} " if dport(rule_resource)
nftables_rule << "#{rule_resource.protocol} sport #{port_to_s(rule_resource.sport)} " if rule_resource.sport
nftables_rule << "#{rule_resource.protocol} dport #{port_to_s(rule_resource.dport)} " if rule_resource.dport
when :esp, :ah
firewall_rule << "#{ip_family} #{ip_family == :ip6 ? 'nexthdr' : 'protocol'} #{rule_resource.protocol} "
nftables_rule << "#{ip_family} #{ip_family == :ip6 ? 'nexthdr' : 'protocol'} #{rule_resource.protocol} "
# nothing to do default :ipv6, :none
# nothing to do default :ipv6, :none
end
firewall_rule << "ct state #{Array(rule_resource.stateful).join(',').downcase} " if rule_resource.stateful
firewall_rule << "#{TARGET[rule_resource.command.to_sym]} "
firewall_rule << " to #{rule_resource.redirect_port} " if rule_resource.command == :redirect
firewall_rule << "comment \"#{rule_resource.description}\" " if rule_resource.include_comment
firewall_rule.strip!
firewall_rule
nftables_rule << "ct state #{Array(rule_resource.stateful).join(',').downcase} " if rule_resource.stateful
nftables_rule << "#{TARGET[rule_resource.command.to_sym]} "
nftables_rule << " to #{rule_resource.redirect_port} " if rule_resource.command == :redirect
nftables_rule << "comment \"#{rule_resource.description}\" " if rule_resource.include_comment
nftables_rule.strip!
nftables_rule
end
def log_nftables
shell_out!('nft -n list ruleset')
rescue Mixlib::ShellOut::ShellCommandFailed
Chef::Log.info('log_nftables failed!')
rescue Mixlib::ShellOut::CommandTimeout
Chef::Log.info('log_nftables timed out!')
end
def default_ruleset(current_node)
current_node['sys']['firewall']['defaults']['ruleset'].to_h
def default_ruleset(new_resource)
rules = {
'add table inet filter' => 1,
"add chain inet filter input { type filter hook input priority 0 ; policy #{new_resource.input_policy}; }" => 2,
"add chain inet filter output { type filter hook output priority 0 ; policy #{new_resource.output_policy}; }" => 2,
"add chain inet filter forward { type filter hook forward priority 0 ; policy #{new_resource.forward_policy}; }" => 2,
}
if new_resource.table_ip_nat
rules['add table ip nat'] = 1
rules['add chain ip nat postrouting { type nat hook postrouting priority 100 ;}'] = 2
rules['add chain ip nat prerouting { type nat hook prerouting priority -100 ;}'] = 2
end
if new_resource.table_ip6_nat
rules['add table ip6 nat'] = 1
rules['add chain ip6 nat postrouting { type nat hook postrouting priority 100 ;}'] = 2
rules['add chain ip6 nat prerouting { type nat hook prerouting priority -100 ;}'] = 2
end
rules
end
def ensure_default_rules_exist(current_node, new_resource)
def ensure_default_rules_exist(new_resource)
input = new_resource.rules || {}
input.merge!(default_ruleset(current_node).to_h)
new_resource.rules(input)
input.merge!(default_ruleset(new_resource))
end
end
end
......
......@@ -16,4 +16,4 @@ supports 'debian'
depends 'line', '< 1.0'
depends 'chef-vault'
version '1.65.2'
version '1.66.0'
#
# Cookbook Name:: sys
# Recipe:: firewall
#
# Copyright 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH
#
# Authors:
# Matthias Pausch (m.pausch@gsi.de)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# This code is an adjustment of https://github.com/sous-chefs/firewall
#
return unless node['sys']['firewall']['manage']
return unless Gem::Requirement.new('>= 12.15').satisfied_by?(Gem::Version.new(Chef::VERSION))
fw_action = node['sys']['firewall']['disable'] ? :disable : :install
firewall 'default' do
action fw_action
end
firewall_rule 'allow loopback' do
interface 'lo'
protocol :none
command :allow
only_if { node['sys']['firewall']['allow_loopback'] }
end
firewall_rule 'allow icmp' do
protocol :icmp
command :allow
only_if { node['sys']['firewall']['allow_icmp'] }
end
firewall_rule 'allow world to ssh' do
port 22
only_if { node['sys']['firewall']['allow_ssh'] }
end
# allow established connections
firewall_rule 'established' do
position 40
stateful [:related, :established]
protocol :none # explicitly don't specify protocol
command :allow
only_if { node['sys']['firewall']['allow_established'] }
end
#
# Cookbook Name:: sys
# Resource:: firewall
# Cookbook:: sys
# Resource:: nftables
#
# Copyright 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH
# Copyright:: 2022 GSI Helmholtzzentrum fuer Schwerionenforschung GmbH
#
# Authors:
# Matthias Pausch (m.pausch@gsi.de)
......@@ -25,96 +25,71 @@
if Gem::Requirement.new('>= 12.15').satisfied_by?(Gem::Version.new(Chef::VERSION))
action_class do
include Sys::Helpers::Firewall
def lookup_or_create_service(name)
begin
nftables_service = Chef.run_context.resource_collection.find(service: name)
rescue
nftables_service = service name do
action :nothing
end
end
nftables_service
end
def lookup_or_create_rulesfile(name)
begin
nftables_file = Chef.run_context.resource_collection.find(file: name)
rescue
nftables_file = file name do
action :nothing
end
end
nftables_file
end
include Sys::Helpers::Nftables
end
#unified_mode true
provides :firewall, os: 'linux', platform: %w(debian)
property :rules, Hash