June 3, 2020

Building a WordPress Server on an AWS EC2 Instance

EC2 Instance in AWS

Starting with an newly launched Linux EC2 instance, log in

Install Apache

Before installing anything, we want to perform the updates. You will need sudo for these actions, rather than typing sudo in front of every command, I use sudo -i

sudo -i
apt-get update
apt-get upgrade

With the updates out of the way, we now install apache2

apt install apache2

Check Ubuntu’s Uncomplicated Firewall FW (aka UFW) on the host to verify it’s not going to be blocking the HTTP (TCP port 80) and HTTPS (TCP port 443) traffic

ufw app list

The result of that command reveals the Available Applications

Available applications:
  Apache
  Apache Full
  Apache Secure
  OpenSSH

To explore the Available Applications, type ufw app info then the name of the application above. Start with the first one, Apache

ufw app info Apache

The result is…

Profile: Apache
Title: Web Server
Description: Apache v2 is the next generation of the omnipresent Apache web
server.

Port:
  80/tcp

Notice that port TCP 80 is open. Now take a look at Apache Full.

ufw app info "Apache Full"

The result is…

Profile: Apache Full
Title: Web Server (HTTP,HTTPS)
Description: Apache v2 is the next generation of the omnipresent Apache web
server.

Ports:
  80,443/tcp

Notice that both TCP 80 and 443 are open. Lets see what’s in Apache Secure.

ufw app info "Apache Secure"

The result is…

Profile: Apache Secure
Title: Web Server (HTTPS)
Description: Apache v2 is the next generation of the omnipresent Apache web
server.

Port:
  443/tcp

Notice that TCP 443 is open. Clearly at the point, we’re good on the ports being open. Now let’s verify from the Internet. We need to know the public IP address of the server. You can do this from the AWS Console or from the command line that you are currently using by typing, curl ident.me

curl ident.me

The result is…

root@ip-172-16-1-1:~# curl ident.me
3.12.13.37root@ip-172-16-1-1:~# 

In the result, notice the second line contains the public IP of 3.12.13.37 followed by the prompt. So now it we go to http://3.12.13.37 we should see the apache default page.

At this point we can be confident that Apache is working.

Install MySQL

If you are still logged in as root, you can execute all the below commands as I have. If you get access denied, type sudo in front of the commands. Or you can do like I did and type sudo -i, and not have to type sudo for every command.

apt install mysql-server

You will be asked for a password. Make it more than 14 characters and use all of the available character sets (UPPER, lower, numb3r5, symbol$). Don’t make it a dictionary word with the predictable I-wanna-be-hacker substitution cipher. Everyone has been on to this since 2002 or so…

  • a = @ or 4
  • e = 3
  • i = ! or 1 or |
  • g = 9
  • o = 0 or ()
  • s = 5 or $
  • z = 2

Enter something like the password below where it doesn’t spell a dictionary word, and isn’t memorable. You will need to store this in LastPass or your password manager of choice.

mysql password = Ha(*U&^TR^JIL2-

Make your install of MySQL more secure

mysql_secure_installation

Answer y to the below question

Securing the MySQL server deployment.

Connecting to MySQL using a blank password.

VALIDATE PASSWORD PLUGIN can be used to test passwords and improve security. It checks the strength of password and allows the users to set only those passwords which are secure enough. Would you like to setup VALIDATE PASSWORD plugin?

Press y|Y for Yes, any other key for No: y

Answer y to the below question

VALIDATE PASSWORD PLUGIN can be used to test passwords
and improve security. It checks the strength of password
and allows the users to set only those passwords which are
secure enough. Would you like to setup VALIDATE PASSWORD plugin?

Press y|Y for Yes, any other key for No: y

We will choose the MEDIUM option here

There are three levels of password validation policy:

LOW Length >= 8
MEDIUM Length >= 8, numeric, mixed case, and special characters
STRONG Length >= 8, numeric, mixed case, special characters and dictionary file

Please enter 0 = LOW, 1 = MEDIUM and 2 = STRONG: 1
Please set the password for root here.

Set the password, by entering it twice.

New password:

Re-enter new password:

The result is…

Estimated strength of the password: 100
Do you wish to continue with the password provided?(Press y|Y for Yes, any other key for No) : y

You want the strength of the password to be 100. If it’s not, you can type n and retry another password. Otherwise type y to continue.

By default, a MySQL installation has an anonymous user,
allowing anyone to log into MySQL without having to have
a user account created for them. This is intended only for
testing, and to make the installation go a bit smoother.
You should remove them before moving into a production
environment.

Remove this anonymous user account, by typing y.

Remove anonymous users? (Press y|Y for Yes, any other key for No) : y

The result is…

Success.

Next we will set where you can connect and log into MySQL from. The only place that is required for this installation is localhost, since we are installing everything on a single server. However, more advanced installations will typically have the database separated from the web application. But until you need that, leave it at localhost only for security purposes.

Normally, root should only be allowed to connect from
'localhost'. This ensures that someone cannot guess at
the root password from the network.

Disallow root login remotely? (Press y|Y for Yes, any other key for No) : y

The result is…

Success.

Just like above, when we removed the anonymous user account, the question comes up for a test database. Remove this test database buy typing y.

By default, MySQL comes with a database named 'test' that
anyone can access. This is also intended only for testing,
and should be removed before moving into a production
environment.

Remove test database and access to it? (Press y|Y for Yes, any other key for No) : y

The result is…

Dropping test database…
Success.
Removing privileges on test database…
Success.

When asked to privilege tables, type y.

Reloading the privilege tables will ensure that all changes
made so far will take effect immediately.

Reload privilege tables now? (Press y|Y for Yes, any other key for No) : y

The results is…

Success.

Interesting note:

If at this point you stop, log out, restart machine, or otherwise become a regular privileged user (not sudo or root), you won’t be able to log into mysql with the correct password.

ec2-user@ip-172-16-1-1:~$ mysql -uroot
ERROR 1698 (28000): Access denied for user 'root'@'localhost'
ec2-user@ip-172-16-1-1:~$ sudo mysql -uroot -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 8

Create the WordPress Database

We need to create the database that WordPress will use, we’ll call it database4wp.

mysql> CREATE DATABASE database4wp DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
Query OK, 1 row affected (0.00 sec)

With the database4wp database created. We need to create the MySQL user account that WordPress will use to connect to the database. In this example, the user is stimpy42r. You can use anything, but make it not a dictionary word or easily guessable.

mysql> GRANT ALL ON database4wp.* TO 'stimpy42r'@'localhost' IDENTIFIED BY '78GBVwed-6rr95';
Query OK, 0 rows affected, 1 warning (0.00 sec

(optional) Notice the “0 rows affected” string in the results of the previous command. if you want to verify that the user was actually created, we need to go to the user table in the database called mysql, not our new database named database4wp.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| database4wp        |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

mysql> use mysql
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Now that you are in the mysql database, run a select statement to show the user accounts

mysql> select Host,User from user;
+-----------+------------------+
| Host      | User             |
+-----------+------------------+
| localhost | debian-sys-maint |
| localhost | stimp42r         |
| localhost | mysql.session    |
| localhost | mysql.sys        |
| localhost | root             |
+-----------+------------------+
5 rows in set (0.00 sec)

Now that we’ve verified the user account is there. We flush the privileges so that mysql knows to use them.

mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)

Install PHP

Now that the web server and database are operational, we can move on to PHP, which is what WordPress uses. It also uses MySQL and Apache, so we will add PHP libraries so it can access them.

apt install php libapache2-mod-php php-mysql

Once that completes after a few seconds, we need to tell Apache to prefer PHP files over html. Otherwise everyone who goes to your site will only see the Apache default page above. We do that by editing the /etc/apache2/mods-enabled/dir.conf file.

nano /etc/apache2/mods-enabled/dir.conf

When we open it, it will appear similar to this.

<IfModule mod_dir.c>
        DirectoryIndex index.html index.cgi index.pl index.php index.xhtml index.htm
</IfModule>

You want index.php first. We don’t need .cgi, .pl, or .xhtml so we can delete those. We’ll keep .html and .htm for now, however we might delete them later if all goes well. The resulting file should look like when we are done.

<IfModule mod_dir.c>
	DirectoryIndex index.php index.html index.htm
</IfModule>

At this point Apache doesn’t know of the changes, we need to restart Apache so it will pick up the new config changes.

systemctl restart apache2

Now that Apache it looking for index.php, let’s write some test php code in a file named index.php located in the /var/www/html folder

/var/www/html# nano index.php

Add paste this into the page and save it.

<!DOCTYPE html>
<html>
<body>

<h1>Test PHP page</h1>

<?php
echo "the php code has executed.";
?> 

</body>
</html>

Now that basic PHP is operational. We need to add some PHP extensions for WordPress.

apt install php-curl php-gd php-mbstring php-xml php-xmlrpc php-soap php-intl php-zip

After a few seconds that completes.

Modifying Apache for .htaccess Overrides

At this point using .htaccess is disabled, we need to open this up for WordPress and it plugins to function properly. Edit the 000-default.conf file inside the /etc/apache2/sites-available folder

/etc/apache2/sites-available# nano 000-default.conf

Here is what the file looks like before we modify it.

<VirtualHost *:80>
        # The ServerName directive sets the request scheme, hostname and port that
        # the server uses to identify itself. This is used when creating
        # redirection URLs. In the context of virtual hosts, the ServerName
        # specifies what hostname must appear in the request's Host: header to
        # match this virtual host. For the default virtual host (this file) this
        # value is not decisive as it is used as a last resort host regardless.
        # However, you must set it for any further virtual host explicitly.
        #ServerName www.example.com

        ServerAdmin webmaster@localhost
        DocumentRoot /var/www/html

        # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
        # error, crit, alert, emerg.
        # It is also possible to configure the loglevel for particular
        # modules, e.g.
        #LogLevel info ssl:warn

        ErrorLog ${APACHE_LOG_DIR}/error.log
        CustomLog ${APACHE_LOG_DIR}/access.log combined

        # For most configuration files from conf-available/, which are
        # enabled or disabled at a global level, it is possible to
        # include a line for only one particular virtual host. For example the
        # following line enables the CGI configuration for this host only
        # after it has been globally disabled with "a2disconf".
        #Include conf-available/serve-cgi-bin.conf
</VirtualHost>

# vim: syntax=apache ts=4 sw=4 sts=4 sr noet

We need to insert this into that file inside the <VirtualHost> node.

<Directory /var/www/html/>
    AllowOverride All
</Directory>

I pasted it just inside the closing </VirtualHost> tag, like so…

/etc/apache2/sites-available# cat 000-default.conf 
<VirtualHost *:80>
	# The ServerName directive sets the request scheme, hostname and port that
	# the server uses to identify itself. This is used when creating
	# redirection URLs. In the context of virtual hosts, the ServerName
	# specifies what hostname must appear in the request's Host: header to
	# match this virtual host. For the default virtual host (this file) this
	# value is not decisive as it is used as a last resort host regardless.
	# However, you must set it for any further virtual host explicitly.
	#ServerName www.example.com

	ServerAdmin webmaster@localhost
	DocumentRoot /var/www/html

	# Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
	# error, crit, alert, emerg.
	# It is also possible to configure the loglevel for particular
	# modules, e.g.
	#LogLevel info ssl:warn

	ErrorLog ${APACHE_LOG_DIR}/error.log
	CustomLog ${APACHE_LOG_DIR}/access.log combined

	# For most configuration files from conf-available/, which are
	# enabled or disabled at a global level, it is possible to
	# include a line for only one particular virtual host. For example the
	# following line enables the CGI configuration for this host only
	# after it has been globally disabled with "a2disconf".
	#Include conf-available/serve-cgi-bin.conf
	<Directory /var/www/wordpress/>
    		AllowOverride All
	</Directory>

</VirtualHost>

WordPress has a feature called permalink, we need another module to allow that to work

a2enmod rewrite

The result of that let’s us know that we need to restart Apache for it to take affect

Enabling module rewrite.
To activate the new configuration, you need to run:
  systemctl restart apache2

We issue the systemctl restart apache2 command to bounce the Apache service.

systemctl restart apache2

Install WordPress

To install wordpress with apt, we make sure that it’s available

apt list

This returns hundreds of entries, you can scroll back up to find it, or just look for it specifically.

apt list wordpress

The result shows it is available to apt

Listing... Done
wordpress/bionic 4.9.5+dfsg1-1 all

To install WordPress

apt-get install wordpress

Type Y for yes on the prompt to use some disk space for it. Then WordPress is downloaded.

Get:1 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/main amd64 libogg0 amd64 1.3.2-1 [17.2 kB]
Get:2 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/main amd64 javascript-common all 11 [6066 B]
Get:3 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/main amd64 libao-common all 1.2.2+20180113-1ubuntu1 [6644 B]
Get:4 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/main amd64 libao4 amd64 1.2.2+20180113-1ubuntu1 [35.1 kB]
Get:5 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/main amd64 libflac8 amd64 1.3.2-1 [213 kB]
Get:6 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/universe amd64 libjs-prototype all 1.7.1-3 [44.2 kB]
Get:7 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/universe amd64 libjs-scriptaculous all 1.9.0-2 [107 kB]
Get:8 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/universe amd64 libjs-cropper all 1.2.2-1 [139 kB]
Get:9 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic-updates/universe amd64 libphp-phpmailer all 5.2.14+dfsg-2.3+deb9u1build0.18.04.1 [135 kB]
Get:10 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/main amd64 libspeex1 amd64 1.2~rc1.2-1ubuntu2 [52.1 kB]
Get:11 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/main amd64 libvorbis0a amd64 1.3.5-4.2 [86.4 kB]
Get:12 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/main amd64 libvorbisenc2 amd64 1.3.5-4.2 [70.7 kB]
Get:13 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/main amd64 libvorbisfile3 amd64 1.3.5-4.2 [16.0 kB]
Get:14 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/universe amd64 php-getid3 all 1.9.15+dfsg-1 [339 kB]
Get:15 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/universe amd64 vorbis-tools amd64 1.4.0-10.1 [183 kB]
Get:16 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/universe amd64 wordpress all 4.9.5+dfsg1-1 [4472 kB]
Get:17 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/universe amd64 wordpress-l10n all 4.9.5+dfsg1-1 [4365 kB]
Get:18 http://us-east-2.ec2.archive.ubuntu.com/ubuntu bionic/universe amd64 wordpress-theme-twentyseventeen all 4.9.5+dfsg1-1 [891 kB]
Fetched 11.2 MB in 1s (16.6 MB/s)                     
Selecting previously unselected package libogg0:amd64.
(Reading database ... 85649 files and directories currently installed.)
Preparing to unpack .../00-libogg0_1.3.2-1_amd64.deb ...
Unpacking libogg0:amd64 (1.3.2-1) ...
Selecting previously unselected package javascript-common.
Preparing to unpack .../01-javascript-common_11_all.deb ...
Unpacking javascript-common (11) ...
Selecting previously unselected package libao-common.
Preparing to unpack .../02-libao-common_1.2.2+20180113-1ubuntu1_all.deb ...
Unpacking libao-common (1.2.2+20180113-1ubuntu1) ...
Selecting previously unselected package libao4:amd64.
Preparing to unpack .../03-libao4_1.2.2+20180113-1ubuntu1_amd64.deb ...
Unpacking libao4:amd64 (1.2.2+20180113-1ubuntu1) ...
Selecting previously unselected package libflac8:amd64.
Preparing to unpack .../04-libflac8_1.3.2-1_amd64.deb ...
Unpacking libflac8:amd64 (1.3.2-1) ...
Selecting previously unselected package libjs-prototype.
Preparing to unpack .../05-libjs-prototype_1.7.1-3_all.deb ...
Unpacking libjs-prototype (1.7.1-3) ...
Selecting previously unselected package libjs-scriptaculous.
Preparing to unpack .../06-libjs-scriptaculous_1.9.0-2_all.deb ...
Unpacking libjs-scriptaculous (1.9.0-2) ...
Selecting previously unselected package libjs-cropper.
Preparing to unpack .../07-libjs-cropper_1.2.2-1_all.deb ...
Unpacking libjs-cropper (1.2.2-1) ...
Selecting previously unselected package libphp-phpmailer.
Preparing to unpack .../08-libphp-phpmailer_5.2.14+dfsg-2.3+deb9u1build0.18.04.1_all.deb ...
Unpacking libphp-phpmailer (5.2.14+dfsg-2.3+deb9u1build0.18.04.1) ...
Selecting previously unselected package libspeex1:amd64.
Preparing to unpack .../09-libspeex1_1.2~rc1.2-1ubuntu2_amd64.deb ...
Unpacking libspeex1:amd64 (1.2~rc1.2-1ubuntu2) ...
Selecting previously unselected package libvorbis0a:amd64.
Preparing to unpack .../10-libvorbis0a_1.3.5-4.2_amd64.deb ...
Unpacking libvorbis0a:amd64 (1.3.5-4.2) ...
Selecting previously unselected package libvorbisenc2:amd64.
Preparing to unpack .../11-libvorbisenc2_1.3.5-4.2_amd64.deb ...
Unpacking libvorbisenc2:amd64 (1.3.5-4.2) ...
Selecting previously unselected package libvorbisfile3:amd64.
Preparing to unpack .../12-libvorbisfile3_1.3.5-4.2_amd64.deb ...
Unpacking libvorbisfile3:amd64 (1.3.5-4.2) ...
Selecting previously unselected package php-getid3.
Preparing to unpack .../13-php-getid3_1.9.15+dfsg-1_all.deb ...
Unpacking php-getid3 (1.9.15+dfsg-1) ...
Selecting previously unselected package vorbis-tools.
Preparing to unpack .../14-vorbis-tools_1.4.0-10.1_amd64.deb ...
Unpacking vorbis-tools (1.4.0-10.1) ...
Selecting previously unselected package wordpress.
Preparing to unpack .../15-wordpress_4.9.5+dfsg1-1_all.deb ...
Unpacking wordpress (4.9.5+dfsg1-1) ...
Selecting previously unselected package wordpress-l10n.
Preparing to unpack .../16-wordpress-l10n_4.9.5+dfsg1-1_all.deb ...
Unpacking wordpress-l10n (4.9.5+dfsg1-1) ...
Selecting previously unselected package wordpress-theme-twentyseventeen.
Preparing to unpack .../17-wordpress-theme-twentyseventeen_4.9.5+dfsg1-1_all.deb ...
Unpacking wordpress-theme-twentyseventeen (4.9.5+dfsg1-1) ...
Setting up php-getid3 (1.9.15+dfsg-1) ...
Setting up wordpress-theme-twentyseventeen (4.9.5+dfsg1-1) ...
Setting up libao-common (1.2.2+20180113-1ubuntu1) ...
Setting up libspeex1:amd64 (1.2~rc1.2-1ubuntu2) ...
Setting up libogg0:amd64 (1.3.2-1) ...
Setting up libphp-phpmailer (5.2.14+dfsg-2.3+deb9u1build0.18.04.1) ...
Setting up javascript-common (11) ...
apache2_invoke: Enable configuration javascript-common
Setting up libjs-prototype (1.7.1-3) ...
Setting up libvorbis0a:amd64 (1.3.5-4.2) ...
Setting up libvorbisfile3:amd64 (1.3.5-4.2) ...
Setting up libao4:amd64 (1.2.2+20180113-1ubuntu1) ...
Setting up libflac8:amd64 (1.3.2-1) ...
Setting up libjs-scriptaculous (1.9.0-2) ...
Setting up libjs-cropper (1.2.2-1) ...
Setting up libvorbisenc2:amd64 (1.3.5-4.2) ...
Setting up wordpress (4.9.5+dfsg1-1) ...
Added to /var/lib/wordpress/wp-content/plugins: akismet index.php 
Added to /var/lib/wordpress/wp-content/languages: admin-ar.mo admin-bs_BA.mo admin-ca.mo admin-cy.mo admin-da_DK.mo admin-de_DE.mo admin-el.mo admin-es_ES.mo admin-es_PE.mo admin-et.mo admin-eu.mo admin-fa_IR.mo admin-fi.mo admin-fr_FR.mo admin-gl_ES.mo admin-he_IL.mo admin-hr.mo admin-it_IT.mo admin-ja.mo admin-ka_GE.mo admin-ko_KR.mo admin-my_MM.mo admin-network-ar.mo admin-network-bs_BA.mo admin-network-ca.mo admin-network-cy.mo admin-network-da_DK.mo admin-network-de_DE.mo admin-network-el.mo admin-network-es_ES.mo admin-network-es_PE.mo admin-network-et.mo admin-network-fa_IR.mo admin-network-fi.mo admin-network-fr_FR.mo admin-network-gl_ES.mo admin-network-he_IL.mo admin-network-hr.mo admin-network-it_IT.mo admin-network-ja.mo admin-network-ka_GE.mo admin-network-ko_KR.mo admin-network-my_MM.mo admin-network-ru_RU.mo admin-network-sk_SK.mo admin-network-sl_SI.mo admin-network-sq.mo admin-network-sr_RS.mo admin-network-sv_SE.mo admin-network-ug_CN.mo admin-network-zh_CN.mo admin-network-zh_TW.mo admin-ru_RU.mo admin-sk_SK.mo admin-sl_SI.mo admin-sq.mo admin-sr_RS.mo admin-sv_SE.mo admin-ug_CN.mo admin-zh_CN.mo admin-zh_TW.mo ar.mo bg_BG.mo bn_BD.mo bs_BA.mo ca.mo ckb.mo continents-cities-bg_BG.mo continents-cities-bs_BA.mo continents-cities-cs_CZ.mo continents-cities-cy.mo continents-cities-da_DK.mo continents-cities-de_DE.mo continents-cities-el.mo continents-cities-en_CA.mo continents-cities-es_PE.mo continents-cities-et.mo continents-cities-eu.mo continents-cities-fa_IR.mo continents-cities-fi.mo continents-cities-fr_FR.mo continents-cities-gd.mo continents-cities-he_IL.mo continents-cities-hr.mo continents-cities-hu_HU.mo continents-cities-it_IT.mo continents-cities-ja.mo continents-cities-ka_GE.mo continents-cities-ko_KR.mo continents-cities-lv.mo continents-cities-mk_MK.mo continents-cities-my_MM.mo continents-cities-nb_NO.mo continents-cities-nn_NO.mo continents-cities-pl_PL.mo continents-cities-pt_BR.mo continents-cities-pt_PT.mo continents-cities-ro_RO.mo continents-cities-ru_RU.mo continents-cities-sk_SK.mo continents-cities-sl_SI.mo continents-cities-sq.mo continents-cities-sr_RS.mo continents-cities-su_ID.mo continents-cities-sv_SE.mo continents-cities-ta_LK.mo continents-cities-tr_TR.mo continents-cities-ug_CN.mo continents-cities-vi.mo continents-cities-zh_CN.mo continents-cities-zh_TW.mo cs_CZ.mo cy.mo da_DK.mo de_DE.mo el.mo en_CA.mo eo.mo es_CL.mo es_ES.mo es_PE.mo et.mo eu.mo fa_IR.mo fi.mo fr_FR.mo gd.mo gl_ES.mo he_IL.mo hr.mo hu_HU.mo id_ID.mo it_IT.mo ja.mo jv_ID.mo ka_GE.mo ko_KR.mo lv.mo mk_MK.mo ms_MY.mo my_MM.mo nb_NO.mo nl.mo nl_NL.mo pl_PL.mo pt_BR.mo pt_PT.mo ro_RO.mo ru_RU.mo ru_UA.mo si_LK.mo sk_SK.mo sl_SI.mo sq.mo sr_RS.mo su_ID.mo sv_SE.mo sw.mo ta_LK.mo th.mo tr_TR.mo ug_CN.mo uk.mo ur.mo vi.mo zh_CN.mo zh_HK.mo zh_TW.mo 
Setting up wordpress-l10n (4.9.5+dfsg1-1) ...
Setting up vorbis-tools (1.4.0-10.1) ...
Processing triggers for mime-support (3.60ubuntu1) ...
Processing triggers for libc-bin (2.27-3ubuntu1) ...
Processing triggers for man-db (2.8.3-2ubuntu0.1) ...

When it completes, it won’t be in the directory where Apache can use it. Using updatedb and locate commands, we can find out where it got downloaded.

updatedb
locate wordpress

The results in a huge list of every file. We are looking for where he bulk of them landed and subfolders named wp-admin, wp-content, and wp-includes since we know WordPress requires those. Scrolling up quite a way we start to notice the location of a few important files under the /usr/share directory.

...
/usr/share/wordpress/wp-admin/index.php
...
/usr/share/wordpress/wp-content/plugins/index.php
...
/usr/share/wordpress/wp-includes/wp-db.php
...

We also notice wordpress files in other directories such as…

/etc/wordpress
/etc/wordpress/htaccess
/usr/share/wordpress
/usr/share/doc/wordpress
...
/var/lib/wordpress
/var/lib/dpkg/info/wordpress-l10n.list
...
/var/lib/wordpress/wp-content
/var/lib/wordpress/wp-content/index.php
/var/lib/wordpress/wp-content/languages
/var/lib/wordpress/wp-content/plugins
/var/lib/wordpress/wp-content/themes
/var/lib/wordpress/wp-content/uploads
...

We want to copy from /usr/share/wordpress into /var/www/html since we know apache is configured to look into that directory to serve up index.php. Speaking of which we want to remove the existing index.php file we created earlier, so we are sure to use the one from WordPress.

cd /var/www/html
rm index.php

To copy WordPress /usr/share/wordpress into the current directory of /var/www/html

cp -a /usr/share/wordpress/. ./

The resulting directory structure

ec2-user@ip-172-16-1-1:/var/www/html# ls -la
total 192
drwxr-xr-x  5 root root  4096 Apr 18 13:04 .
drwxr-xr-x  3 root root  4096 Apr 17 02:48 ..
lrwxrwxrwx  1 root root    23 Apr  7  2018 .htaccess -> /etc/wordpress/htaccess
-rw-r--r--  1 root root 10918 Apr 17 02:48 index.html
-rw-r--r--  1 root root   418 Apr  6  2018 index.php
-rw-r--r--  1 root root  7440 Apr  7  2018 readme.html
-rw-r--r--  1 root root  5697 Apr  7  2018 wp-activate.php
drwxr-xr-x  9 root root  4096 Apr 18 13:04 wp-admin
-rw-r--r--  1 root root   364 Apr  6  2018 wp-blog-header.php
-rw-r--r--  1 root root  1627 Apr  6  2018 wp-comments-post.php
-rw-r--r--  1 root root  2853 Apr  6  2018 wp-config-sample.php
-rw-r--r--  1 root root  2381 Apr  7  2018 wp-config.php
drwxr-xr-x  5 root root  4096 Apr 18 13:04 wp-content
-rw-r--r--  1 root root  3669 Apr  6  2018 wp-cron.php
drwxr-xr-x 18 root root 12288 Apr 18 13:04 wp-includes
-rw-r--r--  1 root root  2422 Apr  6  2018 wp-links-opml.php
-rw-r--r--  1 root root  3306 Apr  6  2018 wp-load.php
-rw-r--r--  1 root root 36593 Apr  6  2018 wp-login.php
-rw-r--r--  1 root root  8048 Apr  6  2018 wp-mail.php
-rw-r--r--  1 root root 16246 Apr  6  2018 wp-settings.php
-rw-r--r--  1 root root 30071 Apr  6  2018 wp-signup.php
-rw-r--r--  1 root root  4620 Apr  6  2018 wp-trackback.php
-rw-r--r--  1 root root  3065 Apr  6  2018 xmlrpc.php
ec2-user@ip-172-16-1-1:/var/www/html# 

At this point we might be curious to see what happens if we fire up a browser and see what it looks like. There is a 0% chance this works since WordPress hasn’t even been configured, nor does it know where the MySQL database is, but just for humor sake…

WordPress not ready yet

One thing to notice from the ls -la command executed above is that root is the owner of all the files and subdirectories, that won’t work. The user that Apache is running under needs access to the wordpress file structure. To locate that user, lets look at the available users in /etc/passwd, the user with the home directory of /var/www is likely the user that we are looking for.

cat /etc/passwd | grep /var/www
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

Looks like www-data is the one we want. Let’s make www-data user and group the owner of the html directory structure.

chown -R www-data:www-data /var/www/html

Let’s take a look…

ec2-user@ip-172-16-1-1:/var/www/html# ls -la
total 192
drwxr-xr-x  5 www-data www-data  4096 Apr 18 13:04 .
drwxr-xr-x  3 root     root      4096 Apr 17 02:48 ..
lrwxrwxrwx  1 www-data www-data    23 Apr  7  2018 .htaccess -> /etc/wordpress/htaccess
-rw-r--r--  1 www-data www-data 10918 Apr 17 02:48 index.html
-rw-r--r--  1 www-data www-data   418 Apr  6  2018 index.php
-rw-r--r--  1 www-data www-data  7440 Apr  7  2018 readme.html
-rw-r--r--  1 www-data www-data  5697 Apr  7  2018 wp-activate.php
drwxr-xr-x  9 www-data www-data  4096 Apr 18 13:04 wp-admin
-rw-r--r--  1 www-data www-data   364 Apr  6  2018 wp-blog-header.php
-rw-r--r--  1 www-data www-data  1627 Apr  6  2018 wp-comments-post.php
-rw-r--r--  1 www-data www-data  2853 Apr  6  2018 wp-config-sample.php
-rw-r--r--  1 www-data www-data  2381 Apr  7  2018 wp-config.php
drwxr-xr-x  5 www-data www-data  4096 Apr 18 13:04 wp-content
-rw-r--r--  1 www-data www-data  3669 Apr  6  2018 wp-cron.php
drwxr-xr-x 18 www-data www-data 12288 Apr 18 13:04 wp-includes
-rw-r--r--  1 www-data www-data  2422 Apr  6  2018 wp-links-opml.php
-rw-r--r--  1 www-data www-data  3306 Apr  6  2018 wp-load.php
-rw-r--r--  1 www-data www-data 36593 Apr  6  2018 wp-login.php
-rw-r--r--  1 www-data www-data  8048 Apr  6  2018 wp-mail.php
-rw-r--r--  1 www-data www-data 16246 Apr  6  2018 wp-settings.php
-rw-r--r--  1 www-data www-data 30071 Apr  6  2018 wp-signup.php
-rw-r--r--  1 www-data www-data  4620 Apr  6  2018 wp-trackback.php
-rw-r--r--  1 www-data www-data  3065 Apr  6  2018 xmlrpc.php
ec2-user@ip-172-16-1-1:/var/www/html#

WordPress Configuration File

The file we need to configure is wp-config.php. Let’s take a peek at it.

...
echo "Neither <b>$debian_file</b> nor <b>$debian_main_file</b> could be found. <br/> Ensure one of them exists, is readable by the webserver and contains the right password/username.";
...

Looking at this file, it appears to be the source of the error string that we received, when we got curious with the browser. This config file is for servers that contain more than one website. We are only interested in a single website on this server. Let’s rename this config file to something else like wp-config-multi.php. Then copy the wp-config-sample.php as wp-config.php.

mv wp-config.php wp-config-multi.php
cp wp-config-sample.php wp-config.php

Now looking at this version of he wp-config.php, we notice there are configuration settings for the database as well as unique keys and salts for authentication.

ec2-user@ip-172-16-1-1:/var/www/html# cat wp-config.php
<?php
/**
 * The base configuration for WordPress
 *
 * The wp-config.php creation script uses this file during the
 * installation. You don't have to use the web site, you can
 * copy this file to "wp-config.php" and fill in the values.
 *
 * This file contains the following configurations:
 *
 * * MySQL settings
 * * Secret keys
 * * Database table prefix
 * * ABSPATH
 *
 * @link https://codex.wordpress.org/Editing_wp-config.php
 *
 * @package WordPress
 */

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', 'database_name_here');

/** MySQL database username */
define('DB_USER', 'username_here');

/** MySQL database password */
define('DB_PASSWORD', 'password_here');

/** MySQL hostname */
define('DB_HOST', 'localhost');

/** Database Charset to use in creating database tables. */
define('DB_CHARSET', 'utf8');

/** The Database Collate type. Don't change this if in doubt. */
define('DB_COLLATE', '');

/**#@+
 * Authentication Unique Keys and Salts.
 *
 * Change these to different unique phrases!
 * You can generate these using the {@link https://api.wordpress.org/secret-key/1.1/salt/ WordPress.org secret-key service}
 * You can change these at any point in time to invalidate all existing cookies. This will force all users to have to log in again.
 *
 * @since 2.6.0
 */
define('AUTH_KEY',         'put your unique phrase here');
define('SECURE_AUTH_KEY',  'put your unique phrase here');
define('LOGGED_IN_KEY',    'put your unique phrase here');
define('NONCE_KEY',        'put your unique phrase here');
define('AUTH_SALT',        'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT',   'put your unique phrase here');
define('NONCE_SALT',       'put your unique phrase here');

/**#@-*/

/**
 * WordPress Database Table prefix.
 *
 * You can have multiple installations in one database if you give each
 * a unique prefix. Only numbers, letters, and underscores please!
 */
$table_prefix  = 'wp_';

/**
 * For developers: WordPress debugging mode.
 *
 * Change this to true to enable the display of notices during development.
 * It is strongly recommended that plugin and theme developers use WP_DEBUG
 * in their development environments.
 *
 * For information on other constants that can be used for debugging,
 * visit the Codex.
 *
 * @link https://codex.wordpress.org/Debugging_in_WordPress
 */
define('WP_DEBUG', false);

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

/** Absolute path to the WordPress directory. */
if ( !defined('ABSPATH') )
	define('ABSPATH', dirname(__FILE__) . '/');

/** Sets up WordPress vars and included files. */
require_once(ABSPATH . 'wp-settings.php');
ec2-user@ip-172-16-1-1:/var/www/html# 

To generate the unique keys

curl -s https://api.wordpress.org/secret-key/1.1/salt/

The result are several keys and salts. THE VALUES BELOW ARE AN EXAMPLE, WHEN YOU ARE CONFIGURING YOUR SERVER GENERATE YOUR OWN VALUES! Otherwise everyone else reading this on the Internet knows your keys and salts. I’m not even going to use these value ;-}

define('AUTH_KEY',         '5:IU$hf(MwDhEgAlA6Z]c$x`wrsbD=;|)B%[+Q?`oKX%!5FKqENGyLh}p3{`s`CX');
define('SECURE_AUTH_KEY',  'd/)7*/x|Lv2_+GtGDe,o+D57Q+vWB5PacI=5G](QsZ2X-1+ksnYW-Z>H]_qR}{-<');
define('LOGGED_IN_KEY',    'meA{$RoG!%s*XC Z!39Jm!7QF`d8Fp)y)|Y9*) |PT+L(F@GItFdBL^4#0h3)K&q');
define('NONCE_KEY',        'UbYv>k%2d%`^$b{G,u/^?LK&zza`J(xrn|%~`AO~#r{Zk*M+u&C+Qo.<-} q+t00');
define('AUTH_SALT',        'A+D,Y7E+Qi6m7Uae9@EO~nA#m-#-pzPu5;i/HAm!n#W)IGe=L ^i/e38O])_KF%0');
define('SECURE_AUTH_SALT', 'XT)NoBz4[|JiM/@-ulu-zI/fxh&I@aS+^J:YC!Tk+JaB-hb0nJf K4VN2TFCzTz.');
define('LOGGED_IN_SALT',   '9ADnHuIm[GFe0oUOPy:DZo$/%m}+7|mSY0I,wZkiwXbxMv`(2xXBLMKpU`Lz+Q^Y');
define('NONCE_SALT',       'UwP@PT8QY:;/Qlt/b+t|YO_XJ;3(m#uTHiYbJc!P48=RE$lQ^p.7r>2Nmbjv;q9m');

Copy those lines before opening the wp-config.php. Because you will be pasting those lines in place of the existing lines that look like this

define('AUTH_KEY',         'put your unique phrase here');
define('SECURE_AUTH_KEY',  'put your unique phrase here');
define('LOGGED_IN_KEY',    'put your unique phrase here');
define('NONCE_KEY',        'put your unique phrase here');
define('AUTH_SALT',        'put your unique phrase here');
define('SECURE_AUTH_SALT', 'put your unique phrase here');
define('LOGGED_IN_SALT',   'put your unique phrase here');
define('NONCE_SALT',       'put your unique phrase here');

To remove the existing lines in nano press Ctrl+K on the line that you want to remove. Which is way easier than pressing the delete key a few hundred times.

nano wp-config.php

After pasting those lines of keys and salts in there, enter the settings for the MySQL database. Remember, earlier we used:

  • Database = database4wp
  • Username = stimpy42r
  • Password = 78GBVwed-6rr95
  • Hostname = localhost, since the database is on this host.

After we are done entering the MySQL values…

// ** MySQL settings - You can get this info from your web host ** //
/** The name of the database for WordPress */
define('DB_NAME', 'database4wp');

/** MySQL database username */
define('DB_USER', 'stimpy42r');

/** MySQL database password */
define('DB_PASSWORD', '78GBVwed-6rr95');

/** MySQL hostname */
define('DB_HOST', 'localhost');

Now at this point you can head back to the browser and finish the setup. Enter Site Name, Username, password

Golden Image (optional)

At this point, this site is a clean as it’s ever going to be. I created a snapshot of my volume in AWS. So the next time I need a site spun up. I can start from here, and not from the very beginning. Obviously usernames, keys, and salts will need to be changed, but that’s easy stuff.

HTTPS Configuration

I don’t put this section in the golden image since every site will have it’s own unique certificate (public and private keys).

I prefer letsencrypt.org which takes you to certbot.eff.org. I’m using Apache on Ubuntu 18.04 LTS so I start here https://certbot.eff.org/lets-encrypt/ubuntubionic-apache

Add Certbot PPA to the repos list

sudo -i
apt-get update
apt-get install software-properties-common
add-apt-repository universe
add-apt-repository ppa:certbot/certbot
apt-get update

Install Certbot

apt-get install certbot python-certbot-apache

Set Certbot to edit Apache config

certbot --apache

Follow the steps in the command prompt. During this interaction is where you tell certbot what your domain name is.