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
11 changes: 11 additions & 0 deletions roles/wordpress-setup/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,14 @@ php_fpm_pm_start_servers: 1
php_fpm_pm_min_spare_servers: 1
php_fpm_pm_max_spare_servers: 3
php_fpm_pm_max_requests: 500

# Optional hardening mode: run php-fpm as a non-deploy user and grant writes only to writable paths.
# Add extra writable paths as needed (for example current/web/app/cache).
wordpress_runtime_hardened: false
wordpress_runtime_user: www-data
wordpress_runtime_group: www-data
wordpress_runtime_writable_paths:
- shared/uploads

# Optional hardening refinement: run WP cron as runtime user when hardening is enabled.
wordpress_runtime_cron_as_runtime_user: false
23 changes: 21 additions & 2 deletions roles/wordpress-setup/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@
loop_control:
label: "{{ item.key }}"

- name: Ensure configured WordPress runtime group exists
getent:
database: group
key: "{{ wordpress_runtime_group }}"
when: wordpress_runtime_hardened

- name: Ensure configured WordPress runtime user exists
getent:
database: passwd
key: "{{ wordpress_runtime_user }}"
when: wordpress_runtime_hardened

- name: Ensure hardened writable paths exist and are owned by runtime user
include_tasks: runtime-writable-paths.yml
loop: "{{ wordpress_sites | dict2items }}"
loop_control:
label: "{{ item.key }}"
when: wordpress_runtime_hardened

- name: Create WordPress php-fpm configuration file
template:
src: php-fpm-pool-wordpress.conf.j2
Expand All @@ -49,7 +68,7 @@
cron:
name: "{{ item.key }} WordPress cron"
minute: "{{ item.value.cron_interval | default('*/15') }}"
user: "{{ web_user }}"
user: "{{ (wordpress_runtime_hardened and wordpress_runtime_cron_as_runtime_user) | ternary(wordpress_runtime_user, web_user) }}"
job: "cd {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }} && wp cron event run --due-now > /dev/null 2>&1"
cron_file: "wordpress-{{ item.key | replace('.', '_') }}"
state: "{{ (cron_enabled and not item.value.multisite.enabled) | ternary('present', 'absent') }}"
Expand All @@ -61,7 +80,7 @@
cron:
name: "{{ item.key }} WordPress network cron"
minute: "{{ item.value.cron_interval_multisite | default('*/30') }}"
user: "{{ web_user }}"
user: "{{ (wordpress_runtime_hardened and wordpress_runtime_cron_as_runtime_user) | ternary(wordpress_runtime_user, web_user) }}"
job: "cd {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }} && (wp site list --field=url | xargs -n1 -I \\% wp --url=\\% cron event run --due-now) > /dev/null 2>&1"
cron_file: "wordpress-multisite-{{ item.key | replace('.', '_') }}"
state: "{{ (cron_enabled and item.value.multisite.enabled) | ternary('present', 'absent') }}"
Expand Down
13 changes: 13 additions & 0 deletions roles/wordpress-setup/tasks/runtime-writable-paths.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
- name: Ensure hardened writable paths exist and are owned by runtime user
# Keep ownership non-recursive so existing release/shared contents are not mass-chowned.
file:
path: "{{ www_root }}/{{ item.key }}/{{ path }}"
owner: "{{ wordpress_runtime_user }}"
group: "{{ wordpress_runtime_group }}"
mode: '0775'
state: directory
loop: "{{ item.value.runtime_writable_paths | default(wordpress_runtime_writable_paths) }}"
loop_control:
loop_var: path
label: "{{ item.key }} -> {{ path }}"
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@

[wordpress]
listen = /var/run/php-fpm-wordpress.sock
; Keep socket owner/group aligned with Nginx's service user so Nginx can always connect to php-fpm.
listen.owner = www-data
listen.group = www-data
user = {{ web_user }}
group = {{ web_group }}
user = {{ wordpress_runtime_hardened | ternary(wordpress_runtime_user, web_user) }}
group = {{ wordpress_runtime_hardened | ternary(wordpress_runtime_group, web_group) }}
pm = {{ php_fpm_pm }}
pm.max_children = {{ php_fpm_pm_max_children }}
pm.start_servers = {{ php_fpm_pm_start_servers }}
Expand Down
1 change: 1 addition & 0 deletions tests/templates/render.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def trust_as_template(value: str) -> str:

DEFAULTS_FILES = (
REPO_ROOT / "roles/nginx/defaults/main.yml",
REPO_ROOT / "roles/php/defaults/main.yml",
REPO_ROOT / "roles/wordpress-setup/defaults/main.yml",
)

Expand Down
28 changes: 28 additions & 0 deletions tests/templates/test_php_fpm_pool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from tests.templates.render import render_template


def test_render_php_fpm_pool_default_runtime_user() -> None:
rendered = render_template(
"roles/wordpress-setup/templates/php-fpm-pool-wordpress.conf.j2",
overrides={"web_user": "web", "web_group": "www-data"},
)

assert "user = web" in rendered
assert "group = www-data" in rendered
assert "php_admin_value[open_basedir] = /srv/www/:/tmp" in rendered


def test_render_php_fpm_pool_hardened_runtime_user() -> None:
rendered = render_template(
"roles/wordpress-setup/templates/php-fpm-pool-wordpress.conf.j2",
overrides={
"web_user": "web",
"web_group": "www-data",
"wordpress_runtime_hardened": True,
"wordpress_runtime_user": "php-runner",
"wordpress_runtime_group": "php-runner",
},
)

assert "user = php-runner" in rendered
assert "group = php-runner" in rendered