How to Lock Down WordPress on Plesk with a Two-User Model

By default, Plesk runs PHP-FPM as the same user that owns WordPress files — convenient for updates but risky for security. This guide shows how to separate runtime and deployment users so PHP can’t modify your codebase. The result: stronger WordPress security, with uploads and cache still working smoothly.

Table of Contents

If you’ve ever managed WordPress on Plesk, you know the default setup: each subscription gets a system user, and PHP-FPM runs under that user. That same account also owns the WordPress files.

And in most cases, that’s actually a good thing. Site owners usually want to install plugins, apply WordPress core updates, or upload new themes directly from the WordPress dashboard. The default Plesk model makes this seamless — PHP can write wherever it needs to, so updates “just work.”

But convenience comes at a cost. Because PHP runs as the same user that owns the files, a vulnerability in a plugin or theme means PHP can also rewrite your entire site code. In other words, if an attacker slips in through WordPress, they have free rein.

For sites where security outweighs convenience — financial services, membership sites, healthcare portals, or any installation where WordPress should not be able to write its own code — there’s a better way.

This guide introduces a modified two-user approach:

  • Runtime user (site_runtime) → executes PHP-FPM (Plesk subscription system user).
  • Deploy user (site_owner) → owns the WordPress code and handles deployments.

With this model, PHP can still upload files and generate cache data, but it can’t overwrite your plugins, themes, or WordPress core. Tradeoff: WordPress in-dashboard updates and plugin installs will no longer work. Updates must happen through Git, SFTP, or another deployment method. If your goal is maximum convenience, stick with Plesk’s defaults. If your goal is maximum security, read on.


Prerequisite: Install ACL (required)

Why: We use Linux Access Control Lists to grant site_owner just the rights it needs — without loosening global directory modes or fighting Plesk defaults.

Install the tools (setfacl, getfacl) and verify:

Debian/Ubuntu

RHEL/CentOS/Rocky/Alma

Amazon Linux

SUSE / SLES

Check it works:

If setfacl returns Operation not supported, add the acl mount option for the filesystem that backs /var/www/vhosts (ext4 example):


Step 1: Create the subscription in Plesk (this defines the runtime user)

What & why: First, create the site as a Plesk subscription. Plesk provisions /var/www/vhosts/<domain> and creates the subscription system user — this will be your runtime user (site_runtime) that runs PHP-FPM.

  • Plesk UI: Websites & Domains → Add Domain (or Customers → Add Subscription) → set example.com. In subscription settings, set the System user to site_runtime (or rename the existing one). Ensure PHP is enabled and set to PHP-FPM.
  • The rest of this guide assumes this user and the vhost home already exist.

Step 2: Create the Deploy User (site_owner)

What & why: site_owner owns the WordPress code and performs deployments. It does not run PHP. Separating code ownership from the PHP runtime enforces least privilege. We set the shell to /bin/bash for a normal admin experience.


Step 3: Confirm the Runtime User (site_runtime, created by Plesk)

What & why: The subscription’s system user already exists — that’s who runs PHP-FPM. We’ll share its home with site_owner, but keep code ownership separate.


Step 4: Hand Over the Code to site_owner

What & why: Move ownership of all WordPress files to site_owner:psacln, then set sane read-only permissions (755 for directories, 644 for files). This makes the code immutable to PHP, even if a plugin is compromised.

Result: site_runtime (PHP) can read/execute code, but not write it. site_owner can still deploy changes.


Step 5: Give PHP a Sandbox to Write In

What & why: WordPress needs to write to uploads and usually cache. We allow that and only that. We use 2775 so the psacln group sticks, and ACLs so site_runtime always gets write access, including for new files and subfolders.

Need additional writable paths (e.g., a custom storage directory)? Add them to the setup script with -w "path1 path2".


Step 6: Lock Down WordPress from the Inside

What & why: Disable the built-in file editor and, optionally, automatic updates. With the filesystem locked down, the editor won’t work anyway; turning it off removes a tempting attack surface. Many teams also disable automatic updates to keep deployments controlled.

Heads-up for NFS-mounted uploads: if wp-content/uploads (or any writable path) lives on an NFS share, setfacl may print “Operation not permitted” when the script tries to apply ACLs. That’s expected on many NFS exports and safe to ignore. The recipe still works thanks to chown + chmod 2775; just make sure your NFS export/umask keeps new files group-writable so site_runtime can write.


Step 7: Automate Everything (with ACL preflight & readable vhost root)

What & why: Our setup automation makes the environment reproducible and fast to recover. It verifies ACL support, grants site_owner read+traverse (rx) on the shared home (so ls /var/www/vhosts/<domain> works), pre-creates dotfolders, applies ACLs for writable paths, and drops a repair script into /var/www/vhosts/<domain>/scripts/.

Get the script from GitHub:

Typical usage (example):

The script generates a repair utility at
/var/www/vhosts/<domain>/scripts/wp_two_user_repair_<domain>.sh
to quickly re-apply ownership/permissions/ACLs after Plesk repairs.


Step 8: Prepare for Plesk “Repairs”

What & why: Commands like plesk repair fs or some WordPress Toolkit actions can reset ownership and modes. Use the generated repair script to restore everything in seconds.

Tip: run this after major Plesk operations, or schedule a weekly cron as root or site_owner.


Step 9: Deployment Workflow

What & why: With the two-user model, deployments happen as site_owner, and PHP runs as site_runtime. This keeps runtime and code ownership separate, enforcing least privilege. Use Git+SSH or SFTP to deploy, and run build steps (Composer/NPM) as site_owner. Avoid giving site_runtime extra rights it doesn’t need.


Conclusion

Plesk’s default model is excellent for convenience and in-dashboard updates. When you need stronger guarantees, this ACL-based, two-user model prevents PHP from editing your codebase while preserving normal WordPress behavior for uploads and caching. Security up, blast radius down.


Quick reference

  • Repo: https://github.com/reliablepenguin/rp_wp_plesk_two_user
  • Requires: setfacl/getfacl + filesystem mounted with ACLs
  • Runtime user: site_runtime (Plesk system user)
  • Deploy user: site_owner (owns code, /bin/bash)
  • Generated repair script: /var/www/vhosts/<domain>/scripts/wp_two_user_repair_<domain>.sh

Addendum: Handling sshd Ownership and ACL Issues on Plesk

In some Plesk environments the WordPress owner user’s home directory is mapped to the vhost path, for example:

with permissions like:

Because this directory is not owned by the SSH login user and carries default ACLs (+), OpenSSH may reject key authentication with the error:

Even if the Unix bits look correct, the presence of default ACLs makes sshd treat the home as “potentially writable.”


Solution: Root-Managed Authorized Keys

Rather than fighting the ACLs on the vhost, you can move the SSH authorized keys outside of the webroot and instruct sshd to look there.

1. Create a root-owned directory

2. Create a per-user keys file

Edit the file and paste in the public keys:

3. Update sshd_config

Append a Match User block:

4. Test and reload


Benefits

  • Keeps StrictModes enabled: sshd will enforce proper permissions.
  • Removes dependence on vhost ACLs and ownership quirks.
  • Works cleanly with the two-user model: the site_owner account has a safe login path, while the site_runtime account continues to serve files and run PHP.

With this adjustment, you can lock down WordPress on Plesk even when the owner’s $HOME is not directly owned by the SSH login user.


Addendum: Granting Site Owner Access to Logs

By default, Plesk protects the logs directory for each domain so that only root and the Plesk administrative daemons can read webserver logs. This is secure, but sometimes you may want the site owner account to traverse into /var/www/vhosts/<domain>/logs to view access and error logs directly.

Because Plesk logs often live on filesystems that support POSIX ACLs, the cleanest approach is to add an Access Control List entry rather than changing ownership or relaxing group permissions.

Step 1: Verify ACL Support

Check that your filesystem supports ACLs:

If the mount options include acl (for example rw,relatime,acl), you are good to go. If not, update /etc/fstab to include acl for the /var/www/vhosts mount and remount:

Make sure the acl package is installed:

Step 2: Grant Read and Traverse Permission

Use setfacl to add read/execute permission for the site owner user on the logs directory:

Replace site_owner with the actual system user for your subscription (e.g. lottery_owner).

Step 3: Apply Access Recursively (Optional)

If you want the site owner to read the existing log files inside the directory, apply recursively:

Step 4: Verify Access

Switch to the site owner account and list the logs:

You should now see the log files and be able to view them with less or tail.


⚠️ Security Note: Granting the site owner access to logs may expose sensitive information such as query strings, cookies, or request headers. Only apply this change if you are comfortable with the user having that visibility.


Addendum: Creating a Private Folder for the Site Owner

In some cases, the site owner account needs to upload files that should not be directly accessible from the website. For example, you may want to store backups, staging uploads, or other artifacts outside of the web-accessible httpdocs directory.

The simplest way to achieve this is to create a private folder at the vhost root. This folder will be owned entirely by the site_owner user and inaccessible to the runtime user.

Step 1: Create the Directory

Run as root, replacing <domain> and site_owner with the actual values for your subscription:

  • Ownership: The folder is owned by site_owner.
  • Permissions: Mode 700 ensures only the owner can read, write, or traverse.

Step 2: Verify Access

Switch to the site_owner account and confirm write access:

Now switch to the runtime user and attempt to list the folder:

This should fail with Permission denied.

Step 3: Usage Notes

  • Files in /private are not web-accessible since the folder is outside of httpdocs.
  • This provides the site owner with a secure area for private uploads or staging files.
  • If you need the runtime user (site_runtime) to access certain files, you can selectively grant ACLs:

Use this only if PHP or the webserver legitimately needs read-only access.


⚠️ Security Note: Keeping sensitive files outside of the document root reduces the risk of accidental disclosure. Always use restrictive permissions (700) unless you have a specific reason to relax them.

 

Have a project or a problem?

Talk with a senior engineer for practical recommendations—no obligation.

Leave a comment

Leave a Reply

Your email address will not be published. Required fields are marked *

Related Posts

Categories

Get a free consultation from Reliable Penguin

Submit the form—or for immediate service call 866-649-7984.