Virtualmin LAMP Server (Ubuntu)

Building on the basic Ubuntu Cloud Server (with Emerging Threats Protection) we will create an all-in-one internet hosting server using the Virtualmin web hosting control panel.

Latest Apache2, PHP and OpenSSL

To make sure we stay up-to-date with the latest versions of Apache Web Server, PHP and OpenSSL, we will install PPAs (personal package archive) by Ondřej Surý, who is a Debian developer and an important figure in the DNS community. He maintains many packages for Debian repository, including Apache, BIND, MariaDB, PHP etc. He is also one of the maintainers of the official certbot PPA. So I have trust in his PPAs and use them on my servers.

add-apt-repository ppa:ondrej/apache2


Latest PHP

add-apt-repository ppa:ondrej/php


Reload Package Index

apt update


Download & Install Virtualmin

cd /tmp
wget http://software.virtualmin.com/gpl/scripts/install.sh
chmod +rx /tmp/install.sh

Before running the installation script, decide if you will locally host mail for end users (including IMAP/POP clients). The minimal install will exclude the full mail processing stack (SpamAssassin and ClamAV). This will save about 1GB of system resources.

If you will NOT host email, then do a minimal install

/tmp/install.sh --minimal


If you WILL host email, then do the standard full install

/tmp/install.sh

Verify the installation completed successfully.

Setup Firewall

Configure Fail2Ban

virtualmin config-system --include Fail2banFirewalld


Enable Fail2Ban

systemctl enable fail2ban ; systemctl restart fail2ban


Move MySQL Data Directory

I like to locate the MySQL data under the /home directory so everything for Virtualmin is under a single path. This can be helpful if you later need to add disk space my moving /home to a separate drive.

Stop the MySQL service

service mysql stop


Create new data directory

mkdir /home/mysql
chown mysql:mysql /home/mysql


Define new data directory in MySQL config file

cp /etc/mysql/mysql.conf.d/mysqld.cnf /etc/mysql/mysql.conf.d/mysqld.cnf.save
sed -i '/datadir/d' /etc/mysql/mysql.conf.d/mysqld.cnf
echo "datadir = /home/mysql" >> /etc/mysql/mysql.conf.d/mysqld.cnf


Update AppArmor

echo "alias /var/lib/mysql/ -> /home/mysql/," >> /etc/apparmor.d/tunables/alias
service apparmor restart


Initialize New Data Directory (don’t worry, we will add a password during the Virtualmin setup later)

mysqld --initialize-insecure


Start MySQL

service mysql start


Create SQL System Maintenance User. (This will include a new longer random password for the maintenance user)

RANDOM1=`< /dev/urandom tr -dc '[:alnum:]' | head -c${1:-64}`
cp /etc/mysql/debian.cnf /etc/mysql/debian.cnf.bak
cat > /etc/mysql/debian.cnf <<EOF
# Automatically generated for Debian scripts. DO NOT TOUCH!
[client]
host     = localhost
user     = debian-sys-maint
password = $RANDOM1
socket   = /var/run/mysqld/mysqld.sock
[mysql_upgrade]
host     = localhost
user     = debian-sys-maint
password = $RANDOM1
socket   = /var/run/mysqld/mysqld.sock
basedir  = /usr
EOF
echo "CREATE USER 'debian-sys-maint'@'localhost' IDENTIFIED BY '$RANDOM1';" | mysql -u root
echo "GRANT ALL PRIVILEGES ON *.* TO 'debian-sys-maint'@'localhost';" | mysql -u root
echo "GRANT PROXY ON ''@'' TO 'debian-sys-maint'@'localhost' WITH GRANT OPTION;" | mysql -u root


Create Random Password for SQL root user

RANDOM1=`< /dev/urandom tr -dc '[:alnum:]' | head -c${1:-32}`
echo "ALTER USER 'root'@'localhost' IDENTIFIED BY '$RANDOM1';" | mysql -u root
echo "
MySQL root password has been set to $RANDOM1
Please record password for use later in the setup 
and save in a password manager."


Apache Modifications

Enable mod_rewrite

a2enmod rewrite
service apache2 restart


Customization of HTTP request and response headers

cat > /etc/apache2/conf-available/security.conf <<EOF
ServerTokens Prod
ServerSignature Off
TraceEnable Off
Header unset ETag
FileETag None
Header set X-XSS-Protection "1; mode=block"
Header set X-Content-Type-Options nosniff
EOF


Enable the mod_headers

a2enmod headers
service apache2 restart


Instruct browsers to allow cacheable content to be fetched from the browser’s cache for up to a week

cat > /etc/apache2/mods-available/expires.conf <<EOF
<IfModule mod_expires.c>
    ExpiresActive On 
    ExpiresDefault "access plus 1 week"
</IfModule>
EOF


Enable the mod_expires

a2enmod expires
service apache2 restart


Allow output from your server to be compressed before being sent to the browser

cat > /etc/apache2/mods-available/deflate.conf <<EOF
<IfModule mod_deflate.c>
<IfModule mod_filter.c>
AddOutputFilterByType DEFLATE application/javascript
AddOutputFilterByType DEFLATE application/rss+xml
AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
AddOutputFilterByType DEFLATE application/x-font
AddOutputFilterByType DEFLATE application/x-font-opentype
AddOutputFilterByType DEFLATE application/x-font-otf
AddOutputFilterByType DEFLATE application/x-font-truetype
AddOutputFilterByType DEFLATE application/x-font-ttf
AddOutputFilterByType DEFLATE application/x-javascript
AddOutputFilterByType DEFLATE application/xhtml+xml
AddOutputFilterByType DEFLATE application/xml
AddOutputFilterByType DEFLATE font/opentype
AddOutputFilterByType DEFLATE font/otf
AddOutputFilterByType DEFLATE font/ttf
AddOutputFilterByType DEFLATE image/svg+xml
AddOutputFilterByType DEFLATE image/x-icon
AddOutputFilterByType DEFLATE text/css
AddOutputFilterByType DEFLATE text/html
AddOutputFilterByType DEFLATE text/javascript
AddOutputFilterByType DEFLATE text/plain
AddOutputFilterByType DEFLATE text/xml
</IfModule>
</IfModule>
EOF


Enable mod_deflate

a2enmod deflate
service apache2 restart


Install additional PHP Packages

apt install php8.0 php8.0-{bcmath,bz2,cgi,cli,common,curl,fpm,gd,igbinary,imagick,mbstring,memcached,mysql,opcache,readline,redis,xml,zip} php php-{bcmath,bz2,cgi,cli,common,curl,fpm,gd,igbinary,imagick,json,mbstring,memcached,mysql,pear,readline,redis,xml,zip}


Enable new PHP modules

phpenmod bcmath bz2 curl gd igbinary imagick mbstring memcached opcache readline redis xml zip


Virtualmin Post-Installation Wizard

From a web browser, log in to the Virtualmin console at port 10000, using the root user credentials, and complete the Post-Installation Wizard. (https://myserver:10000)

Select System Settings on the left menu, then select the Re-check and refresh configuration button (this may fail if the system was not rebooted after installation of Virtualmin).

Disable Unnecessary Services

If you plan to host DNS elsewhere, disable Bind DNS

systemctl mask bind9


If you plan to host email elsewhere, disable Dovecot Mail Server

systemctl mask dovecot


Disable Proftp server (I strongly encourage the use of ssh-based sftp instead of ftp/ftps)

systemctl mask proftpd


Harden Email Encryption


Create Diffie-Hellman Key Pairs

openssl dhparam -out /etc/ssl/dhparam.pem 2048


Create Initial Self-Signed Postfix Cert

touch ~/.rnd
openssl req -new -x509 -nodes -out /etc/ssl/postfix.pem -keyout /etc/ssl/postfix.key -days 3650 -subj "/C=US/O=$HOSTNAME/OU=Email/CN=$HOSTNAME"


Configure Email SSL/TLS

postconf -e tls_medium_cipherlist=ECDH+AESGCM+AES128:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES128:ECDH+AES:DHE+AES128:DHE+AES:RSA+AESGCM+AES128:RSA+AESGCM:\!aNULL:\!SHA1:\!DSS
postconf -e tls_preempt_cipherlist=yes
postconf -e smtpd_use_tls=yes
postconf -e smtpd_tls_loglevel=1
postconf -e smtpd_tls_security_level=may
postconf -e smtpd_tls_auth_only=yes
postconf -e smtpd_tls_protocols=\!SSLv2,\!SSLv3,\!TLSv1,\!TLSv1.1
postconf -e smtpd_tls_ciphers=medium
postconf -e smtpd_tls_mandatory_protocols=\!SSLv2,\!SSLv3,\!TLSv1,\!TLSv1.1
postconf -e smtpd_tls_mandatory_ciphers=medium
postconf -e smtpd_tls_cert_file=/etc/ssl/postfix.pem
postconf -e smtpd_tls_key_file=/etc/ssl/postfix.key
postconf -e smtpd_tls_dh1024_param_file=/etc/ssl/dhparam.pem
postconf -e smtp_use_tls=yes
postconf -e smtp_tls_loglevel=1
postconf -e smtp_tls_security_level=may
postconf -e smtp_tls_protocols=\!SSLv2,\!SSLv3,\!TLSv1,\!TLSv1.1
postconf -e smtp_tls_ciphers=medium
postconf -e smtp_tls_mandatory_protocols=\!SSLv2,\!SSLv3,\!TLSv1,\!TLSv1.1
postconf -e smtp_tls_mandatory_ciphers=medium
postconf -e smtp_tls_cert_file=/etc/ssl/postfix.pem
postconf -e smtp_tls_key_file=/etc/ssl/postfix.key
systemctl restart postfix


Restrict Mail protocols

If you are not using the Virtualmin’s mail services, then let’s lock down the Postfix SMTP server so it cannot be an attack target. We cannot disable it completely as it will be needed to send outbound email from your server. We configure it so connections are only accepted from the server itself.

postconf -e inet_interfaces=127.0.0.1
systemctl restart postfix


SSL Security

The default SSL/TLS configuration for Apache lacks good security. These changes will apply Best Industry Practices and pass compliance checking for PCI, HIPAA, and NIST standards.

Create New Apache SSL Config File

cp /etc/apache2/mods-available/ssl.conf /etc/apache2/mods-available/ssl.conf.save
cat > /etc/apache2/mods-available/ssl.conf <<EOF
<IfModule mod_ssl.c>
SSLRandomSeed startup builtin
SSLRandomSeed startup file:/dev/urandom 512
SSLRandomSeed connect builtin
SSLRandomSeed connect file:/dev/urandom 512
AddType application/x-x509-ca-cert .crt
AddType application/x-pkcs7-crl .crl
SSLPassPhraseDialog exec:/usr/share/apache2/ask-for-passphrase
SSLSessionCache         shmcb:${APACHE_RUN_DIR}/ssl_scache(512000)
SSLSessionCacheTimeout  300
SSLProtocol -All +TLSv1.2 +TLSv1.3
SSLCipherSuite ECDH+AESGCM+AES128:ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES128:ECDH+AES:!aNULL:!SHA1:!AESCCM
SSLCipherSuite TLSv1.3 TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
SSLHonorCipherOrder On
SSLOpenSSLConfCmd DHParameters "/etc/ssl/dhparam.pem"
SSLUseStapling on
SSLStaplingCache "shmcb:logs/stapling-cache(150000)"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
</IfModule>
EOF


Modify Apache2 Config File

sed -i '/SSLProtocol/D' /etc/apache2/apache2.conf
sed -i '/SSLCipherSuite/D' /etc/apache2/apache2.conf


Restart Apache

service apache2 restart


Setup Default Apache Site to Block IP URL Requests

a2dissite 000-default
openssl req -new -x509 -nodes -out /etc/ssl/snakeoil.pem -keyout /etc/ssl/snakeoil.key -days 3650 -subj '/CN=*'
VirtualHost1=""
VirtualHost2=""
ServerName="ServerName"
ServerAlias="ServerAlias"
AliasFlag=0
for i in `hostname -I`
do
VirtualHost1="$VirtualHost1 $i:80"
VirtualHost2="$VirtualHost2 $i:443"
if [ $AliasFlag = 0 ] ; then
        ServerName="$ServerName $i"
        AliasFlag=1
else
        ServerAlias="$ServerAlias $i"
        AliasFlag=2
fi
done
if [ $AliasFlag = 1 ] ; then
        ServerAlias=""
fi
cat > /etc/apache2/sites-available/000-default.conf << EOF
<VirtualHost $VirtualHost1>
$ServerName
$ServerAlias
DocumentRoot /var/www/html/
RedirectMatch 400 /(.*)\$
ErrorLog /var/log/apache2/default_error_log
CustomLog /var/log/apache2/default_access_log combined
</VirtualHost>
<VirtualHost $VirtualHost2>
$ServerName
$ServerAlias
DocumentRoot /var/www/html/
RedirectMatch 400 /(.*)\$
ErrorLog /var/log/apache2/default_error_log
CustomLog /var/log/apache2/default_access_log combined
SSLEngine on
SSLCertificateFile /etc/ssl/snakeoil.pem
SSLCertificateKeyFile /etc/ssl/snakeoil.key
SSLCACertificateFile /etc/ssl/snakeoil.pem
</VirtualHost>
EOF
a2ensite 000-default
service apache2 restart


Finished – Reboot

reboot


NOTE: After adding a new SSL enabled virtual server in Virtualmin, execute these commands to make sure the new server stays compliant and doesn’t override the SSL Protocol and SSL Ciphersuite that is defined by Apache SSL Config File.

sed -i '/SSLProtocol/D' /etc/apache2/sites-available/*
sed -i '/SSLCipherSuite/D' /etc/apache2/sites-available/*
service apache2 restart
Scroll to Top