
August 25, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Getting Ubuntu file permissions explained correctly is the difference between a secure production server and one that leaks data or breaks after every deploy. Whether you are configuring storage directories for a Laravel application, securing uploads on a legal-tech portal, or hardening a shared hosting environment, understanding the interaction between users, groups, and mode bits prevents both downtime and security breaches. This guide covers the exact permission models I use on production Ubuntu 22.04 and 24.04 servers running PHP-FPM, Nginx, and Apache.
www-data:www-data, use 755 for directories, 644 for files, and apply POSIX ACLs for developer access without compromising security.If you are managing infrastructure alongside development, getting this foundation right saves hours of debugging later. I have detailed broader server management strategies in my guide on securing websites and servers in Nepal, but this article focuses strictly on the filesystem layer where most application-level security failures originate.
How Do Linux File Permission Bits Actually Work?
At the kernel level, every file and directory on an Ubuntu system has a 12-bit mode field. The first three bits define special flags (setuid, setgid, sticky), while the remaining nine bits represent three triplets: owner, group, and others. Each triplet consists of read (r=4), write (w=2), and execute (x=1). When you run chmod 755, you are setting owner=rwx (4+2+1=7), group=r-x (4+0+1=5), and others=r-x (4+0+1=5).
A critical distinction many developers miss: for files, execute means "run as a program." For directories, execute means "traverse into or list contents." A directory with mode 644 (rw-r--r--) is effectively inaccessible—you cannot cd into it or open any file inside, even if the file itself has 777 permissions. This is why directory permissions almost always include the execute bit (755 or 750), while application files typically do not (644).
Checking Current Permissions
Use ls -la for human-readable output or stat -c '%A %U:%G %n' filename for scriptable checks. On production servers, I prefer getfacl because it reveals POSIX ACLs that standard ls hides entirely—a common source of confusion when permissions look correct but access still fails.
# Check standard permissions
ls -la /var/www/laravel/storage/
# Check including ACLs (essential for production debugging)
getfacl /var/www/laravel/storage/logs/
# Find all world-writable files (security audit)
find /var/www -perm -o+w -type f -ls What Are the Correct File Permissions for Laravel and PHP Applications?
For Laravel 12.x running on Ubuntu 24.04 with PHP-FPM 8.4, the standard permission model assumes the web server process runs as www-data. Application code should be owned by your deployment user (e.g., deploy) with group www-data, while writable directories (storage/, bootstrap/cache/) must be owned by www-data:www-data or use ACLs.
| Path / Resource | Owner:Group | Mode | Rationale |
|---|---|---|---|
/var/www/app/ (root) | deploy:www-data | 755 | Deploy user manages code; web server reads |
app/, config/, routes/ | deploy:www-data | 644 (files), 755 (dirs) | Immutable in production; no write needed |
storage/, bootstrap/cache/ | www-data:www-data | 775 (dirs), 664 (files) | PHP-FPM writes logs, cache, sessions |
.env | deploy:www-data | 640 | Contains secrets; block "others" entirely |
public/uploads/ | www-data:www-data | 755 (dirs), 644 (files) | User-uploaded content; never executable |
Executable scripts (artisan) | deploy:www-data | 755 | CLI execution required for cron/jobs |
On a recent legal-tech portal I built for document attestation services, we initially set storage/ to 777 to "fix" permission errors during testing. Within days, automated scanners flagged the server. We switched to proper ownership plus ACLs, and the issue resolved permanently. Never use 777 in production—it allows any compromised process on the system to modify your application.
Applying Permissions Safely
- Set ownership recursively:
sudo chown -R deploy:www-data /var/www/app - Fix directory modes:
find /var/www/app -type d -exec chmod 755 {} \; - Fix file modes:
find /var/www/app -type f -exec chmod 644 {} \; - Restore writability on storage:
sudo chown -R www-data:www-data /var/www/app/storage /var/www/app/bootstrap/cache - Secure sensitive configs:
chmod 640 /var/www/app/.env
If you are building APIs that handle sensitive user data, these filesystem controls complement the authentication patterns described in my Laravel API best practices guide. Permissions are your last line of defense when application-level validation fails.
How Do You Manage Developer Access Without Breaking Web Server Permissions?
The classic problem: your deploy user needs to write code, PHP-FPM needs to write logs, and giving either full ownership creates conflicts. The naive solution—adding deploy to the www-data group and setting 775 everywhere—works until a developer accidentally creates a file as root during debugging, breaking the entire application. POSIX ACLs solve this cleanly by granting named users specific permissions independent of traditional ownership.
Setting Up ACLs for Laravel Storage
# Ensure filesystem supports ACLs (ext4/xfs do by default on Ubuntu 22.04+)
sudo apt install acl
# Set base ownership
sudo chown -R www-data:www-data /var/www/app/storage
# Grant deploy user persistent read-write-execute via ACL
sudo setfacl -Rm u:deploy:rwx /var/www/app/storage
sudo setfacl -Rm u:www-data:rwx /var/www/app/storage
# Set DEFAULT ACLs so new files/dirs inherit these rules
sudo setfacl -Rdm u:deploy:rwx /var/www/app/storage
sudo setfacl -Rdm u:www-data:rwx /var/www/app/storage
# Verify
getfacl /var/www/app/storage/logs/laravel.log This approach eliminates the need for umask manipulation or setgid bits on directories. Every file created by PHP-FPM or your deploy user automatically carries the correct ACL entries. I use this pattern on every Laravel project deployed via Deployer 7, including multi-site setups where several applications share the same server infrastructure.
When Should You Use Setuid, Setgid, and Sticky Bits?
Beyond the basic 9 permission bits, three special mode bits control advanced behavior. Misunderstanding these causes subtle bugs or security vulnerabilities.
- Setgid (2xxx) on directories: New files inherit the directory's group instead of the creator's primary group. Useful for shared team directories where ACLs are overkill. Apply with
chmod g+s /path/to/dir. - Sticky bit (1xxx) on directories: Only the file owner, directory owner, or root can delete/rename files within. Essential for
/tmpand shared upload directories. Apply withchmod +t /path/to/dir. - Setuid (4xxx) on executables: Process runs with the file owner's privileges regardless of who executes it. Never set this on scripts or interpreters—only on compiled binaries that require elevated privileges (e.g.,
passwd). Setting setuid on PHP or shell scripts is a critical security vulnerability.
In practice, modern Laravel and WordPress deployments rarely need setuid or setgid. ACLs handle shared-access scenarios more safely. The sticky bit remains relevant for temporary directories and user-upload zones where multiple processes write to the same location. If you are integrating payment gateways or handling sensitive legal documents, ensuring upload directories have the sticky bit prevents one compromised session from deleting another user's files.
How Do You Audit and Fix Permission Drift in Production?
Permissions drift. Deployments run as different users, manual debugging sessions create root-owned files, and package updates reset modes. Without proactive auditing, your server accumulates subtle inconsistencies that eventually cause outages at 2 AM.
Daily Audit Script
#!/bin/bash
# /usr/local/bin/audit-permissions.sh
APP_DIR="/var/www/app"
ALERT_EMAIL="ops@example.com"
# Find world-writable files (should be zero in production)
WORLD_WRITABLE=$(find "$APP_DIR" -perm -o+w -type f 2>/dev/null)
# Find files owned by root in app directory (usually wrong)
ROOT_OWNED=$(find "$APP_DIR" -user root -not -path "*/vendor/*" 2>/dev/null)
# Find executable files in storage (should never exist)
EXEC_STORAGE=$(find "$APP_DIR/storage" -type f -executable 2>/dev/null)
if [ -n "$WORLD_WRITABLE" ] || [ -n "$ROOT_OWNED" ] || [ -n "$EXEC_STORAGE" ]; then
echo "PERMISSION DRIFT DETECTED on $(hostname)" | mail -s "Permission Alert" "$ALERT_EMAIL"
# Log details for remediation
logger -t perm-audit "World-writable: $WORLD_WRITABLE"
logger -t perm-audit "Root-owned: $ROOT_OWNED"
logger -t perm-audit "Exec in storage: $EXEC_STORAGE"
fi Add this to /etc/cron.daily/ or integrate it into your monitoring stack. On projects using Deployer 7 with GitLab CI, I include a permission-check task in the post-deploy hook that automatically fixes known-safe drift (e.g., resetting storage ownership) while alerting on unexpected changes. This catches problems before they manifest as 500 errors.
Recovery Commands
When drift occurs, resist the urge to chmod -R 777. Instead, restore the intended state surgically:
sudo chown -R deploy:www-data /var/www/app— reset base ownershipsudo find /var/www/app -type d -exec chmod 755 {} \;— fix directoriessudo find /var/www/app -type f -exec chmod 644 {} \;— fix filessudo setfacl -Rdm u:deploy:rwx,u:www-data:rwx /var/www/app/storage— restore ACLssudo systemctl reload php8.4-fpm— clear opcache if PHP files were touched
For teams managing multiple client sites, documenting these recovery steps in your runbook prevents panic during incidents. If you are evaluating whether to handle this internally or bring in specialist help, my article on hiring web developers in Nepal covers what to expect from professional DevOps support.
Ubuntu File Permissions Explained: Securing Your Production Stack
Getting Ubuntu file permissions explained and implemented correctly is foundational to running secure PHP applications. The core principles are simple: least privilege by default, ACLs for shared access, never 777 in production, and automated auditing to catch drift. These patterns have kept Laravel, WordPress, and custom legal-tech portals secure across dozens of production deployments I have managed on Ubuntu servers.
Start by auditing your current setup with the commands above. Fix ownership before modes, apply ACLs to writable directories, and set up daily drift detection. If your application handles payments, personal data, or legal documents, treat filesystem permissions as a security control equal in importance to input validation and HTTPS. For hands-on assistance with server hardening or Laravel deployment pipelines, reach out through my contact page to discuss your specific infrastructure needs.

