Skip to content

Commit a2eaff9

Browse files
authored
Merge pull request #2637 from puppetlabs/MODULES-11857
(MODULES-11857) Scaffold OWASP CRS v4 support on EL10 via crs_source enum
2 parents a014c47 + 72d9595 commit a2eaff9

9 files changed

Lines changed: 473 additions & 15 deletions

File tree

.fixtures.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
---
22
fixtures:
33
repositories:
4+
archive: "https://github.com/voxpupuli/puppet-archive.git"
45
concat: "https://github.com/puppetlabs/puppetlabs-concat.git"
56
facts: 'https://github.com/puppetlabs/puppetlabs-facts.git'
67
portage: "https://github.com/gentoo/puppet-portage.git"

README.md

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
[Installing arbitrary modules]: #installing-arbitrary-modules
1616
[Installing specific modules]: #installing-specific-modules
1717
[Load balancing examples]: #load-balancing-examples
18+
[Configuring mod_security and the OWASP Core Rule Set]: #configuring-mod_security-and-the-owasp-core-rule-set
1819
[apache affects]: #what-the-apache-module-affects
1920

2021
[Reference]: #reference
@@ -183,6 +184,7 @@
183184
[`mod_python`]: http://modpython.org/
184185
[`mod_rewrite`]: https://httpd.apache.org/docs/current/mod/mod_rewrite.html
185186
[`mod_security`]: https://www.modsecurity.org/
187+
[`puppet/archive`]: https://forge.puppet.com/modules/puppet/archive
186188
[`mod_ssl`]: https://httpd.apache.org/docs/current/mod/mod_ssl.html
187189
[`mod_status`]: https://httpd.apache.org/docs/current/mod/mod_status.html
188190
[`mod_version`]: https://httpd.apache.org/docs/current/mod/mod_version.html
@@ -274,6 +276,7 @@
274276
3. [Usage - The classes and defined types available for configuration][Usage]
275277
- [Configuring virtual hosts - Examples to help get started][Configuring virtual hosts]
276278
- [Load balancing with exported and non-exported resources][Load balancing examples]
279+
- [Configuring mod_security and the OWASP Core Rule Set][Configuring mod_security and the OWASP Core Rule Set]
277280
4. [Reference - An under-the-hood peek at what the module is doing and how][Reference]
278281
5. [Limitations - OS compatibility, etc.][Limitations]
279282
6. [License][License]
@@ -741,6 +744,62 @@ apache::balancer { 'puppet01':
741744

742745
Load balancing scheduler algorithms (`lbmethod`) are listed [in mod_proxy_balancer documentation](https://httpd.apache.org/docs/current/mod/mod_proxy_balancer.html).
743746

747+
<a id="configuring-mod_security-and-the-owasp-core-rule-set"></a>
748+
### Configuring mod_security and the OWASP Core Rule Set
749+
750+
The [`apache::mod::security`][] class configures [`mod_security`][] and, optionally, the OWASP Core Rule Set (CRS) rules that run on top of it. How CRS is obtained is controlled by the `crs_source` parameter:
751+
752+
| `crs_source` | Behavior | Typical use |
753+
| --- | --- | --- |
754+
| `package` | Installs `crs_package` (CRS v2/v3) and activates rules via per-rule symlinks. Default on RHEL/CentOS 7, 8, and 9. | Distros where an EPEL/OS `mod_security_crs` package still exists. |
755+
| `archive` | Downloads a CRS v4 tarball with [`puppet/archive`][] from `crs_archive_source` and wires up the v4 `crs-setup.conf` + `rules/*.conf` includes. | RHEL/CentOS 10 and other platforms with no CRS package, when you can fetch the tarball from the internet or an internal mirror. |
756+
| `path` | Wires up an already-extracted CRS v4 directory at `crs_path`; nothing is downloaded. | Air-gapped hosts with no reachable download source, where CRS is pre-staged by other means. |
757+
| `none` | Manages the `mod_security`/`mod_security2` engine only; no CRS rules are installed or activated. Default on RHEL/CentOS 10. | You want the engine without CRS, or you're not yet ready to opt in to CRS on EL10. |
758+
759+
> As of RHEL/CentOS 10, there is no `mod_security_crs` package available (from EPEL or otherwise), so `crs_source` defaults to `none` there. CRS is opt-in via `archive` or `path`. RHEL/CentOS 7/8/9 are unaffected and keep the `package` default.
760+
761+
#### `package` (RHEL/CentOS 7, 8, 9 — default, unchanged)
762+
763+
```puppet
764+
class { 'apache::mod::security': }
765+
```
766+
767+
#### `archive` (download a CRS v4 tarball, e.g. from an internal mirror)
768+
769+
`crs_archive_source` and `crs_version` are required. There is no module-shipped default URL or version — you pin both yourself, so upgrading CRS is always an explicit, deliberate action:
770+
771+
```puppet
772+
class { 'apache::mod::security':
773+
crs_source => 'archive',
774+
crs_archive_source => 'https://mirror.example.com/coreruleset-4.27.0-minimal.tar.gz',
775+
crs_version => '4.27.0',
776+
crs_archive_checksum => '<sha256 checksum of the tarball>',
777+
}
778+
```
779+
780+
`crs_archive_source` accepts any source [`puppet/archive`][] supports (an `https://` URL, an internal Artifactory/Nexus mirror, or even a local `file://` path), which lets restricted environments point at an internal proxy instead of the public internet. `crs_archive_checksum` is optional — omit it only when the source is already trusted (for example, a controlled internal mirror).
781+
782+
The tarball is expected to unpack to a versioned `coreruleset-<crs_version>/` directory containing `crs-setup.conf.example` and `rules/*.conf`, matching the layout of the [upstream CRS releases](https://github.com/coreruleset/coreruleset/releases). By default it's extracted under `/usr/share`; override the extraction base with `crs_path`.
783+
784+
#### `path` (pre-staged directory, no download — for air-gapped hosts)
785+
786+
```puppet
787+
class { 'apache::mod::security':
788+
crs_source => 'path',
789+
crs_path => '/opt/crs',
790+
}
791+
```
792+
793+
`crs_path` must point at a directory that already contains `crs-setup.conf` and `rules/*.conf` (for example, staged there by a separate content-delivery process). The module only wires up the `IncludeOptional` directives — it does not fetch or validate the rule content.
794+
795+
#### `none` (engine only, no CRS — default on RHEL/CentOS 10)
796+
797+
```puppet
798+
class { 'apache::mod::security':
799+
crs_source => 'none',
800+
}
801+
```
802+
744803
<a id="reference"></a>
745804
## Reference
746805

REFERENCE.md

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6173,8 +6173,11 @@ Installs and configures `mod_security`.
61736173

61746174
* **Note** On RHEL/EL 10 the ModSecurity engine is provided by EPEL (enable EPEL
61756175
yourself; this module does not manage it). The OWASP CRS package
6176-
(`mod_security_crs`) is not available on EL10, so the class manages the
6177-
engine only there and does not install or activate CRS rules.
6176+
(`mod_security_crs`) is not available on EL10, so `crs_source` defaults to
6177+
`none` (engine only). CRS v4 can be opted into there via `crs_source =>
6178+
'archive'` (downloaded from `crs_archive_source`, e.g. an internal mirror)
6179+
or `crs_source => 'path'` (a pre-staged directory). EL7/8/9 keep the
6180+
package-based default unchanged.
61786181

61796182
* **See also**
61806183
* https://github.com/SpiderLabs/ModSecurity/wiki
@@ -6189,6 +6192,12 @@ The following parameters are available in the `apache::mod::security` class:
61896192
* [`version`](#-apache--mod--security--version)
61906193
* [`logroot`](#-apache--mod--security--logroot)
61916194
* [`crs_package`](#-apache--mod--security--crs_package)
6195+
* [`crs_source`](#-apache--mod--security--crs_source)
6196+
* [`crs_archive_source`](#-apache--mod--security--crs_archive_source)
6197+
* [`crs_archive_checksum`](#-apache--mod--security--crs_archive_checksum)
6198+
* [`crs_archive_checksum_type`](#-apache--mod--security--crs_archive_checksum_type)
6199+
* [`crs_version`](#-apache--mod--security--crs_version)
6200+
* [`crs_path`](#-apache--mod--security--crs_path)
61926201
* [`activated_rules`](#-apache--mod--security--activated_rules)
61936202
* [`custom_rules`](#-apache--mod--security--custom_rules)
61946203
* [`custom_rules_set`](#-apache--mod--security--custom_rules_set)
@@ -6249,10 +6258,66 @@ Default value: `$apache::params::logroot`
62496258

62506259
Data type: `Optional[String]`
62516260

6252-
Name of package that installs CRS rules.
6261+
Name of package that installs CRS rules. Only used when `crs_source` is `package`.
62536262

62546263
Default value: `$apache::params::modsec_crs_package`
62556264

6265+
##### <a name="-apache--mod--security--crs_source"></a>`crs_source`
6266+
6267+
Data type: `Enum['package', 'archive', 'path', 'none']`
6268+
6269+
How the OWASP Core Rule Set is obtained:
6270+
* `package` - install `crs_package` and activate rules via per-rule symlinks (v2/v3 layout). Default on EL7/8/9.
6271+
* `archive` - download the CRS v4 tarball via `puppet/archive` from `crs_archive_source` (e.g. an internal mirror) and wire the v4 includes.
6272+
* `path` - use a pre-staged CRS v4 directory given by `crs_path` (no download); only wires the v4 includes.
6273+
* `none` - engine only, no CRS managed. Default on EL10.
6274+
6275+
Default value: `$apache::params::modsec_crs_source`
6276+
6277+
##### <a name="-apache--mod--security--crs_archive_source"></a>`crs_archive_source`
6278+
6279+
Data type: `Optional[String[1]]`
6280+
6281+
Source URL or path for the CRS v4 tarball when `crs_source` is `archive`. No module default
6282+
(user-pinned, e.g. an internal mirror) to avoid a version/CVE maintenance treadmill. Required for `archive`.
6283+
6284+
Default value: `$apache::params::modsec_crs_archive_source`
6285+
6286+
##### <a name="-apache--mod--security--crs_archive_checksum"></a>`crs_archive_checksum`
6287+
6288+
Data type: `Optional[String[1]]`
6289+
6290+
Checksum of the CRS v4 tarball for verification when `crs_source` is `archive`.
6291+
6292+
Default value: `$apache::params::modsec_crs_archive_checksum`
6293+
6294+
##### <a name="-apache--mod--security--crs_archive_checksum_type"></a>`crs_archive_checksum_type`
6295+
6296+
Data type: `String[1]`
6297+
6298+
Checksum algorithm for `crs_archive_checksum` (e.g. `sha256`).
6299+
6300+
Default value: `$apache::params::modsec_crs_archive_checksum_type`
6301+
6302+
##### <a name="-apache--mod--security--crs_version"></a>`crs_version`
6303+
6304+
Data type: `Optional[String[1]]`
6305+
6306+
The pinned CRS version (e.g. `4.27.0`), required for `crs_source => archive`. It fixes the
6307+
extracted `coreruleset-<version>` directory name so the include paths are deterministic.
6308+
6309+
Default value: `undef`
6310+
6311+
##### <a name="-apache--mod--security--crs_path"></a>`crs_path`
6312+
6313+
Data type: `Optional[Stdlib::Absolutepath]`
6314+
6315+
For `crs_source => path`: absolute path to the pre-staged CRS v4 directory (contains
6316+
`crs-setup.conf` and `rules/`). For `crs_source => archive`: overrides the extraction base
6317+
directory (default `/usr/share`); the ruleset then lives at `<crs_path>/coreruleset-<crs_version>`.
6318+
6319+
Default value: `undef`
6320+
62566321
##### <a name="-apache--mod--security--activated_rules"></a>`activated_rules`
62576322

62586323
Data type: `Array[String]`

manifests/mod/security.pp

Lines changed: 129 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,34 @@
88
# Configures the location of audit and debug logs.
99
#
1010
# @param crs_package
11-
# Name of package that installs CRS rules.
12-
#
11+
# Name of package that installs CRS rules. Only used when `crs_source` is `package`.
12+
#
13+
# @param crs_source
14+
# How the OWASP Core Rule Set is obtained:
15+
# * `package` - install `crs_package` and activate rules via per-rule symlinks (v2/v3 layout). Default on EL7/8/9.
16+
# * `archive` - download the CRS v4 tarball via `puppet/archive` from `crs_archive_source` (e.g. an internal mirror) and wire the v4 includes.
17+
# * `path` - use a pre-staged CRS v4 directory given by `crs_path` (no download); only wires the v4 includes.
18+
# * `none` - engine only, no CRS managed. Default on EL10.
19+
#
20+
# @param crs_archive_source
21+
# Source URL or path for the CRS v4 tarball when `crs_source` is `archive`. No module default
22+
# (user-pinned, e.g. an internal mirror) to avoid a version/CVE maintenance treadmill. Required for `archive`.
23+
#
24+
# @param crs_archive_checksum
25+
# Checksum of the CRS v4 tarball for verification when `crs_source` is `archive`.
26+
#
27+
# @param crs_archive_checksum_type
28+
# Checksum algorithm for `crs_archive_checksum` (e.g. `sha256`).
29+
#
30+
# @param crs_version
31+
# The pinned CRS version (e.g. `4.27.0`), required for `crs_source => archive`. It fixes the
32+
# extracted `coreruleset-<version>` directory name so the include paths are deterministic.
33+
#
34+
# @param crs_path
35+
# For `crs_source => path`: absolute path to the pre-staged CRS v4 directory (contains
36+
# `crs-setup.conf` and `rules/`). For `crs_source => archive`: overrides the extraction base
37+
# directory (default `/usr/share`); the ruleset then lives at `<crs_path>/coreruleset-<crs_version>`.
38+
#
1339
# @param activated_rules
1440
# An array of rules from the modsec_crs_path or absolute to activate via symlinks.
1541
#
@@ -139,12 +165,21 @@
139165
#
140166
# @note On RHEL/EL 10 the ModSecurity engine is provided by EPEL (enable EPEL
141167
# yourself; this module does not manage it). The OWASP CRS package
142-
# (`mod_security_crs`) is not available on EL10, so the class manages the
143-
# engine only there and does not install or activate CRS rules.
168+
# (`mod_security_crs`) is not available on EL10, so `crs_source` defaults to
169+
# `none` (engine only). CRS v4 can be opted into there via `crs_source =>
170+
# 'archive'` (downloaded from `crs_archive_source`, e.g. an internal mirror)
171+
# or `crs_source => 'path'` (a pre-staged directory). EL7/8/9 keep the
172+
# package-based default unchanged.
144173
class apache::mod::security (
145174
Stdlib::Absolutepath $logroot = $apache::params::logroot,
146175
Integer $version = $apache::params::modsec_version,
147176
Optional[String] $crs_package = $apache::params::modsec_crs_package,
177+
Enum['package', 'archive', 'path', 'none'] $crs_source = $apache::params::modsec_crs_source,
178+
Optional[String[1]] $crs_archive_source = $apache::params::modsec_crs_archive_source,
179+
Optional[String[1]] $crs_archive_checksum = $apache::params::modsec_crs_archive_checksum,
180+
String[1] $crs_archive_checksum_type = $apache::params::modsec_crs_archive_checksum_type,
181+
Optional[String[1]] $crs_version = undef,
182+
Optional[Stdlib::Absolutepath] $crs_path = undef,
148183
Array[String] $activated_rules = $apache::params::modsec_default_rules,
149184
Boolean $custom_rules = $apache::params::modsec_custom_rules,
150185
Optional[Array[String]] $custom_rules_set = $apache::params::modsec_custom_rules_set,
@@ -227,14 +262,82 @@
227262
lib => 'mod_unique_id.so',
228263
}
229264

230-
if $crs_package {
231-
package { $crs_package:
232-
ensure => 'installed',
233-
before => [
234-
File[$apache::confd_dir],
235-
File[$modsec_dir],
236-
],
265+
# Effective on-disk CRS v4 directory used in the include wiring. Kept outside
266+
# $modsec_dir, which is managed with purge => true and would otherwise remove
267+
# the extracted rule tree.
268+
# - path: $crs_path is the ready CRS directory (contains crs-setup.conf + rules/).
269+
# - archive: CRS tarballs unpack to a versioned dir, so the ruleset lives at
270+
# <base>/coreruleset-<crs_version> under the extraction base.
271+
$_crs_extract_base = $crs_path ? {
272+
undef => '/usr/share',
273+
default => $crs_path,
274+
}
275+
$_crs_dir = $crs_source ? {
276+
'archive' => "${_crs_extract_base}/coreruleset-${crs_version}",
277+
default => $crs_path,
278+
}
279+
280+
# CRS acquisition. The activation wiring is selected later by the same
281+
# $crs_source: `package` keeps the legacy per-rule symlinks (v2/v3 layout),
282+
# while `archive`/`path` use the v4 include layout.
283+
case $crs_source {
284+
'package': {
285+
if $crs_package {
286+
package { $crs_package:
287+
ensure => 'installed',
288+
before => [
289+
File[$apache::confd_dir],
290+
File[$modsec_dir],
291+
],
292+
}
293+
}
237294
}
295+
'archive': {
296+
if ! $crs_archive_source {
297+
fail('apache::mod::security: crs_source => "archive" requires crs_archive_source (URL/path to the CRS v4 tarball, e.g. an internal mirror).')
298+
}
299+
if ! $crs_version {
300+
fail('apache::mod::security: crs_source => "archive" requires crs_version (the pinned CRS version, e.g. "4.27.0"); it fixes the extracted coreruleset-<version> directory name.')
301+
}
302+
303+
file { $_crs_extract_base:
304+
ensure => directory,
305+
}
306+
307+
# Both the release "-minimal" asset and the source archive unpack to a
308+
# versioned top-level dir, coreruleset-<crs_version>/. Checksum
309+
# verification is only enabled when a checksum is supplied (a trusted
310+
# internal mirror may legitimately be used without one).
311+
archive { 'coreruleset.tar.gz':
312+
ensure => present,
313+
path => '/var/cache/coreruleset.tar.gz',
314+
source => $crs_archive_source,
315+
checksum => $crs_archive_checksum,
316+
checksum_type => $crs_archive_checksum_type,
317+
checksum_verify => $crs_archive_checksum =~ NotUndef,
318+
extract => true,
319+
extract_path => $_crs_extract_base,
320+
creates => "${_crs_dir}/crs-setup.conf.example",
321+
cleanup => true,
322+
require => File[$_crs_extract_base],
323+
}
324+
325+
# CRS ships crs-setup.conf.example; create the active crs-setup.conf from
326+
# it once. The creates guard prevents clobbering later user edits.
327+
exec { 'apache-crs-setup-conf':
328+
command => ['/bin/cp', "${_crs_dir}/crs-setup.conf.example", "${_crs_dir}/crs-setup.conf"],
329+
creates => "${_crs_dir}/crs-setup.conf",
330+
require => Archive['coreruleset.tar.gz'],
331+
notify => Class['apache::service'],
332+
}
333+
}
334+
'path': {
335+
if ! $crs_path {
336+
fail('apache::mod::security: crs_source => "path" requires crs_path (absolute path to a pre-staged CRS v4 directory).')
337+
}
338+
}
339+
'none': {}
340+
default: {}
238341
}
239342

240343
# Template uses:
@@ -329,7 +432,9 @@
329432
}
330433
}
331434

332-
if $manage_security_crs {
435+
if $manage_security_crs and $crs_source == 'package' {
436+
# Legacy CRS v2/v3 layout: a tuning conf plus per-rule symlinks under
437+
# activated_rules/. Unchanged behaviour for EL7/8/9 package installs.
333438
# Template uses:
334439
# - $_secdefaultaction
335440
# - $critical_anomaly_score
@@ -381,4 +486,16 @@
381486
apache::security::rule_link { $activated_rules: }
382487
}
383488
}
489+
490+
if $manage_security_crs and $crs_source in ['archive', 'path'] {
491+
# CRS v4 layout: load crs-setup.conf then rules/*.conf from the CRS
492+
# directory. Dropped into $modsec_dir so the existing
493+
# `IncludeOptional ${modsec_dir}/*.conf` in security.conf picks it up.
494+
file { "${modsec_dir}/security_crs_v4.conf":
495+
ensure => file,
496+
content => epp('apache/mod/security_crs_v4.conf.epp', { 'crs_dir' => $_crs_dir }),
497+
require => File[$modsec_dir],
498+
notify => Class['apache::service'],
499+
}
500+
}
384501
}

manifests/params.pp

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -806,4 +806,19 @@
806806
$ssl_cipher = 'HIGH:MEDIUM:!aNULL:!MD5:!RC4:!3DES'
807807
$ssl_proxy_cipher_suite = undef
808808
}
809+
810+
# OWASP CRS acquisition mode (see apache::mod::security).
811+
# EL10 has no mod_security_crs package, so default to engine-only there
812+
# (CRS is opt-in via the archive/path modes). Every other platform keeps
813+
# the existing package-based behaviour, so nothing changes for them.
814+
if $facts['os']['family'] == 'RedHat' and versioncmp($facts['os']['release']['major'], '10') >= 0 {
815+
$modsec_crs_source = 'none'
816+
} else {
817+
$modsec_crs_source = 'package'
818+
}
819+
# No module-shipped default version/URL: the archive source is user-pinned
820+
# (e.g. an internal mirror) to keep us off the CRS version/CVE treadmill.
821+
$modsec_crs_archive_source = undef
822+
$modsec_crs_archive_checksum = undef
823+
$modsec_crs_archive_checksum_type = 'sha256'
809824
}

metadata.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
{
1616
"name": "puppetlabs/concat",
1717
"version_requirement": ">= 2.2.1 < 11.0.0"
18+
},
19+
{
20+
"name": "puppet/archive",
21+
"version_requirement": ">= 4.0.0 < 9.0.0"
1822
}
1923
],
2024
"operatingsystem_support": [

0 commit comments

Comments
 (0)