The setup of the LEMP stack (NGINX, MariaDB or MySQL, and PHP) has many caveats which can impact both performance and security. Here is how to do it right.

Set up NGINX

The easiest way to set up NGINX is by installing the official repository by the creators of NGINX.

Add the NGINX repository

Create a file named nginx.repo and save it in the directory:

/etc/yum.repos.d/

then populate it with the following content (for CentOS):

[nginx] name=nginx repo baseurl=http://nginx.org/packages/centos/$releasever/$basearch/ gpgcheck=0 enabled=1

For RHEL, use the above repo configuration with the following baseurl:

baseurl=http://nginx.org/packages/rhel/$releasever/$basearch/

In case of both CentOS and RHEL, you need to manually replace the variable $releasever with the major release number of your OS; for CentOS 7, replace the variable with a 7. The same principle goes for RHEL. When in doubt, visit the repository in your web browser, navigate to the appropriate directory, and grab its URL as the baseurl of your repo.

Install NGINX binaries

Once the repository is in place, you can begin the installation of NGINX binaries:

yum install nginx

After that, it’s all about configuration tweaks.

Recommended configuration options for NGINX

Optimizing the settings of NGINX is an art as much as it is a science.

If nginx is already running, the command:

systemctl status nginx.service

will yield useful information including the location of is main config file (usually /etc/nginx/nginx.conf). This file may reference additional, site-specific config files. Together, they combine into one set of configuration directives divided into so-called blocks that immediately follow a couple of general directives (on worker processes, worker connections, and the like, that apply to the current master process).

There are three types of blocks in NGINX:

a http {} block (one per master process),

block (one per master process), multiple server {} blocks, which may represent virtual hosts (they are always placed outside the http {} block), and

blocks, which may represent virtual hosts (they are always placed outside the http {} block), and one or multiple location {} blocks, always inside a server {} block.

Point the server to the document root

The document root (also known as the web server root) is the directory tree that contains files and directories that are made available for your web server to deliver to visitors.

Your PHP engine may, in certain circumstances, traverse higher-up directories, so you must be very smart about the location of your web server root.

You want your web content to reside in either /var/www/ , /srv , or /usr/share/www .

Never use system directories, the home directory of the root user on the system, or the root directory (/) to serve web content! It’s a no-no. This practice opens up a can of worms and could result in a disaster, should a visitor manage to coerce your web server/your PHP engine to traverse directories up the directory tree (which isn’t impossible, as you will learn later in this article).

The document root should be defined within the server {} block of your site’s configuration file.

server { server_name www.yourdomain.com; root /var/www/nginx-default/; location / { # [...] } location /foo { # [...] } location /bar { # [...] } }

Do NOT place your document root in the standard document root directory that is part of the installation of NGINX on your system. This directory may be overwritten during an upgrade and there is no recourse for data loss. “You should have known better” (or so the theory), so don’t put anything that’s dear to you there.

Tell the server how to listen to incoming requests

Set your listen directive correctly, for example (“listen on port 443 on all network interfaces”):

server { listen 443 ssl http2; # IPv4 listen [::]:443 ssl http2; # IPv6

Never use a hostname in your server’s listen directive and avoid doing so in an upstream location (you only need an upstream location when you are using NGINX as an HTTP load balancer). Using an IP address instead of a hostname is a lot safer because NGINX will be able to bind to it even if it fails to resolve the hostname, for example:

upstream { server http://10.48.41.12; } server { listen 127.0.0.16:80; # [...] }

Configure routing requests to virtual hosts based on server_name

If NGINX listens to requests directed at several virtual servers on only one port, it has to decide how to route these requests based on the header alone. To accomplish this, NGINX uses the server_name directive (“respond to requests directed to these host names based on this server block, otherwise route to the default server”):

server { server_name www.cloudinsidr.com cloudinsidr.com; }

The first server block that NGINX encounters when parsing its configuration files will be its default server. If you don’t like that, use the default_server directive (a property of the listen port, NOT the server name; it defines the default server per port):

listen 443 default_server;

Hide Nginx version number

Hide NGINX version number (put this in the http block in /etc/nginx/nginx.conf):

server_tokens off;

Permit only the most secure TLS versions

Don’t use SSLv3 and stay away from TLS v1/1.1:

ssl_protocols TLSv3 TLSv1.2;

Specify the index (only once!)

Put the index directive either once in the http { } block, or once in each server {} block, but not in both. For example:

http { index index.php index.htm index.html; server { server_name www.yourdomain1.com yourdomain1.com; location / { # [...] } } server { server_name www.yourdomain2.com yourdomain2.com; location / { # [...] } location /foo { # [...] } } }

Do not use If statements: use multiple server and location blocks

Do not use ‘If’ statements (if you can help it, lol!); the ‘If’ implementation in NGINX was a half-baked attempt at appeasing some users who considered it a priority at the time. As a result, ‘If’ statements waste server resources and are terribly inefficient.

Instead of using ifs, use multiple server and location blocks. That’s what they are for. (If you want to know more about the danger of If statements, refer to this article: If Is Evil)

If your purpose for an If statement is checking whether a file exists, NGINX wants you to use try_files instead:

server { root /var/www/www.domain.com; location / { try_files $uri $uri/ /index.html; } }

This tells the server: try $uri and serve it if found; if $uri doesn’t exist, try the next best thing which is a directory of that name ($uri/ with a forward slash at the end), and if that doesn’t exist either, then tough luck, fall back to /index.html.

Instead of using a file as the fallback location you can use a status code ( =404 ), but if you do point to a file for fallback, the file must exist. One other thing worth noting: try_files will not issue an internal redirect for anything but the last parameter.

You still need a location to catch \.php$ URIs as otherwise try_files will serve an $uri that is found to exist as a plain text file.

To get a CMS system such as WordPress, Drupal or Joomla running, use this (adjust the query string for your CMS if appropriate):

try_files $uri $uri/ /index.php?q=$uri&$args;

WordPress can even read from (REQUEST_URI):

try_files $uri $uri/ /index.php;

Implementing redirects: do not use rewrites, use multiple (server) blocks and the return directive

Do not use this syntax (it will cost you dearly in terms of precious performance):

# DO NOT use this syntax!!! Use the return directive instead! rewrite ^/(.*)$ http://exampledomain.tld/$1 permanent;

Instead, put this in the relevant location {} block and it will get you much better results:

return 301 https://exampledomain.tld$request_uri;

For example, if you want to redirect visitors from yoursite.tld using http to www.yoursite.tld using https, this is how to do it the NGINX way: create two separate server blocks and use the return directive like this:

server { listen 80; server_name yoursite.tld; return 301 https://www.yoursite.tld$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name www.yoursite.tld; ... }

This will redirect all http traffic to yoursite.tld using http to www.yoursite.tld using https.

To redirect all traffic from your old domain(s) to your new domain (yournewdomain.tld), use multiple server {} blocks with a return directive (and, optionally, a catch-all server_name) like in this example:

server { listen 443; server_name yournewdomain.tld www.yournewdomain.tld; ... } server { listen 80 default_server; # the following line is a catch-all server name server_name _; return 301 http://yournewdomain.tld$request_uri; }

If you are interested in using regular expressions in server names, try this.

How to activate HTTP/2 with https is covered in this post: How to Activate HTTP/2 with TLS Encryption in NGINX for Secure Connections without a Performance Penalty.

Disallow the execution of PHP files in any directory that allows uploads by a user

Disallowing the execution of PHP files in any directory that contains user uploads is a good practice on any web server. With NGINX, this is how to best accomplish it:

location /path/to/uploads { location ~ \.php$ {return 403;} # [...] }

You can also use the try_files directive to filter out the problem condition:

location ~* \.php$ { try_files $uri =404; fastcgi_pass [backend]; # [...] }

or a nested location:



location ~* \.php$ { location ~ \..*/.*\.php$ {return 404;} fastcgi_pass [backend]; # [...] }

(Optional, for the paranoid) Allow only specific scripts to execute

If you want to allow only specific scripts to execute, use this in your PHP location block ([backend] is your backend of choice):

location ~* (file_a|file_b|file_c)\.php$ { fastcgi_pass [backend]; # [...] }

The parenthesis in the above example contain a list of allowed script files, delimited with a pipe to signify an OR statement.

Use a proxy the right way

If you use a proxy, do it right. One way is this:

server { server_name _; root /var/www/site; location / { try_files $uri $uri/ @proxy; } location @proxy { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/tmp/phpcgi.socket; } }

This will also work fine:

server { server_name _; root /var/www/site; location / { try_files $uri $uri/ /index.php; } location ~ \.php$ { include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_pass unix:/tmp/phpcgi.socket; } }

In the above example, if the URI is not a file or directory, it gets passed on to your proxy.

Enable NGINX on startup

Verify the status of NGINX after installation:

systemctl status nginx

Enable automatic launch of NGINX on system startup:

systemctl enable nginx

You should see output indicating that a symbolic link has been created by the utility:

ln -s '/usr/lib/systemd/system/nginx.service' '/etc/systemd/system/multi-user.target.wants/nginx.service'

Start NGINX

Start NGINX (once):

service nginx start

or

systemctl start nginx

At this point, NGINX should be able to serve static pages, assuming that file system permissions and SELinux security contexts are correctly set on the web server document directory (continue reading for more, and see this post for some tips on SELinux).

Using your web browser, navigate to the IP address of the web server, or to the domain/host name (if correctly configured). You should see the NGINX Welcome page.

Tips for troubleshooting connectivity problems

Aside from the obvious things like a rogue firewall or a missing DNS resolver, there are a couple of things you need to be aware of.

On AWS, the command to find public addresses of your server (ip addr) will yield the “internal” private IPv4, not your public IPv4 addresses, in addition to any IPv6 addresses (those are always both “private” and “public”). In the listen directive of NGINX, you can use the private IPv4 addresses and IPv6, but not public IPv4 as your instance is unaware of them.

Restart nginx whenever you need to apply changes:

service nginx restart

When in doubt, clear the browser cache and flush the DNS cache of your local machine.

Setting correct permissions for NGINX

There are two aspects to setting access permissions: Unix/GNU Linux file system permissions and SELinux.

It is considered a good practice to run NGINX as the user and group nginx. The installation script may have created them; to verify whether such a user and group exist, use this command:

cat /etc/passwd

If these users exist, skip to the section “Create website owners and set ownership on the web server document tree” below.

Create the user and group nginx

Create a group named nginx:

groupadd nginx

If such a user does not yet exist in your /etc/passwd file, create a new system user (-r) named nginx and set that user’s shell (-s) to /sbin/nologin so no one can start an interactive shell session with nginx’ credentials:

useradd -s /sbin/nologin -r -d /var/cache/nginx nginx

The user is created with a home directory located in /var/cache/nginx. (Feel free to empty this directory if it gets populated from the skeleton directory. The contents of /var/cache/nginx are owned by and 777 (rwx) accessible to the user nginx with root as the group at 0 (—) permissions and others at 0 (—) as well. The parent directory is owned by root:root with permissions set at 755. This is the default setup on CentOS 7. See section on SELinux below for advanced configuration.)

Add the user nginx to the group nginx:

usermod -G nginx nginx

Create website owners and set ownership on the web server document tree

The ownership of files and directories inside of document root is of paramount importance when it comes to security.

If you think it’s a good idea to assign the web server document directory tree to the owner and group nginx (nginx:nginx), think again. This is not the safest way to go about web hosting. Rather, create a separate owner for each web server root directory, then assign the ownership to each site’s respective owner, then add each of these site owners to the group nginx alongside the nginx user.

For the simplest configuration possible, you do not need an additional user to own the web server document directory. The user and group nginx:nginx will suffice. However, this is not a good practice when running multiple websites: each website should have an owner of its own and each of these owners should be a member of the nginx group.

If you also happen to run PHP and want to grant the web server the ability to perform automatic updates of your web applications, then the SAPI service (php-fpm) must run with the permissions of the user who owns the contents of the installation directory it will be asked to keep up to date. This also means that you need to ensure that php-fpm starts separate pools for each of the websites with the privileges of their respective owners. In the resulting setup, individual web applications will be running in a “sandboxed” environment of sorts. Should one of them become compromised, it will not spill over to the others.

Step 1. Create users to be owners of each website’s directory tree

For each website (or web application) that you want to “sandbox”, create a user named websiteowner1. If you don’t want this user to have a home (assuming they won’t be signing in) or shell access, create it as a system user (-r) with no shell (-s):

useradd websiteowner1 -r -s /sbin/nologin

To create a user with a home directory (for example, for remote access to the web server directory tree using SFTP with keys), use this command:

useradd -md /home/websiteowner1 -s /sbin/nologin websiteowner1

It creates a user account and the user’s home directory (-m) without granting the user the right to a shell (-s /sbin/nologin) and without a private group (-n).

If the use needs to perform uploads using SFTP, it also needs a shell:

usermod -s /bin/bash username

TIP: If you need to remove a user, userdel is helpful.

Step 2. Add website owners to the web server group

Add the user websiteowner1 to the group nginx:

usermod -G nginx websiteowner1

This user will own the document tree of the website’s root directory with the group set to nginx (websiteowner1:nginx).

Step 3. Correct the ownership on each website’s document directory and its contents

Set the owner and and group of website 1 using the command (-R for recursive):

chown -Rf websiteowner1:nginx /var/www/www.website1-root.tld

Repeat this procedure for each individual website that has its own NGINX root directory. Each site’s web root directory and its contents should be owned by the website owner and read-execute-accessible to the group nginx (see section on SELinux for advanced configuration).

Step 4. Correct file access permissions on the web server directory and its contents

Once you adjust the ownership of the web server document directory for each website/web application, you also need to set correct Unix access permissions on it (750 for directories, 640 for files).

Which specific permissions you need for various objects depends on the type of content you intend to be serving.

Rule No. 1: NEVER use 777. (If 750/640 permissions on directories/files seem insufficient, SELinux could be the problem; for troubleshooting SELinux see further below.) Since the master process of NGINX runs with root permissions and spawning worker processes as user nginx (or a similar unprivileged user), if you were to set permissions to 777, you could enable any user to run any code as root. Do not do this!

Rule No. 2. The web server should never have the ability to modify the files it is executing.

The web server needs the following permissions on its document directory tree:

read (4) permissions on all files it is expected to serve (as a member of the group );

permissions on all files it is expected to serve (as ); read (4) and execute (1) permissions on the directories that contain files it is expected to serve (as a member of the group);

The service that will be writing to the directory tree (php-fpm) needs the following permissions on files and directories contained within:

read (4), write (2), and execute (1) permissions on upload directories (preferably as the owner, NOT a member of the group).

In short:

640 for files

750 for directories,

ownership of the directory and its contents by the website owner: group with the member nginx (the same user that the worker processes run as).

Set correct permissions for interpreted scripts and directories

Unlike binaries and shell scripts, interpreted scripts such as PHP or Ruby work just fine without the execute permission (x). However, in order to traverse (enter) a directory, the execute permission on that directory is nonetheless required. The web server needs this permission to list the contents of a directory or serve any files that are located inside of it.

To set permissions on the web server document directory tree for files and directories, use these commands:

find /var/path/to/web/directory -type f -exec chmod 640 {} \; find /var/path/to/web/directory -type d -exec chmod 750 {} \;

All files receive the permissions 640 (-rw-r—–) and all directories are 750 (drwxr-x—). Because the group nginx is assigned to all files and directories, the user nginx who belongs to this group alongside the website owners can read these files, but (unlike the website owners) cannot write to them. Directories that must be written to, such as the upload directory, are a special case and will be discussed later below.

WARNING: Be careful to apply the above permissions to the home directory of each site and its contents only. Changing the parent path in this way will produce the dreaded “No input file specified.” error. This simply means that NGINX is denied access to the web server document directory (see Step 5 below for details). The correct permissions on it are:

drwxr-xr-x. root root system_u:object_r:httpd_sys_content_t:s0 www

Step 5. Verify if NGINX is allowed to traverse the directory tree down to the root directory of each site

To traverse (enter) a directory and list its contents, the web server needs the execute permission (x) on that directory and all its parent directories. When it comes to serving content from a directory, it also needs the read permission on the files contained within it. Granting the web server the execute permission for the directory tree and the read permission for files as a member of the group (not the owner, and not “other” users) should suffice.

Verify that NGINX is allowed to traverse directories down to the web server document directory of each site:

namei -l /var/www/www.youwebsite.tld/ f: /var/www/www.youwebsite.tld/ dr-xr-xr-x root root / drwxr-xr-x root root var drwxr-xr-x nginx nginx www drwxr-x--- websiteowner1 nginx www.youwebsite.tld

In the above example, the x privilege allows everyone including “other users” to traverse the directory tree down to /var/www. The website directory is accessible to the website owner and the group. The web server, as a member of this group, can traverse the directory (x) and list its contents.

Once this is done, there are still a few things on the to-do list:

correct SELinux security labels (allowing writes where appropriate);

(allowing writes where appropriate); ensure that the service that is going to perform updates has ownership of the files; for php-fpm this means that the pool that corresponds to each website/web application runs as the user who is the website’s owner (see further down).

Special considerations for PHP applications with the ability to write to the web server document directory

When running a CMS that is expected to have the ability to upload files (such as updates) you will have to make special provisions in order to ensure security while giving some processes the ability to write to web server directories.

Before you go there, however, you need to get PHP up and running.

Set Up PHP 7.x

Nginx uses php-fpm as it’s SAPI (server API). We covered the installation of PHP 7.x in this post. In this post, you’ll learn how to configure it to work with NGINX.

In short: install the remi repo, install PHP with modules, enable it to launch during system startup, configure, restart, done!

Install the repository

In your web browser, navigate to the Remi repo configuration wizard:

https://rpms.remirepo.net/wizard/

Select the parameters of your desired installation and follow the directions.

Here is an example for PHP 7.2.6 on Fedora 28.

First, install the remi repository configuration package:

dnf install http://rpms.remirepo.net/fedora/remi-release-28.rpm

Next, install your PHP modules, for example:

dnf --enablerepo=remi-php70 install php70-php-pear php70-php-bcmath php70-php-pecl-jsond-devel php70-php-mysqlnd php70-php-gd php70-php-common php70-php-fpm php70-php-intl php70-php-cli php70-php php70-php-xml php70-php-opcache php70-php-pecl-apcu php70-php-pecl-jsond php70-php-pdo php70-php-gmp php70-php-process php70-php-pecl-imagick php70-php-devel php70-php-mbstring php70-php-mcrypt

dnf install php php-mcrypt php-cli php-gd php-curl php-mysql php-ldap php-zip php-fileinfo

Enable PHP launch on system startup

Start PHP once:

systemctl start php-fpm

Should the PHP-FPM service fail to start, read this post on troubleshooting SELinux for php-fpm.

Enable automatic launch on system startup:

systemctl enable php-fpm

You should see output resembling this line:

ln -s '/usr/lib/systemd/system/php-fpm.service' '/etc/systemd/system/multi-user.target.wants/php-fpm.service'

Find info.php on your system

To find out the status of php-fpm, ask your system:

systemctl status php-fpm.service

This means that the php-fpm.service is running with a master process pool spun in accordance with the configuration file:

/etc/php-fpm.conf

Navigate to the parent directory of the main php-fpm configuration file on your system (php-fpm.conf). List the contents of this directory, and you will find php.ini, the main configuration file that controls the behavior of the PHP interpreter, as well as various other configuration files including those that initiate pools:

# ls -lat . | grep -i php drwxr-xr-x. 2 root root 4096 Jun 18 15:50 php-fpm.d drwxr-xr-x. 2 root root 4096 Jun 18 15:50 php.d drwxr-xr-x. 2 root root 4096 Jun 18 15:50 php-zts.d -rw-r--r--. 1 root root 4023 May 23 08:26 php-fpm.conf -rw-r--r--. 1 root root 62221 May 23 08:26 php.ini

Next, adjust settings in your php.ini.

Fix your server’s Path Info behavior in php.ini (What is PATH_INFO and why would you care?)

Path info is information appended to the URI of a PHP script. Path info, generally speaking, starts with a forward slash and ends before the query arguments that begin with a question mark (?).

http://www.domain.com/path/to/a/php-script.php/PATH_INFO?query_args=foo

With cgi.fix_pathinfo enabled, PHP can tell PATH_INFO and SCRIPT_FILENAME apart (the latter one defined as the script file name prefixed with the document root). This behavior opens up a security vulnerability, however: when calling a non-existent PHP script that’s appended to an image file, it is possible to execute malicious code in that file. (Since image files can contain arbitrary code, it is possible for a malicious user to craft an image that contains valid PHP that may be executed when the attacker exploits PATH_INFO.) An example URI with that effect could look something like this:

http://www.domain.com/malicious.jpg/nonexistent.php

Since nonexistent.php cannot be found in the file system, while malicious.jpg is found in the location stipulated by the URI, nonexistent.php is deemed to be PATH_INFO and malicious.jpg it is considered a script, and executed.

A partial fix to the problem consists of disallowing the execution of anything that does not end in .php. Recent versions of PHP-FPM set the default correctly, but it never hurts to verify.

To confirm that the version installed on your system is using correct defaults, open the configuration file www.conf (and its equivalent for each of the other pools you configured) on your system and find a parameter named security.limit_extensions. The default value in PHP 7 is:

security.limit_extensions = .php

Should you come across some compatibility issues, you may need to adjust this setting, for example:

security.limit_extensions = .php7

Even disallowing the execution of anything that does not end in .php is not completely fool-proof. For this very reason, the makers of NGINX recommend you set the cgi.fix_pathinfo parameter in your php.ini to 0:

cgi.fix_pathinfo=0

With this option set, the PHP interpreter is only allowed to execute the script specified literally by the URI, not any variations of it.

Tip: if your configuration changes don’t reflect in your browser, clear caches (browser and DNS resolver), or change the browser.

Configure the initial pool

In the same directory as php.ini, you will find the directory php-fpm.d and in it the file www.conf. This file sets up the initial pool.

HINT: If you are not sure how to find the location, execute

systemctl status php-fpm | grep "master process"

in the command line and look for the location of the php-fpm master process.

The file you are looking for is located at the same location, inside of the php-fpm.d directory.

Open it in a text editor of your choice. Find these two lines:

user = apache group = apache

and change them to:

user = nginx group = nginx

This setting necessitates that nginx be the owner of the web server document directory and its contents; otherwise, the web application won’t be able to change files (membership in the group should not be sufficient for write access! inside of the web server document directory). It is a good practice to create separate Unix/Linux users and assign them ownership of their respective website directories (see section Adding pools for multiple website owners below for details on how to do this).

FastCGI requests between NGINX and php-fpm can be passed back and forth either with the help of a Unix socket or via TCP/IP; sockets are slightly faster but a lot less scaleable than TCP/IP connections. If you want to use a Unix socket, you’ll have to uncomment the lines

;listen.owner = nobody ;listen.group = nobody

(otherwise, leave them unchanged), and also adjust the corresponding setting in your site’s config file (otherwise, don’t touch the defaults).

Adding pools for multiple website owners

If you want your PHP application(s) to be able to write to a website’s document directory, PHP must spawn a pool for this application with the permissions of the owner of that document directory (a web server document directory and its contents must not be writable by the group!).

For php-fpm to initialize an additional pool, create a copy of the www.conf file:

cp www.conf www.website1.tld.conf

rename the pool it contains:

[www] [websiteowner1]

If you are using TCP sockets, assign the new pool a higher port number for the TCP socket:

listen = 127.0.0.1:9009

Change the user and group as follows:

user = websiteowner1 group = nginx

In the NGINX configuration file of the same website, remember to adjust the port number to the exact same port its pool will be using.

After that, restart both services (php-fpm and nginx).

Correct the SELinux labels on the configuration file of the newly created pool:

chcon -R -v system_u:object_r:etc_t:s0 www.websitename1.com.conf restorecon www.websitename1.com.conf

If you encounter any other problems, they are most likely due to a misconfiguration of SELinux (see this post on troubleshooting SELinux for TCP sockets).

Set up MariaDB (a replacement for MySQL)

MariaDB 5.x is drop-in compatible with MySQL. Having said that, you may want to remove MySQL if it is installed on your system, and use the 10.x branch of MariaDB (or above).

Add the official repository

Create a new text file containing the configuration of the official MariDB repository for your system, for example for CentOS 7:

[mariadb] name = MariaDB baseurl = http://yum.mariadb.org/10.1/centos7-amd64 gpgkey=https://yum.mariadb.org/RPM-GPG-KEY-MariaDB gpgcheck=1

and save it in this directory:

/etc/yum.repos.d/

Install MariaDB

Install MariaDB using:

yum install MariaDB-server MariaDB-client

On Fedora:

dnf install mariadb mariadb-server

Unless you already have the MariaDB GPG Signing key on your system, YUM will prompt you to install it after downloading the packages and before installing them. Manual installation of the GPG key using the rpm takes this oneliner:

sudo rpm --import https://yum.mariadb.org/RPM-GPG-KEY-MariaDB

Start MariaDB

Start MariaDB (once):

systemctl start mariadb

Enable automatic launch of MariaDB on system startup:

systemctl enable mariadb

Verify if MariaDB is running:

systemctl status mariadb

Secure your installation immediately

Securing your installation of MariaDB works just the same as for MySQL and begins with the command:

mysql_secure_installation

Follow the onscreen instructions.

If you are seeing this error:

YourPHP installation appears to be missing the MySQL extension which is required by WordPress.

you need to install the extension:

dnf install php-mysql

Set up WordPress (or another CMS of your choice)

Download your CMS with wget to a temp directory (grab the download link from the download page of your CMS). Unzip the archive and copy its contents into the web server document directory for your intended website.

In order for the CMS to access your database, you need an unprivileged database user (never the database root!) and the database. The CMS system has a way to store this information for future access. Follow the setup instructions. For WordPress, the installation is described below but the principle is always more or less the same.

Create an unprivileged database user and a database

To create a database user and the database in MySQL or MariaDB, you can use the mysql client in the command line. Connect using:

mysql -u root -p

Enter the root password. Once you are signed in and greeted with the MySQL prompt, create a new user and the database:

CREATE USER 'dbuser'@'localhost'; select * from mysql.user; create database db_name; GRANT ALL PRIVILEGES ON db_name.* TO dbuser@localhost IDENTIFIED BY 'mysqldbuserpassword';

As soon as you create a new database, you have to IMMEDIATELY grant all privileges on it to the mysql root:

GRANT ALL PRIVILEGES ON *.* TO root@'::1' IDENTIFIED BY 'rootsownpassword' WITH GRANT OPTION; SET PASSWORD FOR 'root'@'::1' = PASSWORD('rootsownpassword');

To verify the changes, use these commands:

SHOW GRANTS FOR 'root'@'localhost'; SHOW GRANTS FOR 'Info_wrdp1'@'localhost';

Now you can flush privileges and exit:

flush privileges; exit;

Enter database access credentials of the unprivileged user into the WP configuratIon

If you are using WordPress, copy the file wp-config-sample.php to its intended location:

cp wp-config-sample.php wp-config.php

Next, open it in a text editor of your choice.

Find the lines that define the four parameters and make adjustments so that the parameters’s values match the settings in mysql:

/** The name of the database for WordPress */ define('DB_NAME', 'db_name'); /** MySQL database username */ define('DB_USER', 'dbuser'); /** MySQL database password */ define('DB_PASSWORD', 'mysqldbuserpassword'); /** MySQL hostname */ define('DB_HOST', 'localhost');

Also, make sure you follow the instructions detailed in the section Authentication Unique Keys and Salts in wp-config.php.

Save the file.

Access your WordPress installation in a web browser to create an admin account

Next, navigate to your WordPress installation in a web browser of your choice in order to set up the access of the first WordPress administrator to the administrative backend of your CMS. Do NOT enter here any user credentials that you have ever previously used: this is NOT a Unix/Linux system user and NOT a mysql user. This is a new administrator of the CMS that will only ever access it in the web browser.

If you decide to use a different WordPress Database Table prefix than the default, make sure that you replace the default (wp_) with the new value in wp-config.php in a line that looks like this:

$table_prefix = 'wp_';

Allow write operations for updates and plug-in installs: running WordPress without a need for (S)FTP access

In the default configuration, WordPress is unable to upload updates or anything else for that matter without FTP/FTPS credentials. Since FTP can present a security vulnerability, you may want to opt out of this entirely and either install a plug-in for key pair based authentication or allow for direct uploads via PHP.

In order to bypass the default request for FTP(S) credentials, open wp-config.php and enter this line:

define('FS_METHOD','direct');

right before the editable section ends with this line:

/* That's all, stop editing! Happy blogging. */

Whether this setting immediately reflects in the behavior of WordPress depends on whether your file system access privileges and the SELinux security contexts or relevant files and directories are set correctly. This is the next thing you should tackle (see below).

Setting SELinux security context for web server document directories of NGINX

Even after you have adjusted the ownership of files and directories within the web server root and set correct access permissions on the entire web server directory tree and its contents, NGINX may still refusing to serve files with the 403 excuse (access denied).

Or, in a more subtle but no less unnerving scenario, your web application may fail to perform activities that depend on its ability to create or write to directories, such as installing or updating plug-ins by WordPress.

If either is the case, you should look into adjusting alternative access methods such as SELinux.

(Nothing else, not even adding

define('FS_METHOD','direct');

to wp-config.php will fix the problem unless SELinux context labels are correctly set.) Still need convincing? A look at the audit log can help you find the culprit:

tail /var/log/audit/audit.log

Simply deactivating SELinux is not the wisest course of action. SELinux is not the evil it’s made out to be. Don’t deactivate it, tame it.

In order to understand what’s at stake here it helps being able to view the SELinux labels on files and directories.

List files and directories with their respective security context

For a full directory listing that includes SELinux security context, use the -Z option with ls, for example:

ls -laZ /var/www/www.yourwebstite.tld/

where www.yourwebstite.tld is the web server root directory.

View the current SELinux security context for the NGINX master and worker processes while running

In order to view the SELinux context for the master and worker processes of NGINX, use the following command:

ps -efZ | grep 'nginx'

The command reveals the user NGINX is running as. It selects ‘nginx’ from all running processes (option -e is interchangeable with -A for ‘all’) and requests full-format listing (-f ; use option -F for extra full options).

To verify which adjustments of the SELinux security context are required to conform to the defaults, run restorecon without writing any changes (-n):

restorecon -RFv -n /var/www/

The verbose output of this command should give you an idea as of what’s going to happen when you omit the -n flag.

In order to apply SELinux defaults that are inherited from the parent directory /var/www, use the above command without the -n option:

restorecon -RFv /var/www/

The defaults will enforce SELinux security to the point of making updates, uploads, and plug-in installations all but impossible. No amount of modifying the ownerships and privileges the old-fashioned way (chmod/chown/umask etc.) will get your CMS system to work properly, until you apply some corrections withing SELinux security context of the relevant directory trees.

For WordPress, adjust the SELinux label to establish proper security context on the website directory:

chcon system_u:object_r:httpd_sys_content_t:s0 /var/www/www.website1.tld

Use this command on the WordPress installation directory to allow write access to its contents (-R):

chcon -R system_u:object_r:httpd_sys_rw_content_t:s0 www.website1.tld/*

You need to apply the above label to the entire directory tree of your web application (WordPress in the above example) in order to make it writeable.

After executing the above command, verify that your web application is working correctly. In order to make this change permanent, use:

semanage fcontext -a -t httpd_sys_rw_content_t "/var/www/www.website1root.tld(/.*)?"

The above command will add a rule to apply the specified context change permanently so that the command restorecon (which you may issue on the above directory in the future):

restorecon -Rv /var/www/www.website1root.tld/pathto/wp-content/

can use the new policy just the way you intended. You can view the rule (but not edit it directly) using this command:

cat /etc/selinux/targeted/contexts/files/file_contexts.local

Should you ever need to remove a rule, use the -d option (-d for delete):

semanage fcontext -d "/var/www/www.website1.tld/pathtoremoverulefor(/.*)?"

Your rule will look something like this (don’t edit directly!):

/var/www/www.website1.tld(/.*)? system_u:object_r:httpd_sys_rw_content_t:s0

IMPORTANT: Always use absolute paths when changing SELinux contexts!

Once you rein in SELinux, your web application should be up and running, and able to perform auto-updates via PHP.

For a more detailed explanation of Unix/GNU Linux permissions and alternative access methods, see this post.

If SELinux is giving you trouble, see these troubleshooting posts:

For more practical tips and configuration howtos, subscribe to our newsletter, it’s free!

[wysija_form id=”1″]