diff --git a/manifests/mod/security.pp b/manifests/mod/security.pp index 3d00e61b27..bbcf497f11 100644 --- a/manifests/mod/security.pp +++ b/manifests/mod/security.pp @@ -137,7 +137,10 @@ # @see https://github.com/SpiderLabs/ModSecurity/wiki for additional documentation. # @see https://coreruleset.org/docs/ for addional documentation # -# @note Unsupported platforms: RedHat: 10 +# @note On RHEL/EL 10 the ModSecurity engine is provided by EPEL (enable EPEL +# yourself; this module does not manage it). The OWASP CRS package +# (`mod_security_crs`) is not available on EL10, so the class manages the +# engine only there and does not install or activate CRS rules. class apache::mod::security ( Stdlib::Absolutepath $logroot = $apache::params::logroot, Integer $version = $apache::params::modsec_version, diff --git a/manifests/params.pp b/manifests/params.pp index 6ca2e15703..48763db608 100644 --- a/manifests/params.pp +++ b/manifests/params.pp @@ -115,12 +115,23 @@ $mellon_cache_size = 100 $mellon_post_directory = undef $modsec_version = 1 - $modsec_crs_package = 'mod_security_crs' + # EL10 base/EPEL ships the ModSecurity engine but not the mod_security_crs + # package, so install the engine only and leave CRS unmanaged there. + $modsec_crs_package = $facts['os']['release']['major'] ? { + '10' => undef, # RedHat 10 doesn't ship mod_security_crs (CRS tracked separately) + default => 'mod_security_crs', + } $modsec_dir = '/etc/httpd/modsecurity.d' $secpcrematchlimit = 1500 $secpcrematchlimitrecursion = 1500 $modsec_secruleengine = 'On' - if $facts['os']['family'] == 'RedHat' and versioncmp($facts['os']['release']['major'], '7') <= 0 { + if $facts['os']['family'] == 'RedHat' and versioncmp($facts['os']['release']['major'], '10') >= 0 { + # Engine-only on EL10: no CRS package, so no rules are activated. The path + # is kept at the EL8+ default since it is only referenced when rules are + # activated (none here). + $modsec_crs_path = '/usr/share/mod_modsecurity_crs' + $modsec_default_rules = [] + } elsif $facts['os']['family'] == 'RedHat' and versioncmp($facts['os']['release']['major'], '7') <= 0 { $modsec_crs_path = '/usr/lib/modsecurity.d' $modsec_default_rules = [ 'base_rules/modsecurity_35_bad_robots.data', @@ -260,12 +271,23 @@ $mellon_cache_size = 100 $mellon_post_directory = undef $modsec_version = 1 - $modsec_crs_package = 'mod_security_crs' + # EL10 base/EPEL ships the ModSecurity engine but not the mod_security_crs + # package, so install the engine only and leave CRS unmanaged there. + $modsec_crs_package = $facts['os']['release']['major'] ? { + '10' => undef, # RedHat 10 doesn't ship mod_security_crs (CRS tracked separately) + default => 'mod_security_crs', + } $modsec_dir = '/etc/httpd/modsecurity.d' $secpcrematchlimit = 1500 $secpcrematchlimitrecursion = 1500 $modsec_secruleengine = 'On' - if $facts['os']['family'] == 'RedHat' and versioncmp($facts['os']['release']['major'], '7') <= 0 { + if $facts['os']['family'] == 'RedHat' and versioncmp($facts['os']['release']['major'], '10') >= 0 { + # Engine-only on EL10: no CRS package, so no rules are activated. The path + # is kept at the EL8+ default since it is only referenced when rules are + # activated (none here). + $modsec_crs_path = '/usr/share/mod_modsecurity_crs' + $modsec_default_rules = [] + } elsif $facts['os']['family'] == 'RedHat' and versioncmp($facts['os']['release']['major'], '7') <= 0 { $modsec_crs_path = '/usr/lib/modsecurity.d' $modsec_default_rules = [ 'base_rules/modsecurity_35_bad_robots.data', diff --git a/spec/classes/mod/security_spec.rb b/spec/classes/mod/security_spec.rb index 1263777670..b9ff1a8de9 100644 --- a/spec/classes/mod/security_spec.rb +++ b/spec/classes/mod/security_spec.rb @@ -33,7 +33,12 @@ ) } - it { is_expected.to contain_package('mod_security_crs') } + if facts[:os]['release']['major'].to_i >= 10 + # EL10 ships the engine via EPEL but not the mod_security_crs package. + it { is_expected.not_to contain_package('mod_security_crs') } + else + it { is_expected.to contain_package('mod_security_crs') } + end if (facts[:os]['release']['major'].to_i > 6 && facts[:os]['release']['major'].to_i <= 7) || (facts[:os]['release']['major'].to_i >= 8) it { @@ -73,7 +78,10 @@ ) } - if facts[:os]['release']['major'].to_i <= 7 + if facts[:os]['release']['major'].to_i >= 10 + # EL10 is engine-only (no CRS package), so no rules are activated. + it { is_expected.not_to contain_apache__security__rule_link('rules/crawlers-user-agents.data') } + elsif facts[:os]['release']['major'].to_i <= 7 it { is_expected.to contain_apache__security__rule_link('base_rules/modsecurity_35_bad_robots.data') } it { @@ -384,4 +392,50 @@ end end end + + # FacterDB does not yet ship a RedHat-named EL10 factset, so on_supported_os + # cannot generate a redhat-10 context. Exercise the engine-only EL10 path + # explicitly with hardcoded facts. + context 'on RedHat 10 (engine-only via EPEL)' do + let :facts do + { + os: { + 'architecture' => 'x86_64', + 'family' => 'RedHat', + 'hardware' => 'x86_64', + 'name' => 'RedHat', + 'release' => { 'full' => '10.0', 'major' => '10' }, + 'selinux' => { 'enabled' => false }, + }, + } + end + + it { is_expected.to compile.with_all_deps } + + it { + expect(subject).to contain_apache__mod('security').with( + id: 'security2_module', + lib: 'mod_security2.so', + ) + } + + it { + expect(subject).to contain_apache__mod('unique_id').with( + id: 'unique_id_module', + lib: 'mod_unique_id.so', + ) + } + + # EL10 ships the engine via EPEL but not the mod_security_crs package, so + # the CRS package must not be managed and no rules are activated. + it { is_expected.not_to contain_package('mod_security_crs') } + it { is_expected.not_to contain_apache__security__rule_link('rules/crawlers-user-agents.data') } + + # The engine config is still written. + it { + expect(subject).to contain_file('security.conf').with( + path: '/etc/httpd/conf.modules.d/security.conf', + ) + } + end end