Skip to content

Commit e32ec79

Browse files
retlehsclaude
andauthored
Add opt-in WordPress runtime hardening controls (#1649)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 837f35e commit e32ec79

File tree

6 files changed

+77
-4
lines changed

6 files changed

+77
-4
lines changed

roles/wordpress-setup/defaults/main.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,14 @@ php_fpm_pm_start_servers: 1
7474
php_fpm_pm_min_spare_servers: 1
7575
php_fpm_pm_max_spare_servers: 3
7676
php_fpm_pm_max_requests: 500
77+
78+
# Optional hardening mode: run php-fpm as a non-deploy user and grant writes only to writable paths.
79+
# Add extra writable paths as needed (for example current/web/app/cache).
80+
wordpress_runtime_hardened: false
81+
wordpress_runtime_user: www-data
82+
wordpress_runtime_group: www-data
83+
wordpress_runtime_writable_paths:
84+
- shared/uploads
85+
86+
# Optional hardening refinement: run WP cron as runtime user when hardening is enabled.
87+
wordpress_runtime_cron_as_runtime_user: false

roles/wordpress-setup/tasks/main.yml

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@
2525
loop_control:
2626
label: "{{ item.key }}"
2727

28+
- name: Ensure configured WordPress runtime group exists
29+
getent:
30+
database: group
31+
key: "{{ wordpress_runtime_group }}"
32+
when: wordpress_runtime_hardened
33+
34+
- name: Ensure configured WordPress runtime user exists
35+
getent:
36+
database: passwd
37+
key: "{{ wordpress_runtime_user }}"
38+
when: wordpress_runtime_hardened
39+
40+
- name: Ensure hardened writable paths exist and are owned by runtime user
41+
include_tasks: runtime-writable-paths.yml
42+
loop: "{{ wordpress_sites | dict2items }}"
43+
loop_control:
44+
label: "{{ item.key }}"
45+
when: wordpress_runtime_hardened
46+
2847
- name: Create WordPress php-fpm configuration file
2948
template:
3049
src: php-fpm-pool-wordpress.conf.j2
@@ -49,7 +68,7 @@
4968
cron:
5069
name: "{{ item.key }} WordPress cron"
5170
minute: "{{ item.value.cron_interval | default('*/15') }}"
52-
user: "{{ web_user }}"
71+
user: "{{ (wordpress_runtime_hardened and wordpress_runtime_cron_as_runtime_user) | ternary(wordpress_runtime_user, web_user) }}"
5372
job: "cd {{ www_root }}/{{ item.key }}/{{ item.value.current_path | default('current') }} && wp cron event run --due-now > /dev/null 2>&1"
5473
cron_file: "wordpress-{{ item.key | replace('.', '_') }}"
5574
state: "{{ (cron_enabled and not item.value.multisite.enabled) | ternary('present', 'absent') }}"
@@ -61,7 +80,7 @@
6180
cron:
6281
name: "{{ item.key }} WordPress network cron"
6382
minute: "{{ item.value.cron_interval_multisite | default('*/30') }}"
64-
user: "{{ web_user }}"
83+
user: "{{ (wordpress_runtime_hardened and wordpress_runtime_cron_as_runtime_user) | ternary(wordpress_runtime_user, web_user) }}"
6584
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"
6685
cron_file: "wordpress-multisite-{{ item.key | replace('.', '_') }}"
6786
state: "{{ (cron_enabled and item.value.multisite.enabled) | ternary('present', 'absent') }}"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
- name: Ensure hardened writable paths exist and are owned by runtime user
3+
# Keep ownership non-recursive so existing release/shared contents are not mass-chowned.
4+
file:
5+
path: "{{ www_root }}/{{ item.key }}/{{ path }}"
6+
owner: "{{ wordpress_runtime_user }}"
7+
group: "{{ wordpress_runtime_group }}"
8+
mode: '0775'
9+
state: directory
10+
loop: "{{ item.value.runtime_writable_paths | default(wordpress_runtime_writable_paths) }}"
11+
loop_control:
12+
loop_var: path
13+
label: "{{ item.key }} -> {{ path }}"

roles/wordpress-setup/templates/php-fpm-pool-wordpress.conf.j2

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
[wordpress]
44
listen = /var/run/php-fpm-wordpress.sock
5+
; Keep socket owner/group aligned with Nginx's service user so Nginx can always connect to php-fpm.
56
listen.owner = www-data
67
listen.group = www-data
7-
user = {{ web_user }}
8-
group = {{ web_group }}
8+
user = {{ wordpress_runtime_hardened | ternary(wordpress_runtime_user, web_user) }}
9+
group = {{ wordpress_runtime_hardened | ternary(wordpress_runtime_group, web_group) }}
910
pm = {{ php_fpm_pm }}
1011
pm.max_children = {{ php_fpm_pm_max_children }}
1112
pm.start_servers = {{ php_fpm_pm_start_servers }}

tests/templates/render.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ def trust_as_template(value: str) -> str:
3636

3737
DEFAULTS_FILES = (
3838
REPO_ROOT / "roles/nginx/defaults/main.yml",
39+
REPO_ROOT / "roles/php/defaults/main.yml",
3940
REPO_ROOT / "roles/wordpress-setup/defaults/main.yml",
4041
)
4142

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
from tests.templates.render import render_template
2+
3+
4+
def test_render_php_fpm_pool_default_runtime_user() -> None:
5+
rendered = render_template(
6+
"roles/wordpress-setup/templates/php-fpm-pool-wordpress.conf.j2",
7+
overrides={"web_user": "web", "web_group": "www-data"},
8+
)
9+
10+
assert "user = web" in rendered
11+
assert "group = www-data" in rendered
12+
assert "php_admin_value[open_basedir] = /srv/www/:/tmp" in rendered
13+
14+
15+
def test_render_php_fpm_pool_hardened_runtime_user() -> None:
16+
rendered = render_template(
17+
"roles/wordpress-setup/templates/php-fpm-pool-wordpress.conf.j2",
18+
overrides={
19+
"web_user": "web",
20+
"web_group": "www-data",
21+
"wordpress_runtime_hardened": True,
22+
"wordpress_runtime_user": "php-runner",
23+
"wordpress_runtime_group": "php-runner",
24+
},
25+
)
26+
27+
assert "user = php-runner" in rendered
28+
assert "group = php-runner" in rendered

0 commit comments

Comments
 (0)