Deploying Laravel 8 on Apache & Ubuntu 20.04 LTS with extra security (Modsecurity and Fail2ban)

Balakrishna Prasad Ganne
8 min readJan 23, 2022

Deploying PHP applications has always been the same (almost) over the years. But, I have seen people struggle for simpler things that are generally obvious or apparent for people who are familiar with the Ubuntu-PHP ecosystem or for the people who deploy more often. This is a simple article that lists the commands and explains why we need those commands along the way.

At a high level, the software components that we will be using in this article are as follows —

  • Ubuntu 20.04 LTS (The base distribution/OS of our server)
  • Apache2 (The web server)
  • Mariadb (The open source fork of MySQL)
  • Few php extensions (that our laravel app needs)
  • Modsecurity (Apache mod to work as a WAF or Web Application Firewall)
  • Fail2ban (Intrusion Prevention Software)

Updating Ubuntu 20.04 LTS

The first thing that needs to be done is to update the OS. This is to make sure that our software catalogue is up to date.

Run the following command (without the dolloar symbol — it just indicates a normal user) to drop into a sudo prompt.

$ sudo -s

Run the following command (without the hash/pound symbol — it indicates sudo prompt) to start the update.

# apt update -y && apt upgrade -y && apt dist-upgrade -y && apt autoremove -y && apt autoclean -y

-y allows the command to run without asking for approval,
apt update — Update the list of available packages,
apt upgrade — Upgrade the system by installing/upgrading packages,
apt dist-upgrade — Distribution upgrade,
apt autoremove — Remove automatically all unused packages,
apt autoclean — Erase old downloaded archive files,
One can also run apt full-upgrade -y to upgrade the system by removing/installing/upgrading packages. See man apt for more information on these commands.

This command (technically, it’s a list of commands) takes a while to execute if it has a lot of stuff to update. And it is recommended to reboot the instance after the update.

Installing Apache2

Apache is generally pre-installed in instances deployed in cloud. If it is, you can safely skip this step. I recommend that you follow along.

Apache is available in Ubuntu repositories with the name apache2. It can be installed using the following command.

# apt install apache2

Once installed, Apache needs to run as a daemon (or service) in the background to serve content. Apache service can be started with the following command.

# systemctl start apache2

To keep the service persistent over reboots or restarts, the service needs to be started right after the boot, which can be done by adding the service to startup.

# systemctl enable apache2

After starting any service, few complications might stop the service from runnig. It’s always a good idea to verify this by running the following.

# systemctl status apache2

If this command shows that the service is active and running, then we’re good to go. If not, it indicates that there is a problem with apache configuration. When these problems do occur, it is most likely that the port 80 is being used by another service (you’ll have to kill that service for this to work), or there is some bad configuration file for apache (you’ll have to check the config or purge the existing config and restart again).

If everything works as expected, do a curl request to http://127.0.0.1/ and it should return the default page.

$ curl http://127.0.0.1/ 

Installing Mariadb

Mariadb is the open source fork of Mysql. It is available in Ubuntu repositories with the name mariadb-server.

# apt install mariadb-server

Start the service and enable it to start the service during boot.

# systemctl start mysql
# systemctl enable mysql

Optionally run mysql_secure_installation to keep secure settings for Mysql.

Once done, run sudo mysql -u root to drop into a SQL shell. We need to create a user for accessing the DB from Laravel and also create a database for our application.

mysql> CREATE DATABASE laravel_db_name;
mysql> GRANT ALL ON laravel_db_name.* TO LARAVEL_USER'@'localhost' IDENTIFIED BY 'RANDOM_LARAVEL_PASSWORD';

Installing PHP and other required PHP extensions

By default, Ubuntu 20.04 repositories contain php 7. For the sake of future proofing our installation, let’s proceed with php 8. To add php 8 to our repositories, we need to add the php apt repository to our sources.

# add-apt-repository ppa:ondrej/php
# apt install software-properties-common
# apt update -y

For our Laravel application to run properly, it depends on a few extensions on top of PHP. Use the following command to install PHP and the extensions.

# apt install php8.0 php8.0-cli php8.0-common php8.0-curl php8.0-gd php8.0-mbstring php8.0-intl php8.0-mysql php8.0-xml php8.0-zip php8.0-fpm libapache2-mod-fcgid unzip zip

Install PHP Composer using the following command. We need composer for installing our laravel dependancies.

# php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"
# php -r "if (hash_file('sha384', 'composer-setup.php') === '906a84df04cea2aa72f40b5f787e49f22d4c2f19492ac310e8cba5b96ac8b64115ac402c8cd292b8a03482574915d1a8') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;"
# php composer-setup.php
# php -r "unlink('composer-setup.php');"
# mv composer.phar /usr/local/bin/composer

Now we have php, composer and our extensions. Let’s verify this by running the below command. This should list the version of composer. Before this, open a new SSH session on the instance for using without sudo. Now we have one shell to run our sudo commands (ones that start with # ) and another shell to run normal commands (ones that start with $).

$ composer --version

Deploy the application code

We will be using /var/www/html as our application root and will be deploying our code in that folder.

If your application code is in any remote git repository, you can simply use git to download the code by cloning the repo. For example, if your remote git repository is https://github.com/aayush420/bare-laravel8-application, you can clone the repo to /var/www/html using the following command.

$ cd /var/www
$ git clone https://github.com/aayush420/bare-laravel8-application html

This clones the repo to /var/www/html.

If your application code is not in git, you will have to transfer the code to your instance using scp or any other remote copy tool.

Set correct permissions to your webroot.

# chown -R www-data:www-data /var/www/html
# chmod -R 775 /var/www/html/storage
# usermod -a -G www-data ubuntu
# find /var/www/html -type f -exec chmod 644 {} \;
# find /var/www/html -type d -exec chmod 775 {} \;
# cd /var/www/html
# chgrp -R www-data storage bootstrap/cache
# chmod -R ug+rwx storage bootstrap/cache

Now that our code is on the instance, we first need to install our composer dependencies and set app_key in .env file.

$ cd /var/www/html
$ composer install --no-dev
$ cp .env.example .env
$ php artisan key:generate

Edit your .env file to contain your app_name, and your newly create database credentials. Below are a few values that needs to be changed. Consider these as a starting point and edit other variables as per your convinience.

APP_NAME="OUR_LARAVEL_APPLICATION_NAME"
APP_ENV=production
...
omitted for brevity
...
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel_db_name
DB_USERNAME=LARAVEL_USER
DB_PASSWORD=RANDOM_LARAVEL_PASSWORD
...
omitted for brevity
...

Note: Do not include the “omitted for brevity” string and … in your .env file. Change the required variables only.

Migrate your table schema using the artisan commands as shown below.

$ php artisan migrate:fresh --seed

Point Apache to our Laravel app

Edit the virtualhost file /etc/apache2/sites-enabled/000-default.conf and paste the following content. This file needs to be edited as root or using sudo.

<VirtualHost *:80>
ServerAdmin your_email@gmail.com
DocumentRoot /var/www/html/public
<Directory "/var/www/html/public">
AllowOverride All
Options +FollowSymLinks -Indexes
Order allow,deny
Allow from all
</Directory>
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
ServerName www.yourservername.com
</VirtualHost>

This will ensure that our laravel application is served by Apache on port 80.

Once the file is written, we need to restart the service and our application will be up and running on port 80. It can be accessed at http://127.0.0.1:80/ or http://server_ip/.

At this point, our application is ready. You can stop here if needed. Upcoming sections describe how to add security on top of your application.

Installing Modsecurity

Modsecurity is available in Ubuntu repositories as libapache2-mod-security2 and can be installed using the following command.

# apt install libapache2-mod-security2 -y

Enable the headers mod for apache2.

# a2enmod headers rewrite php_fpm

Copy the recommended Modsecurity conf as the main configuration file.

# cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf

Edit /etc/modsecurity/modsecurity.conf and modify “SecRuleEngine DetectionOnly” to “SecRuleEngine On” for preventing malicious traffic instead of just detection.

Delete existing core ruleset and download the latest copy using the following set of commands.

# rm -rf /usr/share/modsecurity-crs
# git clone https://github.com/coreruleset/coreruleset /usr/share/modsecurity-crs
# mv /usr/share/modsecurity-crs/crs-setup.conf.example /usr/share/modsecurity-crs/crs-setup.conf
# mv /usr/share/modsecurity-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf.example /usr/share/modsecurity-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf

Edit /etc/apache2/mods-available/security2.conf and add the following content to the file.

<IfModule security2_module>
SecDataDir /var/cache/modsecurity
Include /usr/share/modsecurity-crs/crs-setup.conf
Include /usr/share/modsecurity-crs/rules/*.conf
</IfModule>

Enable SecRuleEngine in 000-default.conf virtualhost. Add the line “SecRuleEngine On” inside the <VirtualHost *:80></VirtualHost> block inside the file /etc/apache2/sites-enabled/000-default-le-ssl.conf.

Now, Modsecurity has been configured properly. All we need to is to restart apache and Modsecurity will be up and running.

# systemctl restart apache2

Installing Fail2ban

Now, let’s install fail2ban and enable it to monitor SSH & Apache connections. Fail2ban scans log files (e.g. /var/log/apache/error_log) and bans IPs that show the malicious signs — too many password failures, seeking for exploits, etc. Generally Fail2Ban is then used to update firewall rules to reject the IP addresses for a specified amount of time, although any arbitrary other action (e.g. sending an email) could also be configured. Out of the box Fail2Ban comes with filters for various services (apache, courier, ssh, etc).

Fail2Ban will be able to reduce the rate of incorrect authentications attempts however it cannot eliminate the risk that weak authentication presents. Configure services to use only two factor or public/private authentication mechanisms if you really want to protect services.

# apt install fail2ban
# cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local

Edit the file /etc/fail2ban/jail.local and replace the default [apache-auth] rule with the following.

[apache-auth]
enabled = true
port = http,https
logpath = /var/log/apache2/error.log
maxretry = 3
findtime = 600
bantime = 1h

Also, enable the rules [apache-badbots], [apache-botsearch], [apache-modsecurity], [php-url-fopen] by adding the line enabled = true inside those rule blocks. Increase the bantime to 172800 by adding the line bantime = 172800 inside the [apache-modsecurity] rule block.

Start the Fail2ban service and enable it to add it to the startup as shown in the following code block.

# systemctl restart fail2ban
# systemctl enable fail2ban

To verify that fail2ban is running, check it with the following command.

# fail2ban-client status

Now, we have everything working. To test whether fail2ban is working on not, try sending 10 continous requests to the page /some_random_page.php?exec=/etc/passwd. If the first 2 requests give a 404 error and the remaining show a 403 error, that would show that the configuration is working. To reassure that fail2ban is working properly, try the following.

# grep 'Ban' /var/log/fail2ban.log
# iptables -L

If these commands show the banned IPs, then everything is working properly.

That’s it. Congratulations 🎊🎉. We’ve successfully deployed our laravel application and also added security on top of our application. We’re blocking malicious payloads from executing and also blocking those IPs for a while before they could do anything more malicious.

Featured image

--

--