Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Ubuntu File Permissions Explained

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.

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).

Permission Bit Structure (Mode 755)Owner (7)Read (4)Write (2)Execute (1)rwxGroup (5)Read (4)No WriteExecute (1)r-xOthers (5)Read (4)No WriteExecute (1)r-x
Ubuntu file permissions explained: the three-triplet structure for owner, group, and others

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 / ResourceOwner:GroupModeRationale
/var/www/app/ (root)deploy:www-data755Deploy user manages code; web server reads
app/, config/, routes/deploy:www-data644 (files), 755 (dirs)Immutable in production; no write needed
storage/, bootstrap/cache/www-data:www-data775 (dirs), 664 (files)PHP-FPM writes logs, cache, sessions
.envdeploy:www-data640Contains secrets; block "others" entirely
public/uploads/www-data:www-data755 (dirs), 644 (files)User-uploaded content; never executable
Executable scripts (artisan)deploy:www-data755CLI 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

  1. Set ownership recursively: sudo chown -R deploy:www-data /var/www/app
  2. Fix directory modes: find /var/www/app -type d -exec chmod 755 {} \;
  3. Fix file modes: find /var/www/app -type f -exec chmod 644 {} \;
  4. Restore writability on storage: sudo chown -R www-data:www-data /var/www/app/storage /var/www/app/bootstrap/cache
  5. 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.

Traditional Groups (Fragile)deploy userin www-data groupwww-data userowns storage/shared grpProblem: New files inherit creator's UIDdeploy creates file → owned by deploywww-data cannot write → app breaksWorkaround: setgid + umask hacksComplex, error-prone, breaks on sudoPOSIX ACLs (Recommended)deploy userACL: rwxwww-data userACL: rwxSolution: Default ACLs propagateNew files inherit ACL entries automaticallyBoth users always have correct accessCommand: setfacl -Rdmu:deploy:rwx,u:www-data:rwx
Traditional group permissions versus POSIX ACLs for managing developer and web server access on Ubuntu

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 /tmp and shared upload directories. Apply with chmod +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.
Special Bits Decision TreeNeed special behavior?Shared directory?Team writes, group consistencyPublic writable dir?/tmp, uploads, shared scratchBinary needs root?Compiled C only, NEVER scriptsUse SETGID (2xxx)chmod g+s /shared/dirUse STICKY (1xxx)chmod +t /tmp/uploadsUse SETUID (4xxx)EXTREME CAUTION ONLYPrefer ACLs over setgid for web apps — more granular, safer
Decision tree for applying setuid, setgid, and sticky bits on Ubuntu production servers

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 ownership
  • sudo find /var/www/app -type d -exec chmod 755 {} \; — fix directories
  • sudo find /var/www/app -type f -exec chmod 644 {} \; — fix files
  • sudo setfacl -Rdm u:deploy:rwx,u:www-data:rwx /var/www/app/storage — restore ACLs
  • sudo 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.

Frequently Asked Questions

Owner, group, and others. Owner is the file creator, group allows shared access for team members, and others applies to every remaining system user.

Use chmod -R followed by the numeric mode and target path. Always verify with ls -lR afterward to confirm changes applied correctly without breaking parent directories.

755 grants execute permission to all users; 644 restricts execution to nobody. Directories typically need 755 for traversal while files use 644 unless executable.

The storage and bootstrap/cache directories often lack write access for the web server user. On Ubuntu servers running PHP-FPM as www-data, I run chown -R www-data:www-data storage bootstrap/cache and chmod -R 775 on those paths during every Deployer release hook. Forgetting this step causes immediate 500 errors when Laravel attempts to write logs or cache files in production.

Setting the sticky bit with chmod +t ensures only the file owner, directory owner, or root can delete files within that directory. This is critical for shared upload folders in applications like Ajako Deal where multiple vendor roles write to common paths. Without it, one user could accidentally remove another user's uploaded documents or media assets, causing data loss and broken references in the database.

Access Control Lists provide granular per-user or per-group permissions beyond the basic owner/group/others model. Use setfacl when multiple distinct users need different access levels to the same file. In my experience managing legal-tech portals like Mijar Law Associates, ACLs solve cases where lawyers, paralegals, and admins require unique read/write combinations on client documents without creating excessive system groups or changing ownership repeatedly.

Running composer install as root creates vendor files owned by root, which PHP-FPM cannot read. Always run Composer as the deploy user or www-data. If damage is done, execute chown -R www-data:www-data vendor to restore correct ownership. I include this ownership correction in GitLab CI pipelines as a safety net because developers occasionally test deployments manually as root during debugging sessions on staging servers.

Set umask 002 in your deployment scripts and shell profiles to ensure new files get 664 and directories get 775 by default. This allows group-writable permissions without exposing write access to others. On Ubuntu servers hosting multiple Laravel sites via Deployer 7, I configure this in the deploy user bashrc and the PHP-FPM pool configuration. Incorrect umask values are the most common cause of permission drift after automated deployments.

Set wp-content/uploads to 755 and files within to 644, owned by www-data. Never use 777 even temporarily. Add an .htaccess or Nginx directive to disable PHP execution in the uploads folder. On WooCommerce stores like Petals Nepal, this prevents malicious script uploads from executing. Combine with fail2ban monitoring for repeated upload failures, which often indicate brute-force attempts exploiting misconfigured directory permissions.

OpenSSH refuses to use private keys readable by group or others as a security precaution against credential theft. Run chmod 600 ~/.ssh/id_rsa immediately after copying keys. During GitLab CI runner setup on Ubuntu, forgetting this step causes silent authentication failures that look like network errors. The SSH client error message explicitly states bad permissions, but many developers miss it in verbose CI logs and waste hours troubleshooting connectivity instead.

Use find /path -perm /o+w to locate world-writable files and find /path -nouser -o -nogroup to detect orphaned files. Combine with stat --format='%A %U %G %n' for readable output. I schedule these checks weekly via cron on production servers hosting sensitive applications like Court Marriage In Nepal. Pipe results to a log file reviewed during maintenance windows. Automated auditing catches permission creep before attackers exploit overly permissive configurations left behind after emergency fixes.

Database dumps must be 600 owned by root or the backup service account. Never store backups in web-accessible directories. On Ubuntu servers, I place dumps in /var/backups/mysql with restrictive permissions and encrypt them using gpg before transferring offsite. For legal-tech platforms handling sensitive client data, this prevents accidental exposure through misconfigured Nginx aliases or directory listing vulnerabilities. Backup files are high-value targets; treating them with the same security as private keys is non-negotiable.

Check /var/log/php*-fpm.log for specific file paths mentioned in permission errors. Verify ownership matches the PHP-FPM pool user defined in /etc/php/8.4/fpm/pool.d/www.conf. Common culprits include session save paths, opcache directories, and application storage folders. After fixing ownership, always restart PHP-FPM with systemctl restart php8.4-fpm rather than just reloading, as some permission caches persist across reloads. Document the exact path and fix in your deployment runbook to prevent recurrence.

Apply setgid with chmod g+s on directories where multiple users create files that must remain group-accessible. New files automatically inherit the directory group instead of the creator primary group. This is essential for shared Laravel storage folders accessed by both deploy users and www-data. Without setgid, files created during manual debugging sessions end up owned by the developer personal group, causing subsequent automated deployments to fail when PHP-FPM cannot read or overwrite those files.

The chattr +i command sets an immutable flag preventing any modification, deletion, or renaming even by root. Apply this to /etc/passwd, SSH authorized_keys, and production .env files after configuration is finalized. To edit later, remove with chattr -i first. On servers hosting multiple client sites, I use this to prevent accidental overwrites during bulk find-replace operations or compromised admin sessions. Immutable attributes add defense-in-depth beyond standard Unix permissions against both human error and automated attack scripts.

Share this article

Quick Contact Options
Choose how you want to connect me: