Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Breaking Changes

* **role:apache_httpd**: A `conf-available` snippet and the Apache module it configures have to be enabled together from now on. The snippets no longer carry an `<IfModule>` guard, so enabling one without its module aborts the run at the config test instead of being ignored in silence. Getting the role's own defaults consistent changed two of them: the `deflate` and `mod_security` snippets are no longer enabled, matching their modules, which never were. A host that switched the `deflate` module on in its inventory had compression and loses it, so enable the snippet alongside the module to keep it.
* **role:php**: The PHP-FPM pool configuration changed for existing hosts. Sessions now live in a per-pool directory (the default `www` pool moves from `/var/lib/php/session` to `/var/lib/php/session/www`), so logged-in users are signed out once after the upgrade. `memory_limit`, `max_execution_time`, `max_input_vars`, `post_max_size`, `upload_max_filesize`, `session.save_handler` and `session.save_path` are now enforced as `php_admin_value`, so applications can no longer change them at runtime via `ini_set()`. A pool that stores its sessions in redis or memcached sets `php_admin_value_session_save_handler` accordingly. `soap.wsdl_cache_dir` moves from the shared `/var/lib/php/wsdlcache` to a per-pool directory below it. On Debian the pool socket now belongs to `root` and grants the web server access through a POSIX ACL entry, where it used to be owned by the web server user; anything of your own that checks the socket's ownership rather than connecting to it needs adjusting. Worker processes recycle after 500 requests (`pm.max_requests`) instead of running indefinitely, and a worker still serving a single request after 60 seconds is killed (`request_terminate_timeout`, previously off). Hosts with legitimately long-running web requests raise `php__fpm_pool_conf_request_terminate_timeout__group_var`.
* **role:keycloak**: Rename `keycloak__state` to `keycloak__service_state`, the name every other LFOps role uses. The value `reloaded` is gone: Keycloak's systemd unit has no `ExecReload`, so a reload never worked; use `restarted` instead.
* **role:keycloak**: The role installs the OpenJDK its Keycloak version needs (OpenJDK 17 for Keycloak 24, OpenJDK 21 for 25 and newer) instead of relying on the `apps` role, which `setup_keycloak` no longer runs. Hosts that used `apps__apps__*_var` through this playbook to install further packages have to run the `apps` playbook for them.
Expand Down Expand Up @@ -60,6 +61,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* **role:apache_httpd**: The `cgi` and `cgid` modules are loaded when the inventory enables them; until now the entry was accepted but the module never ended up in the running server.
* **role:apache_httpd**: Enabling an Apache module restarts httpd instead of reloading it, so the module actually takes effect. A reload loads the module but skips its initialization, which left HTTP/2 configured but silently inactive on RHEL 8 ([#339](https://github.com/Linuxfabrik/lfops/issues/339)).
* **role:kvm_vm**: Two VMs created from the same inventory host no longer get the same MAC address, which made the second one fail with `The MAC address ... is in use by another virtual machine`. The generated MAC follows the VM name now. Hosts that leave `kvm_vm__name` at its default keep the MAC they had; on a host that sets it, a VM created from now on gets a different MAC, so check DHCP reservations and firewall rules keyed on it before recreating such a VM. Running VMs are not touched.
* **role:keycloak**: A run against an unchanged host reports no changes any more. The tarball is only downloaded and extracted when the installed version differs from `keycloak__version`, and `kc.sh build` only runs when the installation or `keycloak.conf` actually changed, which also takes minutes off an ordinary run.
Expand Down
19 changes: 16 additions & 3 deletions roles/apache_httpd/EXAMPLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,6 @@ A minimal working "app" vHost definition for a PHP-FPM application, located unde
```yaml
apache_httpd__conf_server_admin: 'webmaster@example.com'
apache_httpd__mods__host_var:
- filename: 'cgi'
enabled: true
state: 'present'
- filename: 'proxy_fcgi'
enabled: true
state: 'present'
Expand All @@ -47,7 +44,17 @@ apache_httpd__vhosts__host_var:

### Reverse Proxy

Compression is off by default, so this example turns it on: `mod_deflate` and `mod_filter` supply
the filter, and the `deflate` conf snippet is what actually applies it to the MIME types listed in
it. Both halves are needed; enabling only the modules compresses nothing, enabling only the conf
fails the config test.

```yaml
apache_httpd__conf__host_var:
- filename: 'deflate'
enabled: true
state: 'present'
template: 'deflate'
apache_httpd__mods__host_var:
- filename: 'deflate'
enabled: true
Expand Down Expand Up @@ -131,8 +138,14 @@ apache_httpd__vhosts__host_var:

This is an Apache configuration that is close to the RHEL default configuration, without any CIS remediations.

It enables `cgid` because the stock RHEL configuration loads it, not because the vHost below needs it: that vHost is a pure reverse proxy and never runs a CGI script. Loading the module starts the external CGI daemon and creates its `ScriptSock` at every start, so drop the entry unless you actually serve CGI.

```yaml
apache_httpd__conf__host_var:
- filename: 'deflate'
enabled: true
state: 'present'
template: 'deflate'
- filename: 'expires'
enabled: false
state: 'present'
Expand Down
10 changes: 7 additions & 3 deletions roles/apache_httpd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ The config is split into several files forming the configuration hierarchy outli
`-- sites-enabled/
```

We avoid using `<IfModule>` in vHost definitions and in the global `httpd.conf` to facilitate debugging. Without `<IfModule>`, a missing module causes a clear startup error instead of silently dropping configuration. `<IfModule>` is only used in `mods-available/` and `conf-available/` where it is necessary to guard module-specific configuration.
We avoid `<IfModule>` throughout. Without it, a missing module causes a clear startup error instead of silently dropping configuration, which is otherwise impossible to spot: `httpd -t` reports `Syntax OK` and the setting simply never applies.

The consequence is a pairing rule: **a `conf-available` snippet and the module it configures have to be enabled together.** Enabling the `deflate` snippet without `mod_deflate` and `mod_filter` aborts the run at the config test with `Invalid command 'AddOutputFilterByType'`, which is the intended behaviour. The role's own defaults are consistent, so this only concerns snippets and modules you enable yourself via `apache_httpd__conf__*_var` and `apache_httpd__mods__*_var`.

Two exceptions remain, both guarding against something other than a mismatch: `mods-available/wsgi_python3.conf` uses `<IfModule !wsgi_module>` so that two mod_wsgi builds never load into one process, and `conf-available/php.conf` guards its user-directory block with `<IfModule userdir_module>` because the role does not manage mod_userdir at all.

`mod_info` is not enabled. It serves the complete configuration on `/server-info`, including the credentials other modules carry in their directives. The endpoint stays configured in the localhost vHost and answers with an empty response until the `info` module is enabled via `apache_httpd__mods__group_var` / `apache_httpd__mods__host_var`.

Expand Down Expand Up @@ -885,14 +889,14 @@ apache_httpd__skip_mod_security_coreruleset: true

`apache_httpd__mod_ssl_ssl_use_stapling`

* See [SSLUseStapling](https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslusestapling).
* Whether the server staples an OCSP response into the TLS handshake. Off, because a Let's Encrypt certificate carries no OCSP responder URL, so there is nothing to staple and Apache logs a warning per certificate on every start. Set it to `'on'` for a certificate from a CA that does publish an OCSP endpoint. See [SSLUseStapling](https://httpd.apache.org/docs/2.4/mod/mod_ssl.html#sslusestapling).
* Type: String.
* Default: `'off'`

Example:
```yaml
# optional - mod_ssl
apache_httpd__mod_ssl_ssl_use_stapling: 'on'
apache_httpd__mod_ssl_ssl_use_stapling: 'off'
```


Expand Down
9 changes: 7 additions & 2 deletions roles/apache_httpd/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,13 @@ apache_httpd__conf__group_var: []
apache_httpd__conf__host_var: []
# Apache conf used internally by this role.
apache_httpd__conf__role_var:
- filename: 'autoindex'
enabled: false
state: 'absent'
template: 'autoindex'

- filename: 'deflate'
enabled: true
enabled: false
state: 'present'
template: 'deflate'

Expand Down Expand Up @@ -113,7 +118,7 @@ apache_httpd__conf__role_var:
template: 'mime_magic'

- filename: 'mod_security'
enabled: true
enabled: false
state: 'present'
template: 'mod_security'

Expand Down
2 changes: 1 addition & 1 deletion roles/apache_httpd/meta/argument_specs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ argument_specs:
apache_httpd__mod_ssl_ssl_use_stapling:
type: 'str'
required: false
default: 'on'
default: 'off'
description: >-
Whether OCSP stapling is used (`SSLUseStapling`).

Expand Down
Original file line number Diff line number Diff line change
@@ -1,47 +1,47 @@
# {{ ansible_managed }}
# 2022102501
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}

# mod_autoindex.c
<IfModule autoindex_module>
Alias /icons/ "/usr/share/httpd/icons/"
<Directory "/usr/share/httpd/icons">
Options Indexes MultiViews FollowSymlinks
AllowOverride None
Require all granted
</Directory>
AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip
AddIconByType (IMG,/icons/image2.gif) image/*
AddIconByType (SND,/icons/sound2.gif) audio/*
AddIconByType (TXT,/icons/text.gif) text/*
AddIconByType (VID,/icons/movie.gif) video/*
AddIcon /icons/a.gif .ps .ai .eps
AddIcon /icons/back.gif ..
AddIcon /icons/binary.gif .bin .exe
AddIcon /icons/binhex.gif .hqx
AddIcon /icons/blank.gif ^^BLANKICON^^
AddIcon /icons/bomb.gif */core.*
AddIcon /icons/bomb.gif /core
AddIcon /icons/c.gif .c
AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip
AddIcon /icons/dvi.gif .dvi
AddIcon /icons/f.gif .for
AddIcon /icons/folder.gif ^^DIRECTORY^^
AddIcon /icons/hand.right.gif README
AddIcon /icons/layout.gif .html .shtml .htm .pdf
AddIcon /icons/p.gif .pl .py
AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl
AddIcon /icons/tar.gif .tar
AddIcon /icons/tex.gif .tex
AddIcon /icons/text.gif .txt
AddIcon /icons/uuencoded.gif .uu
AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv
DefaultIcon /icons/unknown.gif
# autoindex_module
# Requires the autoindex module. No <IfModule> guard: enabling this conf without the module has to
# fail the config test rather than do nothing in silence.
Alias /icons/ "/usr/share/httpd/icons/"
<Directory "/usr/share/httpd/icons">
Options Indexes MultiViews FollowSymlinks
AllowOverride None
Require all granted
</Directory>
AddIconByEncoding (CMP,/icons/compressed.gif) x-compress x-gzip
AddIconByType (IMG,/icons/image2.gif) image/*
AddIconByType (SND,/icons/sound2.gif) audio/*
AddIconByType (TXT,/icons/text.gif) text/*
AddIconByType (VID,/icons/movie.gif) video/*
AddIcon /icons/a.gif .ps .ai .eps
AddIcon /icons/back.gif ..
AddIcon /icons/binary.gif .bin .exe
AddIcon /icons/binhex.gif .hqx
AddIcon /icons/blank.gif ^^BLANKICON^^
AddIcon /icons/bomb.gif */core.*
AddIcon /icons/bomb.gif /core
AddIcon /icons/c.gif .c
AddIcon /icons/compressed.gif .Z .z .tgz .gz .zip
AddIcon /icons/dvi.gif .dvi
AddIcon /icons/f.gif .for
AddIcon /icons/folder.gif ^^DIRECTORY^^
AddIcon /icons/hand.right.gif README
AddIcon /icons/layout.gif .html .shtml .htm .pdf
AddIcon /icons/p.gif .pl .py
AddIcon /icons/script.gif .conf .sh .shar .csh .ksh .tcl
AddIcon /icons/tar.gif .tar
AddIcon /icons/tex.gif .tex
AddIcon /icons/text.gif .txt
AddIcon /icons/uuencoded.gif .uu
AddIcon /icons/world2.gif .wrl .wrl.gz .vrml .vrm .iv
DefaultIcon /icons/unknown.gif

HeaderName /HEADER.html
ReadmeName README.html
IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t assets favicon*
IndexStyleSheet "/assets/style.css"
</IfModule>
HeaderName /HEADER.html
ReadmeName README.html
IndexIgnore .??* *~ *# HEADER* README* RCS CVS *,v *,t assets favicon*
IndexStyleSheet "/assets/style.css"
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
# {{ ansible_managed }}
# 2022082301
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}

# mod_deflate.c
<IfModule deflate_module>
# mod_filter.c
<IfModule filter_module>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript
AddOutputFilterByType DEFLATE application/x-javascript application/javascript application/ecmascript
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/wasm
AddOutputFilterByType DEFLATE application/xml
</IfModule>
</IfModule>
# deflate_module, filter_module
# AddOutputFilterByType comes from mod_filter and the DEFLATE filter from mod_deflate, so this
# snippet needs both modules enabled. It carries no <IfModule> guard on purpose: enabling the conf
# without the modules has to fail the config test rather than compress nothing in silence.
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript
AddOutputFilterByType DEFLATE application/x-javascript application/javascript application/ecmascript
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/wasm
AddOutputFilterByType DEFLATE application/xml
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# {{ ansible_managed }}
# 2021120101
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}

# security2_module
<IfModule security2_module>
SecDataDir /tmp/
SecPcreMatchLimit 100000
SecPcreMatchLimitRecursion 100000
</IfModule>
# Requires the security2 module. No <IfModule> guard: enabling this conf without the module has to
# fail the config test rather than leave the tuning silently unapplied.
SecDataDir /tmp/
SecPcreMatchLimit 100000
SecPcreMatchLimitRecursion 100000
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# {{ ansible_managed }}
# 2021111201
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}
Expand Down Expand Up @@ -33,7 +33,7 @@ AddType text/html .php
# To re-enable PHP in user directories comment the following lines
# (from <IfModule ...> to </IfModule>.) Do NOT set it to On as it
# prevents .htaccess files from disabling it.
<IfModule userdir>
<IfModule userdir_module>
<Directory /home/*/public_html>
php_admin_flag engine Off
</Directory>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# {{ ansible_managed }}
# 2021110301
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}

<IfModule authn_core_module>
LoadModule authn_file_module {{ __apache_httpd__modules_path }}/mod_authn_file.so
</IfModule>
# The Require directives this module provides are consumed by mod_authn_core, which the role enables
# by default. Loading it without authn_core is not an error, so there is nothing to guard against.
LoadModule authn_file_module {{ __apache_httpd__modules_path }}/mod_authn_file.so
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# {{ ansible_managed }}
# 2021110301
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}

<IfModule authz_core_module>
LoadModule authz_host_module {{ __apache_httpd__modules_path }}/mod_authz_host.so
</IfModule>
# The Require directives this module provides are consumed by mod_authz_core, which the role enables
# by default. Loading it without authz_core is not an error, so there is nothing to guard against.
LoadModule authz_host_module {{ __apache_httpd__modules_path }}/mod_authz_host.so
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# {{ ansible_managed }}
# 2021110301
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}

<IfModule authz_core_module>
LoadModule authz_user_module {{ __apache_httpd__modules_path }}/mod_authz_user.so
</IfModule>
# The Require directives this module provides are consumed by mod_authz_core, which the role enables
# by default. Loading it without authz_core is not an error, so there is nothing to guard against.
LoadModule authz_user_module {{ __apache_httpd__modules_path }}/mod_authz_user.so
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# {{ ansible_managed }}
# 2021110301
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}

# This configuration file loads a CGI module appropriate to the MPM
# which has been activated. mod_cgid should be used
# with a threaded MPM; mod_cgi with the prefork MPM.
<IfModule mpm_prefork_module>
LoadModule cgi_module {{ __apache_httpd__modules_path }}/mod_cgi.so
</IfModule>
# mod_cgi forks a CGI process out of the httpd worker. Pair it with mpm_prefork; on a threaded MPM
# the fork replicates every thread of the parent, which is what mod_cgid exists to avoid. It does
# work on mpm_event and mpm_worker, so this is a sizing decision, not a hard requirement.
# Enable either this module or cgid, never both: they both handle `cgi-script`, and the one loaded
# first wins while the other only costs resources.
LoadModule cgi_module {{ __apache_httpd__modules_path }}/mod_cgi.so
17 changes: 7 additions & 10 deletions roles/apache_httpd/templates/etc/httpd/mods-available/cgid.conf.j2
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
# {{ ansible_managed }}
# 2021110301
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}

# This configuration file loads a CGI module appropriate to the MPM
# which has been activated. mod_cgid should be used
# with a threaded MPM; mod_cgi with the prefork MPM.
<IfModule mpm_worker_module>
LoadModule cgid_module {{ __apache_httpd__modules_path }}/mod_cgid.so
</IfModule>
<IfModule mpm_event_module>
LoadModule cgid_module {{ __apache_httpd__modules_path }}/mod_cgid.so
</IfModule>
# mod_cgid runs CGI scripts through an external daemon instead of forking out of a worker, which is
# what a threaded MPM wants. Pair it with mpm_event (the role default) or mpm_worker. The daemon and
# its ScriptSock are created at startup whether or not a CGI is ever requested.
# Enable either this module or cgi, never both: they both handle `cgi-script`, and the one loaded
# first wins while the other only costs resources.
LoadModule cgid_module {{ __apache_httpd__modules_path }}/mod_cgid.so
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# {{ ansible_managed }}
# 2021110301
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}

# PHP 7
<IfModule mpm_prefork_module>
LoadModule php7_module {{ __apache_httpd__modules_path }}/libphp7.so
</IfModule>
# otherwise use PHP-FPM
# mod_php needs a non-threaded MPM (mpm_prefork). No <IfModule> guard: on a threaded MPM PHP itself
# refuses with "Apache is running a threaded MPM, but your PHP Module is not compiled to be
# threadsafe" and Apache does not start, which is a better message than a skipped LoadModule.
# The role defaults to mpm_event, so use PHP-FPM there instead.
LoadModule php7_module {{ __apache_httpd__modules_path }}/libphp7.so
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
# {{ ansible_managed }}
# 2021110301
# 2026090201
{% if item['by_role'] | d() %}
# Generated by Ansible role: {{ item['by_role'] }}
{% endif %}

# PHP 8+
<IfModule mpm_prefork_module>
LoadModule php_module {{ __apache_httpd__modules_path }}/libphp.so
</IfModule>
# otherwise use PHP-FPM
# mod_php needs a non-threaded MPM (mpm_prefork). No <IfModule> guard: on a threaded MPM PHP itself
# refuses with "Apache is running a threaded MPM, but your PHP Module is not compiled to be
# threadsafe" and Apache does not start, which is a better message than a skipped LoadModule.
# The role defaults to mpm_event, so use PHP-FPM there instead.
LoadModule php_module {{ __apache_httpd__modules_path }}/libphp.so
Loading