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
chmod +rx /tmp/

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/ --minimal

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


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/
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!
host     = localhost
user     = debian-sys-maint
password = $RANDOM1
socket   = /var/run/mysqld/mysqld.sock
host     = localhost
user     = debian-sys-maint
password = $RANDOM1
socket   = /var/run/mysqld/mysqld.sock
basedir  = /usr
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

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"

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/
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

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_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=
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/
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 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"

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=*'
for i in `hostname -I`
VirtualHost1="$VirtualHost1 $i:80"
VirtualHost2="$VirtualHost2 $i:443"
if [ $AliasFlag = 0 ] ; then
        ServerName="$ServerName $i"
        ServerAlias="$ServerAlias $i"
if [ $AliasFlag = 1 ] ; then
cat > /etc/apache2/sites-available/000-default.conf << EOF
<VirtualHost $VirtualHost1>
DocumentRoot /var/www/html/
RedirectMatch 400 /(.*)\$
ErrorLog /var/log/apache2/default_error_log
CustomLog /var/log/apache2/default_access_log combined
<VirtualHost $VirtualHost2>
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
a2ensite 000-default
service apache2 restart

Finished – 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