Modern PHP Developer Part 1: Setting Up and Securing Our Personal Server
For the first post in this series we will configure a basic LAMP server, harden it with some basic security measures, and use a simple PHP script to test our setup. Read to the end for a bonus!
I will be using the following major software packages and versions:
- PHP 7.0.7
- Apache 2.4.20
- MySQL 5.7.12
- Ubuntu 14.04
This simple setup can get you up and running to start creating your first simple todo app or even your own PHP blog.
I am going to walk through each set of commands that we run and explain what they accomplish and why they are important. I do assume a basic understanding of UNIX-based operating systems(OSX, Linux) so I don’t go into everything in detail. If any issues do come up for you try to look through stackoverflow and other sources to find the root of the problem. I have tested all of these commands on a Vagrant development server.
Step 1: Obtain our VPS
For this exercise I will be using DigitalOcean and their web interface to create an Ubuntu 14.04 VPS with 1 GB of memory, 1 virtual CPU core, and 30 GB of disk space. However, there are many other VPS providers such as Linode, AWS EC2, etc so choose whichever you are most comfortable with.
Step 2: Create a non-root user and lock down our server’s SSH access
If you haven’t already setup public/private keys for accessing your root user then we will do that now, if you have then feel free to skip some of these commands.
First we need to generate a set of SSH keys on our local machine(public/private):
When prompted accept the default file location and enter a passphrase. You will be prompted to enter this passphrase each time you use your private key file to authenticate. However, your OS’s default ssh-agent program will typically load it into memory after prompting you once so you should only need to re-enter your passphrase again after system reboot.
We need to create a ~/.ssh/authorized_keys file for our root user on the remote machine that contains our new public key. We can do this easily with the ssh-copy-id command:
Enter your root user’s password on the remote machine when prompted.
From now on let’s use SSH keys instead of passwords as we will be disallowing their use later on in this post.
SSH into your remote root user account and run the following commands, when the commands prompt you for further information such as the new user’s password enter it accordingly.
Note: All of these commands should be run on the remote server unless explicitly mentioned otherwise.
Create the deploy user and make them able to use sudo by adding them to the sudo group:
Now that we have created our new user let’s setup our public key authentication so that we can SSH into our remote server as the new user and not root: Note: This is run from your local machine.
Now that we don’t have to use our root user to SSH into let’s secure our OpenSSH server.
Open the /etc/ssh/sshd_config file as root:
A quick side note: Have 2 ssh sessions open for your root user to ensure that if you misconfigure something you don’t get locked out of your VPS! You will still have the second connection to fix the issue and restart the ssh service.
And change the lines within as shown below:
Now let’s restart the ssh service:
By this point we can SSH into our non-root user, use sudo, and don’t allow either logging in as the root user or the use of passwords for SSH authentication.
Step 3: Harden our server a bit
Now let’s add some firewall rules to secure the inbound connections to our server:
Now let’s set some good default policies for our INPUT, OUTPUT, and FORWARD chains:
These policy changes will ensure that we drop non-matched inbound packets and forwarded packets by default. I think for usability it is alright to default to allowing all outbound packets to be sent. If you require a more enterprise-like production setup with egress filtering your knowledge is already far beyond the scope of what I am covering here.
You could typically stop here, in fact you could delete that last firewall rule we added(not the policy changes) as the INPUT chain now drops all unmatched packets. But I want to show you how to setup some very basic logging of dropped packets:
The default log file for the above is /var/log/kern.log
Currently our firewall rules only live in memory, we need to save them to disk and ensure that they are loaded on system startup. For this we will use the iptables-persistent package:
If you didn’t opt to save your current rule set to disk when installing the package then do it now with:
The service will dump the in-memory rules to the /etc/iptables/rules.v4 file where you can take a look if you are interested. It will also load the rules from that file on system startup so if you change its contents be mindful that they will take affect on the server’s next restart.
Step 4: Install PHP 7.0.7 and PHP-FPM
To ensure that we can get the latest version of these PHP packages we will need to add a third-party APT repository. This is important as a lot of the core package versions could either be slightly, or very old versions. After adding a third-party repository we need to pull the package list that repository provides into our local cache so that apt install will use that information when determining the installation candidate.
Side Note: A great place to look for third-party APT repositories is https://launchpad.net/
PHP-FPM is a factcgi process manager that will be responsible for processing any web requests for PHP files. This is a much better strategy than using apache’s mod_php module to process PHP file requests for a number of reasons that I won’t get into here. This decoupling from apache also makes it easier for you to switch to using a different web server, such as nginx, in the future.
We will need to make a change to the default PHP-FPM pool configuration file as it assumes that we want apache to send it requests via a UNIX socket but we want to use a network port for now. Open the /etc/php/7.0/fpm/pool.d/www.conf file and make the following change:
And restart the service so that our new configuration changes are picked up:
This will configure PHP-FPM to listen on localhost port 9000 for requests to process PHP files that are sent to it from apache.
Step 5: Install MySQL 5.7.12
We will do the same as we did for PHP and add a third-party repository to get the latest version of MySQL 5.7(which at the time of writing is 5.7.12):
Now we need to run the following script from one of the packages we just installed to configure our MySQL server a bit more securely. Respond ‘y’ to all prompts and set a reasonable password for the root user.
If you mess up any of the prompts you can always re-run the script to correct it.
Step 6: Install Apache 2.4.20
The idea is the same here as it was for MySQL:
Step 7: Add the apache configuration and virtualhost
Note: Here we will be using the command-line utilities that the apache2 package(s) provide for simplicity but you may come across other ways of enabling/disabling sites/modules. As long as you understand what either method is doing it doesn’t matter which one you prefer to use.
First we need to enable the apache2 modules that allow us to proxy requests from apache to PHP-FPM:
Next we will disable the default site.
Then we need to add the following configuration snippet to an example.conf file in /etc/apache2/sites-available/:
We will need to create our site’s document root:
We then have to enable the above site(symlink it to /etc/apache2/sites-enabled/:
We need to restart apache to ensure that it picks up the configuration changes:
Step 8: Our entry point index.php script
I know that this script is a bit over-simplified but the point here isn’t to write a maintainable PHP application. The point is to get all of the infrastructure setup so that you can start deploying your PHP code. Create the file /var/www/example.com/public/index.php and put the following in it:
Now we can use a web browser to view the output of our script by browsing to the public IP address of our server(this should be shown in your VPS management screen on your provider’s website).
Bonus:
Pretty URLs:
Since the index.php file is our application’s entry-point all requests should get routed there right? Well then wouldn’t it be nice to not have that filename in all of our application’s URLs if we know that all requests are going there anyways? Well we can do this using an .htaccess file!
First we will need to enable the apache rewrite module that allows us to rewrite incoming requests as we will need that for the upcoming steps.
This line in the VirtualHost block in our site’s configuration file ensures that apache will load our .htaccess file:
Create the .htaccess file within our VHost’s document root(/var/www/example.com/public/) and add this to it:
Now we should be able to enter anything for the resource path of our server’s URL and it will be routed to the index.php script.
ex: http://12.34.108.92/posts/first-blog-post will be routed the same as http://12.34.108.92/index.php/posts/first-blog-post
Using our database with PDO:
We went through all of the trouble to setup a full LAMP stack but didn’t end up using the database!
Let’s change that now with this simple script that leverages PDO(PHP Data Objects) to open a connection to our MySQL server, insert a row into a table, and then query for all of the data that is stored in that table. First we need to create a database and a table for our script to use.
Enter the following statements, where you see the > prompt the statements are being executed through your MySQL client shell(also, enter the root password that you created earlier when prompted):
Then create the following 2 PHP scripts at the paths specified:
/var/www/example.com/settings.php
For the above password value you should use the password that you configured earlier. Normally you wouldn’t want to use the root user account for this purpose but for the sake of brevity we will use it here.
Replace the contents of this file completely, don’t add it to what we already had in there from earlier on in this post. /var/www/example.com/public/index.php
If you again browse to your server’s IP with a web browser you should see atleast one row in the output.
You might be asking why I needed 2 files here. One to store the bulk of the logic(index.php) and the other just to store an array of data(settings.php), and I didn’t even put them both in our site’s document root directory!?! I did this so that if through some mistake your application dies and a verbose error message is shown on your production systems a potential attacker may be able to see your code(which on its own is still very bad!) they will not be able to see your production database credentials! Just to be clear, there are a lot of things about this script that aren’t necessarily “production-ready” but regardless of that I thought this was an important thing for you to think about.
PDO is a database abstraction layer that allows you to use a single API to interact with a number of different supported databases. You call methods, PDO worries about how to implement the functionality based on the specific database that you are connected to(MySQL, PostgreSQL, Oracle, MSSQL, etc.). Another thing you can promote through this is to have fewer raw SQL strings littered throughout your codebase, tricking future maintainers into thinking that practise is ok to continue.
If you liked this post stay tuned for more posts in this series relating to PHP, DevOps, and how to improve your development/deployment workflow.