Installing Nginx with SSL for Drupal with civiCRM on Ubuntu 16.04 using Amazon EC2 instances

In this guide I will be laying out the steps I took to install Nginx with SSL, Drupal with civiCRM and Drush on Ubuntu 16.04 using Amazon EC2 instances. This guide is tailored to the specific needs I have for my setup. It would be best to think of this not as a “this is how it’s done”, but more of a “this is how I did it”. Also, I will be setting up Let’s Encrypt with my domain name. To follow along you will need a domain name of your own to use SSL certificates from Let’s Encrypt.

Guide outline

In general the setup is fairly simple. I am using 3 AWS EC2 instances: 1 for the drupal web server, and 2 for mysql databases (drupal and civiCRM). I won’t go too in depth into how the EC2 system works. Below are the specs for the entire setup:

Amazon EC2 instance setup
VPC with a single public subnet
 - VPC: 10.0.0.0/16
 - subnet: 10.0.1.0/24
2 Security Groups
 - web server: ports for SSH, HTTP, HTTPS (22, 80, 443)
 - mysql servers: ports for SSH, mysql (22, 3306 open only to members of the web server sec group)
3 t2.micro instances
 - web:
    - hvm:ebs-ssd
    - OS: Ubuntu 16.04 LTS
    - region: us-east-2
    - Community AMI: 04c305e118636bc7d
    - auto-assign public IP enabled
    - storage: 8gb, general purpose ssd
    - sec group: web server
 - 2 x db (drupal & civiCRM):
    - hvm:ebs-ssd
    - OS: Ubuntu 16.04 LTS
    - region: us-east-2
    - Community AMI: 04c305e118636bc7d
    - auto-assign public IP enabled
    - storage: 8gb, general purpose ssd
    - sec group: mysql server

If you are planning to follow along with the SSL encryption setup, now is a good time to grab the public IP address of the web instance. Using your domain manager create a DNS record that points to the web server IP (ex: www.example.com -> x.x.x.x).

Setting up the web server (nginx)

log into the web instance using the private key for EC2 (I made a key specifically for this project). For the Ubuntu image I chose the user ID is: ubuntu. Amazon EC2 instances use encryption keys for SSH, so there is no need to input the password for the ubuntu user. Root level commands using sudo will also not require a password to be entered.

ssh -i .ssh/aws-key.pem ubuntu@x.x.x.x

Next, it’s important to make sure the system is up-to-date with the latest packages. On Ubuntu this is done using the apt package manager. Run the following commands in the terminal once you are connected to the EC2 instance.

sudo apt update
sudo apt upgrade
sudo apt dist-upgrade
sudo reboot (if necessary)

I would recommend running these updates for each of the 3 EC2 instances at this point. If you skip this for the other 2 instances you can always run these updates later when setting up the mysql servers.
Install the nginx web server and the drupal PHP dependencies. Note: since this install is happening on Ubuntu 16.04 (which is the older LTS release) the version of PHP is 7.0, which is not the most up-to-date. This command will need to be updated if you choose to use a newer version of Ubuntu.

sudo apt install nginx php7.0-cli php7.0-fpm php7.0-mysql php7.0-json php7.0-opcache php7.0-mbstring php7.0-xml php7.0-gd php7.0-curl zip unzip php7.0-zip

The next step is to configure nginx for basic HTTP access. Later in the guide I’ll cover the steps to enable SSL and HTTPS using Let’s Encrypt.
Add the following 2 files to the snippets directory in /etc/nginx/snippets. Although at this stage these files do very little, they will be needed to configure for SSL.

letsencrypt.conf

location ^~ /.well-known/acme-challenge/ {
  allow all;
  root /var/lib/letsencrypt/;
  default_type "text/plain";
  try_files $uri =404;
}

ssl.conf

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

ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;

ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 30s;

add_header Strict-Transport-Security "max-age=15768000; includeSubdomains; preload";
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;

Nginx will need a server block in it’s configuration for the drupal web server. For now, this file will contain only the HTTP setup. This will allow Let’s Encrypt to run a host check that will create an SSL certificate. Add the following code to /etc/nginx/sites-available/example.com (replace example.com with your domain).

server {
    listen 80;
    server_name example.com;
    root /var/www/drupal;

    include snippets/letsencrypt.conf;
}

Next, enable the server block by creating a link in the sites-enabled directory.

sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/

That is all for now on the nginx web server. Next I’ll cover setting up the mysql servers for drupal and civiCRM.

Setting up the mysql servers (for drupal and civiCRM)

Each of the 2 mysql database servers are configured very similarly. In general, follow the steps below and any differences between the 2 servers will be pointed at where needed.
Install the mysql server software using apt:

sudo apt install mysql-server

When prompted during the install create a “root” user with a secure password. We will use this root account to setup the databases and their users, so don’t forget it! At this point, if you haven’t done so, I recommend performing the system apt update.
Each database is configured differently for users and permissions. Below are the commands to run on each server, specific to it’s role.
For Drupal:
Open a mysql session with the following command

mysql -u root -p

At the mysql prompt enter the following commands. This will create a database (drupal), a user (drupaluser) and permissions for that user:

CREATE DATABASE drupal CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
GRANT ALL ON drupal.* TO 'drupaluser'@'%' IDENTIFIED BY 'VeryGoodPassword';
FLUSH PRIVILEGES;

We also need to tell mysql to listen for incoming connections outside of localhost. This is done by editing the /etc/mysql/mysql.conf.d/mysqld.cnf file and changing the bind address to:

bind-address = 0.0.0.0

The security issue of opening this server to the world is mitigated by only allowing incoming connections from servers in the web security group (an AWS EC2 feature explained previously in this guide).
Perform the same steps for the civiCRM database, however, there will be one extra step of adding SELECT permissions for the drupaluser on this database as well (for drupal Views integration with civiCRM).
For civiCRM:

CREATE DATABASE civicrm CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
GRANT ALL ON civicrm.* TO 'civicrmuser'@'%' IDENTIFIED BY 'VeryGoodPassword';
GRANT SELECT ON civicrm.* TO 'drupaluser'@'%' IDENTIFIED BY 'VeryGoodPassword';
FLUSH PRIVILEGES;

Once complete it would be a good idea to restart the mysql server to check if the changes have been done correctly.

Install drupal using composer

I used composer to install drupal because it also handles installing any dependencies. Below are the commands needed to install composer and the various dependencies we’ll need. Running this command as sudo produces a warning, which I chose to ignore, since I am working as a non www-data user. Permissions will be fixed later on in the guide.

Install composer globally:

sudo curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/local/bin --filename=composer

Install Drupal 7 and dependencies:

sudo composer create-project drupal-composer/drupal-project:7.x-dev /var/www/drupal --stability dev --no-interaction
sudo composer require 'drupal/views:^3.20'
sudo composer require 'drupal/backup_migrate:^3.5'

Drupal should now be installed in /var/www/drupal. Verify that a directory structure like this exists: /var/www/drupal/web/sites/default/files. If the files directory does not exist, create it with the following command:

sudo mkdir /var/www/drupal/web/sites/default/files

2 other directories are needed as well for this setup:

sudo mkdir /var/www/drupal/web/sites/default/civicrm_extensions
sudo mkdir /var/www/drupal/web/sites/default/files/private

Next, some directory ownership and permissions need to be set.

sudo chown -R www-data: /var/www/drupal
sudo chmod -R 755 /var/www/drupal
sudo chmod -R g+w /var/www/drupal/web/sites/default/files
chmod -R 2775 /var/www/drupal/web/sites/default/files
sudo chmod 775 civicrm_extensions

This ends the essential install of drupal, however, drupal itself will require further configuration later. Next I will cover the steps needed to convert the site to HTTPS with SSL from Let’s Encrypt. You can skip this stage if you don’t plan to enable this security.

Configuring SSL for HTTPS

I chose to use certbot to assist in the creation of the SSL certificates to enable HTTPS on this site. To install certbot we need to add a PPA for the certbot app and install it:

sudo add-apt-repository ppa:certbot/certbot
sudo apt update
sudo apt install python-certbot-nginx

With certbot installed I followed this guide on how to create a good SSL setup:

sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
sudo mkdir -p /var/lib/letsencrypt/.well-known
sudo chgrp www-data /var/lib/letsencrypt
sudo chmod g+s /var/lib/letsencrypt
sudo certbot certonly --agree-tos --email youremail@example.com --webroot -w /var/lib/letsencrypt/ -d example.com -d example.com

That takes care of creating strong encryption for the SSL certificates, as well as creating a directory structure that we will add to the nginx config.
Next we can reconfigure our nginx server block to redirect HTTP to HTTPS. This is also the point where all the directory security and nginx drupal setup will go. This file is the same as we configured in a previous step (/etc/nginx/sites-available/example.com). This file is based on the example found here.

example.com

# Redirect HTTP -> HTTPS
server {
    listen 80;
    server_name example.com;

    include snippets/letsencrypt.conf;
    return 301 https://example.com$request_uri;
}

server {
    listen 443 ssl http2;
    server_name example.com;

    root /var/www/drupal/web;

    # SSL parameters
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    include snippets/ssl.conf;
    include snippets/letsencrypt.conf;

    # log files
    access_log /var/log/nginx/example.com.access.log;
    error_log /var/log/nginx/example.com.error.log;

    location = /favicon.ico {
        log_not_found off;
        access_log off;
    }

    location = /robots.txt {
        allow all;
        log_not_found off;
        access_log off;
    }

    location ~ \..*/.*\.php$ {
        return 403;
    }

    # prevents access to private directory
    location ~ ^/sites/.*/private/ {
        return 403;
    }

    # Block access to scripts in site files directory
    location ~ ^/sites/[^/]+/files/.*\.php$ {
        deny all;
    }

    # Block access to "hidden" files and directories whose names begin with a
    # period. This includes directories used by version control systems such
    # as Subversion or Git to store control files.
    location ~ (^|/)\. {
        return 403;
    }

    # Allow "Well-Known URIs" as per RFC 5785
    location ~* ^/.well-known/ {
        allow all;
    }

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

    location @rewrite {
        rewrite ^/(.*)$ /index.php?q=$1;
    }

    # Don't allow direct access to PHP files in the vendor directory.
    location ~ /vendor/.*\.php$ {
        deny all;
        return 404;
    }

    location ~ '\.php$|^/update.php' {
        fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
        include fastcgi_params;
        # Block httpoxy attacks. See https://httpoxy.org/.
        fastcgi_param HTTP_PROXY "";
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
        fastcgi_param PATH_INFO $fastcgi_path_info;
        fastcgi_param QUERY_STRING $query_string;
        fastcgi_intercept_errors on;
        fastcgi_pass unix:/run/php/php7.0-fpm.sock;
    }

    # Fighting with Styles? This little gem is amazing.
    # location ~ ^/sites/.*/files/imagecache/ { # For Drupal <= 6 location ~ ^/sites/.*/files/styles/ { # For Drupal >= 7
        try_files $uri @rewrite;
    }

    # Handle private files through Drupal. Private file's path can come
    # with a language prefix.
    location ~ ^(/[a-z\-]+)?/system/files/ { # For Drupal >= 7
        try_files $uri /index.php?$query_string;
    }

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
        try_files $uri @rewrite;
        expires max;
        log_not_found off;
    }

    # for use with drush
    location = /backup {
        deny all;
        }

    # Very rarely should these ever be accessed outside of your lan
    location ~* \.(txt|log)$ {
        allow 10.0.0.0/16;
        deny all;
    }

    # configure to secure civiCRM directories
    location ~* ^/sites/(.*)/files/civicrm/(ConfigAndLog|templates_c|upload|custom) {
        deny all;
    }

}

Note: a very critical line will need to be changed if you use a version of PHP other than 7.0.

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

Change the socket file to match the version of PHP your server is using.
To ensure that the certificates get renewed on time I setup a crontab job to run the renew function of the certbot. The following line added to /etc/crontab will run the renewal check every week:

@weekly root certbot -q renew --renew-hook "systemctl reload nginx"

At this point the drupal site should be accessible through HTTPS. In the next step I’ll explain how to run the drupal install from the browser and connect to the database server instance.

Run the Drupal Installer

From a browser navigate to the drupal server (ex: https://www.example.com). This will automatically run the installer. This process is well covered in other guides so I’ll simply list the settings I used to configure my server.

 - standard install
 - language: english
 - database type: mysql
 - database name: drupal
 - database username: drupaluser
 - database password: VeryGoodPassword
 - advanced options:
    - database host: x.x.x.x (LAN IP address of the drupal mysql server instance)
    - database port: 3306
 - site name: example.com
 - site e-mail: admin@example.com
 - username: admin
 - password: VeryGoodPassword

Continue to fill in the forms based on your desired setup and click save and continue. The drupal site should now be configured and running. The final step will be to install and integrate civiCRM.

Install and integrate civiCRM

To install civiCRM, nvaigate to the modules section of your drupal site. Once on the modules page click on Install new module. In the form that opens add the following URL to the Install from URL text field:

https://sourceforge.net/projects/civicrm/files/civicrm-stable/5.6.1/civicrm-5.6.1-drupal.tar.gz

Note: this is the version I chose to match Drupal 7.x, I recommend using the latest version of civiCRM if possible. Next I chose to also install the command line app cv. I used cv to setup cron for civiCRM. To install cv:

sudo curl -LsS https://download.civicrm.org/cv/cv.phar -o /usr/local/bin/cv
sudo chmod +x /usr/local/bin/cv

To run the installer for civiCRM navigate to the following URL on your site https://example.com/sites/all/modules/civicrm/install/index.php. This will run the civiCRM installer. This installer will ask for info similar to drupal. Remember that we created a separate database for civiCRM, therefor that set of info is needed (the IP, username and password for the civiCRM mysql database). To finish the setup for using cron run the following command:

sudo cv api job.execute --user=admin --cwd=/var/www/drupal/web/sites/default

To allow for the extensions directory to be set using the settings file add the following code to /var/www/drupal/web/sites/default/civicrm.settings.php:

global $civicrm_setting;
$civicrm_setting['Directory Preferences']['extensionsDir'] = '/var/www/drupal/web/sites/default/civicrm_extensions';

The final installation step involves setting up drupal to access the civiCRM database for creating views with data from civiCRM. Following this guide I added the database connection string to my drupal settings file. To find the correct info to add to the settings I first had to enable the views module. Do so by going to Modules -> Views and enabling the views and views UI module.

Now when you navigate to http://example.com/civicrm/admin/setting/uf?reset=1 you should see the database connection string.

Copy the entire connection string and add it to the settings.php file. I added mine just below the connection string for the drupal database. It was important to give SELECT permissions to the drupaluser on the civiCRM database to allow drupal to read from this db. This marks the end of the drupal with civiCRM install. At this point, I checked all the directory permissions and cleaned up after the install. I recommend running on all 3 servers:

sudo apt autoclean
sudo apt autoremove --purge

It’s also a good idea to check the SSL setup for the site. This can be done through the site www.ssllabs.com. The following step will cover the optional (but highly recommended) site backup process.

Setting up site backups

The easiest way I’ve found so far to handle drupal site backups is with the Backup and Migrate module. If you followed along to this guide carefully you will have noticed we installed this module using composer. If you didn’t do so already, run the following command to install the Backup and Migrate module:

sudo composer require 'drupal/backup_migrate:^3.5'

Next, enable the module in the drupal modules page.

I won’t go into immense detail on how this module works. In general: I set it up to create a backup of the entire site (code, files & db). I run this backup daily, weekly and monthly. These backups are saved to the private backups directory on the web server. From here I recommend using rsync to copy those backup files to an external server for safe keeping.¬†At the very least, you can just download the backups manually (see image below). More info on the backup module can be found here.


This concludes the installation of Drupal with civiCRM on Ubuntu 16.04 LTS using AWS EC2 instances. As I’ve said already this isn’t the absolute definitive guide to installing drupal. This is simply how I chose to do the installation, based on my needs. There are many great guides out there. I recommend having a look at the following references for further assistance.

References:
https://docs.civicrm.org/sysadmin/en/latest/integration/drupal/views/
https://docs.civicrm.org/sysadmin/en/latest/customize/settings/
https://www.drupal.org/docs/7/install/before-installation
https://linuxize.com/post/how-to-install-drupal-on-ubuntu-18-04/
https://linuxize.com/post/secure-nginx-with-let-s-encrypt-on-ubuntu-18-04/
https://www.nginx.com/resources/wiki/start/topics/recipes/drupal/
https://wiki.civicrm.org/confluence/display/CRMDOC41/Drupal+Installation+Guide+for+CiviCRM+4.1+-+Drupal+7