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