Linux Users, Groups & Permissions for Multi-Website Hosting
We now connect the three major security layers:
Nginx
↓
PHP-FPM
↓
Linux permissions
↓
Website files
The goal is:
Site 1 should not be able to modify Site 2’s files.
This is one of the most important concepts in a hosting platform.
1. Why Linux Permissions Matter
Suppose your server contains:
/storage/websites/
├── site1.com/
├── site2.com/
└── site3.com/
If all websites run with the same Linux identity:
www-data
then a compromised application may potentially access files belonging to other sites that www-data can access.
A better model is:
site1 → user1
site2 → user2
site3 → user3
2. Linux User
A Linux user has an identity.
Example:
site1
The operating system assigns it a:
UID
UID means:
User ID
For example:
site1 → UID 1001
site2 → UID 1002
The actual numbers are assigned by the system and should not be hard-coded in your automation without checking.
3. Linux Group
A group is a collection of users.
Example:
site1
└── group: site1
site2
└── group: site2
Groups allow you to control shared access to files and directories.
4. User + Group
A file may have:
owner
group
permissions
Example:
site1:site1
means:
owner = site1
group = site1
5. Check Your User
Run:
id
You may see:
uid=1000(username) gid=1000(username) groups=...
For another user:
id site1
6. Create a Hosting User
For example:
sudo useradd -m -s /bin/bash site1
This creates a Linux user.
However, for a production hosting platform, your creation script should explicitly define the account model rather than blindly using commands like this.
7. Check the User
id site1
You should see the user’s:
UID
GID
groups
8. User Home Directory
If created with -m, the user normally gets a home directory:
/home/site1
For your hosting architecture, website files can instead live under:
/storage/websites/site1.com/
9. Ownership
Check ownership with:
ls -la
For a website:
ls -la /storage/websites/site1.com
You may see:
site1 site1
or another ownership model depending on your configuration.
10. ls -l
Example:
-rw-r--r-- 1 site1 site1 1234 index.php
Break it down:
-rw-r--r--
│
├── file type
├── owner permissions
├── group permissions
└── other permissions
11. The Three Permission Classes
Linux permissions are divided into:
User
Group
Other
Often abbreviated:
u
g
o
Example:
u = owner
g = group
o = everyone else
12. Read
r
means:
Read the contents.
For a file:
r = 4
13. Write
w
means:
Modify the contents.
Numeric value:
w = 2
14. Execute
x
means:
Execute the file.
Numeric value:
x = 1
For directories, x has an important additional meaning: it permits traversal/searching through the directory.
15. Numeric Permissions
Combine:
r = 4
w = 2
x = 1
Therefore:
rwx = 7
rw- = 6
r-x = 5
r-- = 4
-wx = 3
-w- = 2
--x = 1
--- = 0
16. 755
A very common permission mode:
755
means:
Owner = rwx = 7
Group = r-x = 5
Other = r-x = 5
So:
rwxr-xr-x
17. 644
Another common mode:
644
means:
Owner = rw- = 6
Group = r-- = 4
Other = r-- = 4
So:
rw-r--r--
18. Why Files and Directories Differ
For a file:
r
means read its contents.
For a directory:
r
means list directory entries.
For a directory:
x
means traverse/search the directory.
This distinction is essential when troubleshooting WordPress permissions.
19. Example Directory
Suppose:
/storage/websites/site1.com/public
has:
755
Then:
site1
can:
read
write
enter
while other users may be able to:
read
enter
but not modify its contents.
20. File Permissions
Suppose:
index.php
has:
644
Then the owner can:
read
write
while group and others can:
read
but not modify it.
21. chmod
chmod changes permissions.
Example:
chmod 644 index.php
or:
chmod 755 public
22. Recursive Permissions
You can use:
chmod -R ...
but be careful.
Running:
chmod -R 777 /storage/websites/site1.com
is a bad hosting practice.
It gives excessively broad permissions.
23. Why 777 Is Dangerous
777
means:
owner rwx
group rwx
other rwx
Everyone can potentially modify the files.
For a web hosting environment this can create serious security problems.
Avoid using:
chmod -R 777
as a generic solution to permission errors.
24. chown
chown changes ownership.
Example:
sudo chown site1:site1 index.php
Now:
owner = site1
group = site1
25. Recursive chown
You can change an entire site:
sudo chown -R site1:site1 /storage/websites/site1.com
But again, don’t blindly apply recursive ownership changes to directories containing files that intentionally need different ownership.
Understand the desired ownership model first.
26. The Basic Hosting Model
For independent sites:
site1.com
owner = site1
group = site1
site2.com
owner = site2
group = site2
Then:
site1
should not automatically have write access to:
site2.com
27. Test Isolation
Create:
/storage/websites/site1.com/test.txt
owned by:
site1:site1
Then attempt access as site2:
sudo -u site2 cat /storage/websites/site1.com/test.txt
If permissions are correctly isolated, site2 should be denied.
This is an excellent security test.
28. Don’t Test as Root
Remember:
root
can bypass many normal permission restrictions.
Therefore:
sudo -u site2 ...
is useful for testing what the actual hosting user can do.
29. PHP-FPM User
Now connect this to the previous lesson.
Suppose:
site1 PHP-FPM pool
runs as:
site1
Then PHP code executed by that pool has the Linux identity:
site1
This is extremely important.
30. The Security Chain
For Site 1:
Browser
↓
Nginx
↓
PHP-FPM pool 1
↓
Linux user site1
↓
Site 1 files
For Site 2:
Browser
↓
Nginx
↓
PHP-FPM pool 2
↓
Linux user site2
↓
Site 2 files
31. What Happens During a Compromise?
Suppose:
site1.com
has a vulnerable plugin.
An attacker manages to execute PHP code through that application.
The PHP process should ideally run as:
site1
Therefore the attacker’s access is constrained by the permissions of:
site1
rather than a broad shared account.
This is not a complete security guarantee, but it significantly improves isolation.
32. Site 1 Should Not Own Site 2
Bad model:
site1
site2
site3
↓
www-data
Better model:
site1 → site1
site2 → site2
site3 → site3
33. Nginx Complication
Nginx workers may still run as:
www-data
So Nginx needs enough access to:
read static files
traverse directories
access PHP-FPM sockets
without gaining unnecessary write access.
This requires careful permissions.
34. Important Principle
A good architecture is:
Nginx
→ read-only access to website files where possible
PHP-FPM
→ write access only where the application needs it
This is safer than giving the web server universal write access.
35. WordPress Needs Some Write Access
WordPress may need to write to:
wp-content/uploads/
and sometimes other directories depending on plugins and update methods.
Therefore you should not blindly make the entire WordPress installation writable.
36. Better WordPress Permission Model
Conceptually:
WordPress core
→ mostly read-only
themes
→ mostly read-only
plugins
→ mostly read-only
uploads
→ writable where needed
The exact model depends on how WordPress updates and plugin management are handled.
37. Why wp-content/uploads?
When you upload:
image.jpg
through WordPress:
WordPress
↓
PHP
↓
wp-content/uploads/
↓
image.jpg
Therefore the PHP process must have appropriate write access.
38. But Don’t Make Everything Writable
Avoid:
chmod -R 777 public
Instead, identify exactly what needs to be writable.
For example:
public/
├── index.php
├── wp-admin/
├── wp-includes/
├── wp-content/
│ ├── plugins/
│ ├── themes/
│ └── uploads/
The required write permissions should be deliberately designed.
39. WordPress Updates
There is an architectural decision:
Model A
PHP can modify WordPress core/plugin files.
Advantages:
easy automatic updates
Disadvantages:
larger write surface
Model B
Core/plugin files are more restricted.
Updates are performed through controlled deployment/administrative mechanisms.
Advantages:
stronger security
Disadvantages:
more management complexity
For a hosting platform, this tradeoff matters.
40. Directory Traversal
Suppose:
/storage/
has restrictive permissions.
Even if:
public/
is readable, PHP may still fail if the process cannot traverse the parent directories.
Remember:
directory x permission
=
ability to traverse/search
41. Example
Suppose:
/storage
is:
700 root:root
Then:
site1
may not be able to reach:
/storage/websites/site1.com/public
even if the final directory is:
755
The entire path matters.
42. Check Every Level
Use:
namei -l /storage/websites/site1.com/public
This is an extremely useful command.
It shows permissions and ownership for each component of the path.
43. Example
Conceptually:
f: /storage/websites/site1.com/public
drwxr-xr-x root root storage
drwxr-xr-x root root websites
drwx------ site1 site1 site1.com
drwxr-xr-x site1 site1 public
Now you can identify exactly where traversal fails.
44. umask
umask controls default permission restrictions applied when new files/directories are created.
Check it:
umask
You might see:
0022
or another value depending on the process/user.
45. Why umask Matters
When PHP creates:
newfile.txt
the resulting permissions depend partly on:
requested mode
+
umask
Therefore your hosting environment should have predictable defaults.
46. Setgid Directory
A directory can use the setgid bit.
For example:
chmod 2770 directory
The leading:
2
sets the setgid bit.
For directories, this can cause newly created files/directories to inherit the directory’s group.
This can be useful when multiple trusted processes/users need to share a group.
47. Why Setgid Can Help
Suppose:
site1
has:
group = site1
and multiple processes need to cooperate through that group.
A setgid directory can help maintain consistent group ownership.
But don’t add it everywhere without a clear group-sharing requirement.
48. ACL
Linux also supports:
Access Control Lists
ACLs allow more granular permissions than the basic:
owner
group
other
model.
For example:
site1
could have special access without changing the primary group model.
49. Check ACL
If ACLs are installed and used:
getfacl /storage/websites/site1.com
You may see:
user::...
group::...
other::...
plus additional entries.
50. setfacl
ACLs can be modified with:
setfacl
For example, you can grant a specific user read access without changing ownership.
However, for your hosting platform, start with normal Unix ownership and permissions.
Add ACLs only when there is a real requirement.
51. Recommended Starting Model
For each site:
Linux user = site1
Primary group = site1
Website ownership = site1:site1
PHP-FPM pool = site1
PHP worker user = site1
Database = site1_database
Database user = site1_dbuser
This gives a consistent identity across the system.
52. Directory Structure
Use:
/storage/websites/
└── site1.com/
├── public/
├── logs/
├── backups/
└── private/
Only:
public/
should be exposed through Nginx.
53. Public Directory
Example:
/storage/websites/site1.com/public
contains:
index.php
wp-admin/
wp-content/
wp-includes/
Nginx:
root /storage/websites/site1.com/public;
54. Private Directory
You could maintain:
/storage/websites/site1.com/private
for internal files.
Do not configure Nginx to serve it.
For example:
private/
├── backups/
├── temporary/
└── configuration/
55. Logs
Site-specific logs can be stored separately:
/storage/websites/site1.com/logs/
But be careful with ownership and Nginx’s ability to write logs.
An alternative is to keep logs under:
/var/log/nginx/
with appropriate per-site log configuration.
56. Backup Directory
Backups should ideally not be directly web-accessible.
Bad:
public/backups/database.sql
because someone could potentially request:
https://site1.com/backups/database.sql
if the file is exposed.
Better:
/storage/backups/site1/
outside the web root.
57. Database Credentials
Similarly, never store a backup such as:
database.sql
inside the public web root.
And never expose:
.env
backup.zip
database.sql
through HTTP.
58. Site Isolation Test
Suppose:
site1
site2
exist.
Test:
sudo -u site1 touch /storage/websites/site1.com/public/test1.txt
This should succeed.
Then:
sudo -u site1 touch /storage/websites/site2.com/public/test2.txt
This should fail if site2 is properly isolated.
59. Test PHP Identity
Create a temporary PHP diagnostic file carefully:
<?php
echo get_current_user();
However, get_current_user() isn’t necessarily the best way to determine the OS process identity.
For an actual process identity, you can use server-side diagnostics carefully, then delete the diagnostic file immediately after testing.
Never leave debugging information exposed on a production website.
60. Better Conceptual Test
The important question is:
Which Linux user is executing PHP?
If Site 1’s PHP process runs as:
site1
then its filesystem operations are governed by Site 1’s permissions.
61. Nginx and PHP-FPM Relationship
Your final permission model could look like:
NGINX
│
reads public files
│
▼
PHP-FPM
│
┌───────────┴───────────┐
▼ ▼
site1 site2
user user
│ │
▼ ▼
site1 files site2 files
62. What Site 1 Should NOT Access
Site 1 should not have write access to:
site2/public/
site2/private/
site2/backups/
site2/configuration/
site2/database credentials
unless explicitly required.
63. What Root Can Access
Remember:
root
is different.
A hosting administrator/root account can access all sites.
Therefore:
customer isolation
≠
administrator isolation
Your goal is tenant-to-tenant isolation.
64. Linux Security Is Layered
Do not depend on only one mechanism.
Use:
Linux users
+
groups
+
filesystem permissions
+
PHP-FPM pools
+
database users
+
Nginx isolation
+
firewall
+
application updates
65. A Complete Site Identity
For:
learn.cresignsys.com
you can conceptually define:
SITE ID
learn
DOMAIN
learn.cresignsys.com
LINUX USER
learn
LINUX GROUP
learn
WEB ROOT
/storage/websites/learn.cresignsys.com/public
PHP-FPM POOL
learn
PHP SOCKET
/run/php/learn.sock
DATABASE
learn_cresignsys
DATABASE USER
learn_dbuser
This is an excellent pattern for automation.
66. Why Consistent Naming Matters
Your scripts become much easier if everything follows a predictable naming scheme.
For example:
Domain:
learn.cresignsys.com
Linux user:
learn
PHP pool:
learn
Socket:
learn.sock
Database:
learn_db
Database user:
learn_dbuser
For domains with unusual characters, your automation should sanitize and generate safe identifiers rather than using arbitrary domain strings directly.
67. Don’t Use Domain as a Linux Username Blindly
A domain may contain:
www.example.com
but system account naming rules and your platform’s conventions may make a normalized identifier preferable.
Your provisioning script should generate a safe unique site ID.
Example:
SITE_ID = learn_cresignsys_com
or another collision-resistant identifier.
68. Hosting Control Panel Data
Eventually your control panel database might contain:
sites
with:
site_id
domain
linux_user
document_root
php_version
php_pool
database_name
database_user
ssl_status
created_at
Then your automation can generate the actual server configuration.
69. Control Plane → Server
Conceptually:
Hosting Panel
↓
Site record
↓
Provisioning script
↓
Linux user
↓
Directories
↓
PHP-FPM
↓
Nginx
↓
MySQL
↓
SSL
This is the beginning of a real hosting control plane.
70. Permission Troubleshooting
When WordPress says:
Unable to create directory
don’t immediately run:
chmod -R 777
Instead ask:
Which user is PHP running as?
↓
Does that user own the directory?
↓
Does it have write permission?
↓
Can it traverse every parent directory?
↓
Is an ACL involved?
↓
Is the filesystem read-only?
71. Useful Diagnostic Commands
Current identity
id
File ownership
ls -la
Full path permissions
namei -l /storage/websites/site1.com/public
Ownership
stat /storage/websites/site1.com
ACL
getfacl /storage/websites/site1.com
Processes
ps aux | grep php-fpm
72. A Practical Diagnostic
Suppose:
WordPress
↓
Unable to write uploads
Check:
ls -ld /storage/websites/site1.com/public/wp-content/uploads
Then:
namei -l /storage/websites/site1.com/public/wp-content/uploads
Then determine the PHP-FPM pool’s user.
Finally test:
sudo -u site1 touch /storage/websites/site1.com/public/wp-content/uploads/test.txt
If this fails, the problem is likely filesystem permissions/path access rather than WordPress itself.
73. Don’t Forget SELinux/AppArmor
Linux security can have additional layers such as:
SELinux
AppArmor
Ubuntu commonly uses AppArmor for certain services.
Therefore:
Unix permissions correct
does not always guarantee:
application access allowed
This becomes an advanced troubleshooting topic.
74. Permission Layers
Think of access as:
Application
↓
Linux process identity
↓
Unix permissions
↓
ACL
↓
AppArmor/other security policy
↓
Filesystem
All relevant controls must permit the operation.
75. The Hosting Security Principle
For every website:
Identity
+
Ownership
+
Permissions
+
Process isolation
+
Database isolation
should agree with each other.
For example:
site1
should consistently map to:
site1 files
site1 PHP-FPM
site1 database
site1 database user
76. Lesson 062 — Core Principle
The most important idea is:
PHP-FPM should execute a site’s PHP as that site’s Linux identity whenever your hosting architecture calls for per-site isolation.
Then:
site1 PHP
↓
Linux user site1
↓
site1 permissions
↓
site1 files
while:
site2 PHP
↓
Linux user site2
↓
site2 permissions
↓
site2 files
This creates the foundation for secure multi-tenant hosting.
Next Lesson — 063
WordPress Installation Automation — From Empty Domain to Working Website
We will now combine the previous lessons into one complete provisioning workflow:
Domain
↓
DNS
↓
Website directory
↓
Linux user
↓
Permissions
↓
MySQL database
↓
Database user
↓
PHP-FPM pool
↓
Nginx server block
↓
WordPress download
↓
wp-config.php
↓
WordPress installation
↓
SSL
↓
Final testing
The next lesson will turn these individual concepts into a repeatable command-line installation procedure for any domain on your CresignSys VPS.
Leave a Reply