Securing a LAMP Server in 2019

0
8549

LAMP is an acronym for a stack typically consisting of the Linux operating system, the Apache HTTP Server, the MySQL relational database management system, and the PHP programming language. It is a platform mainly used to develop dynamic websites. Let’s take a look at how we can secure a LAMP server today.

Linux, Apache, MySQL and PHP – the famed LAMP stack comprises easily one of the most popular open source technology components used in conjunction with each other to run the Web. Although newer technologies with promising features have emerged over the years, LAMP has stood the test of time, still powering most of the Internet today.

As a technology grows popular, it gets attention from both attackers and developers. Although LAMP has become very mature over the years and is powering a lot of big organisations all around the world, it isn’t perfect and has its upsides and downsides.

Most of the weaknesses can be addressed by simply following a list of best practices and that doesn’t take long. Let us look at some of the ways we can make Web applications significantly stronger and attack-resistant.

Let’s start by tweaking a weak system!

Securing Linux

We are going to use Ubuntu 18.04 for this tutorial, although the steps are almost the same in some other non-Debian based variants.

User access control and privileges: Log in to the system for the first time as the root user, as follows:

ssh root@ip_address_of_your_server

After you are done logging into your root account, create a new user to operate the server and do not use the root account.

This is because the root has elevated privileges to do anything in the system, which could allow other users to make destructive changes. Using the root account as default is a bad idea because we humans tend to make a lot of mistakes and cannot take chances in a production environment.

We create a new user called webadmin that we will be using to handle our Web server.

# adduser webadmin

Then you will be prompted to add a password and other credentials. Set a strong password and then hit Enter to skip any field.

To allow the user to temporarily go to an elevated level and use root privileges, we will add the user to the sudo group using the following command:

usermod -aG sudo webadmin

Now, you can use sudo to call commands as a super user.

Setting up a basic firewall: On a public network like the Internet, we cannot keep our gates open everywhere to allow everyone in. As a rule of thumb, restrict access to everything in the system, and allow access to a specific audience only whenever needed.

Let’s set up a basic firewall known as Uncomplicated Fire Wall (UFW).

To see the list of applications registered in UFW, use the command below:

sudo ufw app list

Deny all incoming connections by default, as per policy:

sudo ufw default deny incoming

To make sure SSH is allowed through UFW for us to connect, we allow OpenSSH through UFW, as follows:

sudo ufw allow OpenSSH
Figure 1: UFW app list

SSH access: Since we are going to control the remote server via SSH, we need a way to authenticate ourselves to log in to the system.

One way is by using passwords. Password authentication is susceptible to brute-force attacks and is generally discouraged. The safer way is to set up SSH keys to communicate with the remote server and the local machine.

This process involves generating an RSA keypair using the command ssh-keygen and passing the public key to the server (generally using ssh-copy-id). Once the key has been copied, you can go ahead and disable password authentication for SSH by editing the /etc/ssh/sshd_config file and uncommenting/adding the following directive:

PasswordAuthentication no

This ensures that you can log in to the server via SSH only using the generated keys and not the weaker password authentication system.

Warning: Make sure you are able to log in using key-based authentication on the non-root account with sudo permissions before disabling password authentication.

If your server supports SSH through only a specific IP range (like Amazon AWS), you can use it to set a range of specific IP addresses to be able to access the machine remotely using SSH.

Securing Apache

First, allow Apache in UFW, as follows:

sudo ufw allow in “Apache Full”

Disable open indexes: Open indexes can expose your directory structure which may prove to be fatal. You can disable open indexes by editing /etc/apache2/apache2.conf:

<Directory /var/www/html>

Options -Indexes

</Directory>

Or just add Options -Indexes in the .htaccess file in the root directory.

Install mod_evasive and mod_security: mod_security acts as a Web application firewall (WAF) to tighten app security, and mod_evasive helps protect it from potential attacks like brute force and DDoS. To install these, give the following command:

sudo apt-get install libapache2-mod-security2 libapache2-mod-evasive

While the standard installation should be enough for most people, you might have to configure it separately to set things according to your needs.

Deny access to certain directories: You might want to deny access to certain directories, in which case we can use the Deny from all directives for this in the apache.conf file or in htaccess.

<Directory ~ “\secret”>

Options None

Order allow,deny

Deny from all

</Directory>

Using SSL: Secure Socket Layer (SSL) can be used to encrypt connections between the user and the server. It allows your website to be accessible over port 443 (HTTPS). You need to obtain an SSL certificate from a vendor like Comodo or Digicert. You can also get domain-validated certificates from Letsencrypt for free.

Apache uses the mod_ssl module to support SSL certificates. Use the default_ssl configuration file for ease:

sudo ln -s /etc/apache2/sites-available/default-ssl.conf /etc/apache2/sites-enabled/your-site-ssl.conf

Once you get the signed SSL certificate, you can add it to your new virtualhost configuration:

<VirtualHost *:443>

...

SSLEngine on

SSLCertificateFile /path/to/your/crt/file.crt

SSLCertificateKeyFile /path/to/your/key/file.key

SSLCertificateChainFile /path/to/your/bundle/file.crt

...

</VirtualHost>

Finally, enable the SSL module:

sudo a2enmod ssl

sudo systemctl restart apache2

Securing MySQL

Although setting up the security of MySQL can be done at a later time, after installation, we should ideally start installing it in the most secure way possible.

We start off by installing MySQL by using the following command:

sudo apt-get install mysql-server

Then we set up the mysql-server by creating a directory layout for our databases:

sudo mysql_install_db

After that, we run the following command to run a wizard that will help us remove some potentially dangerous defaults. This will remove the ability for anyone to log into MySQL by default, disable logging in remotely with the administrator account, and remove some test databases that are insecure.

sudo mysql_secure_installation

The MySQL configuration file: The main configuration file for MySQL is located at /etc/mysql/my.cnf in Ubuntu. Tweaking this, we can lock down the MySQL server. In the [mysqld] section, set the bind address to the local loopback network device. This will make sure only this machine can access this MySQL instance and not remotely.

bind-address = 127.0.0.1

We will now shut off a function that allows access to the underlying file system from within MySQL, which can be potentially dangerous if allowed, by using the following command:

local-infile=0

This will disable loading files from the file system from within MySQL.

Figure 2: HTTPS enabled website

Logging: Logging is important to check for events and suspicious activities. This is usually enabled by default, but we can set where the log file is stored, as follows:

log=/var/log/mysql-log

We should also make sure that neither other users without high privileges, nor the public, are able to view the file.

User access control: As per our previous rule of thumb, we should provide users access to only those resources that are absolutely necessary.

For instance, suppose we have a user called ‘noob’ that we want to allow read access to, because noob – being a noob, makes a lot of mistakes and we cannot allow that in production. Noob is given an isolated database – MirrorDB – which mirrors the actual production database to teach the structure to it.

Creating the noob user: Use the following command to create the noob user:

CREATE USER ‘noob’@’localhost’ IDENTIFIED BY ‘noobpassword’;

And then we grant the necessary permissions, as follows:

GRANT SELECT ON MirrorDB.* TO ‘noob’@’localhost’;

If we want to revoke noob’s access, we use the following command:

REVOKE SELECT ON MirrorDB.* FROM ‘noob’@’localhost’;

And then we finalise the changes and check noob’s privileges, as shown below:

FLUSH PRIVILEGES;

SHOW grants FOR ‘noob’@’localhost;

These are a few baby steps we can take to ensure minimal security in our database. An exhaustive resource for securing MySQL can be found at https://www.symantec.com/connect/articles/securing-mysql-step-step.

Securing PHP

Do use PHP 7+ because unless you are working on a huge monolithic legacy codebase from 2009, there’s no reason to still use PHP 5 or earlier. The number of improvements in terms of security and performance in PHP 7 is enormous, and it has been getting better with every update. PHP 7.4 and PHP 8 are currently in development.

The best thing you can do to improve security in PHP is to update it. PHP 7+ addressed most of the problems that PHP critics had pointed out earlier. PHP 7.3.2 is already out and it is recommended that you stay with the updated stable version. Although migration may take time, since there are a lot of backwards-incompatible changes, it is worth all the developer’s time.

Enabling strict types: One of the most basic things you can do to make your life easier and write better code is enabling strict types in PHP. This will take you a long way towards the goal of making your application safe from bugs.

To make sure you don’t end up passing a string to an integer argument, start all your PHP files as follows:

<?php declare(strict_types=1);

And now you can define a function as shown below:

function getUser(int id) : User

{

...

}

With modern IDEs like PHPStorm, you will get a warning directly if you pass an argument with a wrong type while calling the function.

Using PDO and prepared statements: PDO (PHP Data Objects) is the object-oriented way to talk to a database from PHP and should be the only way to do so. It is a database abstraction layer (DAL), which abstracts away the low-level details for connecting to the database in a safe manner. mysql_* functions have long been deprecated and removed in PHP 7.

PDO takes away the dangers of the popular MySQL injections using prepared statements and bind parameters in its queries. Its reusable API allows you to connect to any database without changing much code.

In 2019, on a modular Web application, you probably would want to consider ORMs (object-relational mappers) with query builders like Doctrine for adding further abstraction.

Validating user input and sanitising output: Any security engineer will tell you to never trust user inputs. As a rule of thumb, validate every input you get from the user and store it into the database only after every test passes, or fails completely.

There is no one standard way to achieve this; although a few open source validation libraries do exist, it is generally not a good option to use monolithic validator classes to validate everything, since they usually end up doing too many things and violating the single responsibility principle.

One way is to use value objects and pass them as arguments to your entity classes in the model layer. The type checking should take care of the correctness of the data and throw an exception on failure.

Here is an example. Create an immutable email value object class to hold the email data, as follows:

class Email

{

private $email;

public function __construct(string $email)

{

if ($this->isValid($email))

$this->email = $email;

else

throw new \InvalidArgumentException();

}

}

Then pass it as an argument to your model class that stores user data, as follows:

class User

{

public function __construct(…, Email $email)

{

// Do something with email

}

This way we can simplify the process of validating data using value objects. A repository that I am currently working on to provide a set of predefined common value objects can be found at https://github.com/2DSharp/Phypes.

Sanitisation, on the other hand, should not be done while inserting data into the database. It should rather be done while displaying data from the database. Sanitising can protect against XSS attacks by turning HTML special characters to appear as regular text. For example, PHP’s filter_var() functions htmlspecialchars() can be used to sanitise output data.

Enable ‘display errors’ only during development and not production: A common mistake I have seen a lot of websites make is displaying too much information to the end user if an error occurs; sometimes, the whole PHP runtime error is displayed. This is an annoyance to the user and an opportunity for an attacker. You should always turn off ‘error display’ during production.

PHP comes with two config files – php.ini-development and php.ini-production. While you are developing the application locally, or in the test phase, use the development file as your default php.ini which is configured to log and display all types of errors.

As soon as you deploy your website live, you should start using the production that will have the ‘error display’ turned off and other typical production configurations set by default.

Don’t store sensitive data in code but use environment variables: When we need to connect to a database, we have to let the API know its credentials. Similarly, API keys have to be passed to the language to access certain services. Often, programmers hard code these values inside the code, and this code is pushed to the version control server. The problem with this is that all the developers now have access to the database server credentials, which is inherently a bad idea.

To avoid this, we can use environment variables provided by Apache. You can achieve this by editing the virtual host configuration file for your website, which is usually available at /etc/apache2/sites-available/ with a .conf extension.

<VirtualHost *:80>

…

SetEnv DB_USER webadmin

SetEnv DB_PASS aC0mpl3XPa$sWord

...

</VirtualHost>

You can add variables for database access in this manner. After restarting the server once, you can access them with PHP later on using the following command:

getenv(‘DB_USER’)

This will ensure that you do not expose sensitive data like database credentials in your production/development code and, instead, have it stored in the Apache configuration which can only be accessed by a user with higher permissions (the sysadmin).

You can also use other .env files to store your database credentials with a popular library: https://github.com/vlucas/phpdotenv. This will allow you to automatically import the variables stored in .env files to the getenv() or $_ENV variables in PHP.

Warning: Never allow your sensitive data to be stored in code or anywhere in the source code tree, including .env files!

This, in no way, is all you can do to keep your LAMP server secure. There are plenty of resources online that can help you get started with this. One such thread is at https://serverfault.com/questions/212269/tips-for-securing-a-lamp-server.

LEAVE A REPLY

Please enter your comment!
Please enter your name here