Fixing File ACLs For Visa Go Theme
Server Hardware And Operating System
I provision a new server. The server is a virtual machine. The provider is a cloud host. The server has 4 CPU cores. The server has 8 gigabytes of RAM. The server has 100 gigabytes of NVMe storage. The operating system is Ubuntu 24.04 LTS. I get an IPv4 address. I get a root password. I open my local terminal. I use the SSH client. I type ssh root@192.168.10.50. I press enter. The system asks for the password. I type the password. I press enter. The terminal shows the welcome message.
I update the package lists. I type apt update. The system connects to the Ubuntu archive servers. The system downloads the index files. The download finishes. I upgrade the installed packages. I type apt upgrade -y. The system downloads new versions. The system unpacks the files. The system configures the software. I wait three minutes. The prompt returns. I clear the screen. I type clear.
I create a standard user. I do not use the root user for daily tasks. I type adduser deploy. I type a strong password. I type the password again. I press enter five times to skip the user details. I type y to confirm. I add the user to the sudo group. I type usermod -aG sudo deploy. I switch to the new user. I type su - deploy. The prompt changes. I am now the deploy user.
Network Security
I set up the firewall. The server needs protection. I use UFW. The tool is standard on Ubuntu. I type sudo ufw default deny incoming. I type sudo ufw default allow outgoing. I allow SSH traffic. I type sudo ufw allow 22/tcp. I allow HTTP traffic. I type sudo ufw allow 80/tcp. I allow HTTPS traffic. I type sudo ufw allow 443/tcp. I enable the firewall. I type sudo ufw enable. The system asks for confirmation. I type y. The system enables the firewall. I check the rules. I type sudo ufw status numbered. I see the correct ports.
I disable password login for SSH. I open the SSH config file. I type sudo nano /etc/ssh/sshd_config. I find the line PasswordAuthentication yes. I change the word yes to no. I save the file. I press Ctrl+O. I press enter. I exit the editor. I press Ctrl+X. I restart the SSH service. I type sudo systemctl restart ssh. The server only accepts SSH keys now.
Database Backend
I install MariaDB. It is a drop-in replacement for MySQL. I type sudo apt install mariadb-server -y. The system downloads the packages. The system starts the service. I check the status. I type systemctl status mariadb. The text is green. The service runs. I press q to exit the status view.
I secure the database. I run the built-in script. I type sudo mysql_secure_installation. The script asks for the current root password. I press enter. The script asks to switch to unix_socket authentication. I type n. The script asks to change the root password. I type y. I type a new secure password. I type it again. The script asks to remove anonymous users. I type y. The script asks to disallow remote root login. I type y. The script asks to remove the test database. I type y. The script asks to reload privilege tables. I type y. The script finishes.
I create the application database. I log into the database shell. I type sudo mysql -u root -p. I enter the password. I see the MariaDB prompt. I create the database. I type CREATE DATABASE visadb;. I create the application user. I type CREATE USER 'visauser'@'localhost' IDENTIFIED BY 'visapassword999';. I grant all privileges to the user. I type GRANT ALL PRIVILEGES ON visadb.* TO 'visauser'@'localhost';. I apply the changes. I type FLUSH PRIVILEGES;. I exit the shell. I type exit.
Web Server And PHP Setup
I install Nginx. I type sudo apt install nginx -y. The system installs the web server. I check the status. I type systemctl status nginx. The service is active.
I install PHP. I use PHP 8.3. I install the core and the extensions. I type sudo apt install php8.3-fpm php8.3-mysql php8.3-curl php8.3-gd php8.3-mbstring php8.3-xml php8.3-zip -y. The system downloads many files. The system starts the PHP-FPM service.
I configure PHP settings. I open the main initialization file. I type sudo nano /etc/php/8.3/fpm/php.ini. I use the search function. I press Ctrl+W. I type memory_limit. I press enter. I change the value from 128M to 512M. I press Ctrl+W. I type upload_max_filesize. I press enter. I change the value from 2M to 64M. I press Ctrl+W. I type post_max_size. I press enter. I change the value from 8M to 64M. I press Ctrl+W. I type max_execution_time. I press enter. I change the value from 30 to 120. I save the file. I exit the editor.
I configure the PHP-FPM pool. I type sudo nano /etc/php/8.3/fpm/pool.d/www.conf. I find the pm setting. The value is dynamic. I leave it. I find pm.max_children. I change the value to 50. I find pm.start_servers. I change the value to 5. I find pm.min_spare_servers. I change the value to 5. I find pm.max_spare_servers. I change the value to 20. I save the file. I exit the editor. I restart the PHP service. I type sudo systemctl restart php8.3-fpm.
I create the web root folder. I type sudo mkdir -p /var/www/visasite. I change the owner to the web server user. I type sudo chown -R www-data:www-data /var/www/visasite. I set the permissions. I type sudo chmod -R 755 /var/www/visasite.
I create the Nginx server block. I type sudo nano /etc/nginx/sites-available/visasite.conf. I type the configuration lines. I type server {. I type listen 80;. I type server_name immigration-example.com;. I type root /var/www/visasite;. I type index index.php index.html index.htm;. I type location / {. I type try_files $uri $uri/ /index.php?$args;. I type }. I type location ~ \.php$ {. I type include snippets/fastcgi-php.conf;. I type fastcgi_pass unix:/run/php/php8.3-fpm.sock;. I type }. I type }. I save the file. I exit the editor.
I enable the site. I type sudo ln -s /etc/nginx/sites-available/visasite.conf /etc/nginx/sites-enabled/. I test the configuration. I type sudo nginx -t. The output says syntax is ok. I reload Nginx. I type sudo systemctl reload nginx.
Core System Installation
I download the core files. I go to the web directory. I type cd /var/www/visasite. I use wget. I type sudo wget https://wordpress.org/latest.tar.gz. The server downloads the file. I extract the archive. I type sudo tar -xzf latest.tar.gz. The system creates a wordpress folder. I move the files up one level. I type sudo mv wordpress/* .. I delete the empty folder. I type sudo rm -rf wordpress. I delete the archive. I type sudo rm latest.tar.gz.
I set up the configuration file. I copy the sample file. I type sudo cp wp-config-sample.php wp-config.php. I edit the file. I type sudo nano wp-config.php. I find the database name line. I type define('DB_NAME', 'visadb');. I find the user line. I type define('DB_USER', 'visauser');. I find the password line. I type define('DB_PASSWORD', 'visapassword999');. I save the file. I exit the editor. I reset the file ownership. I type sudo chown -R www-data:www-data /var/www/visasite.
Automated Theme Deployment
A consulting company needs a website. The company handles visas. The company buys a specific template. The product is the Visa Go - Immigration and Visa Consulting WordPress Theme. The developer gives me the zip file. The developer wants an automated deployment. The developer updates the theme code often. I write a bash script. The script deploys the theme.
I create the script file in my home directory. I type nano ~/deploy_theme.sh. I write the code. I type #!/bin/bash. I type THEME_ZIP=$1. I type TARGET_DIR="/var/www/visasite/wp-content/themes/". I type echo "Starting deployment...". I type cp $THEME_ZIP $TARGET_DIR. I type cd $TARGET_DIR. I type unzip -o $(basename $THEME_ZIP). I type rm $(basename $THEME_ZIP). I type chown -R www-data:www-data .. I type echo "Deployment finished.". I save the script. I exit the editor. I make the script executable. I type chmod +x ~/deploy_theme.sh.
I test the script. I upload the theme zip from my local machine using SCP. I type scp visa-go.zip deploy@192.168.10.50:/home/deploy/. The file uploads. I go to the server terminal. I run the script. I type sudo ./deploy_theme.sh visa-go.zip. The script runs. The script prints "Starting deployment...". The script extracts the files. The script prints "Deployment finished.".
The developer logs into the site. The developer activates the theme. The site works. The theme looks correct.
The Observation
The developer uses the site for two days. The developer adds team member photos. The developer adds background images. Then the developer reports a problem. The new images do not load on the public website. The browser shows a broken image icon.
I check the site. I open the browser. I press F12 to open developer tools. I click the Network tab. I reload the page. I see the image requests. The server returns an HTTP 403 status code. The status means Forbidden. Nginx refuses to serve the image files.
The site does not crash. The PHP pages load fine. The database works fine. Only new image uploads fail.
Standard Log Investigation
I read the Nginx error log. I type sudo tail -n 20 /var/log/nginx/error.log. I see many error lines. The lines look like this. [error] 1405#1405: *231 open() "/var/www/visasite/wp-content/uploads/2023/10/passport.jpg" failed (13: Permission denied). The error is clear. The web server process cannot read the file.
I check the file permissions. I go to the uploads folder. I type cd /var/www/visasite/wp-content/uploads/2023/10/. I list the files with details. I type ls -la. I look at the output.
The output shows -rw-r--r-- 1 www-data www-data 45000 Oct 15 10:00 passport.jpg.
The owner is www-data. The group is www-data. The permissions are 644. The owner can read and write. The group can read. Others can read. The Nginx worker runs as www-data. The Nginx worker should read this file. Standard Linux permissions say this is perfectly fine. But Nginx still gets a Permission denied error.
I test reading the file directly as the web user. I use sudo. I type sudo -u www-data cat passport.jpg > /dev/null. The command fails. The output says cat: passport.jpg: Permission denied. The standard ls output lies. Something else blocks the access.
Access Control List Analysis
Linux has another permission layer. Web agencies build many sites. Agencies Download WordPress Themes and use complex deployment tools. Sometimes folders inherit hidden rules. These rules are Access Control Lists (ACLs).
I check the ACLs on the file. I use the getfacl command. I type getfacl passport.jpg. The command prints detailed information.
I read the output.
# file: passport.jpg
# owner: www-data
# group: www-data
user::rw-
user:deploy:rwx
group::r--
mask::---
other::r--
I see the problem immediately. The ACL has a mask entry. The mask is ---. The mask limits the maximum effective permissions for named users and groups. Even though the standard owner is www-data, and standard permissions are 644, the ACL mask overrides it. The mask is empty. It strips all permissions. The effective permission for www-data is zero.
I check the parent directory. I type getfacl .. The directory is 10.
I read the output.
# file: .
# owner: www-data
# group: www-data
user::rwx
group::r-x
other::r-x
default:user::rwx
default:user:deploy:rwx
default:group::r-x
default:mask::---
default:other::r-x
The directory has default ACLs. Any new file created inside this directory inherits these default ACLs. The default:mask::--- is the root cause. PHP creates the uploaded image. PHP applies the default ACL. The mask becomes ---. Nginx cannot read the image.
Tracking The Source With Auditd
I need to know how the default ACL got there. The standard mkdir command does not create ACLs. Something modifies the permissions. I use auditd. It tracks system calls.
I install the tool. I type sudo apt install auditd -y. The service starts automatically. I configure a watch. I want to watch the wp-content directory. I want to see which process changes file attributes.
I add a rule. I type sudo auditctl -w /var/www/visasite/wp-content/ -p a -k acl_monitor. The -w sets the path. The -p a monitors attribute changes. The -k sets a key name to filter logs later.
I need to trigger the issue again. I delete the entire themes directory. I type sudo rm -rf /var/www/visasite/wp-content/themes/visa-go. I run my deployment script again. I type sudo ./deploy_theme.sh visa-go.zip. The script finishes.
I search the audit logs. I use ausearch. I type sudo ausearch -k acl_monitor -i. The -i flag interprets the user IDs into names. The log is very long. I filter it. I type sudo ausearch -k acl_monitor -i | grep -C 5 "setxattr". ACLs use extended attributes. The system call is setxattr.
I find the matching event. I read the block.
type=SYSCALL msg=audit(10/15/2023 11:00:00.000:150): arch=x86_64 syscall=setxattr success=yes exit=0 a0=7ffe123456 a1=7ffe654321 a2=7ffe987654 a3=1c items=1 ppid=5432 pid=5433 auid=deploy uid=root gid=root euid=root suid=root fsuid=root egid=root sgid=root fsgid=root tty=pts0 ses=4 comm=unzip exe=/usr/bin/unzip key=acl_monitor
I analyze the log line. The command is unzip. The user who started the session (auid) is deploy. The effective user (euid) is root. My bash script runs unzip using sudo. The unzip tool restores extended attributes from the zip archive. The zip archive for the theme contains ACL data from the developer's local Mac or Linux machine. The developer's machine had strange default ACLs on the theme folders. The unzip command faithfully extracts those ACLs and applies them to my server.
When unzip creates the theme folder, it applies the bad default mask. Later, WordPress creates the uploads folder. The uploads folder inherits the bad default mask from the parent wp-content if the permissions cascade, or in this case, the zip file included the entire wp-content structure with ACLs.
The Resolution
I stop the audit daemon. I remove the rule. I type sudo auditctl -W /var/www/visasite/wp-content/. I uninstall auditd to save resources. I type sudo apt remove auditd -y.
I fix the current files. I use setfacl. I strip all ACLs from the web root. I type sudo setfacl -R -b /var/www/visasite/. The -R makes it recursive. The -b removes all extended ACL entries. It leaves only the standard Linux owner, group, and other permissions.
I check the image file again. I type getfacl /var/www/visasite/wp-content/uploads/2023/10/passport.jpg. The mask line is gone. The plus sign next to the permissions in ls -l is gone.
I go to the browser. I reload the page. The image loads. The HTTP status is 200. Nginx can read the file.
I must prevent this in the future. I modify my deployment script. I open the file. I type nano ~/deploy_theme.sh. I find the line unzip -o $(basename $THEME_ZIP).
I add a new line right below it. I type setfacl -R -b .. This line strips any ACLs extracted by the unzip command immediately after extraction.
I review the script.
#!/bin/bash
THEME_ZIP=$1
TARGET_DIR="/var/www/visasite/wp-content/themes/"
echo "Starting deployment..."
cp $THEME_ZIP $TARGET_DIR
cd $TARGET_DIR
unzip -o $(basename $THEME_ZIP)
setfacl -R -b .
chown -R www-data:www-data .
echo "Deployment finished."
I save the file. I exit the editor. I run a test deployment one more time. I type sudo ./deploy_theme.sh visa-go.zip. The script runs. I check the folder permissions. I type getfacl /var/www/visasite/wp-content/themes/visa-go. The ACLs are clean. The standard permissions are correct.
The developer uploads a new image in the WordPress dashboard. The image processes normally. The image displays correctly on the frontend. The standard permissions apply. The server operates as expected. I log out of the server. I type exit. The terminal window closes.
评论 0