Skip to content

fix(deploy): make hardening survive deploys - #1693

Open
faisalahammad wants to merge 3 commits into
roots:masterfrom
faisalahammad:fix/1691-runtime-hardening-permissions
Open

faisalahammad wants to merge 3 commits into
roots:masterfrom
faisalahammad:fix/1691-runtime-hardening-permissions

Conversation

@faisalahammad

@faisalahammad faisalahammad commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Submission Review Guidelines

  • I have read the Contributing Guidelines.
  • I have tested these changes locally and verified they do not cause regressions.
  • My commit messages follow the project's git commit standards.

Summary

With wordpress_runtime_hardened enabled, two things break after a deploy: the .env file gets mode 0600 so the PHP runtime user can no longer read it (Dotenv throws InvalidPathException), and per-release writable paths like web/app/cache come back unwritable because each new release is extracted with default permissions. Deploys also failed with EPERM when chmodding shared directories owned by the runtime user.

This fixes all three. Deploys run as the web user without root, so the fix uses group permissions instead of chown: web's primary group is www-data, which is the default runtime user's group.

Fixes #1691

Changes

.env file mode

Before:

mode: '0600'

After:

mode: "{{ wordpress_runtime_hardened | default(false) | ternary('0640', '0600') }}"

Why: The file is owned by web with group www-data. In hardened mode PHP runs as www-data, so it needs the group read bit. Same change applied to the provision-time .env in wordpress-install so both paths match.

Per-release writable paths

New file: roles/deploy/tasks/runtime-writable-paths.yml, included from build.yml when hardened.
Why: Paths with the current/ prefix (e.g. current/web/app/cache) only exist in the new release, and the provision-time chown does not survive the next deploy. Since deploys run as web without sudo, the directories are made group-writable (0775) on every deploy instead.

Shared directories in share.yml

Why: In hardened mode, shared paths are owned by the runtime user. The deploy task running as web (a non-owner) failed with EPERM when chmodding them back to 0755, so the mode enforcement is skipped in hardened mode.

Provision-time task split

roles/wordpress-setup/tasks/runtime-writable-paths.yml now only handles shared/ paths. Per-release paths have no target on a fresh server and are handled per deploy instead.

Behavior is unchanged when wordpress_runtime_hardened is false (the default).

Screenshots / Recordings

Before After
CLI / no UI change
When wordpress_runtime_hardened: true, deploys fail with EPERM on shared directory chmod, .env is written with mode 0600 (unreadable by runtime user), and new release directories reset to unwritable.
CLI / no UI change
Shared directory chmod skipped in hardened mode, .env receives mode 0640 with www-data group read access, and per-release paths are set to 0775 via deploy task.

Testing

Test 1: hardened provision and deploy

  1. Set wordpress_runtime_hardened: true and add current/web/app/cache to wordpress_runtime_writable_paths
  2. Provision, then deploy twice
  3. Site loads, .env is -rw-r----- web www-data, Acorn cache writes succeed after both deploys

Result: works as expected. Tested manually: confirmed this resolves the reported issue.

Test 2: default mode

  1. Leave wordpress_runtime_hardened at its default (false)
  2. Provision and deploy

Result: identical behavior to before.

Template render tests (pytest, 9 passing) and ansible-playbook --syntax-check on all five playbooks pass.

One note for docs: a custom wordpress_runtime_group needs the runtime user added to web_group (www-data), since deploys cannot chgrp without root. The hardening comment block in wordpress-setup defaults mentions this. Docs on roots.io may want to cover the shared vs per-release path split too.

Milestone

  • Auto-assign next milestone

Changelog Entry

  • Significance: Patch
  • Type: Fix
  • Message: Ensure runtime hardening permissions survive deploys by setting .env mode to 0640 and release paths to group-writable.

Release Communication

  • Feature Highlight
  • Developer Advisory

Non-breaking bug fix for WordPress runtime hardening during deploy tasks. No release advisory needed.

- Make .env group-readable when hardened so the PHP runtime user can read it
- Ensure per-release writable paths are group-writable on every deploy
- Skip shared dir mode enforcement in hardened mode to avoid deploy failure

Fixes roots#1691
@zak-wearecore

Copy link
Copy Markdown
Contributor

Does there also need to be additional nginx conf in roles/wordpress-setup/templates/wordpress-site.conf.j2 to prevent direct execution of .php files in current/web/app/cache? In a similar way to how the uploads dir is handled - web app needs to be able to write but we never want anyone to be able to directly access and execute a .php file via a URL.

- Add location_cache_php block to deny direct execution of .php files in cache directory
- Explicitly set web_group on per-release writable paths and deployed .env
- Add unit tests for location_cache_php in template tests

Addresses PR feedback.

Refs roots#1693
@faisalahammad

Copy link
Copy Markdown
Contributor Author

Added location_cache_php block in roles/wordpress-setup/templates/wordpress-site.conf.j2 to deny direct execution of .php files in cache directory, similar to uploads directory. Also added group permission to per-release writable paths and deployed .env.

@zak-wearecore

Copy link
Copy Markdown
Contributor

@retlehs when could this make it into a release?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

WordPress runtime hardening

2 participants