From 6b1071fcb5af8c942378d289e437c93770d272f1 Mon Sep 17 00:00:00 2001 From: Matthew Alexander LaChance Date: Fri, 16 Aug 2024 15:18:06 -0400 Subject: [PATCH] Add fwknop profile to bastion role --- manifests/profile/fwknop.pp | 81 ++++++++++ manifests/profile/networking/firewall.pp | 6 + manifests/role/bastion.pp | 6 +- spec/classes/profile/fwknop_spec.rb | 177 ++++++++++++++++++++++ templates/profile/fwknop/access.conf.erb | 21 +++ templates/profile/fwknop/fwknopd.conf.erb | 3 + 6 files changed, 293 insertions(+), 1 deletion(-) create mode 100644 manifests/profile/fwknop.pp create mode 100644 spec/classes/profile/fwknop_spec.rb create mode 100644 templates/profile/fwknop/access.conf.erb create mode 100644 templates/profile/fwknop/fwknopd.conf.erb diff --git a/manifests/profile/fwknop.pp b/manifests/profile/fwknop.pp new file mode 100644 index 000000000..a83c09739 --- /dev/null +++ b/manifests/profile/fwknop.pp @@ -0,0 +1,81 @@ +# Copyright (c) 2024 The Regents of the University of Michigan. +# All Rights Reserved. Licensed according to the terms of the Revised +# BSD License. See LICENSE.txt for details. + +# nebula::profile::fwknop +# +# @param pcap_filter Protocol and port to sniff packets to +# @param default_open_ports Default ports clients are allowed to request +# @param default_source Default allowed client CIDRs +# @param server_gpg_fingerprint GPG key fingerprint for decrypting packets +# @param server_gpg_passphrase GPG key passphrase for decrypting packets +# @param access Array of client configs +# +# @example +# nebula::profile::fwknop::access: +# - username: "bob" +# symmetric: "kgohbCga6D5a4YZ0dtbL8SEVbjI1A5KYrRvj0oqcKEk=" +# hmac_key: "Zig9ZYcqj5gYl2S/UpFNp76RlD7SniyN5Ser5WoIKM7z" +# - username: "alice" +# source: "ANY" +# open_ports: "tcp/22, tcp/123" +# gpg: "7234ABCD" +# hmac_key: "STQ9m03hxj+WXwOpxMuNHQkTAx/EtfAKaXQ3tK8+Azcy" +# - username: "john" +# source: "3.3.3.0/24, 4.4.0.0/16" +# open_ports: "tcp/80" +# symmetric: "bOx25a5kjXf8/TmNQO1IRD3s/E9iLoPaqUbOv8X4VBA=" +# hmac_key: "i0mIhR//1146/T+IMxDVZm1gosNVatvpqpCfkv4X6Xzv" +class nebula::profile::fwknop ( + String $pcap_filter = 'udp port 62201', + String $default_open_ports = 'tcp/22, tcp/993', + String $default_source = '10.0.0.0/8, 192.168.0.0/16', + String $server_gpg_fingerprint = 'abcd1234', + String $server_gpg_passphrase = 'efgh5678', + Array[Hash[String, String]] $access = [], +) { + $primary_interface = $facts['networking']['primary'] + $access.each |$index, $account| { + if $account["username"] == undef { + $account_id = "nebula::profile::fwknop::access[${index}]" + } else { + $account_id = "nebula::profile::fwknop::access[${index}] (for ${account['username']})" + } + + if $account["hmac_key"] == undef { + fail("${account_id} missing required hmac_key") + } + + if $account["symmetric"] == undef { + if $account["gpg"] == undef { + fail("${account_id} missing required symmetric or gpg key") + } + } else { + if $account["gpg"] != undef { + fail("${account_id} cannot have both symmetric and gpg key") + } + } + } + + package { 'fwknop-server': } + service { 'fwknop-server': + require => Package['fwknop-server'], + } + + file { + default: + owner => 'root', + group => 'root', + mode => '0600', + notify => Service['fwknop-server'], + ; + + '/etc/fwknop/access.conf': + content => template('nebula/profile/fwknop/access.conf.erb'), + ; + + '/etc/fwknop/fwknopd.conf': + content => template('nebula/profile/fwknop/fwknopd.conf.erb'), + ; + } +} diff --git a/manifests/profile/networking/firewall.pp b/manifests/profile/networking/firewall.pp index f8cff0dd1..29cb0bb11 100644 --- a/manifests/profile/networking/firewall.pp +++ b/manifests/profile/networking/firewall.pp @@ -95,6 +95,12 @@ ] } + 'fwknop': { + $input_ignore = ['-j FWKNOP_INPUT'] + $output_ignore = [] + $forward_ignore = [] + } + default: { $input_ignore = [] $output_ignore = [] diff --git a/manifests/role/bastion.pp b/manifests/role/bastion.pp index 8c9f104e6..8bee77cd8 100644 --- a/manifests/role/bastion.pp +++ b/manifests/role/bastion.pp @@ -3,8 +3,12 @@ # BSD License. See LICENSE.txt for details. class nebula::role::bastion { - include nebula::role::minimum + class { 'nebula::role::minimum': + internal_routing = 'fwknop', + } + include nebula::profile::bolt + include nebula::profile::fwknop include nebula::profile::root_ssh_private_keys # These three are effectively the requirements for getting user login diff --git a/spec/classes/profile/fwknop_spec.rb b/spec/classes/profile/fwknop_spec.rb new file mode 100644 index 000000000..bf0a854a4 --- /dev/null +++ b/spec/classes/profile/fwknop_spec.rb @@ -0,0 +1,177 @@ +# frozen_string_literal: true + +# Copyright (c) 2024 The Regents of the University of Michigan. +# All Rights Reserved. Licensed according to the terms of the Revised +# BSD License. See LICENSE.txt for details. +require 'spec_helper' + +describe 'nebula::profile::fwknop' do + on_supported_os.each do |os, os_facts| + context "on #{os}" do + let(:facts) { os_facts } + + context "with defaults" do + it { is_expected.to compile } + + it { is_expected.to contain_package("fwknop-server") } + it { is_expected.to contain_service("fwknop-server").that_requires("Package[fwknop-server]") } + + it { is_expected.to contain_file("/etc/fwknop/fwknopd.conf").with_owner("root") } + it { is_expected.to contain_file("/etc/fwknop/fwknopd.conf").with_group("root") } + it { is_expected.to contain_file("/etc/fwknop/fwknopd.conf").with_mode("0600") } + it { is_expected.to contain_file("/etc/fwknop/fwknopd.conf").that_notifies("Service[fwknop-server]") } + + it do + is_expected.to contain_file("/etc/fwknop/fwknopd.conf").with_content( + <<~CONFIG + # Managed by puppet (nebula/profile/fwknop/fwknopd.conf.erb) + PCAP_INTF #{facts[:networking]["primary"]}; + PCAP_FILTER udp port 62201; + CONFIG + ) + end + + it { is_expected.to contain_file("/etc/fwknop/access.conf").with_owner("root") } + it { is_expected.to contain_file("/etc/fwknop/access.conf").with_group("root") } + it { is_expected.to contain_file("/etc/fwknop/access.conf").with_mode("0600") } + it { is_expected.to contain_file("/etc/fwknop/access.conf").that_notifies("Service[fwknop-server]") } + + it do + is_expected.to contain_file("/etc/fwknop/access.conf").with_content( + <<~CONFIG + # Managed by puppet (nebula/profile/fwknop/access.conf.erb) + CONFIG + ) + end + end + + context "with bare minimum access config" do + let(:params) { { access: [{ "hmac_key" => "eghmac", "symmetric" => "egkey" }] } } + + it { is_expected.to compile } + + it do + is_expected.to contain_file("/etc/fwknop/access.conf").with_content( + <<~CONFIG + # Managed by puppet (nebula/profile/fwknop/access.conf.erb) + SOURCE 10.0.0.0/8, 192.168.0.0/16 + OPEN_PORTS tcp/22, tcp/993 + REQUIRE_SOURCE_ADDRESS Y + KEY_BASE64 egkey + HMAC_KEY_BASE64 eghmac + CONFIG + ) + end + + context "with default_open_ports set to tcp and udp 53" do + let(:params) { super().merge("default_open_ports" => "tcp/53, udp/53") } + + it { is_expected.to compile } + + it do + is_expected.to contain_file("/etc/fwknop/access.conf").with_content( + <<~CONFIG + # Managed by puppet (nebula/profile/fwknop/access.conf.erb) + SOURCE 10.0.0.0/8, 192.168.0.0/16 + OPEN_PORTS tcp/53, udp/53 + REQUIRE_SOURCE_ADDRESS Y + KEY_BASE64 egkey + HMAC_KEY_BASE64 eghmac + CONFIG + ) + end + end + + context "with default_source set to ANY" do + let(:params) { super().merge("default_source" => "ANY") } + + it { is_expected.to compile } + + it do + is_expected.to contain_file("/etc/fwknop/access.conf").with_content( + <<~CONFIG + # Managed by puppet (nebula/profile/fwknop/access.conf.erb) + SOURCE ANY + OPEN_PORTS tcp/22, tcp/993 + REQUIRE_SOURCE_ADDRESS Y + KEY_BASE64 egkey + HMAC_KEY_BASE64 eghmac + CONFIG + ) + end + end + end + + context "with bare minimum access config but without hmac" do + let(:params) { { access: [{ "symmetric" => "egkey" }] } } + it { is_expected.not_to compile } + end + + context "with bare minimum access config but without key" do + let(:params) { { access: [{ "hmac_key" => "eghmac" }] } } + it { is_expected.not_to compile } + end + + context "with bare minimum access config but with both gpg and symmetric" do + let(:params) { { access: [{ "hmac_key" => "eghmac", "symmetric" => "egkey", "gpg" => "eggpg" }] } } + it { is_expected.not_to compile } + end + + context "with defaults" do + let(:params) do + { + access: [{ + "username" => "bob", + "symmetric" => "kgohbCga6D5a4YZ0dtbL8SEVbjI1A5KYrRvj0oqcKEk=", + "hmac_key" => "Zig9ZYcqj5gYl2S/UpFNp76RlD7SniyN5Ser5WoIKM7z", + }, { + "username" => "alice", + "source" => "ANY", + "open_ports" => "tcp/22, tcp/123", + "gpg" => "7234ABCD", + "hmac_key" => "STQ9m03hxj+WXwOpxMuNHQkTAx/EtfAKaXQ3tK8+Azcy", + }, { + "username" => "john", + "source" => "3.3.3.0/24, 4.4.0.0/16", + "open_ports" => "tcp/80", + "symmetric" => "bOx25a5kjXf8/TmNQO1IRD3s/E9iLoPaqUbOv8X4VBA=", + "hmac_key" => "i0mIhR//1146/T+IMxDVZm1gosNVatvpqpCfkv4X6Xzv", + }] + } + end + + it { is_expected.to compile } + + it do + is_expected.to contain_file("/etc/fwknop/access.conf").with_content( + <<~CONFIG + # Managed by puppet (nebula/profile/fwknop/access.conf.erb) + SOURCE 10.0.0.0/8, 192.168.0.0/16 + REQUIRE_USERNAME bob + OPEN_PORTS tcp/22, tcp/993 + REQUIRE_SOURCE_ADDRESS Y + KEY_BASE64 kgohbCga6D5a4YZ0dtbL8SEVbjI1A5KYrRvj0oqcKEk= + HMAC_KEY_BASE64 Zig9ZYcqj5gYl2S/UpFNp76RlD7SniyN5Ser5WoIKM7z + + SOURCE ANY + REQUIRE_USERNAME alice + OPEN_PORTS tcp/22, tcp/123 + REQUIRE_SOURCE_ADDRESS Y + GPG_REMOTE_ID 7234ABCD + GPG_DECRYPT_ID abcd1234 + GPG_DECRYPT_PW efgh5678 + HMAC_KEY_BASE64 STQ9m03hxj+WXwOpxMuNHQkTAx/EtfAKaXQ3tK8+Azcy + + SOURCE 3.3.3.0/24, 4.4.0.0/16 + REQUIRE_USERNAME john + OPEN_PORTS tcp/80 + REQUIRE_SOURCE_ADDRESS Y + KEY_BASE64 bOx25a5kjXf8/TmNQO1IRD3s/E9iLoPaqUbOv8X4VBA= + HMAC_KEY_BASE64 i0mIhR//1146/T+IMxDVZm1gosNVatvpqpCfkv4X6Xzv + CONFIG + ) + end + end + end + end +end diff --git a/templates/profile/fwknop/access.conf.erb b/templates/profile/fwknop/access.conf.erb new file mode 100644 index 000000000..009b5b5b1 --- /dev/null +++ b/templates/profile/fwknop/access.conf.erb @@ -0,0 +1,21 @@ +# Managed by puppet (nebula/profile/fwknop/access.conf.erb) +<% @access.each_index do |index| -%> +<% if index > 0 -%> + +<% end -%> +SOURCE <%= @access[index].fetch("source", @default_source) %> +<% if @access[index].has_key?("username") -%> +REQUIRE_USERNAME <%= @access[index]["username"] %> +<% end -%> +OPEN_PORTS <%= @access[index].fetch("open_ports", @default_open_ports) %> +REQUIRE_SOURCE_ADDRESS Y +<% if @access[index].has_key?("symmetric") -%> +KEY_BASE64 <%= @access[index]["symmetric"] %> +<% end -%> +<% if @access[index].has_key?("gpg") -%> +GPG_REMOTE_ID <%= @access[index]["gpg"] %> +GPG_DECRYPT_ID <%= @server_gpg_fingerprint %> +GPG_DECRYPT_PW <%= @server_gpg_passphrase %> +<% end -%> +HMAC_KEY_BASE64 <%= @access[index]["hmac_key"] %> +<% end -%> diff --git a/templates/profile/fwknop/fwknopd.conf.erb b/templates/profile/fwknop/fwknopd.conf.erb new file mode 100644 index 000000000..b2ad42ac6 --- /dev/null +++ b/templates/profile/fwknop/fwknopd.conf.erb @@ -0,0 +1,3 @@ +# Managed by puppet (nebula/profile/fwknop/fwknopd.conf.erb) +PCAP_INTF <%= @primary_interface %>; +PCAP_FILTER <%= @pcap_filter %>;