Ubuntu 18.04, NGINX 1.14, MySQL 8.0.12 GA, PHP 7.2.8

Fully current, highly secure, Ubuntu LEMP Stack Abridged Setup Instructions

Jeff Hawkins, 2018/08/03

A minimal set of steps to produce a secure LEMP server, using the latest stable, long term support versions.

Preparation:

  1. Make a plain text copy of this document in your editor of choice. (Select all, copy and paste.)
  2. Replace all occurrences of “example.com” with your domain name.

Conventions:

Monotype font grey:         Commands to be run. Each numbered item is intended to be run as a single (sometimes long) line.

Monotype font pink:         This is text to be copied according to directions preceding it.

Monotype font green:         This text needs to be modified as described before being used.

Monotype font blue:         This is existing text copied here to give a sense for where to make changes.

Requirements:

These instructions require you to have a registered domain name (FQDN).

This document requires a default deployment of Ubuntu 18.04 LTS Server, ready to access via the terminal. You must have sudo privileges. All testing was done on a virtual/cloud server through Amazon Web Services (AWS) EC2. The EC2 instance was built from the official Canonical 18.04 Server AMI for hvm:ebs-ssd, as found at https://cloud-images.ubuntu.com/locator/ec2/. And the result is hosting this site.

If you don't yet have a server, see AWS EC2 Provisioning (A.K.A. Launching a Virtual Server). It is a walkthrough, compatible with Amazon's free tier, of setting up a server like the one used here.

Begin:

Add repos for NGINX, PHP, Certbot, and MySQL:

  1. gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv ABF5BD827BD9BF62 && gpg --export --armor ABF5BD827BD9BF62 | sudo apt-key add -

  1. sudo add-apt-repository -y 'deb http://nginx.org/packages/ubuntu/ bionic nginx';sudo add-apt-repository -y ppa:ondrej/php;sudo add-apt-repository -y ppa:certbot/certbot;cd /tmp/;sudo wget https://dev.mysql.com/get/mysql-apt-config_0.8.10-1_all.deb

  1. sudo dpkg -i mysql-apt-config_0.8.10-1_all.deb

Arrow-key down to select “OK” and enter.

Update manifests and upgrade existing software:

  1. sudo apt update && sudo apt -y full-upgrade

Install NGINX, PHP, and common tools:

  1. sudo apt install -y nginx php7.2 php7.2-fpm php7.2-mbstring php7.2-xml php7.2-curl php7.2-gd php7.2-mysql python-certbot-nginx pwgen zip unzip

Install MySQL:

Create a strong password using more than 8 characters, including at least one of each of uppercase, lowercase, number, and special characters. You will need to enter it when installing MySQL.

NOTE: Through at least PHP 7.2.8, PHP is incompatible with MySQL's new caching_sha2_password. You must choose the "Use Legacy Authentication" when asked during MySQL install.

  1. sudo apt install -y mysql-server

Use the MySQL secure installation script for some basic security improvements:

  1. sudo mysql_secure_installation

Do MySQL Database Setup:

Set up at least one user and one schema for testing. You can also create and apply permissions to other user accounts now if you know what will be needed.

  1. mysql -u root -p

You should now see "mysql>" at the command prompt. You are now at the MySQL command line. You may run each of these lines individually, or it will also work if you select all lines here (9 through 16) and paste them at once.  

  1. CREATE SCHEMA testDB; USE testDB;
  2. CREATE TABLE testTable (testString varchar(256));
  3. INSERT INTO testTable VALUES ("Good. This text came from the database.");
  4. CREATE USER 'testUser'@'localhost' IDENTIFIED BY 'ThisIs0nlyTemp!';    
  5. GRANT ALL PRIVILEGES ON testDB.* TO 'testUser'@'localhost';
  6. CREATE USER 'testUser'@'%' IDENTIFIED BY 'ThisIs0nlyTemp!';    
  7. GRANT ALL PRIVILEGES ON testDB.* TO 'testUser'@'%';
  8. FLUSH PRIVILEGES;

To confirm your users exist:

  1. SELECT User, Host FROM mysql.user;
  2. quit

Make website directories and set permissions…

  1. sudo mkdir -p /var/www/example.com/html;sudo chown -R $USER:www-data /var/www;sudo chmod 755 -R /var/www;sudo find /var/www -type f -exec sudo chmod 644 {} \;sudo chown -R www-data:www-data /var/log/nginx;sudo chmod -R 755 /var/log/nginx;

  1. sudo mkdir /etc/nginx/sites-available;sudo mkdir /etc/nginx/sites-enabled;sudo mkdir /etc/nginx/snippets;sudo mkdir /etc/nginx/ssl

Add abstraction for sites...

  1. sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/sites-available/example.com;sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/example.com

Change the highlighted lines below in nginx.conf (or replace the entire text with this):

  1. sudo nano /etc/nginx/nginx.conf

user  www-data;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
   worker_connections  1024;
}

http {
   include       /etc/nginx/mime.types;
   default_type  application/octet-stream;

   log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for"';

   access_log  /var/log/nginx/access.log  main;

   sendfile        on;
   #tcp_nopush     on;

   keepalive_timeout  65;

   #gzip  on;

    include /etc/nginx/snippets/defaults.conf;
    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;
}

Set unknown URLs (i.e. IP Address based URLs) to redirect to domain site. Copy red text into the empty file and save:

  1. sudo nano /etc/nginx/snippets/defaults.conf

server {
   listen 80 default_server;
   listen [::]:80 default_server;
   server_name _;
   return 301 https://
example.com$request_uri;
}

server {

    listen 443 default_server;

    listen [::]:443 default_server;

    server_name _;

    ssl_certificate     /etc/nginx/ssl/nginx-selfsigned.crt;

    ssl_certificate_key /etc/nginx/ssl/nginx-selfsigned.key;

    return 301 https://example.com$request_uri;

}

Replace content of site config with http/2 SSL configured version:

  1. sudo nano /etc/nginx/sites-enabled/example.com

server {

    listen 443 ssl http2;

    listen [::]:443 ssl http2;

    root /var/www/example.com/html;

    index index.html index.php;

    server_name example.com www.example.com;

    location / {

                try_files $uri $uri/ =404;

    }

    error_page   500 502 503 504  /50x.html;

    location = /50x.html { }

    location ~ \.php {

        fastcgi_index   index.php;

        fastcgi_pass    unix:/var/run/php/php7.2-fpm.sock;

        include         /etc/nginx/fastcgi_params;

        fastcgi_param   SCRIPT_FILENAME    $request_filename;

        fastcgi_param   PATH_INFO          $fastcgi_path_info;

    }

    ## SSL Configuration

    ssl_stapling_verify on;

    ssl_protocols       TLSv1 TLSv1.1 TLSv1.2;

    ssl_prefer_server_ciphers on;

    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:ECDHE-RSA-AES128-GCM-SHA256:AES256+EECDH:DHE-RSA-AES128-GCM-SHA256:AES256+EDH:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";

    ssl_session_cache   shared:SSL:10m;

    ssl_session_timeout 10m;

    ssl_ecdh_curve      secp384r1;

    ssl_dhparam         /etc/nginx/ssl/dhparam.pem;

    ssl_stapling        on;

    gzip off;

    ## Headers

    add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";

    add_header X-Frame-Options DENY;

    add_header X-Content-Type-Options nosniff;

}

Generate better Diffie–Hellman key for improved security.

  1. sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

Add self-signed certificate for unexpected URLs. Nano into the OpenSSL config:

  1. sudo nano /etc/ssl/openssl.cnf

Uncomment the line beginning with  “req_extensions = v3_req”. Then, add the following to the bottom of the [ v3_req ] section:

subjectAltName = @alt_names

[alt_names]

IP.1 = YourIPAddress

Create the self-signed certificate:

  1. sudo openssl req -x509 -nodes -days 10000 -newkey rsa:2048 -keyout /etc/nginx/ssl/nginx-selfsigned.key -out /etc/nginx/ssl/nginx-selfsigned.crt -extensions v3_req

NGINX config must be free of syntax errors for certbot to succeed. So test and correct any issues before proceeding.

  1. sudo nginx -t

Add externally validated certificate for domain and subdomain named websites.

  1. sudo certbot --nginx

Create a PHP/HTML file to test that NGINX is configured to serve your site. Please forgive the minified format. This will not work until after the next (reboot) step:

  1. sudo nano /var/www/example.com/html/index.php

<?php function getPDOResults($pdo, $sql, $sqlParamArray = null, $arrayType = PDO::FETCH_BOTH) {  $statement = $pdo->prepare($sql); $statement->execute($sqlParamArray); do {$result[] = $statement->fetchAll($arrayType);} while ($statement->nextRowset()); return $result; } $theOptions = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false,]; $pdo = new PDO("mysql:host=example.com;dbname=testDB;port=3306;charset=utf8", 'testUser', 'ThisIs0nlyTemp!', $theOptions); $sql = 'SELECT * FROM testTable;'; $testOutput = getPDOResults($pdo, $sql)[0][0][0]; ?> <!DOCTYPE html><html lang=en><head><meta charset=UTF-8><title>PHP Test Page</title></head><body><p>PHP Status: <?= 'Good. PHP is operating. ' ?> <br />PHP-MySQL Connection Status: <?= getPDOResults($pdo, $sql)[0][0][0] ?></p><hr /><p>This site is obviously under construction. Consider playing a game of <a href=https://codehawkins.com/hangman/hangman.html style=font-size:large;font-weight:bold>HANGMAN</a> while the dust settles.</p></body></html>

Finally, reboot. This will set NGINX listening on the configured ports, and will confirm that all services are properly set to start on reboot.

  1. sudo reboot

Server Deployment Test:

Basic setup is now complete.

The server is now ready for a real website to be copied to /var/www/example.com/html. As configured, the server expects the entry point to be index.php if index.php exists. And index.html if it doesn't. Configuring for other files or directories is fairly easy, but beyond the scope of this document.

Good Luck!