Skip to content

fix: stop provisioning exposing wp-config.php - #303

Merged
chubes4 merged 1 commit into
mainfrom
fix/302-wp-config-permissions
Jul 27, 2026
Merged

fix: stop provisioning exposing wp-config.php#303
chubes4 merged 1 commit into
mainfrom
fix/302-wp-config-permissions

Conversation

@chubes4

@chubes4 chubes4 commented Jul 26, 2026

Copy link
Copy Markdown
Member

Summary

Phase 6 of provisioning grants the service user write access to the site so it can edit themes and plugins:

usermod -a -G www-data "$SERVICE_USER"
chmod -R g+w "$SITE_PATH"
chown -R www-data:www-data "$SITE_PATH"

The recursive grant also sweeps in wp-config.php, which holds the database credentials, salts, and auth keys. Observed on two provisioned hosts:

-rw-rw-r--  <service-user>:www-data  wp-config.php
  • World-readable — any local account can read the database credentials.
  • Group-writable by a service user that is a member of www-data — the coding agent can rewrite the file defining the site's database connection and security keys.

The agent gains nothing from either. It needs to edit wp-content; wp-config.php is written once at install and read thereafter.

Fix

harden_wp_config_permissions runs after the site-wide grant and sets the credentials file to 0640 owned by www-data. PHP-FPM and nginx both run as www-data on a standard provision, so they keep reading it; world read and group write are gone.

Applied unconditionally, not only to fresh sites. This is the part that matters most: without it every already-provisioned host stays exposed, since nobody re-installs from scratch. Re-running provisioning now corrects a mode an earlier install left loosened.

A missing wp-config.php is not an error — provisioning may run before WordPress is installed. DRY_RUN still routes through run_cmd so a dry run reports the calls it would make.

Verified on two live sites

Applied chmod 640 by hand to both affected hosts before writing this, to confirm the fix is safe rather than assuming it:

Host Before After Verified
a client WordPress site 644 www-data:www-data 640 front page, /register/, /shop/ all 200; registration block still renders
the host running this agent 664 <service-user>:www-data 640 front page and /wp-login.php 200; WP-CLI reads options

Nothing broke on either.

Tests

tests/wp-config-permissions.sh, following the convention in tests/cli-channel-perms.sh — the function is extracted and exercised against a temp site with chown recorded rather than performed, since the suite does not run as root.

Covers:

  1. the state provisioning actually leaves behind (664) is tightened to 640
  2. re-provisioning corrects a previously loosened mode — the regression that would otherwise leave existing hosts exposed
  3. an already-correct config stays correct
  4. a missing wp-config.php does not fail the phase
  5. world permission bits are cleared, since that is the specific bit that matters for a credentials file

Verified load-bearing: reverting the mode to 664 fails the new test with expected 0640 after hardening, got 0664.

Full suite: 33 passed, 3 failedcli-channel-binary-path, datamachine-worker, and kimaki-agent-fallback all fail identically on pristine main with these changes stashed, so they are pre-existing and unrelated.

Deliberately not included

Whether the grant should scope to wp-content rather than $SITE_PATH is a larger question. Core files are replaced wholesale by WordPress updates and are not something an agent should be editing in place, so a narrower grant would express the intended capability more precisely. That is a behavior change worth discussing separately rather than folding into a security fix.

Closes #302

Phase 6 grants the service user write access to the site with a
recursive chmod -R g+w so it can edit themes and plugins. That grant
also sweeps in wp-config.php, which holds the database credentials,
salts, and auth keys, leaving it world-readable and group-writable by a
user that is a member of www-data.

Two consequences, neither of which the agent needs: any local account
can read the database credentials, and the coding agent can rewrite the
file defining the site's database connection.

The credentials file is now set to 0640 owned by www-data after the
site-wide grant. PHP-FPM and nginx both run as www-data on a standard
provision, so they keep working; world read and group write are gone.

Applied unconditionally rather than only to fresh sites, so re-running
provisioning corrects a mode an earlier install left loosened. Without
that, every already-provisioned host stays exposed.

Closes #302
@chubes4
chubes4 merged commit f06c1c8 into main Jul 27, 2026
13 checks passed
@chubes4
chubes4 deleted the fix/302-wp-config-permissions branch July 27, 2026 00:12
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.

Provisioning leaves wp-config.php world-readable and agent-writable

1 participant