Skip to content
1 change: 1 addition & 0 deletions .fixtures.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
fixtures:
repositories:
archive: "https://github.com/voxpupuli/puppet-archive.git"
concat: "https://github.com/puppetlabs/puppetlabs-concat.git"
facts: 'https://github.com/puppetlabs/puppetlabs-facts.git'
portage: "https://github.com/gentoo/puppet-portage.git"
Expand Down
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
[Installing arbitrary modules]: #installing-arbitrary-modules
[Installing specific modules]: #installing-specific-modules
[Load balancing examples]: #load-balancing-examples
[Configuring mod_security and the OWASP Core Rule Set]: #configuring-mod_security-and-the-owasp-core-rule-set
[apache affects]: #what-the-apache-module-affects

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

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

<a id="configuring-mod_security-and-the-owasp-core-rule-set"></a>
### Configuring mod_security and the OWASP Core Rule Set

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:

| `crs_source` | Behavior | Typical use |
| --- | --- | --- |
| `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. |
| `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. |
| `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. |
| `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. |

> 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.

#### `package` (RHEL/CentOS 7, 8, 9 — default, unchanged)

```puppet
class { 'apache::mod::security': }
```

#### `archive` (download a CRS v4 tarball, e.g. from an internal mirror)

`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:

```puppet
class { 'apache::mod::security':
crs_source => 'archive',
crs_archive_source => 'https://mirror.example.com/coreruleset-4.27.0-minimal.tar.gz',
crs_version => '4.27.0',
crs_archive_checksum => '<sha256 checksum of the tarball>',
}
```

`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).

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`.

#### `path` (pre-staged directory, no download — for air-gapped hosts)

```puppet
class { 'apache::mod::security':
crs_source => 'path',
crs_path => '/opt/crs',
}
```

`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.

#### `none` (engine only, no CRS — default on RHEL/CentOS 10)

```puppet
class { 'apache::mod::security':
crs_source => 'none',
}
```

<a id="reference"></a>
## Reference

Expand Down
71 changes: 68 additions & 3 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6173,8 +6173,11 @@ Installs and configures `mod_security`.

* **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.
(`mod_security_crs`) is not available on EL10, so `crs_source` defaults to
`none` (engine only). CRS v4 can be opted into there via `crs_source =>
'archive'` (downloaded from `crs_archive_source`, e.g. an internal mirror)
or `crs_source => 'path'` (a pre-staged directory). EL7/8/9 keep the
package-based default unchanged.

* **See also**
* https://github.com/SpiderLabs/ModSecurity/wiki
Expand All @@ -6189,6 +6192,12 @@ The following parameters are available in the `apache::mod::security` class:
* [`version`](#-apache--mod--security--version)
* [`logroot`](#-apache--mod--security--logroot)
* [`crs_package`](#-apache--mod--security--crs_package)
* [`crs_source`](#-apache--mod--security--crs_source)
* [`crs_archive_source`](#-apache--mod--security--crs_archive_source)
* [`crs_archive_checksum`](#-apache--mod--security--crs_archive_checksum)
* [`crs_archive_checksum_type`](#-apache--mod--security--crs_archive_checksum_type)
* [`crs_version`](#-apache--mod--security--crs_version)
* [`crs_path`](#-apache--mod--security--crs_path)
* [`activated_rules`](#-apache--mod--security--activated_rules)
* [`custom_rules`](#-apache--mod--security--custom_rules)
* [`custom_rules_set`](#-apache--mod--security--custom_rules_set)
Expand Down Expand Up @@ -6249,10 +6258,66 @@ Default value: `$apache::params::logroot`

Data type: `Optional[String]`

Name of package that installs CRS rules.
Name of package that installs CRS rules. Only used when `crs_source` is `package`.

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

##### <a name="-apache--mod--security--crs_source"></a>`crs_source`

Data type: `Enum['package', 'archive', 'path', 'none']`

How the OWASP Core Rule Set is obtained:
* `package` - install `crs_package` and activate rules via per-rule symlinks (v2/v3 layout). Default on EL7/8/9.
* `archive` - download the CRS v4 tarball via `puppet/archive` from `crs_archive_source` (e.g. an internal mirror) and wire the v4 includes.
* `path` - use a pre-staged CRS v4 directory given by `crs_path` (no download); only wires the v4 includes.
* `none` - engine only, no CRS managed. Default on EL10.

Default value: `$apache::params::modsec_crs_source`

##### <a name="-apache--mod--security--crs_archive_source"></a>`crs_archive_source`

Data type: `Optional[String[1]]`

Source URL or path for the CRS v4 tarball when `crs_source` is `archive`. No module default
(user-pinned, e.g. an internal mirror) to avoid a version/CVE maintenance treadmill. Required for `archive`.

Default value: `$apache::params::modsec_crs_archive_source`

##### <a name="-apache--mod--security--crs_archive_checksum"></a>`crs_archive_checksum`

Data type: `Optional[String[1]]`

Checksum of the CRS v4 tarball for verification when `crs_source` is `archive`.

Default value: `$apache::params::modsec_crs_archive_checksum`

##### <a name="-apache--mod--security--crs_archive_checksum_type"></a>`crs_archive_checksum_type`

Data type: `String[1]`

Checksum algorithm for `crs_archive_checksum` (e.g. `sha256`).

Default value: `$apache::params::modsec_crs_archive_checksum_type`

##### <a name="-apache--mod--security--crs_version"></a>`crs_version`

Data type: `Optional[String[1]]`

The pinned CRS version (e.g. `4.27.0`), required for `crs_source => archive`. It fixes the
extracted `coreruleset-<version>` directory name so the include paths are deterministic.

Default value: `undef`

##### <a name="-apache--mod--security--crs_path"></a>`crs_path`

Data type: `Optional[Stdlib::Absolutepath]`

For `crs_source => path`: absolute path to the pre-staged CRS v4 directory (contains
`crs-setup.conf` and `rules/`). For `crs_source => archive`: overrides the extraction base
directory (default `/usr/share`); the ruleset then lives at `<crs_path>/coreruleset-<crs_version>`.

Default value: `undef`

##### <a name="-apache--mod--security--activated_rules"></a>`activated_rules`

Data type: `Array[String]`
Expand Down
141 changes: 129 additions & 12 deletions manifests/mod/security.pp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,34 @@
# Configures the location of audit and debug logs.
#
# @param crs_package
# Name of package that installs CRS rules.
#
# Name of package that installs CRS rules. Only used when `crs_source` is `package`.
#
# @param crs_source
# How the OWASP Core Rule Set is obtained:
# * `package` - install `crs_package` and activate rules via per-rule symlinks (v2/v3 layout). Default on EL7/8/9.
# * `archive` - download the CRS v4 tarball via `puppet/archive` from `crs_archive_source` (e.g. an internal mirror) and wire the v4 includes.
# * `path` - use a pre-staged CRS v4 directory given by `crs_path` (no download); only wires the v4 includes.
# * `none` - engine only, no CRS managed. Default on EL10.
#
# @param crs_archive_source
# Source URL or path for the CRS v4 tarball when `crs_source` is `archive`. No module default
# (user-pinned, e.g. an internal mirror) to avoid a version/CVE maintenance treadmill. Required for `archive`.
#
# @param crs_archive_checksum
# Checksum of the CRS v4 tarball for verification when `crs_source` is `archive`.
#
# @param crs_archive_checksum_type
# Checksum algorithm for `crs_archive_checksum` (e.g. `sha256`).
#
# @param crs_version
# The pinned CRS version (e.g. `4.27.0`), required for `crs_source => archive`. It fixes the
# extracted `coreruleset-<version>` directory name so the include paths are deterministic.
#
# @param crs_path
# For `crs_source => path`: absolute path to the pre-staged CRS v4 directory (contains
# `crs-setup.conf` and `rules/`). For `crs_source => archive`: overrides the extraction base
# directory (default `/usr/share`); the ruleset then lives at `<crs_path>/coreruleset-<crs_version>`.
#
# @param activated_rules
# An array of rules from the modsec_crs_path or absolute to activate via symlinks.
#
Expand Down Expand Up @@ -139,12 +165,21 @@
#
# @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.
# (`mod_security_crs`) is not available on EL10, so `crs_source` defaults to
# `none` (engine only). CRS v4 can be opted into there via `crs_source =>
# 'archive'` (downloaded from `crs_archive_source`, e.g. an internal mirror)
# or `crs_source => 'path'` (a pre-staged directory). EL7/8/9 keep the
# package-based default unchanged.
class apache::mod::security (
Stdlib::Absolutepath $logroot = $apache::params::logroot,
Integer $version = $apache::params::modsec_version,
Optional[String] $crs_package = $apache::params::modsec_crs_package,
Enum['package', 'archive', 'path', 'none'] $crs_source = $apache::params::modsec_crs_source,
Optional[String[1]] $crs_archive_source = $apache::params::modsec_crs_archive_source,
Optional[String[1]] $crs_archive_checksum = $apache::params::modsec_crs_archive_checksum,
String[1] $crs_archive_checksum_type = $apache::params::modsec_crs_archive_checksum_type,
Optional[String[1]] $crs_version = undef,
Optional[Stdlib::Absolutepath] $crs_path = undef,
Array[String] $activated_rules = $apache::params::modsec_default_rules,
Boolean $custom_rules = $apache::params::modsec_custom_rules,
Optional[Array[String]] $custom_rules_set = $apache::params::modsec_custom_rules_set,
Expand Down Expand Up @@ -227,14 +262,82 @@
lib => 'mod_unique_id.so',
}

if $crs_package {
package { $crs_package:
ensure => 'installed',
before => [
File[$apache::confd_dir],
File[$modsec_dir],
],
# Effective on-disk CRS v4 directory used in the include wiring. Kept outside
# $modsec_dir, which is managed with purge => true and would otherwise remove
# the extracted rule tree.
# - path: $crs_path is the ready CRS directory (contains crs-setup.conf + rules/).
# - archive: CRS tarballs unpack to a versioned dir, so the ruleset lives at
# <base>/coreruleset-<crs_version> under the extraction base.
$_crs_extract_base = $crs_path ? {
undef => '/usr/share',
default => $crs_path,
}
$_crs_dir = $crs_source ? {
'archive' => "${_crs_extract_base}/coreruleset-${crs_version}",
default => $crs_path,
}

# CRS acquisition. The activation wiring is selected later by the same
# $crs_source: `package` keeps the legacy per-rule symlinks (v2/v3 layout),
# while `archive`/`path` use the v4 include layout.
case $crs_source {
'package': {
if $crs_package {
package { $crs_package:
ensure => 'installed',
before => [
File[$apache::confd_dir],
File[$modsec_dir],
],
}
}
}
'archive': {
if ! $crs_archive_source {
fail('apache::mod::security: crs_source => "archive" requires crs_archive_source (URL/path to the CRS v4 tarball, e.g. an internal mirror).')
}
if ! $crs_version {
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.')
}

file { $_crs_extract_base:
ensure => directory,
}

# Both the release "-minimal" asset and the source archive unpack to a
# versioned top-level dir, coreruleset-<crs_version>/. Checksum
# verification is only enabled when a checksum is supplied (a trusted
# internal mirror may legitimately be used without one).
archive { 'coreruleset.tar.gz':
ensure => present,
path => '/var/cache/coreruleset.tar.gz',
source => $crs_archive_source,
checksum => $crs_archive_checksum,
checksum_type => $crs_archive_checksum_type,
checksum_verify => $crs_archive_checksum =~ NotUndef,
extract => true,
extract_path => $_crs_extract_base,
creates => "${_crs_dir}/crs-setup.conf.example",
cleanup => true,
require => File[$_crs_extract_base],
}

# CRS ships crs-setup.conf.example; create the active crs-setup.conf from
# it once. The creates guard prevents clobbering later user edits.
exec { 'apache-crs-setup-conf':
command => "/bin/cp ${_crs_dir}/crs-setup.conf.example ${_crs_dir}/crs-setup.conf",
creates => "${_crs_dir}/crs-setup.conf",
require => Archive['coreruleset.tar.gz'],
notify => Class['apache::service'],
}
Comment thread
Copilot marked this conversation as resolved.
}
'path': {
if ! $crs_path {
fail('apache::mod::security: crs_source => "path" requires crs_path (absolute path to a pre-staged CRS v4 directory).')
}
}
'none': {}
default: {}
}

# Template uses:
Expand Down Expand Up @@ -329,7 +432,9 @@
}
}

if $manage_security_crs {
if $manage_security_crs and $crs_source == 'package' {
# Legacy CRS v2/v3 layout: a tuning conf plus per-rule symlinks under
# activated_rules/. Unchanged behaviour for EL7/8/9 package installs.
# Template uses:
# - $_secdefaultaction
# - $critical_anomaly_score
Expand Down Expand Up @@ -381,4 +486,16 @@
apache::security::rule_link { $activated_rules: }
}
}

if $manage_security_crs and $crs_source in ['archive', 'path'] {
# CRS v4 layout: load crs-setup.conf then rules/*.conf from the CRS
# directory. Dropped into $modsec_dir so the existing
# `IncludeOptional ${modsec_dir}/*.conf` in security.conf picks it up.
file { "${modsec_dir}/security_crs_v4.conf":
ensure => file,
content => epp('apache/mod/security_crs_v4.conf.epp', { 'crs_dir' => $_crs_dir }),
require => File[$modsec_dir],
notify => Class['apache::service'],
}
}
}
15 changes: 15 additions & 0 deletions manifests/params.pp
Original file line number Diff line number Diff line change
Expand Up @@ -806,4 +806,19 @@
$ssl_cipher = 'HIGH:MEDIUM:!aNULL:!MD5:!RC4:!3DES'
$ssl_proxy_cipher_suite = undef
}

# OWASP CRS acquisition mode (see apache::mod::security).
# EL10 has no mod_security_crs package, so default to engine-only there
# (CRS is opt-in via the archive/path modes). Every other platform keeps
# the existing package-based behaviour, so nothing changes for them.
if $facts['os']['family'] == 'RedHat' and versioncmp($facts['os']['release']['major'], '10') >= 0 {
$modsec_crs_source = 'none'
} else {
$modsec_crs_source = 'package'
}
# No module-shipped default version/URL: the archive source is user-pinned
# (e.g. an internal mirror) to keep us off the CRS version/CVE treadmill.
$modsec_crs_archive_source = undef
$modsec_crs_archive_checksum = undef
$modsec_crs_archive_checksum_type = 'sha256'
}
4 changes: 4 additions & 0 deletions metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
{
"name": "puppetlabs/concat",
"version_requirement": ">= 2.2.1 < 11.0.0"
},
{
"name": "puppet/archive",
"version_requirement": ">= 4.0.0 < 9.0.0"
}
],
"operatingsystem_support": [
Expand Down
Loading
Loading