diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..711263e --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,22 @@ +--- +name: lint + +'on': + pull_request: + push: + branches: + - '**' + +jobs: + cookstyle: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + bundler-cache: true + - uses: r7kamura/rubocop-problem-matchers-action@v1 # this shows the failures in the PR + - name: Run cookstyle + working-directory: ./resources + run: bundle exec cookstyle diff --git a/.github/workflows/rpm.yml b/.github/workflows/rpm.yml new file mode 100644 index 0000000..73dd728 --- /dev/null +++ b/.github/workflows/rpm.yml @@ -0,0 +1,83 @@ +name: RPM Build and Upload + +on: + push: + branches: + - 'master' + - 'main' + +jobs: + build: + runs-on: ubuntu-latest + + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: true + + steps: + - name: Checkout Repository + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Create tag based on metadata.rb + id: create_tag + run: | + TAG=$(grep -o 'version\s*["'\''][^"'\'']*' ./resources/metadata.rb | sed 's/version\s*["'\'']//;s/["'\'']//') + echo "TAG=$TAG" >> $GITHUB_ENV + shell: bash + + - name: Check if Tag Exists + id: check_tag + run: | + if git rev-parse "refs/tags/${{ env.TAG }}" >/dev/null 2>&1; then + echo "Tag ${{ env.TAG }} already exists, exiting." + exit 1 + fi + shell: bash + + - name: Set Version + if: success() + run: echo "VERSION=${{ env.TAG }}" >> $GITHUB_ENV + + - name: Run Docker Container + if: success() + run: docker run --privileged -d --name builder --network host rockylinux:9 /bin/sleep infinity + + - name: Install build tools RPM + if: success() + run: | + docker cp ./ builder:/build + docker exec builder bash -c "yum install -y epel-release && yum install -y make git mock" + docker exec builder bash -c "rm -rf /etc/mock/default.cfg" + + - name: Setup SDK + if: success() + run: | + docker exec builder bash -c "curl https://raw.githubusercontent.com/redBorder/repoinit/master/sdk9.cfg > /build/sdk9.cfg" + docker exec builder bash -c "echo \"config_opts['use_host_resolv'] = True\" >> /build/sdk9.cfg" + docker exec builder bash -c "ln -s /build/sdk9.cfg /etc/mock/default.cfg" + + - name: Build RPM using mock + if: success() + run: | + docker exec builder bash -c "git config --global --add safe.directory /build" + docker exec builder bash -c "cd /build/ && VERSION=${{ env.TAG }} make rpm" + + - name: Copy RPMS + if: success() + run: | + docker cp builder:/build/packaging/rpm/pkgs/. ./rpms + + - name: Delete non-.rpm files + if: success() + run: | + find ./rpms -type f -not -name '*.rpm' -exec rm {} \; + + - name: Release + if: success() + uses: softprops/action-gh-release@v1 + with: + files: ./rpms/* + tag_name: ${{ env.TAG }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0918a01 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,17 @@ +cookbook-rb-firewall CHANGELOG +=============== + +## 0.0.2 + + - Luis Blanco + - [edc37b1] open rsyslog port + - [6d82494] remove execution permission. Cookbooks generally don't need it + - [bb31d1b] fix wrong pkg name + - [8d29494] cookbook build instructions + - nilsver + - [b857350] fix helper file and refactor + - [7792e08] add workflow + +## 0.0.1 +- Nils Verschaeve + - Initial release of firewall cookbook diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..9734e78 --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +source 'https://rubygems.org' + +gem 'cookstyle', '= 7.32.1' +gem 'rspec', '= 3.11' +gem 'rubocop', '= 1.25.1' diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a2b1ae8 --- /dev/null +++ b/Makefile @@ -0,0 +1,4 @@ +all: rpm + +rpm: + $(MAKE) -C packaging/rpm diff --git a/README.md b/README.md new file mode 100644 index 0000000..1707895 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# cookbook-rb-firewall + +cookbook to install and configure redborder firewall + +### Platforms + +- Rocky Linux 9 + +### Chef + +- Chef 15.1 or later + +## Building + +- Build rpm package for redborder platform: + * git clone https://github.com/redborder/cookbook-rb-firewall.git + * cd cookbook-rb-firewall + * make + * RPM packages is under packaging/rpm/pkgs/ + +## Contributing + +1. Fork the repository on Github +2. Create a named feature branch (like `add_component_x`) +3. Write your change +4. Write tests for your change (if applicable) +5. Run the tests, ensuring they all pass +6. Submit a Pull Request using Github + +## License and Authors + +Authors: Nils Verschaeve diff --git a/packaging/rpm/Makefile b/packaging/rpm/Makefile new file mode 100644 index 0000000..59895cf --- /dev/null +++ b/packaging/rpm/Makefile @@ -0,0 +1,54 @@ +PACKAGE_NAME?= cookbook-rb-firewall + +VERSION?= $(shell git describe --abbrev=6 --tags HEAD --always | sed 's/-/_/g') + +BUILD_NUMBER?= 1 + +MOCK_CONFIG?=default + +RESULT_DIR?=pkgs + +all: rpm + + +SOURCES: + mkdir -p SOURCES + +archive: SOURCES + cd ../../ && \ + git archive --prefix=$(PACKAGE_NAME)-$(VERSION)/ \ + -o packaging/rpm/SOURCES/$(PACKAGE_NAME)-$(VERSION).tar.gz HEAD + + +build_prepare: archive + mkdir -p $(RESULT_DIR) + rm -f $(RESULT_DIR)/$(PACKAGE_NAME)*.rpm + + +srpm: build_prepare + /usr/bin/mock \ + -r $(MOCK_CONFIG) \ + --define "__version $(VERSION)" \ + --define "__release $(BUILD_NUMBER)" \ + --resultdir=$(RESULT_DIR) \ + --buildsrpm \ + --spec=${PACKAGE_NAME}.spec \ + --sources=SOURCES + @echo "======= Source RPM now available in $(RESULT_DIR) =======" + + +rpm: srpm + /usr/bin/mock \ + -r $(MOCK_CONFIG) \ + --define "__version $(VERSION)"\ + --define "__release $(BUILD_NUMBER)"\ + --resultdir=$(RESULT_DIR) \ + --rebuild $(RESULT_DIR)/$(PACKAGE_NAME)*.src.rpm + @echo "======= Binary RPMs now available in $(RESULT_DIR) =======" + +clean: + rm -rf SOURCES pkgs + +distclean: clean + rm -f build.log root.log state.log available_pkgs installed_pkgs \ + *.rpm *.tar.gz diff --git a/packaging/rpm/cookbook-rb-firewall.spec b/packaging/rpm/cookbook-rb-firewall.spec new file mode 100644 index 0000000..c81ec44 --- /dev/null +++ b/packaging/rpm/cookbook-rb-firewall.spec @@ -0,0 +1,51 @@ +Name: cookbook-rb-firewall +Version: %{__version} +Release: %{__release}%{?dist} +BuildArch: noarch +Summary: Firewall cookbook to install and configure it in redborder environments + +License: AGPL 3.0 +URL: https://github.com/redBorder/cookbook-rb-firewall +Source0: %{name}-%{version}.tar.gz + +%description +%{summary} + +%prep +%setup -qn %{name}-%{version} + +%build + +%install +mkdir -p %{buildroot}/var/chef/cookbooks/rb-firewall +cp -f -r resources/* %{buildroot}/var/chef/cookbooks/rb-firewall +chmod -R 0644 %{buildroot}/var/chef/cookbooks/rb-firewall +install -D -m 0644 README.md %{buildroot}/var/chef/cookbooks/rb-firewall/README.md + +%pre + +%post +case "$1" in + 1) + # This is an initial install. + : + ;; + 2) + # This is an upgrade. + su - -s /bin/bash -c 'source /etc/profile && rvm gemset use default && env knife cookbook upload rb-firewall' + ;; +esac + +%files +%defattr(0644,root,root) +/var/chef/cookbooks/rb-firewall +# %defattr(0644,root,root) +# /var/chef/cookbooks/rb-firewall/README.md + +%doc + +%changelog +* Mon Nov 25 2024 Luis J. Blanco +- remove execution permission to the full path of the cookbook +* Tue Oct 08 2024 Nils Verschaeve +- first spec version diff --git a/resources/attributes/default.rb b/resources/attributes/default.rb new file mode 100644 index 0000000..059914a --- /dev/null +++ b/resources/attributes/default.rb @@ -0,0 +1,31 @@ +default['firewalld']['user'] = 'firewall' + +default['firewall']['roles'] = { + 'manager' => { + 'home' => { + 'tcp_ports' => [ + 53, 443, 514, 2056, 2057, 2058, 2181, 2888, 3888, 4443, + 5432, 7946, 7980, 8080, 8081, 8083, 8084, 8300, 8301, + 8302, 8400, 8500, 9000, 9001, 9092, 27017, 50505], + 'udp_ports' => [123, 161, 162, 514, 1812, 1813, 2055, 5353, 6343], + 'protocols' => ['igmp'], + }, + 'public' => { + 'tcp_ports' => [53, 443, 514, 2056, 2057, 2058, 8080, 8081, 8083, 8084, 9000, 9001], + 'udp_ports' => [53, 161, 162, 123, 514, 2055, 6343, 5353], + 'protocols' => ['112'], + 'rich_rules' => ['rule family="ipv4" source address="224.0.0.18" accept'], + }, + }, + 'proxy' => { + 'public' => { + 'tcp_ports' => [514, 2056, 2057, 2058, 7779], + 'udp_ports' => [161, 162, 1812, 1813, 2055, 6343], + }, + }, + 'ips' => { + 'public' => { + 'udp_ports' => [161, 162], + }, + }, +} diff --git a/resources/libraries/helper.rb b/resources/libraries/helper.rb new file mode 100644 index 0000000..cab4055 --- /dev/null +++ b/resources/libraries/helper.rb @@ -0,0 +1,81 @@ +module Firewall + module Helpers + require 'ipaddr' + require 'socket' + include ::Chef::Mixin::ShellOut + + def apply_rule(type, value, zone, protocol = nil) + case type + when :port + firewall_rule "Allow port #{value}/#{protocol} in #{zone} zone" do + port value + protocol protocol + zone zone + action :create + permanent true + not_if "firewall-cmd --permanent --zone=#{zone} --query-port=#{value}/#{protocol}" + end + when :protocol + firewall_rule "Allow protocol #{value} in #{zone} zone" do + protocols value + zone zone + action :create + permanent true + not_if "firewall-cmd --permanent --zone=#{zone} --query-protocol=#{value}" + end + when :rich_rule + firewall_rule "Adding rich rule #{value} in #{zone} zone" do + rules value + zone zone + action :create + permanent true + not_if "firewall-cmd --permanent --zone=#{zone} --query-rich-rule='#{value}'" + end + end + end + + def get_existing_ip_addresses_in_rules + rich_rules = shell_out!('firewall-cmd --zone=public --list-rich-rules').stdout + existing_ips = [] + rich_rules.split("\n").each do |rule| + if rule.include?('port="9092"') + ip_match = rule.match(/source address="([^"]+)"/) + existing_ips << ip_match[1] if ip_match + end + end + existing_ips + end + + def interface_for_ip(ip_address) + return if ip_address.nil? || ip_address.empty? + interfaces = Socket.getifaddrs + interface = interfaces.find do |ifaddr| + ifaddr.addr.ipv4? && ifaddr.addr.ip_address == ip_address + end + interface.name + end + + def ip_to_subnet(ip_address, prefix = 24) + ip = IPAddr.new(ip_address) + subnet = ip.mask(prefix) + "#{subnet}/#{prefix}" + end + + def is_proxy? + node.role?('proxy-sensor') + end + + def is_manager? + node.role?('manager') + end + + def is_ips? + node.role?('ips-sensor') || node.role?('ipscp-sensor') + end + + def get_ip_of_manager_ips_nodes + sensors = search(:node, 'role:ips-sensor').sort + sensors.map { |s| { ipaddress: s['ipaddress'] } } + end + end +end diff --git a/resources/metadata.rb b/resources/metadata.rb new file mode 100644 index 0000000..c3d8070 --- /dev/null +++ b/resources/metadata.rb @@ -0,0 +1,7 @@ +unified_mode 'true' +name 'rb-firewall' +maintainer 'Eneo TecnologĂ­a S.L.' +maintainer_email 'git@redborder.com' +license 'AGPL-3.0' +description 'Installs/Configures Firewall' +version '0.0.2' diff --git a/resources/providers/config.rb b/resources/providers/config.rb new file mode 100644 index 0000000..453ef7e --- /dev/null +++ b/resources/providers/config.rb @@ -0,0 +1,106 @@ +# Cookbook:: firewall +# Provider:: config + +include Firewall::Helpers + +action :add do + sync_ip = new_resource.sync_ip + ip_addr = new_resource.ip_addr + ip_address_ips_nodes = get_ip_of_manager_ips_nodes + + dnf_package 'firewalld' do + action :upgrade + flush_cache [:before] + end + + template '/etc/firewalld.conf' do + source 'firewalld.conf.erb' + cookbook 'rb-firewall' + notifies :restart, 'service[firewalld]', :delayed + end + + # Add sync interface and subnet to home zone + if is_manager? + sync_interface = interface_for_ip(sync_ip) + sync_subnet = ip_to_subnet(sync_ip) + + firewall_rule 'Add sync interface to home' do + interface sync_interface + zone 'home' + action :create + permanent true + not_if "firewall-cmd --zone=home --query-interface=#{sync_interface}" + end + + firewall_rule 'Add sync subnet to home' do + sources sync_subnet + zone 'home' + action :create + permanent true + not_if "firewall-cmd --zone=home --query-source=#{sync_subnet}" + end + end + + # Applying firewall ports, protocols, and rich rules based on zones + roles = { + 'manager' => %w(home public), + 'proxy' => %w(public), + 'ips' => %w(public), + } + roles.each do |role, zones| + next unless send("is_#{role}?") + zones.each do |zone| + zone_rules = node['firewall']['roles'][role][zone] + next if zone_rules.nil? + zone_rules['tcp_ports']&.each { |port| apply_rule(:port, port, zone, 'tcp') } + zone_rules['udp_ports']&.each { |port| apply_rule(:port, port, zone, 'udp') } + zone_rules['protocols']&.each { |protocol| apply_rule(:protocol, protocol, zone) } + zone_rules['rich_rules']&.each { |rule| apply_rule(:rich_rule, rule, zone) } + end + end + + # Managing port 9092 on the manager only for that specific IPS + if is_manager? && sync_ip != ip_addr + existing_addresses = get_existing_ip_addresses_in_rules + aux = ip_address_ips_nodes.empty? ? existing_addresses : ip_address_ips_nodes.map { |ips| ips[:ipaddress] } + + unless ip_address_ips_nodes.empty? + ips_to_remove = existing_addresses - aux + ips_to_remove.each do |ip| + firewall_rule "Remove Kafka port 9092 for IP: #{ip}" do + rules "rule family='ipv4' source address=#{ip} port port=9092 protocol=tcp accept" + zone 'public' + action :delete + permanent true + only_if "firewall-cmd --permanent --zone=public --query-rich-rule='rule family=\"ipv4\" source address=\"#{ip}\" port port=\"9092\" protocol=\"tcp\" accept'" + end + end + end + + aux.each do |ip| + firewall_rule "Open Kafka port 9092 for IP: #{ip}" do + rules "rule family='ipv4' source address=#{ip} port port=9092 protocol=tcp accept" + zone 'public' + action :create + permanent true + not_if "firewall-cmd --permanent --zone=public --query-rich-rule='rule family=\"ipv4\" source address=\"#{ip}\" port port=\"9092\" protocol=\"tcp\" accept'" + end + end + end + + service 'firewalld' do + service_name 'firewalld' + supports status: true, reload: true, restart: true, start: true, enable: true + action [:enable, :start, :reload] + end + + Chef::Log.info('Firewall configuration has been applied.') +end + +action :remove do + service 'firewalld' do + action [:disable, :stop] + end + + Chef::Log.info('Firewall configuration has been removed.') +end diff --git a/resources/recipes/default.rb b/resources/recipes/default.rb new file mode 100644 index 0000000..d823fee --- /dev/null +++ b/resources/recipes/default.rb @@ -0,0 +1,7 @@ +# Cookbook:: rb-firewall +# Recipe:: default + +# Call the firewall configuration +rb_firewall_config 'Configure Firewall' do + action :add +end diff --git a/resources/resources/config.rb b/resources/resources/config.rb new file mode 100644 index 0000000..7631494 --- /dev/null +++ b/resources/resources/config.rb @@ -0,0 +1,12 @@ +# Cookbook:: firewall +# +# Resource:: config +# + +unified_mode true +actions :add, :remove +default_action :add + +attribute :user, kind_of: String, default: 'firewall' +property :sync_ip, String, required: false +property :ip_addr, String, required: false diff --git a/resources/resources/firewall_rule.rb b/resources/resources/firewall_rule.rb new file mode 100644 index 0000000..53b4aa5 --- /dev/null +++ b/resources/resources/firewall_rule.rb @@ -0,0 +1,72 @@ +resource_name :firewall_rule +provides :firewall_rule + +unified_mode true + +property :port, kind_of: [Integer, Array, Range], required: false +property :protocol, kind_of: [String, Symbol], default: :tcp +property :zone, kind_of: String, default: 'public' +property :protocols, kind_of: String, required: false +property :rules, kind_of: [String, Array], required: false +property :interface, kind_of: String, required: false +property :sources, kind_of: String, required: false +property :permanent, kind_of: [TrueClass, FalseClass], default: true + +action :create do + extend Firewall::Helpers + + if shell_out!('firewall-cmd --state').stdout =~ /^running$/ + if new_resource.port + Array(new_resource.port).each do |port| + command = "firewall-cmd --zone=#{new_resource.zone} --add-port=#{port}/#{new_resource.protocol}" + command += ' --permanent' if new_resource.permanent + shell_out!(command) + end + end + + if new_resource.protocols + Array(new_resource.protocols).each do |prot| + command = "firewall-cmd --zone=#{new_resource.zone} --add-protocol=#{prot}" + command += ' --permanent' if new_resource.permanent + shell_out!(command) + end + end + + if new_resource.rules + Array(new_resource.rules).each do |rule| + command = "firewall-cmd --zone=#{new_resource.zone} --add-rich-rule='#{rule}'" + command += ' --permanent' if new_resource.permanent + shell_out!(command) + end + end + + if new_resource.interface + Array(new_resource.interface).each do |inf| + command = "firewall-cmd --zone=#{new_resource.zone} --add-interface=#{inf}" + command += ' --permanent' if new_resource.permanent + shell_out!(command) + end + end + + if new_resource.sources + Array(new_resource.sources).each do |src| + command = "firewall-cmd --zone=#{new_resource.zone} --add-source=#{src}" + command += ' --permanent' if new_resource.permanent + shell_out!(command) + end + end + else + Chef::Log.warn('firewalld is not running. Firewall rule will not be applied.') + end +end + +action :delete do + extend Firewall::Helpers + if new_resource.rules + Array(new_resource.rules).each do |rule| + command = "firewall-cmd --zone=#{new_resource.zone} --remove-rich-rule='#{rule}'" + command += ' --permanent' if new_resource.permanent + shell_out!(command) + end + end +end diff --git a/resources/templates/default/firewalld.conf.erb b/resources/templates/default/firewalld.conf.erb new file mode 100644 index 0000000..fcf98c9 --- /dev/null +++ b/resources/templates/default/firewalld.conf.erb @@ -0,0 +1,77 @@ +# Generated by Chef for <%= node[:hostname] %> +# Record the rate at which the system clock gains/losses time. +# firewalld config file + +# default zone +# The default zone used if an empty zone string is used. +# Default: public +DefaultZone=public + +# Clean up on exit +# If set to no or false the firewall configuration will not get cleaned up +# on exit or stop of firewalld. +# Default: yes +CleanupOnExit=yes + +# Clean up kernel modules on exit +# If set to yes or true the firewall related kernel modules will be +# unloaded on exit or stop of firewalld. This might attempt to unload +# modules not originally loaded by firewalld. +# Default: no +CleanupModulesOnExit=no + +# Lockdown +# If set to enabled, firewall changes with the D-Bus interface will be limited +# to applications that are listed in the lockdown whitelist. +# The lockdown whitelist file is lockdown-whitelist.xml +# Default: no +Lockdown=no + +# IPv6_rpfilter +# Performs a reverse path filter test on a packet for IPv6. If a reply to the +# packet would be sent via the same interface that the packet arrived on, the +# packet will match and be accepted, otherwise dropped. +# The rp_filter for IPv4 is controlled using sysctl. +# Note: This feature has a performance impact. See man page FIREWALLD.CONF(5) +# for details. +# Default: yes +IPv6_rpfilter=yes + +# IndividualCalls +# Do not use combined -restore calls, but individual calls. This increases the +# time that is needed to apply changes and to start the daemon, but is good for +# debugging. +# Default: no +IndividualCalls=no + +# LogDenied +# Add logging rules right before reject and drop rules in the INPUT, FORWARD +# and OUTPUT chains for the default rules and also final reject and drop rules +# in zones. Possible values are: all, unicast, broadcast, multicast and off. +# Default: off +LogDenied=off + +# FirewallBackend +# Selects the firewall backend implementation. +# Choices are: +# - nftables (default) +# - iptables (iptables, ip6tables, ebtables and ipset) +# Note: The iptables backend is deprecated. It will be removed in a future +# release. +FirewallBackend=nftables + +# FlushAllOnReload +# Flush all runtime rules on a reload. In previous releases some runtime +# configuration was retained during a reload, namely; interface to zone +# assignment, and direct rules. This was confusing to users. To get the old +# behavior set this to "no". +# Default: yes +FlushAllOnReload=yes + +# RFC3964_IPv4 +# As per RFC 3964, filter IPv6 traffic with 6to4 destination addresses that +# correspond to IPv4 addresses that should not be routed over the public +# internet. +# Defaults to "yes". +RFC3964_IPv4=yes +