VPS security · Practical guide

Audit website ownership, file permissions and writable directories on Linux

Use stat, namei and find to diagnose Linux website permissions, separate writable data from code and apply targeted changes instead of chmod 777.

Archive placement: December 2024

Network equipment in server racks

A permission problem is usually about which account runs a process, who owns a file and which directories must be traversed to reach it. Changing everything to world-writable hides that model and increases the damage a compromised application can cause. This guide shows how to diagnose the actual access path before making a focused change.

Map the deployment and runtime accounts

Read-only identity and path inspection
id
ps -eo user,group,comm | grep -E 'nginx|apache2|php-fpm'
stat -c '%U:%G %a %n' /srv/www/example-app
namei -l /srv/www/example-app/public/index.php

The deployment user, web-server worker and application runtime can be different identities. A reverse proxy may read public files while a PHP worker needs access to application configuration and a cache directory. Determine the actual users and groups from your service configuration, not just from a process name copied from another server.

namei shows each directory in the path. Reading a file requires the relevant traversal permissions on its parents as well as permission on the file itself. This explains why a file can have apparently correct read bits but still produce a permission-denied error.

Understand the permission bits you are changing

For ordinary files, read permits reading the contents, write permits changing them and execute permits execution as a program where the format supports it. Directory permissions have different operational effects: execute allows traversal, read lists names and write permits changing directory entries. Removing write access from a file does not necessarily stop someone deleting it through a writable parent directory.

A value such as 755 is an octal representation of owner, group and other permissions. It is not a universal website setting. Common read-only code files might use 644 or 640, but the correct choice depends on the runtime account and whether unrelated users share the machine. Secrets should not become world-readable just because public assets need to be served.

Find broad access and inspect ACLs

Audit the site tree without changing it
SITE_ROOT='/srv/www/example-app'
test -d "$SITE_ROOT"
find "$SITE_ROOT" -xdev -type d -perm -0002 -print
find "$SITE_ROOT" -xdev -type f -perm -0002 -print
find "$SITE_ROOT" -xdev -type f -perm /111 -print

These lists identify world-writable directories, world-writable files and executable files for review; they do not prove that every result is wrong. A deployment helper can legitimately be executable, while an uploaded image normally should not need that permission. -xdev avoids descending into another filesystem, but symlink destinations and application-managed storage still need separate consideration.

If the acl tools are installed, getfacl -p on the affected file or directory reveals access-control entries that ordinary mode bits do not fully explain. Review effective ACL masks and default ACLs before concluding that chmod alone will fix the issue.

Separate application code from writable runtime data

Document which locations genuinely need writes: uploads, cache, sessions, generated files or logs. Keep code and configuration owned by a deployment identity when the application does not need to modify them. Place private configuration outside the public document root where the application supports that layout.

Example for one verified shared runtime directory
# Assumes deploy owns releases and www-data is the verified runtime group.
sudo chown deploy:www-data /srv/www/example-app/var/cache
sudo chmod 2770 /srv/www/example-app/var/cache
stat -c '%U:%G %a %n' /srv/www/example-app/var/cache

This example changes only the named directory, not the entire site. The setgid bit makes new entries inherit the directory's group; application umasks and ACLs still influence the permissions of those entries. Do not apply this ownership model to cPanel or another service whose runtime identity differs.

Test the required operation as the correct identity

Use the application's normal workflow to test an upload, cache write or deployment. On a VPS you administer, you can also test whether the actual runtime account can read or write a specific path. A successful test as root proves very little about an unprivileged worker.

Focused access checks on a self-managed VPS
sudo -u www-data test -r /srv/www/example-app/public/index.php
sudo -u www-data test -w /srv/www/example-app/var/cache
echo "last_check_exit=$?"

Each test returns zero on success and nonzero on failure; the final echo only reports the immediately preceding check. If a permission check passes but the application still fails, inspect its logs and other controls such as AppArmor, container mounts or a read-only filesystem. Avoid repeatedly widening Unix permissions to work around a different enforcement layer.

Keep changes recoverable and account for deployments

Before a larger permissions repair, record current ownership and mode information and take a suitable backup. Make one targeted adjustment, then retest. Do not run recursive chmod or chown against /, an account home or an unverified variable. Restoring access should preserve the hosting platform's expected ownership model.

  • Remove credentials and backup archives from publicly served paths.
  • Give the runtime write access only to the directories that require it.
  • Check new uploads and deployed releases, not just existing files.
  • Keep deployment procedures consistent so the next release does not recreate the fault.

Primary documentation

← Back to all guidesDiscuss your hosting setup ↗