Skip to content
Merged
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:php**: PHP no longer tracks file uploads in the session (`session.upload_progress.enabled = Off`), a confirmed way into the remote code execution CVE-2026-109619 ([GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)) that the advisory recommends closing even on patched PHP, and the PHP-FPM pools enforce this against `.user.ini` files. Upload progress bars that depend on it stop working. Set `php__ini_session_upload_progress_enabled__host_var: 'On'` to restore the feature.
* **role:postgresql_server**: Databases are created with the `C.UTF-8` locale instead of `en_US.UTF-8`, which failed on hosts with only the minimal glibc language pack. `C.UTF-8` sorts text by code point instead of by language rules. Existing databases are unaffected. To create new databases as before, set `lc_collate: 'en_US.UTF-8'` and `lc_ctype: 'en_US.UTF-8'` on their entries in `postgresql_server__databases__*_var`.
* **role:postgresql_server, playbook:setup_mastodon**: `postgresql_server__version` is mandatory, and PostgreSQL is only installed from the PostgreSQL Yum Repository instead of optionally from the distribution packages. Set the major version in your inventory, for example `postgresql_server__version: '18'`. On a host that already runs PostgreSQL from the distribution packages, the run aborts before it installs anything; move the data to the PostgreSQL Yum Repository release first, as described in the role README.
* **role:postgresql_server**: `postgresql_server__pg_hba_entries` is replaced by `postgresql_server__pg_hba_local_entries__host_var` / `__group_var` for `local` records and `postgresql_server__pg_hba_host_entries__host_var` / `__group_var` for all `host*` records. Both are merged with the role defaults instead of replacing them, and an entry with `state: 'absent'` removes a default. Move your `local` entries to the first variable without their `type` key, and all other entries to the second, where `address` is mandatory. Entries from the inventory are written before the role defaults. The run aborts as long as the old variable is set.
Expand Down Expand Up @@ -275,6 +276,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Security

* **role:php**: PHP 8.5 logs fatal errors without a backtrace (`fatal_error_backtraces = Off`), which closes the main way into the remote code execution CVE-2026-109619 ([GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)) on unpatched builds. The role settings only narrow the attack paths: update PHP to 8.2.35, 8.3.36, 8.4.27, 8.5.12 or your distribution's fixed package to close the vulnerability.
* **role:apache_tomcat**: The host-manager webapp only accepts the clients that `apache_tomcat__webapps_manager_context_xml_allow` allows, as documented, instead of those that `apache_tomcat__webapps_docs_context_xml_allow` allows.
* **role:borg_local, role:schedule_reboot, role:tools**: Neither the password of the Icinga API user nor the Rocket.Chat webhook of `schedule_reboot` shows up in the process list or in a file that every local user can read, which moves `schedule-icinga-downtime` from a shell function in `/etc/profile.d/alias.sh` to a command in `/usr/local/sbin` that only root may run.
* **role:openvpn_server**: The server negotiates TLS 1.2 only with forward secrecy and checks client certificates against OpenSSL security level 2 ([#358](https://github.com/Linuxfabrik/lfops/issues/358)).
Expand Down
30 changes: 30 additions & 0 deletions extensions/molecule/php/install/verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,7 @@
@ini_set('memory_limit', '1024M');
echo 'memory_limit_after=' . ini_get('memory_limit') . "\n";
echo 'session_save_path=' . ini_get('session.save_path') . "\n";
echo 'session_upload_progress_enabled=' . (int) ini_get('session.upload_progress.enabled') . "\n";
dest: '/var/www/html/molecule-probe-{{ item }}.php'
owner: 'root'
group: 'root'
Expand All @@ -310,6 +311,19 @@
- 'app1'
- 'www'

# Session upload progress is a confirmed way into CVE-2026-109619 and the role switches it off.
# The directive is PERDIR, so the ini alone would not hold against a .user.ini that switches it
# back on; only the php_admin_flag in the pool does. This .user.ini is that attempt, and the
# probe below has to report the feature as off regardless.
- name: 'Deploy a .user.ini that tries to re-enable session upload progress'
ansible.builtin.copy:
content: "session.upload_progress.enabled = On\n"
dest: '/var/www/html/.user.ini'
owner: 'root'
group: 'root'
mode: 0o644
setype: '{{ "httpd_sys_content_t" if ansible_facts["os_family"] == "RedHat" else omit }}'

# The status page reports the live process manager, which differs per pool (www: the dynamic
# default, app1: the static override). Both pools serve it on the same path, so a wrong answer
# here means the socket reached the wrong pool or the override never arrived.
Expand All @@ -335,9 +349,12 @@
- '__molecule__pool_status_result["results"][1]["stdout"] is search("process manager:\s+dynamic")'
fail_msg: 'status pages: {{ __molecule__pool_status_result["results"] | map(attribute="stdout") | list }}'

# DOCUMENT_ROOT as a web server would send it: php-fpm only reads a .user.ini when it is set
# (sapi/fpm/fpm/fpm_main.c), so without it the .user.ini above would never be tried.
- name: 'Run the probe script through each pool socket'
ansible.builtin.command: 'cgi-fcgi -bind -connect {{ __molecule__runtime_path }}/{{ item }}.sock'
environment:
DOCUMENT_ROOT: '/var/www/html'
QUERY_STRING: ''
REQUEST_METHOD: 'GET'
SCRIPT_FILENAME: '/var/www/html/molecule-probe-{{ item }}.php'
Expand All @@ -359,11 +376,24 @@
- '__molecule__app1 is search("session_save_path=" ~ __molecule__session_path ~ "/app1")'
- '"memory_limit=256M" in __molecule__www'
- '__molecule__www is search("session_save_path=" ~ __molecule__session_path ~ "/www")'
- '"session_upload_progress_enabled=0" in __molecule__app1'
- '"session_upload_progress_enabled=0" in __molecule__www'
fail_msg: 'app1: {{ __molecule__app1 }} www: {{ __molecule__www }}'
vars:
__molecule__app1: '{{ __molecule__pool_probe_result["results"][0]["stdout"] }}'
__molecule__www: '{{ __molecule__pool_probe_result["results"][1]["stdout"] }}'

# The CLI reads the ini only, without a pool, so this proves the z00-linuxfabrik.ini itself.
- name: 'Read session.upload_progress.enabled from the CLI'
ansible.builtin.command: 'php --run "echo (int) ini_get(''session.upload_progress.enabled'');"'
register: '__molecule__php_upload_progress_result'
changed_when: false

- name: 'Assert session upload progress is off for the CLI'
ansible.builtin.assert:
that: '__molecule__php_upload_progress_result["stdout"] == "0"'
fail_msg: 'session.upload_progress.enabled is {{ __molecule__php_upload_progress_result["stdout"] }}, expected 0'

- name: 'stat the per-pool session and WSDL cache directories'
ansible.builtin.stat:
path: '{{ item }}'
Expand Down
20 changes: 20 additions & 0 deletions roles/php/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,15 @@ Variables for `php.ini` directives and their default values, defined and support
* Type: String.
* Default: `'E_ALL & ~E_NOTICE & ~E_DEPRECATED'`

`php__ini_fatal_error_backtraces__group_var` / `php__ini_fatal_error_backtraces__host_var`

* Whether fatal errors include a backtrace. Only deployed from PHP 8.5 on, since older versions do not know the directive. [php.net](https://www.php.net/manual/en/errorfunc.configuration.php)
* The PHP-FPM pools also set it as `php_admin_flag[fatal_error_backtraces]`, so an application cannot switch it back on with `ini_set()`.
* With mod_php the role only deploys the ini, and the handler restarts PHP-FPM only: restart Apache httpd yourself for the value to take effect. A `php_flag` or `php_value` in the Apache configuration or in a `.htaccess` file can switch it back on, since mod_php has no equivalent of the pool's `php_admin_flag` that this role could set.
* Type: String.
* Default: `'Off'`
* Deviates from the upstream default `On`: on PHP 8.5 builds without the fix for the remote code execution CVE-2026-109619 ([GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)), the backtrace a fatal error stores is the confirmed way into it. Patched builds are not affected by this; the role keeps the backtraces off as defense in depth, since production hosts log fatal errors without them. Only updating PHP fixes the vulnerability.

`php__ini_max_execution_time__group_var` / `php__ini_max_execution_time__host_var`

* This sets the maximum time in seconds a script is allowed to run before it is terminated by the parser. This helps prevent poorly written scripts from tying up the server. The default setting is 30. When running PHP from the command line the default setting is 0. [php.net](https://www.php.net/manual/en/info.configuration.php)
Expand Down Expand Up @@ -354,6 +363,15 @@ Variables for `php.ini` directives and their default values, defined and support
* Type: String.
* Default: `'a=href,area=href,frame=src,input=src,form=fakeentry'`

`php__ini_session_upload_progress_enabled__group_var` / `php__ini_session_upload_progress_enabled__host_var`

* Whether PHP records the progress of file uploads in the session, for upload progress bars. [php.net](https://www.php.net/manual/en/session.configuration.php)
* The PHP-FPM pools also set it as `php_admin_flag[session.upload_progress.enabled]`, so a `.user.ini` cannot switch it back on.
* With mod_php the role only deploys the ini, and the handler restarts PHP-FPM only: restart Apache httpd yourself for the value to take effect. A `php_flag` or `php_value` in the Apache configuration or in a `.htaccess` file can switch it back on, since mod_php has no equivalent of the pool's `php_admin_flag` that this role could set.
* Type: String.
* Default: `'Off'`
* Deviates from the upstream default `On`: the feature is a confirmed way into the remote code execution CVE-2026-109619 ([GHSA-6wmf-4rv5-p3rp](https://github.com/php/php-src/security/advisories/GHSA-6wmf-4rv5-p3rp)) on mod_php and PHP-FPM. The advisory considers keeping it off prudent even on patched versions where the feature is not needed, since it is hardly used and hardly tested.

`php__ini_smtp__group_var` / `php__ini_smtp__host_var`

* Host used by the `mail()` function to send mail (Windows only; ignored on Unix, where the `sendmail_path` binary is used). [php.net](https://www.php.net/manual/en/mail.configuration.php)
Expand All @@ -377,6 +395,7 @@ php__ini_default_socket_timeout__host_var: 10
php__ini_display_errors__host_var: 'Off'
php__ini_display_startup_errors__host_var: 'Off'
php__ini_error_reporting__host_var: 'E_ALL & ~E_NOTICE & ~E_DEPRECATED'
php__ini_fatal_error_backtraces__host_var: 'Off'
php__ini_max_execution_time__host_var: 3600
php__ini_max_file_uploads__host_var: 100
php__ini_max_input_time__host_var: -1
Expand All @@ -399,6 +418,7 @@ php__ini_session_cookie_secure__host_var: 'On'
php__ini_session_gc_maxlifetime__host_var: 1440
php__ini_session_sid_length__host_var: 32
php__ini_session_trans_sid_tags__host_var: 'a=href,area=href,frame=src,input=src,form=fakeentry'
php__ini_session_upload_progress_enabled__host_var: 'Off'
php__ini_smtp__host_var: 'localhost'
php__ini_upload_max_filesize__host_var: '10000M'
```
Expand Down
22 changes: 22 additions & 0 deletions roles/php/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,17 @@ php__ini_error_reporting__dependent_var: ''
php__ini_error_reporting__group_var: ''
php__ini_error_reporting__host_var: ''
php__ini_error_reporting__role_var: ''
php__ini_fatal_error_backtraces__combined_var: '{{
php__ini_fatal_error_backtraces__host_var if (php__ini_fatal_error_backtraces__host_var | string | length) else
php__ini_fatal_error_backtraces__group_var if (php__ini_fatal_error_backtraces__group_var | string | length) else
php__ini_fatal_error_backtraces__dependent_var if (php__ini_fatal_error_backtraces__dependent_var | string | length) else
php__ini_fatal_error_backtraces__role_var
}}'

php__ini_fatal_error_backtraces__dependent_var: ''
php__ini_fatal_error_backtraces__group_var: ''
php__ini_fatal_error_backtraces__host_var: ''
php__ini_fatal_error_backtraces__role_var: ''
php__ini_max_execution_time__combined_var: '{{
php__ini_max_execution_time__host_var if (php__ini_max_execution_time__host_var | string | length) else
php__ini_max_execution_time__group_var if (php__ini_max_execution_time__group_var | string | length) else
Expand Down Expand Up @@ -423,6 +434,17 @@ php__ini_session_trans_sid_tags__dependent_var: ''
php__ini_session_trans_sid_tags__group_var: ''
php__ini_session_trans_sid_tags__host_var: ''
php__ini_session_trans_sid_tags__role_var: ''
php__ini_session_upload_progress_enabled__combined_var: '{{
php__ini_session_upload_progress_enabled__host_var if (php__ini_session_upload_progress_enabled__host_var | string | length) else
php__ini_session_upload_progress_enabled__group_var if (php__ini_session_upload_progress_enabled__group_var | string | length) else
php__ini_session_upload_progress_enabled__dependent_var if (php__ini_session_upload_progress_enabled__dependent_var | string | length) else
php__ini_session_upload_progress_enabled__role_var
}}'

php__ini_session_upload_progress_enabled__dependent_var: ''
php__ini_session_upload_progress_enabled__group_var: ''
php__ini_session_upload_progress_enabled__host_var: ''
php__ini_session_upload_progress_enabled__role_var: ''
php__ini_smtp__combined_var: '{{
php__ini_smtp__host_var if (php__ini_smtp__host_var | string | length) else
php__ini_smtp__group_var if (php__ini_smtp__group_var | string | length) else
Expand Down
36 changes: 36 additions & 0 deletions roles/php/meta/argument_specs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -316,6 +316,24 @@ argument_specs:
default: ''
description: 'The error reporting level. Host-level override.'

php__ini_fatal_error_backtraces__dependent_var:
type: 'str'
required: false
default: ''
description: 'Whether fatal errors include a backtrace (PHP 8.5+). Dependent-role injection.'

php__ini_fatal_error_backtraces__group_var:
type: 'str'
required: false
default: ''
description: 'Whether fatal errors include a backtrace (PHP 8.5+). Group-level override.'

php__ini_fatal_error_backtraces__host_var:
type: 'str'
required: false
default: ''
description: 'Whether fatal errors include a backtrace (PHP 8.5+). Host-level override.'

php__ini_max_execution_time__dependent_var:
type: 'raw'
required: false
Expand Down Expand Up @@ -715,6 +733,24 @@ argument_specs:
default: ''
description: 'HTML tags rewritten to include the session ID. Host-level override.'

php__ini_session_upload_progress_enabled__dependent_var:
type: 'str'
required: false
default: ''
description: 'Whether PHP tracks the upload progress of files in the session. Dependent-role injection.'

php__ini_session_upload_progress_enabled__group_var:
type: 'str'
required: false
default: ''
description: 'Whether PHP tracks the upload progress of files in the session. Group-level override.'

php__ini_session_upload_progress_enabled__host_var:
type: 'str'
required: false
default: ''
description: 'Whether PHP tracks the upload progress of files in the session. Host-level override.'

php__ini_smtp__dependent_var:
type: 'str'
required: false
Expand Down
16 changes: 15 additions & 1 deletion roles/php/templates/etc/php-fpm.d/pool.conf.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
; {{ ansible_managed }}
; 2026090801
; 2026102101
{% if item['by_role'] | d() %}
; Generated by Ansible role: {{ item['by_role'] }}
{% endif %}
Expand Down Expand Up @@ -543,6 +543,20 @@ php_admin_value[open_basedir] = {{ item['php_admin_value_open_basedir'] }}
{% endif %}
php_admin_value[post_max_size] = {{ item['php_admin_value_post_max_size'] | d(php__ini_post_max_size__combined_var) }}
php_admin_value[upload_max_filesize] = {{ item['php_admin_value_upload_max_filesize'] | d(php__ini_upload_max_filesize__combined_var) }}
{#
Repeated from the ini as php_admin_flag, because the ini alone does not hold: a .user.ini can
re-enable session.upload_progress.enabled (PERDIR), and ini_set() can re-enable
fatal_error_backtraces (ALL). Both are producers of CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp), and
php_admin_* is applied at worker start, before any request is parsed, and marks the directive
ZEND_INI_SYSTEM (sapi/fpm/fpm/fpm_php.c), so neither a .user.ini nor ini_set() can change it.
Verified with php-fpm 8.5.11 on Rocky 10: without the flags a .user.ini re-enables upload
progress and ini_set() re-enables the backtraces, with them both stay off.
fatal_error_backtraces only exists from PHP 8.5 on.
#}
{% if __php__installed_version is version('8.5', '>=') %}
php_admin_flag[fatal_error_backtraces] = {{ php__ini_fatal_error_backtraces__combined_var }}
{% endif %}
php_admin_flag[session.upload_progress.enabled] = {{ php__ini_session_upload_progress_enabled__combined_var }}

; Set the following data paths to directories owned by the FPM process user.
;
Expand Down
3 changes: 2 additions & 1 deletion roles/php/templates/etc/php.d/7.2-z00-linuxfabrik.ini.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
; {{ ansible_managed }}
; 2024062001
; 2026102101
; php 7.2

[PHP]
Expand Down Expand Up @@ -43,3 +43,4 @@ session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }}
session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }}
session.sid_length = {{ php__ini_session_sid_length__combined_var }}
session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}"
session.upload_progress.enabled = {{ php__ini_session_upload_progress_enabled__combined_var }}
3 changes: 2 additions & 1 deletion roles/php/templates/etc/php.d/7.3-z00-linuxfabrik.ini.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
; {{ ansible_managed }}
; 2026090801
; 2026102101
; php 7.3

[PHP]
Expand Down Expand Up @@ -46,3 +46,4 @@ session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }}
session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }}
session.sid_length = {{ php__ini_session_sid_length__combined_var }}
session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}"
session.upload_progress.enabled = {{ php__ini_session_upload_progress_enabled__combined_var }}
3 changes: 2 additions & 1 deletion roles/php/templates/etc/php.d/7.4-z00-linuxfabrik.ini.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
; {{ ansible_managed }}
; 2026090801
; 2026102101
; php 7.4

[PHP]
Expand Down Expand Up @@ -46,3 +46,4 @@ session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }}
session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }}
session.sid_length = {{ php__ini_session_sid_length__combined_var }}
session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}"
session.upload_progress.enabled = {{ php__ini_session_upload_progress_enabled__combined_var }}
3 changes: 2 additions & 1 deletion roles/php/templates/etc/php.d/8.0-z00-linuxfabrik.ini.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
; {{ ansible_managed }}
; 2026090801
; 2026102101
; php 8.0

[PHP]
Expand Down Expand Up @@ -46,3 +46,4 @@ session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }}
session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }}
session.sid_length = {{ php__ini_session_sid_length__combined_var }}
session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}"
session.upload_progress.enabled = {{ php__ini_session_upload_progress_enabled__combined_var }}
3 changes: 2 additions & 1 deletion roles/php/templates/etc/php.d/8.1-z00-linuxfabrik.ini.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
; {{ ansible_managed }}
; 2026090801
; 2026102101
; php 8.1

[PHP]
Expand Down Expand Up @@ -46,3 +46,4 @@ session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }}
session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }}
session.sid_length = {{ php__ini_session_sid_length__combined_var }}
session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}"
session.upload_progress.enabled = {{ php__ini_session_upload_progress_enabled__combined_var }}
3 changes: 2 additions & 1 deletion roles/php/templates/etc/php.d/8.2-z00-linuxfabrik.ini.j2
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
; {{ ansible_managed }}
; 2026090801
; 2026102101
; php 8.2

[PHP]
Expand Down Expand Up @@ -46,3 +46,4 @@ session.cookie_secure = {{ php__ini_session_cookie_secure__combined_var }}
session.gc_maxlifetime = {{ php__ini_session_gc_maxlifetime__combined_var }}
session.sid_length = {{ php__ini_session_sid_length__combined_var }}
session.trans_sid_tags = "{{ php__ini_session_trans_sid_tags__combined_var }}"
session.upload_progress.enabled = {{ php__ini_session_upload_progress_enabled__combined_var }}
Loading
Loading