Side story: WordPress on shared hosting and on a VPS

Install WordPress

This page is optional. The course does not need WordPress, and every lesson works with the small hand-written PHP site instead. Read on only if you already run WordPress, or have decided to, and want the steps that a WordPress site adds to the ones the lessons already cover.

The database is the same one the lessons use. Create example_db and example_user in the panel on shared hosting, or with MariaDB on a VPS, exactly as lesson 4 and lesson 5 show. WordPress then needs four values: database name, database user, password and host, which is localhost when the database runs on the same machine. The installer stores them in wp-config.php as DB_NAME, DB_USER, DB_PASSWORD and DB_HOST.

There are two ways to install the files. On shared hosting the panel usually has WordPress in its applications section: choose the domain, the folder and the database, and it downloads and configures everything. Manually, download the archive from wordpress.org, unpack it, and upload the contents of the wordpress folder into the site folder, so that index.php and wp-admin/ sit directly in the web root. Open https://example.com/wp-admin/install.php and create the administrator account.

wp-config.php above the web root

The installer writes wp-config.php into the web root. That file holds the database password, so it belongs one level above the web root, next to html/, where the web server cannot serve it. WordPress looks for it there as well as in the installation folder, which is why the move works without any other change. On the VPS:

sudo mv /var/www/example.com/html/wp-config.php /var/www/example.com/wp-config.php

sudo chown example:example /var/www/example.com/wp-config.php

sudo chmod 600 /var/www/example.com/wp-config.php

The owner is changed as well, because the moved file keeps the owner the installer gave it, often www-data, and once lesson 8 runs PHP as example the site could no longer read it.

On shared hosting, move the file up one folder over SFTP: from www/example.com/ to www/. The site keeps working, and a PHP error can no longer print the file or hand it to a visitor.

HTTPS and the site address

WordPress stores its own address in the database, so switching the site to HTTPS is two settings, not one. After the certificate works, open Settings, General and set both the WordPress address and the site address to https://example.com. The same values can be fixed in wp-config.php, which wins over the database and is easier to correct when the admin pages stop loading:

sudo vi /var/www/example.com/wp-config.php

define('WP_HOME', 'https://example.com'); define('WP_SITEURL', 'https://example.com');

Then open the site and look for mixed content: an image or a stylesheet still loaded over http:// is blocked by the browser on an HTTPS page, and the theme looks broken. Update the address in the content, or use a relative path.

Permalinks and try_files

WordPress sends every request that is not a real file to index.php. On Nginx the server block from lesson 5 uses try_files $uri $uri/ =404;, which is right for the small site and returns 404 for a pretty permalink. Change that line for WordPress:

sudo vi /etc/nginx/sites-available/example.com

location / { try_files $uri $uri/ /index.php?$args; }

sudo nginx -t

sudo systemctl reload nginx

Then choose Post name under Settings, Permalinks. On shared hosting with Apache the same job is done by the .htaccess file that WordPress writes itself.

Uploads and PHP in uploads

WordPress writes every uploaded file into wp-content/uploads, so PHP must be able to write there. Until lesson 8 gives the site its own pool, PHP runs as www-data; give that group write access:

sudo -u example mkdir -p /var/www/example.com/html/wp-content/uploads

sudo chgrp -R www-data /var/www/example.com/html/wp-content/uploads

sudo chmod -R g+w /var/www/example.com/html/wp-content/uploads

The Nginx snippets below assume the per-site PHP-FPM pool from lesson 8. Before that pool exists, the socket is unix:/run/php/php8.3-fpm.sock, the default one from lesson 5.

An uploads folder is also the place where an attacker who found an upload hole tries to leave a PHP file. Refuse to execute PHP there, and put the rule before the PHP location of the site:

sudo vi /etc/nginx/sites-available/example.com

location ~* ^/wp-content/uploads/.*\.php$ { deny all; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm-example.sock; }

The login page

/wp-login.php is the page every bot knows. A rate limit from lesson 8 makes guessing slow without blocking a person who mistypes a password once:

sudo vi /etc/nginx/sites-available/example.com

location = /wp-login.php { limit_req zone=perip burst=5 nodelay; limit_req_status 429; include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.3-fpm-example.sock; }

Two-factor authentication on the administrator account is the stronger measure; the limit only removes the noise.

Plugins, themes and updates

unattended-upgrades patches Ubuntu, not the code inside the site folder. In the admin, open Plugins and Themes and switch on automatic updates for every one you keep; WordPress then installs new versions without you. Delete the plugins and themes you do not use, because an inactive plugin is still code on the disk and still a way in.

On a server with WP-CLI, the same job is two commands. Run them as the site user once a week, from a cron line or by hand:

sudo -u example -H wp plugin update --all --path=/var/www/example.com/html

sudo -u example -H wp theme update --all --path=/var/www/example.com/html

Read the output: a plugin that fails to update is a plugin to remove or to replace.

xmlrpc.php and the database check

xmlrpc.php is an old interface used by some desktop apps and by Jetpack. If nothing on your site uses it, block it in the site configuration:

sudo vi /etc/nginx/sites-available/example.com

location = /xmlrpc.php { deny all; }

After moving the site and importing the database, check that the posts arrived. The default table prefix is wp_; if the site used another one, use that name:

sudo mariadb -e "SELECT COUNT(*) FROM example_db.wp_posts;"

+----------+ | COUNT(*) | +----------+ | 24 | +----------+

Quiz

1 Where should wp-config.php live?

  1. In the web root, next to index.php
  2. One level above the web root, where WordPress also looks for it
  3. In /tmp
  4. In the database
Answer

b) The file holds the database password, so it must not be servable. WordPress looks in its installation folder and one directory above it.

2 Which try_files line makes WordPress permalinks work?

  1. try_files $uri $uri/ =404;
  2. try_files $uri $uri/ /index.php?$args;
  3. try_files $uri /wp-login.php;
  4. try_files /index.php $uri;
Answer

b) A pretty permalink is not a file on disk, so the request goes to the front controller. The plain =404 version is correct for the small hand-written site.

3 Why refuse to execute PHP in wp-content/uploads?

  1. It makes uploads faster
  2. An uploaded .php file would run on the server and become a web shell
  3. It saves disk space
  4. It improves the theme
Answer

b) The uploads folder must hold images and documents, never code. The rule goes before the PHP location, because nginx uses the first matching regex location.

4 After the certificate works, the admin keeps redirecting to http://. What is still wrong?

  1. The database is down
  2. The WordPress address and site address still say http://
  3. The uploads folder is read-only
  4. The PTR record is missing
Answer

b) WordPress stores its base address in the database. Set both addresses to https://example.com, in the admin or in wp-config.php.

5 What should you do with xmlrpc.php if no app uses it?

  1. Open it to the world
  2. Block it in the server block
  3. Delete the database
  4. Move it to uploads
Answer

b) It is an old interface with a long history of abuse. If nothing needs it, a deny all rule removes the risk.

Article author: Arthur Isaev