5/5 - (1 vote)

Introduction

Greetings readers! As part of the current series of articles, I talk about how to set up a server for simple projects. This means a server for several sites, with a small load under the most popular CMS such as Bitrix. The main purpose of the article is to point out the mistakes made by junior specialists when performing such a setup. Also point out some things that will make troubleshooting easy and convenient.

It’s not really a LEMP stack as it uses Apache2, but you can use PHP-FPM instead if the developer doesn’t mind implementing such a solution.

In the comments to articles, I often see messages that Apache2 is no longer relevant and other software can be upgraded instead. From myself I can say that until now a large number of small and medium-sized organizations that stand up for maintenance use Apache2 and .htaccess files, so I do not agree with this statement. But then again, if you are an experienced administrator who understands how this bundle works, you can skip this article and bring up what your heart desires.

The article was not written with the aim of taking and mindlessly copying all the commands and getting a ready-made server to host the site. Also, if your particular company uses a different stack, I’m very happy for you, but this does not negate the fact that the software described in this article is still popular, used and administered without any problems.

The previous parts of the articles are available at the following links:

I recommend that you read this article before reading.

Well, let’s get started.

In Parts 1 and 2 of the article series, we did the following:

  • Installed base packages
  • Set up git autocommit to commit changes to system configurations
  • Performed basic configuration of exim4, ssh, ftpd
  • Gave the appropriate rights to the administrator to administer the server
  • Performed basic configuration of Apache2, Nginx, MySQL.

The current article will cover:

  • Installing and configuring PHP
  • Create virtual hosts for sites
  • Setting up mail sending
  • Giving developers access to the platform

Installing and configuring PHP

Let’s start installing PHP. Our site will be based on PHP 7.4 , however you can deploy any version you require.

Install the packages needed to install the gpg key:

apt -y install lsb-release apt-transport-https ca-certificates

Download the sury repository key and add it to apt :

wget -O /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg
echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | tee /etc/apt/sources.list.d/php.list

You can proceed to install PHP and its core modules:

apt-get update && apt-get install php7.4 php7.4-cli php7.4-common php7.4-curl php7.4-gd php7.4-geoip php7.4-imagick php7.4-imap php7.4-intl php7.4-mcrypt php7.4-mysql php7.4-apc

After completing all the steps, check the PHP version:

php -v

PHP 7.4.27 (cli) (built: Dec 20 2021 21:32:33) ( NTS )
Copyright (c) The PHP Group
Zend Engine v3.4.0, Copyright (c) Zend Technologies
    with Zend OPcache v7.4.27, Copyright (c), by Zend Technologies

We also check the currently installed PHP modules:

php -m

[PHP Modules]
apc
apcu
calendar
Core
ctype
curl
date
exif
FFI
fileinfo
filter
ftp
gd
geoip
gettext
hash
iconv
imagick
imap
intl
json
libxml
mcrypt
mysqli
mysqlnd
openssl
pcntl
pcre
PDO
pdo_mysql
Phar
posix
readline
Reflection
session
shmop
sockets
sodium
SPL
standard
sysvmsg
sysvsem
sysvshm
tokenizer
Zend OPcache
zlib

[Zend Modules]
Zend OPcache

You can always install additional modules for your project, here we are talking only about basic modules.

To view the available PHP modules that can be installed via apt , use the command:

apt-cache search 'php7.4'

If the modules you need are not in the sury repository , you can install them via pecl.

Let’s start setting the basic PHP parameters. We have two environments cli and apache2 (in our case), each environment has its own php.ini file:

/etc/php/7.4/apache2/php.ini
/etc/php/7.4/cli/php.ini

The php.ini file located in apache2 is responsible for the PHP settings used when passing scripts to Apache2 for processing , in our case these will be the settings when clients use the site on the site.

The php.ini file located in the cli is used when executing scripts from the console and cron tasks when calling scripts through the php handler.

We will be making changes to both files. The standard changes for basic PHP work are as follows:

-short_open_tag = Off
+short_open_tag = On

-post_max_size = 8M
+post_max_size = 128M

-upload_max_filesize = 2M
+upload_max_filesize = 64M

-;date.timezone = 
+date.timezone = Europe/Moscow

-session.gc_probability = 0
+session.gc_probability = 10

-session.gc_divisor = 1000
+session.gc_divisor = 100

-session.gc_maxlifetime = 1440
+session.gc_maxlifetime = 14400

I will not dwell on these settings in detail, since they are all described in sufficient detail in the PHP manual.

For PHP to work with the minimum possible permissions in the system, you will need to assign the following rights to directories:

chmod -R o-rwx /etc/php/7.4
chmod 751 /etc/php/7.4
chmod -R o+rX /etc/php/7.4/cli
chmod -R o+rX /etc/php/7.4/apache2
chmod -R o+rX /etc/php/7.4/mods-available

At this point, the installation and basic configuration of PHP can be considered complete.

All the minimum necessary components for the site to work have been installed, now we can start creating the site.

Create virtual hosts

In our configuration, each server site will work from its own system user. With this setting, the sites will be isolated from each other, respectively, when accessing the site code if any vulnerability is detected, an attacker will not be able to access the rest of the server sites, as if all sites were working from one user.

Let’s create a system user:

groupadd -g 10002 DOMAIN_NAME
useradd -g 10002 -u 10002 -s /bin/bash -d /var/www/DOMAIN_NAME DOMAIN_NAME

Where DOMAIN_NAME is the site name, for example site.ru .

Next, let’s create a site directory, according to the standards, all sites will be located in /var/www :

mkdir -p /var/www/DOMAIN_NAME

After that, let’s go to the site directory and create a directory for the site files:

cd /var/www/DOMAIN_NAME
mkdir data

Next, we will also create a directory for temporary files, a directory for storing logs, and a directory for storing session files:

mkdir log sess tmp upload log/apache2 log/nginx

After we assign the site the rights and the owner. In our case, file permissions are standard 644 , directory permissions are 755 , while the permissions on the main directory /var/www/DOMAIN_NAME will be 751 , to limit users who are not owners and are not part of the site group:

chown -R DOMAIN_NAME: /var/www/DOMAIN_NAME
chmod 751 /var/www/DOMAIN_NAME
chmod -R o-rwx /var/www/DOMAIN_NAME/*
chmod o+x data log log/nginx

After all the necessary directories have been created, you will need to create Nginx and Apache2 virtual hosts , let’s start with the second one.

Create an Apache2 virtual host file . Create a virtual host file:

touch /etc/apache2/sites-available/DOMAIN_NAME.conf

In our case, its content will look like this:

  	ServerAdmin webmaster@DOMAIN_NAME
    	DocumentRoot /var/www/DOMAIN_NAME/data
    	ServerName DOMAIN_NAME
    	ServerAlias www.DOMAIN_NAME
 
    	AssignUserId DOMAIN_NAME DOMAIN_NAME
 
    	php_admin_value 	session.save_path           	"/var/www/DOMAIN_NAME/sess"
        php_admin_value 	upload_tmp_dir              	"/var/www/DOMAIN_NAME/upload"
        php_admin_value 	open_basedir                	"/var/www/DOMAIN_NAME:."
 
    	CustomLog /var/www/DOMAIN_NAME/log/apache2/access.log combined
    	ErrorLog /var/www/DOMAIN_NAME/log/apache2/error.log
    	LogLevel error
 
    	<Directory "/var/www/DOMAIN_NAME/data">
            	AllowOverride  All
            	Options FollowSymLinks
            	Order allow,deny
            	Allow from all
        

The AssignUserId parameter is responsible for the operation of the web server from the user , in which we specify the name and group of the site user. Since the username and site name are identical, in all cases it will be the name of the site.

Here we set the directory of session files, temporary files and the base directory that restricts web server calls for a specific site:

        php_admin_value     session.save_path               "/var/www/DOMAIN_NAME/sess" 
        php_admin_value     upload_tmp_dir                  "/var/www/DOMAIN_NAME/upload" 
        php_admin_value     open_basedir                    "/var/www/DOMAIN_NAME:."

I would like to note right away that PHP parameters can be set in three places, since this question causes a lot of problems at an interview for a junior vacancy :

  • in the first case, the PHP parameters are set in the site’s .htaccess files; the PHP parameters set here will only be valid for the directory in which such a file is located. Parameters have normal precedence.
  • in the second case, you can set the PHP parameters inside the php.ini file for the appropriate environment, as we did above. These options will also have normal precedence. In this case, if scripts are accessed, then the parameters specified in .htaccess will have a higher priority than the parameters specified in php.ini
  • in the third case, you can set parameters at the Apahce2 virtual host level , such parameters will only apply to the php environment for Apache2, respectively , that is, they will only be applied to http / https requests. At the same time, if you set parameters through the php_value function , then such parameters will be equal in priority to the parameters set in php.ini , but they will be higher relative to them. If the PHP parameter set inside the virtual host is set via php_admin_value, then it will have the highest priority for the apahche2 environment. That is, the parameter set in php_admin_valuewill ignore all identical parameters set elsewhere ( htaccess or php.ini ) Using  php_admin_value allows you to set specific settings for a particular site. Most often, directories are set through this function or, for example, the function of sending mail, for a specific site.

After creating the Apache2 virtual host , it must be applied using the command:

a2ensite DOMAIN_NAME

This command will create a symbolic link from the /sites-available directory to the /sites-enabled directory

After that, be sure to test the Aapche2 syntax for errors and re-read the web server configuration files:

Apache2ctl -t
Service apache2 reload

After creating a virtual host for Apache2 , let’s start creating a virtual host for Nginx :

Create a virtual host file:

touch /etc/nginx/sites-available/DOMAIN_NAME

In the basic configuration, such a file will have the following content:

server {
        listen   80;
            	server_name DOMAIN_NAME www.DOMAIN_NAME;
 
    	access_log /var/www/DOMAIN_NAME/log/nginx/access.log nixys;
    	error_log /var/www/DOMAIN_NAME/log/nginx/error.log;
 
    	location ~ /\.(svn|git|hg) {
            	deny all;
    	}
 
    	location ~* ^.+\.(css|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|js|swf)$ {
 	           root /var/www/DOMAIN_NAME/data;
            	expires max;
            	access_log   off;
    	}
 
    	location / {
            	proxy_pass     	http://127.0.0.1:81; # 	apache
            	proxy_redirect 	off;
 
            	proxy_set_header   Host           	$host;
            	proxy_set_header   X-Real-IP      	$remote_addr;
            	proxy_set_header   X-Forwarded-For	$remote_addr;
            	proxy_set_header   X-Forwarded-Proto  $scheme;
 
            	client_max_body_size   	10m;
            	client_body_buffer_size	1280k;
 
            	proxy_connect_timeout  	90;
            	proxy_send_timeout     	90;
            	proxy_read_timeout     	90;
 
            	proxy_buffer_size      	4k;
            	proxy_buffers          	4 32k;
            	proxy_busy_buffers_size	64k;
            	proxy_temp_file_write_size 64k;
    	}
}

View location :

    	location ~* ^.+\.(css|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|pdf|ppt|txt|tar|mid|midi|wav|bmp|rtf|js|swf)$ {
            	root /var/www/DOMAIN_NAME/data;
            	expires max;
            	access_log   off;
    	}

Responsible for handling static at the Nginx level . The location lists the file formats for processing, if there are statics of other formats among the files of the site, you just need to add the format to the location and re-read the web server configuration.

In the previous article, I was asked how the processing of statics at the Nginx level and the transfer of PHP requests to Apache2 will be separated , due to this location , such requests are transferred.

All other requests will be proxied to Apache2 , namely to server port 81 :

                proxy_pass         http://127.0.0.1:81; #     apache

This completes the Nginx virtual host setup . Let’s create a virtual host symbolic link in sites-enabled :

ln -s /etc/nginx/sites-available/DOMAIN_NAME /etc/nginx/sites-enabled/DOMAIN_NAME

Let’s test the Nginx configuration and reread it:

nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
service nginx reload

After that, be sure to check that the web server is running

service nginx status

The creation of a basic virtual host for Nginx can be considered complete.

We also need to create a database for the future site, for this we go to the MySQL management console and create a database and a user for it:

mysql
CREATE DATABASE DOMAIN_NAME_db CHARSET utf8;
GRANT ALL ON DOMAIN_NAME_db.* TO 'DOMAIN_NAME_usr'@'localhost' IDENTIFIED BY 'XXXXXXXXXXXXXX';
FLUSH PRIVILEGES;

In order to avoid confusion among a large number of databases, we recommend creating a production database for the site according to the following principle, if the site has the site.ru domain name , then the database name should be site_db , and the user name should be site_usr . The password in this case is a random sequence of characters from 15 pieces , we released it earlier in the previous parts of the article.

Providing access to site files to developers

After performing all the operations, it is necessary to give our site user the ability to connect via SSH and FTP :

To issue SSH access, you will need to add a username to the  /etc/ssh/sshd_config file , section

AllowUsers DOMAIN_NAME

After, restart SSH

/etc/init.d/ssh restart

And check the status:

/etc/init.d/ssh status

Next, let’s start configuring FTP , since we configured Vsftpd earlier , we just need to add the site username in the system to the files:

/etc/vsftp/vsftpd.chroot_list
/etc/vsftp/vsftpd.user_list

After we create a file with a description of user access:

touch /etc/vsftp/vsusers/DOMAIN_NAME

Containing lines:

chroot_local_user=YES

local_root=/var/www/DOMAIN_NAME
local_umask=022

After, restart the FTP server

/etc/init.d/vsftpd restart

And check the job status:

/etc/init.d/vsftpd status

After completing all the steps, you need to create a password for the site user:

  • generate a password of 15 characters:
pwgen 15 1
  • assign this password to the site user in the system:
passwd DOMAIN_NAME

A mandatory point for setting up access is to check their correctness, first, let’s go to our machine and check the ability to connect via SSH :

SSH EXTERNAL_IP@DOMAIN_NAME

If the connection went correctly, then there are no problems and you can check the connection via FTP , for these purposes, the easiest way is to use software with a graphical interface, for example FileZilla , install the program on any OS, enter the connection details and check. If the connection fails for some reason, then you probably made a mistake somewhere earlier.

Setting up sending mail from the server

After creating a site and issuing access, we proceed to setting up sending mail from the server. The setup can be considered complete, after receiving the highest spam filter score, the most common SPAM filter at the moment is SpamAssassin .

It is possible to test sending mail from the server using the www.mail-tester.com website , which will analyze your letter and provide all the necessary information and shortcomings. The goal of our setting will be to get 10 points on this resource.

To correctly send mail, we need to create a number of records in the DNS zone of the domain, namely DKIM (public key), SPF, DMARC . You will also need to ask the data center support to specify a PTR record in the reverse zone, or do it yourself if the functionality of your DC provides for such a function.

  • Let’s start with DKIM :

To release the DKIM key , we need to install the opendkim-tools package, the package is available in the standard repositories, no additional steps are required to install it

apt update
apt install opendkim-tools

Next, let’s create a directory in Exim to store all of our DKIM keys:

mkdir /etc/exim4/dkim

And let’s go to it:

cd /etc/exim/dkim

Next, we generate a public and private DKIM key:

opendkim-genkey -D /etc/exim4/dkim/ -d DOMAIN.RU -s DKIM_SELECTOR -b 1024

where  DKIM_SELECTOR  is a pointer to look up the public part of the key (usually  mail ).

In the current directory, two files will be created mail.private (private key) and mail.txt (public key, which will need to be registered in the DNS zone of the domain. For convenience and further use in the exim4 configuration , rename both files:

mv /etc/exim4/dkim/mail.private /etc/exim4/dkim/DOMAIN.RU.key
mv /etc/exim4/dkim/mail.txt /etc/exim4/dkim/DOMAIN.RU.txt

For security reasons, we change the rights to parts of the key and the storage directory:

chown -R root:Debian-exim /etc/exim4/dkim
chmod 750 /etc/exim4/dkim
chmod 640 /etc/exim4/dkim/DOMAIN.RU.key
chmod 640 /etc/exim4/dkim/DOMAIN.RU.txt

Now that we have created the keys, we need to specify the location of the exim4 configuration , for this we add the following lines to the beginning of the /etc/exim4/exim4.conf.template file :

# DKIM settings 

DKIM_DOMAIN                     = ${lc:${domain:$h_from:}}
DKIM_FILE                       = /etc/exim4/dkim/${lc:${domain:$h_from:}}.key
DKIM_PRIVATE_KEY                = ${if exists{DKIM_FILE}{DKIM_FILE}{0}}

We also add the following entries to the remote_smtp block in    the /etc/exim4/exim4.conf.template file :

remote_smtp:
    driver = smtp
    dkim_domain           = DKIM_DOMAIN
    dkim_selector         = $DKIM_SELECTOR
    dkim_private_key      = DKIM_PRIVATE_KEY

where  $DKIM_SELECTOR  must be substituted with the selector specified when the key was created. The DKIM_DOMAIN and DKIM_PRIVATE_KEY parameters are variables here, and have exactly the same form in the file as described in the article.

I would also like to clarify that the mail server will be able to work with only one DKIM selector in the future. Accordingly, if the signature for the first domain is specified as mail, then it must be the same for all subsequent domains for which the DKIM key will be generated.

After we need to add a TXT record to the DNS zone of the domain, the contents of the record are specified in the DOMAIN.RU.txt file . Recording example:

DKIM_SELECTOR._domainkey        IN      TXT     ( "v=DKIM1; h=sha256; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCWtRUr0hse/k2csH1KtA3YrM2hrSiDxyhRDEV53LvDxjcN8rH5913j0N/5P48kotw0tVKlyaW6x9sJJPs7fjCsuoSQNQUpwjJrPKjH/r7fTBESFMOx6SHMpIC57GadCCmQfGEiI0IHPG0zqakDNUKtLaWBc71BLdRAApsbZ97ooQIDAQAB" )  ; ----- DKIM key DKIM_SELECTOR for DOMAIN.RU

After we delete the key that is no longer needed:

rm -f /etc/exim4/dkim/DOMAIN.RU.txt

Restart exim4 and check its operation:

service exim4 restart
service exim4 status

DNS records are updated within 72 hours, but for most DNS servers, the record usually appears within an hour. As a test, we can check if the record exists on the Google DNS servers :

dig @8.8.8.8 +short -ttxt DKIM_SELECTOR._domainkey.DOMAIN.RU

If you receive a public key to the dig request , then all settings are correct. You can also send a letter to any mailbox for which incoming mail is not filtered. From the server, this is done using a command like:

echo "Hello there" | mail -s "world" -r test@DOMAIN_NAME address@recipient

An example of sending a letter for the domain site.ru to the mailbox [email protected] :

echo "Hello there" | mail -s "world" -r [email protected]  [email protected]

After you need to find the letter in the mailbox and open its full text version, if you see a line like this there:

   dkim=pass

This means that the message header was signed with a private key from the exim4 server side and decrypted using a public key registered in the DNS zone. In other words, everything works correctly.

After we start building SPF records. An SPF record is a record that points to those servers from which you can send mail for a given domain. I will not delve into the work of the SPF record, since our site only needs to send mail to a specific server, so we will create a TXT record in the DNS zone of our domain of the form

DOMAIN.RU. IN TXT "v=spf1 +a +mx ip4:EXTERNAL_IP ~all"

Where EXTERNAL_IP is the external IP address of our server, respectively.

An important point, in DNS zones it is also possible to specify an SPF record , this record is obsolete, in order to use an SPF record, it must have exactly the TXT format.

After specifying the SPF record, run the query using dig and check that everything is correct:

dig TXT DOMAIN_NAME

If your SPF record is among the received records, then everything was added correctly.

Next , let’s create a dmarc policy . It tells the receiving server what to do with the message if the DKIM and SPF records are incorrect. In our case, the minimum policy requirements will be specified using the following TXT record:

_dmarc.DOMAIN.RU. IN TXT "v=DMARC1; p=reject; aspf=r; sp=reject"

We do not recommend disabling the dmarc policy , as recommended by mail-tester , since with the policy disabled, messages may end up in SPAM in some mailboxes.

To check the dmarc indication, we use the command:

dig +short -ttxt _dmarc.DOMAIN.RU

After that, you will need to specify the PTR record. It is used when sending a letter, when the mail server is represented by some name. It should bind the domain specified in the primary_hostname of the exim4 configuration and the external IP of our server. Since in our case the primary_hostname parameter is set to the main domain name of the site, we need to specify a PTR record of the form:

DOMAIN.RU. IN TXT "EXTERNAL IP"

Where DOMAIN.RU is the domain name specified in the primary_hostname of the current exim4 server , and EXTERNAL IP is the external IP address of our server.

This entry can be specified in the DC control panel or, if such a function is not provided, ask to specify the entry through the data center support. The following Dig command is used to check the PTR record :

dig -x DOMAIN_NAME

As soon as all the records are reflected and specified, you can start sending a test letter to mail-tester, we send a letter using the already familiar command:

echo "Hello there" | mail -s "world" -r test@DOMAIN_NAME [email protected]

After all the settings done, we should get 10/10 points for the mail-tester service , which is the highest score.

Final steps

This completes the server setup. You can upload the site code to /var/www/DOMAIN_NAME/data (from the site user!), as well as place the site database in MySQL , specify the database details at the code level and check the site. However, before doing this, be sure to check the permissions and the owner of the files.

Be sure to give developers MySQL access to the site DB user , and SSH and FTP access to the site user on the system (DOMAIN_NAME) .

The advantage of the site working from the user is that code developers have access only to the files of the site and work from their user, while the sites are abstracted from each other at the system level, since each site has its own system user.

The articles already turned out to be quite cumbersome, and therefore I will not describe in detail the configuration of the domain via https, there is a lot of information on the network on how to attach an SSL certificate to Nginx . I can only say that if you need a free certificate, you can issue it using certbot on the server, and then, as an example, set up an unconditional redirect from http to https in the desired Nginx virtual host , for this, add in the virtual host at the beginning:

server {
        listen   80;
        
        server_name DOMAIN_NAME www.DOMAIN_NAME;
        return 301 https://DOMAIN_NAME$request_uri;
}

And below, edit the current configuration for port 80 , changing it to 443 and specifying the assembled certificate and key:

server {
        listen 443 ssl;

        server_name DOMAIN_NAME www.DOMAIN_NAME;

    ssl_certificate /etc/nginx/ssl/DOMAIN_NAME/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/DOMAIN_NAME/private.key;