feat(security): harden PHP services and proxy response headers

Proxy:
- server_tokens off, X-Powered-By stripped from upstream responses,
  HSTS max-age raised to one year.
- No global X-Content-Type-Options: under nosniff, ORB (step 3.4)
  blocks no-cors cross-origin responses typed text/html, e.g. tracking
  beacons answered with an empty 204.

PHP (CVE-2026-109618, CVE-2026-109619):
- New role sys-svc-compose-php installs a shared ini (expose_php Off,
  session.upload_progress.enabled 0, fatal_error_backtraces 0 for
  PHP 8.5, interim memory_limit -1) and a PHP-FPM [www] drop-in
  (pm.max_requests = PHP_FPM_MAX_REQUESTS) once per stack host,
  before 04_files renders any compose file.
- The host file names carry a sha1 prefix of the controller-side
  source, so a content change alters every PHP app's compose.yml and
  recreates it, also on remote deploys where compose_volumes cannot
  read the stack host file and hashes the path instead.
- 20 PHP roles mount the ini as a compose/swarm config into their
  image's scan dir; moodle, nextcloud and shopware also mount the FPM
  drop-in. Magento keeps its own pool: its php-fpm.conf has no
  include= and is rendered without notify.
- Joomla's php-upload.ini now loads as zzz-joomla-uploads.ini so its
  own memory_limit wins: the media API compares Content-Length with
  toBytes(memory_limit) and rejects every upload at -1.

TODOs record the memory_limit revert condition and accepted
trade-offs, Akaunting on PHP 8.1, FusionDirectory on PHP 7.4 and the
public Keycloak admin console.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5.5 committed 2026-10-21 21:03:38 +02:00
1 parent 9e62a63ca0
commit d89c74feab
41 files changed
+319 -2

No files matched your search

+1
View File
@@ -4,6 +4,7 @@
- Implement rbac administration interface
- Enable IP6 for docker.
- backup docker to local für ca optimieren
- Drop `memory_limit = -1` from [hardening.ini](roles/sys-svc-compose-php/files/hardening.ini) once every PHP image ships PHP 8.2.35, 8.3.36, 8.4.27, 8.5.12 or later. It is the interim mitigation for CVE-2026-109618 and overrides the per-role `service_memory_mb` share, so a single request can exhaust the container instead of hitting a PHP limit. `web-app-joomla` keeps its own `memory_limit` because its media API compares uploads against it, and `web-app-magento` keeps the 756M its `nginx.conf.sample` (`PHP_VALUE`) and `pub/.user.ini` set; both keep `post_max_size` below a quarter of that limit. While the override is in place, `web-app-nextcloud` (`upload_limit` 5G at 3g) accepts POST bodies above a quarter of its container memory, `web-app-friendica` caps account imports at 64 MiB instead of half the memory limit, EspoCRM's System Requirements page flags `memory_limit` as failed, and Pixelfed (`InstanceService`), phpMyAdmin (import and export) and Snipe-IT (imports and LDAP sync) set a finite limit again via `ini_set` after the request has started.
- Fork [rpardini/docker-registry-proxy](https://github.com/rpardini/docker-registry-proxy) and teach the bundled nginx to handle GCP Artifact Registry redirect URLs (`/artifacts-downloads/namespaces/<ns>/repositories/<repo>/downloads/<token>`). Currently `gcr.io` blob pulls are bypassed via `NO_PROXY=…,gcr.io,storage.googleapis.com,googleusercontent.com` in [compose/registry-cache/proxy.conf](compose/registry-cache/proxy.conf), which is a workaround that disables caching for those registries. Long-term: rebase upstream's `proxy.conf` / `sub_filter` rules to recognise the new redirect shape, ship the fix in our fork, point `compose/registry-cache/Dockerfile` (or its image tag) at the fork, and drop the `NO_PROXY` bypass.
## Testing
+6
View File
@@ -29,6 +29,12 @@ FILE_DATABASE_SECRETS: "{{ [DIR_SECRETS, 'databases.csv'] | path_join }}"
DIR_METRICS_TEXTFILE: "{{ [DIR_VAR_LIB, 'metrics'] | path_join }}"
DIR_PHP_HARDENING: "/etc/{{ SOFTWARE_DOMAIN }}/php"
PHP_HARDENING_INI_DIGEST: "{{ (lookup('file', lookup('path_absolute', 'roles/sys-svc-compose-php/files/hardening.ini')) | hash('sha1'))[:8] }}"
PHP_FPM_HARDENING_CONF_DIGEST: "{{ (lookup('template', lookup('path_absolute', 'roles/sys-svc-compose-php/templates/fpm-hardening.conf.j2')) | hash('sha1'))[:8] }}"
FILE_PHP_HARDENING_INI: "{{ [DIR_PHP_HARDENING, 'hardening-' ~ PHP_HARDENING_INI_DIGEST ~ '.ini'] | path_join }}"
FILE_PHP_FPM_HARDENING_CONF: "{{ [DIR_PHP_HARDENING, 'fpm-hardening-' ~ PHP_FPM_HARDENING_CONF_DIGEST ~ '.conf'] | path_join }}"
DIR_RESCUE_DIAGNOSTICS: "/tmp/infinito-rescue-diagnostics"
FILE_DNS53_SAMPLER_LOG: "/var/log/infinito/dns53-sampler.log"
+2
View File
@@ -49,4 +49,6 @@ POSTGRES_SUPERUSER_RESERVED_CONNECTIONS: 3
POSTGRES_MAX_LOCKS_PER_TRANSACTION: 256
POSTGRES_DELAY: 2
PHP_FPM_MAX_REQUESTS: 200
TIMEOUT_FACTOR: 1
@@ -11,7 +11,7 @@ ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets on;
add_header Strict-Transport-Security max-age=15768000 always;
add_header Strict-Transport-Security max-age=31536000 always;
ssl_stapling on;
ssl_stapling_verify on;
+39
View File
@@ -0,0 +1,39 @@
# sys-svc-compose-php
## Description
Internal helper role that installs the shared PHP hardening assets on the
stack host. PHP services of Docker Compose and Swarm stacks mount them as
configs from their own `meta/volumes.yml`.
## Overview
This role installs one PHP ini (`FILE_PHP_HARDENING_INI`) and one PHP-FPM pool
drop-in (`FILE_PHP_FPM_HARDENING_CONF`). A PHP role opts in by declaring a
`type: config` entry with that file as `source` and a file inside the scan dir
of its image as `target`, e.g. `conf.d/zz-infinito-hardening.ini` or, for the
pool, `php-fpm.d/zz-infinito-hardening.conf`.
## Cosmos
The diagram places sys-svc-compose-php in the Infinito.Nexus cosmos: the components it deploys (capabilities), the central services it consumes (dependencies), and its outward reach (federation and bridged external networks).
```mermaid
flowchart LR
subgraph role [sys-svc-compose-php 💻]
svc_svc_compose_php["svc-compose-php"]
end
```
Solid `1:1` edges are fixed relationships; dashed `0..1` edges are conditional (enabled only in matching deployments); red `0..0` edges are turned off in this role. Node markers show the role's deploy modes (💻 host, 🐳 compose, 🐝 swarm); ❌ marks a service that is explicitly turned off, and ⚙️ an Ansible role dependency declared in `meta/main.yml`.
## Features
- **PHP ini hardening:** `expose_php` off; `session.upload_progress.enabled = 0` and, on PHP 8.5 only, `fatal_error_backtraces = 0` against CVE-2026-109619; the interim `memory_limit = -1` against CVE-2026-109618.
- **PHP-FPM worker recycling:** `pm.max_requests` from `PHP_FPM_MAX_REQUESTS` for images whose `[www]` pool reads `php-fpm.d/*.conf`.
## Credits
Implemented by **[Kevin Veen-Birkenbach](https://www.veen.world)**.
Part of the [Infinito.Nexus Project](https://s.infinito.nexus/code) and maintained by [Kevin Veen-Birkenbach](https://www.veen.world).
Licensed under the [Infinito.Nexus Community License (Non-Commercial)](https://s.infinito.nexus/license).
@@ -0,0 +1,4 @@
expose_php = Off
session.upload_progress.enabled = 0
fatal_error_backtraces = 0
memory_limit = -1
+32
View File
@@ -0,0 +1,32 @@
---
galaxy_info:
author: Kevin Veen-Birkenbach
description: Shared PHP hardening ini and PHP-FPM pool drop-in that PHP services of compose and swarm stacks mount as configs
license: Infinito.Nexus Community License (Non-Commercial)
company: |
Kevin Veen-Birkenbach
https://www.veen.world
galaxy_tags:
- docker
- compose
- php
- security
- hardening
issue_tracker_url: https://s.infinito.nexus/issues
min_ansible_version: "2.9"
platforms:
- name: ArchLinux
versions:
- all
- name: Debian
versions:
- all
- name: EL
versions:
- all
- name: Fedora
versions:
- all
- name: Ubuntu
versions:
- all
@@ -0,0 +1,8 @@
---
svc-compose-php:
lifecycle: beta
required_by:
compose:
categories: [web]
swarm:
categories: [web]
@@ -0,0 +1,29 @@
---
- name: "📁 PHP hardening | ensure directory exists"
become: true
ansible.builtin.file:
path: "{{ DIR_PHP_HARDENING }}"
state: directory
mode: "0755"
owner: root
group: root
- name: "🛡️ PHP hardening | install ini"
become: true
stack_host_copy:
src: "hardening.ini"
dest: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
owner: root
group: root
- name: "🛡️ PHP hardening | render FPM pool"
become: true
stack_host_template:
src: "fpm-hardening.conf.j2"
dest: "{{ FILE_PHP_FPM_HARDENING_CONF }}"
mode: "0444"
owner: root
group: root
- include_tasks: utils/once/flag.yml
+3
View File
@@ -0,0 +1,3 @@
---
- include_tasks: 00_core.yml
when: run_once_sys_svc_compose_php is not defined
@@ -0,0 +1,2 @@
[www]
pm.max_requests = {{ PHP_FPM_MAX_REQUESTS }}
@@ -6,6 +6,10 @@
mode: "0755"
with_dict: "{{ lookup('container', application_id, 'directories') }}"
- name: "🛡️ Ensure PHP hardening assets are installed"
ansible.builtin.include_role:
name: sys-svc-compose-php
- name: "📂 Include git-repo based installation routines for '{{ application_id }}'."
include_tasks: "03_repository.yml"
when: compose_repository_pull | bool
@@ -14,3 +14,4 @@ proxy_hide_header X-Frame-Options;
proxy_hide_header X-XSS-Protection;
proxy_hide_header X-Download-Options;
proxy_hide_header X-Permitted-Cross-Domain-Policies;
proxy_hide_header X-Powered-By;
@@ -19,6 +19,7 @@ http
}
include mime.types;
default_type text/html;
server_tokens off;
add_header 'Vary' 'Origin' always;
proxy_cache_path {{ lookup('nginx','directories.cache.general') }} levels=1:2 keys_zone=cache:20m max_size=20g inactive=14d use_temp_path=off;
proxy_cache_path {{ lookup('nginx','directories.cache.image') }} levels=1:2 keys_zone=imgcache:10m inactive=60m use_temp_path=off;
+1
View File
@@ -1,4 +1,5 @@
# To-dos
- Enable OIDC
- Move off PHP 8.1. The role builds the image from `akaunting/docker` (`master`), whose Dockerfile starts `FROM php:8.1-apache` and runs PHP 8.1.34, which is end of life and gets no upstream fix for CVE-2026-109618 and CVE-2026-109619. Build on a PHP 8.2 or later base instead.
- Add OIDC auto-provisioning so a fresh Keycloak user that lands on the akaunting tile lands on a working akaunting user account instead of a "no permission" surface. Until that is wired, BOTH the playwright biber and administrator personas are blocked via `PERSONA_BIBER_BLOCKED=true` and `PERSONA_ADMINISTRATOR_BLOCKED=true` in `templates/playwright.env.j2` (req 019 Rule 11 explicit opt-out). Once auto-provisioning works, drop both opt-out flags so the personas drive the real journey.
+7
View File
@@ -5,3 +5,10 @@ data:
mounts:
- service: akaunting
target: /var/www/html
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: akaunting
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+7
View File
@@ -31,3 +31,10 @@ client_custom:
target: *var_www_html_client_custom_target
- service: websocket
target: *var_www_html_client_custom_target
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: espocrm
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+7
View File
@@ -19,3 +19,10 @@ ldapauth_config:
- service: friendica
target: /var/www/html/config/ldapauth.config.php
when: "{{ lookup('config', 'web-app-friendica', 'services.ldap.enabled') | bool }}"
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: friendica
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+4
View File
@@ -0,0 +1,4 @@
# To-dos
- Move off PHP 7.4. The pinned `nfrastack/fusiondirectory:1.5` image builds on `ghcr.io/nfrastack/container-nginx-php-fpm` with PHP 7.4 on Alpine 3.15, which is end of life and gets no fix for CVE-2026-109618 and CVE-2026-109619.
- Mount the shared PHP hardening ini (`FILE_PHP_HARDENING_INI`) once the image's scan dir is verified. The nfrastack init scripts manage `conf.d` at runtime, so a read-only mount there has to be tested before it ships.
+9 -1
View File
@@ -12,4 +12,12 @@ php_upload:
mode: "0444"
mounts:
- service: joomla
target: /usr/local/etc/php/conf.d/uploads.ini
target: /usr/local/etc/php/conf.d/zzz-joomla-uploads.ini
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: joomla
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+1
View File
@@ -2,6 +2,7 @@
- Implement working logout for all applications
- Implement general logout button
- Restrict the admin console. `https://auth.<domain>/` redirects to `/admin/`, which any client on the internet can reach. Add a source-network allowlist for `/admin` in the proxy vhost.
## Req 019 rollout: deploy gate
+8
View File
@@ -0,0 +1,8 @@
---
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: lam
target: /etc/php/8.4/apache2/conf.d/zz-infinito-hardening.ini
@@ -5,6 +5,7 @@
{{ lookup('compose_only', 'container_name', LAM_CONTAINER) }}
{{ lookup('container_image', application_id, 'lam') }}
{{ lookup('container_ports', ['lam', 'http', DOCKER_BIND_HOST]) | indent(4) }}
{{ lookup('container_volumes', service_name) | indent(4) }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_networks') | indent(4) }}
+8
View File
@@ -23,3 +23,11 @@ php_fpm_conf:
mounts:
- service: php
target: /usr/local/etc/php-fpm.conf
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: php
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+7
View File
@@ -83,3 +83,10 @@ rspamd_overrides:
mounts:
- service: antispam
target: *overrides_target
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: webmail
target: /etc/php83/conf.d/zz-infinito-hardening.ini
+8
View File
@@ -15,3 +15,11 @@ tmp:
mounts:
- service: matomo
target: /var/www/html/tmp
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: matomo
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+8
View File
@@ -29,3 +29,11 @@ mw_local:
- service: mediawiki
target: /opt/mw-local
read_only: true
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: mediawiki
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+14
View File
@@ -49,3 +49,17 @@ msmtprc:
target: &etc_msmtprc_target /etc/msmtprc
- service: cron
target: *etc_msmtprc_target
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: moodle
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
php_fpm_hardening:
type: config
source: "{{ FILE_PHP_FPM_HARDENING_CONF }}"
mode: "0444"
mounts:
- service: moodle
target: /usr/local/etc/php-fpm.d/zz-infinito-hardening.conf
+16
View File
@@ -60,3 +60,19 @@ infinito_conf:
target: &var_www_infinito_target /var/www/infinito/
- service: cron
target: *var_www_infinito_target
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: nextcloud
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
php_fpm_hardening:
type: config
source: "{{ FILE_PHP_FPM_HARDENING_CONF }}"
mode: "0444"
mounts:
- service: nextcloud
target: /usr/local/etc/php-fpm.d/zz-infinito-hardening.conf
@@ -0,0 +1,8 @@
---
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: phpldapadmin
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
@@ -6,6 +6,7 @@
{{ lookup('compose_only', 'container_name', application_id) }}
{{ lookup('container_image', application_id, 'phpldapadmin') }}
{{ lookup('container_ports', ['phpldapadmin', 'http', DOCKER_BIND_HOST]) | indent(4) }}
{{ lookup('container_volumes', service_name) | indent(4) }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_networks') | indent(4) }}
@@ -0,0 +1,8 @@
---
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: phpmyadmin
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
@@ -6,6 +6,7 @@
{{ lookup('compose_only', 'container_name', PHPMYADMIN_CONTAINER) }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_ports', ['phpmyadmin', 'http', DOCKER_BIND_HOST]) | indent(4) }}
{{ lookup('container_volumes', service_name) | indent(4) }}
{{ lookup('container_depends_on', application_id) | indent(4) }}
{{ lookup('container_networks') | indent(4) }}
{{ lookup('container_healthcheck', service_name) | indent(4) }}
+8
View File
@@ -33,3 +33,11 @@ worker_entrypoint:
mounts:
- service: worker
target: /infinito-pixelfed-worker-entrypoint.sh
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: pixelfed
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+16
View File
@@ -122,3 +122,19 @@ adminopenauth_yaml:
target: *var_www_html_config_packages_adminopenauth_yaml_target
- service: scheduler
target: *var_www_html_config_packages_adminopenauth_yaml_target
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: web
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
php_fpm_hardening:
type: config
source: "{{ FILE_PHP_FPM_HARDENING_CONF }}"
mode: "0444"
mounts:
- service: web
target: /usr/local/etc/php-fpm.d/zz-infinito-hardening.conf
+8
View File
@@ -13,3 +13,11 @@ env:
mounts:
- service: snipe-it
target: /var/www/html/.env
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: snipe-it
target: /etc/php84/conf.d/zz-infinito-hardening.ini
+7
View File
@@ -59,3 +59,10 @@ webmail_oauth:
mounts:
- service: webmail
target: /var/roundcube/config/oauth.inc.php
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: webmail
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+7
View File
@@ -30,3 +30,10 @@ ldap_config:
- service: suitecrm
target: "{{ ['/var/www/html/extensions', SOFTWARE_DOMAIN | replace('.', '-'), 'config/services/ldap/ldap.yaml'] | path_join }}"
when: "{{ lookup('config', 'web-app-suitecrm', 'services.ldap.enabled') | bool }}"
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: suitecrm
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+7
View File
@@ -12,3 +12,10 @@ msmtprc:
mounts:
- service: wordpress
target: /etc/msmtprc
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: wordpress
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
+8
View File
@@ -0,0 +1,8 @@
---
php_hardening:
type: config
source: "{{ FILE_PHP_HARDENING_INI }}"
mode: "0444"
mounts:
- service: yourls
target: /usr/local/etc/php/conf.d/zz-infinito-hardening.ini
@@ -9,6 +9,7 @@
{{ lookup('compose_only', 'container_name', YOURLS_CONTAINER) }}
{% include 'roles/sys-svc-container/templates/base.yml.j2' %}
{{ lookup('container_ports', ['yourls', 'http', DOCKER_BIND_HOST]) | indent(4) }}
{{ lookup('container_volumes', service_name) | indent(4) }}
{{ lookup('container_healthcheck', service_name) | indent(4) }}
{{ lookup('container_depends_on', application_id) | indent(4) }}