A small hosting server with Virtualmin and Debian 12

For managing multiple websites without much traffic, we can create our own hosting solution. I have previously written on this topic, but as time passes, technologies evolve and the knowledge to be shared enriches. Thus, it’s time for a new iteration, with emphasis on security and reducing the attack surface.

I have previously performed similar tasks and described them in other articles:

We will use a DigitalOcean VPS and use as previously their recommendations they provide for setting up a server:

The code for the scripts has been published on this repository:

Create a sudo user

Probably most Linux users, regardless of their level, know that it’s a bad practice to use the root user for regular day to day operations. This is why we will start by setting up a sudo user. In our commands we will assume the user is sudouser, but maybe this is not the best name to use in real life 🙂

  • adduser sudouser
  • usermod -aG sudo sudouser
  • passwd -l root
  • su - sudouser

Setup the ssh private key

Assuming you already have a ssh key generated on your operating system, the next step will be to assign the public key to your newly created user.

On your own system, read the public key, which we will later add to the authorized keys of the server. The key is usually stored in the .ssh folder of your user and you can use multiple commands to get it or manually open the file. One such command is:

  • cat ~/.ssh/id_rsa.pub

On the server, run these commands (they are basic mainly to make what is happening clear):

  • mkdir ~/.ssh
  • touch ~/.ssh/authorized_keys
  • nano ~/.ssh/authorized_keys
  • paste the content of your key and save with Ctrl+X
  • chmod 700 ~/.ssh
  • chmod 400 ~/.ssh/id_rsa
  • chmod 600 ~/.ssh/authorized_keys

More information:

A script to create a new sudoer

We can automate this process with a bash script:

#!/bin/bash

# Check if the script is run as root
if [ "$(id -u)" != "0" ]; then
   echo "This script must be run as root" 1>&2
   exit 1
fi

# Prompt for the new username
read -p "Enter the new sudo user's name: " USERNAME

# Create a new user and add to sudo group
adduser --gecos "" $USERNAME
usermod -aG sudo $USERNAME

# Lock the root account
passwd -l root

# Setup SSH key for the new user
USER_HOME=$(eval echo ~$USERNAME)
mkdir -p $USER_HOME/.ssh
touch $USER_HOME/.ssh/authorized_keys

echo "Please paste the public SSH key. To retrieve it from your system, you can use the terminal command: 'cat ~/.ssh/id_rsa.pub'"
read SSH_KEY

# Check if the key already exists in the authorized_keys
if grep -qsF "$SSH_KEY" $USER_HOME/.ssh/authorized_keys; then
    echo "Key already exists in authorized_keys."
else
    echo "$SSH_KEY" >> $USER_HOME/.ssh/authorized_keys
    # Set permissions
    chown -R $USERNAME:$USERNAME $USER_HOME/.ssh
    chmod 700 $USER_HOME/.ssh
    chmod 600 $USER_HOME/.ssh/authorized_keys
    echo "SSH key added."
fi

echo "User $USERNAME created and configured."Code language: PHP (php)

You can quickly run this script on your system with:

  • wget -q -O - https://raw.githubusercontent.com/cristidraghici/debian-server-bash-scripts/master/add_sudoer_with_ssh.sh | sudo bash

Setup zsh

A nicer way than bash to interact with your system is zsh. We will install it globally and then make use of it for each of the users who want it. To install it on your system, run:

  • sudo apt-get install zsh

We will then create a script to enable antigen for any user who might want to use it with zsh, with the following content:

#!/bin/bash

# Script to install Zsh and Antigen for the current user

# Check the required utils
REQUIRED_UTILS=("git" "zsh")
for UTIL in "${REQUIRED_UTILS[@]}"; do
    if ! command -v $UTIL &> /dev/null; then
        echo "The required utility $UTIL is not installed. Please install it and rerun this script."
        exit
    fi
done

# Check if the script is run by a regular user, not root
if [ "$EUID" -eq 0 ]; then 
  echo "Please run as a regular user, not as root"
  exit
fi

# Define Antigen installation path
ANTIGEN_PATH="$HOME/.antigen"

# Check if Antigen is already installed
if [ -d "$ANTIGEN_PATH" ]; then
    echo "Antigen is already installed for user $(whoami)."
    exit
fi

# Create Antigen directory
mkdir -p "$ANTIGEN_PATH"

# Download Antigen
echo "Downloading Antigen..."
curl -L git.io/antigen > "$ANTIGEN_PATH/antigen.zsh"

# Update .zshrc with Antigen configuration
{
echo "# Antigen configuration"
echo "source $ANTIGEN_PATH/antigen.zsh"
echo "antigen use oh-my-zsh"

echo "antigen bundle git"
echo "antigen bundle command-not-found"
echo "antigen bundle zsh-users/zsh-syntax-highlighting"
echo "antigen bundle zsh-users/zsh-autosuggestions"

echo "antigen theme bira"
echo "antigen apply"
} >> "$HOME/.zshrc"

# Check if current shell is Zsh
if [[ "$SHELL" == *"/zsh" ]]; then
    echo "Antigen has been installed and configured for user $(whoami)."
    echo "Please restart your terminal or run 'source ~/.zshrc' to apply changes."
else
    echo "Antigen has been installed for user $(whoami), but the current shell is not Zsh."
    echo "Please switch to Zsh or change your default shell to Zsh."
fiCode language: PHP (php)

To create the script, run the following commands:

  • sudo nano /usr/local/bin/install_antigen # and paste the script shown above
  • sudo chmod a+x /usr/local/bin/install_antigen

You can quickly install this script on your system with:

  • sudo wget -O /usr/local/bin/install_antigen https://raw.githubusercontent.com/cristidraghici/debian-server-bash-scripts/master/install_antigen.sh && sudo chmod +x /usr/local/bin/install_antigen

To change the default shell of your user you can use sudo nano /etc/passwd and replace the default /bin/bash with /bin/zsh for them.

Initial server setup

The first thing we must do is update the server. For security reasons, it is crucial to keep your system as much to date as possible:

  • sudo apt-get update && sudo apt-get upgrade -y

More information about setting up the server:

Setup the locales

We will update the locales to en_US.UTF-8:

  • sudo locale-gen en_US.UTF-8
  • echo "LANG=en_US.UTF-8" | sudo tee /etc/default/locale
  • echo "LANGUAGE=en_US.UTF-8" | sudo tee -a /etc/default/locale
  • echo "LC_ALL=en_US.UTF-8" | sudo tee -a /etc/default/locale
  • echo "LC_TYPE=en_US.UTF-8" | sudo tee -a /etc/default/locale
  • export LANG=en_US.UTF-8
  • export LANGUAGE=en_US.UTF-8
  • export LC_ALL=en_US.UTF-8
  • export LC_TYPE=en_US.UTF-8

Commands that might be needed:

  • sudo apt install locales
  • sudo dpkg-reconfigure locales

More information:

Create a swap file

  • sudo fallocate -l 4G /swapfile || dd if=/dev/zero of=/swapfile bs=1024 count=$((4*1024*1024))
  • sudo chmod 600 /swapfile
  • sudo mkswap /swapfile
  • sudo swapon /swapfile
  • echo "/swapfile none swap sw 0 0" | sudo tee -a /etc/fstab

Change the ssh port

We will assume that the new port is 2222 and we will also disable root login via ssh:

  • sudo sed -i 's/#Port 22/Port 2222/' /etc/ssh/sshd_config
  • sudo sed -i 's/PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
  • sudo systemctl restart sshd

Enable Fail2ban

Fail2ban is an intrusion prevention software framework that protects computer servers from brute-force attacks.

  • sudo apt-get install fail2ban -y
  • sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
  • sudo systemctl start fail2ban
  • sudo systemctl enable fail2ban

Change the default path for bash

I use sudo su <user> quite a lot and by default, the path where I start is that of my own user. To change that to the user I am switching to, I like to change /etc/bash.bashrc by adding cd ~ on the last line.

Install Virtualmin

To install Virtualmin, run the following commands:

  • wget https://software.virtualmin.com/gpl/scripts/virtualmin-install.sh
  • sudo sh virtualmin-install.sh --minimal --force

For security, the next step is to change the port for the Virtualmin interface. For this, you need to go to the Webmin tab, Webmin > Webmin Configuration > Ports and Addresses and change 10000 to whatever port you desire. Remember to add the new port to the firewall configuration, e.g. sudo ufw allow 9999/udp

The next thing I like to do is go to Virtualmin > System Settings > Virtualmin Configuration and go though all the options. Some of the changes I do are:

  • User interface settings: allow editing the limits when creating a server changed to yes;
  • Defaults for new domains: domain name style in username changed to full domain name;

Since nowadays website are very space consuming, I usually update the default plan’s quota to 2GB.

Another thing I like to do is schedule a daily check for updates which will send me a notification via email and install security updates automatically.

More info:

Enable the firewall

Apparently, Virtualmin now comes with Firewalld installed. It’s not bad and the web interface they provide is actually quite helpful. For a simpler command like solution, we can use ufw. But for that, we must first stop firewalld, as managing the rules in both places might turn out challenging:

  • sudo systemctl stop firewalld
  • sudo systemctl disable firewalld
  • sudo apt purge firewalld

Uncomplicated Firewall (ufw) is a program for managing a netfilter firewall designed to be easy to use, using a command-line interface.

  • sudo apt-get install ufw -y
  • sudo ufw default deny incoming
  • sudo ufw default allow outgoing
  • sudo ufw allow OpenSSH
  • sudo ufw allow in "WWW Full"
  • sudo ufw allow 2222/tcp # use the port set for your ssh server
  • sudo ufw allow 9999/udp # remember to add the port for the virtualmin interface
  • yes | sudo ufw enable

Remove usermin

In case you do not intend to let users use email on your server, you can simply uninstall usermin:

  • sudo usermin stop
  • sudo apt purge usermin
  • sudo rm -rf /etc/usermin

More info:

The first server

What I like to do next is create a server for the same host as the server’s itself. This was we will automatically have a Let's Encrypt certificate for our hosting server. Once created, I update the contents of apache’s root directory with a nice game:

  • sudo su <your new username>
  • cd ~/public_html
  • rm index.html
  • git clone https://github.com/congerh/dino.git .

Enable custom php extensions

One thing we will probably have to do is to enable php extensions (e.g. curl, gd). To do that, we will have to know the version of php we are running (we will assume it’s 8.2.7):

  • sudo apt-get install -y php8.2-curl
  • sudo systemctl restart apache2

A bash script to help with this is the following:

#!/bin/bash

# Script to enable specified PHP extensions

# Ensure the script is run as root
if [ "$EUID" -ne 0 ]; then
  echo "Please run as root"
  exit
fi

# Function to install and enable extensions for a given PHP version
enable_extensions() {
    local php_version=$1
    shift
    local extensions=("$@")

    echo "Installing extensions for PHP $php_version..."
    for ext in "${extensions[@]}"; do
        echo "Installing $ext..."
        apt-get install -y "php${php_version}-${ext}"
    done

    echo "Restarting Apache to apply changes..."
    systemctl restart apache2

    echo "Extensions enabled for PHP $php_version."
}

# Get PHP version
php_version=$(php -v | grep '^PHP' | cut -d' ' -f2 | cut -d'.' -f1,2 | head -n 1)

if [[ -z "$php_version" ]]; then
    echo "PHP is not installed or not found."
    exit 1
fi

# Check if extensions are provided as command line arguments
if [ $# -eq 0 ]; then
    echo "Enter PHP extensions to install (space-separated, e.g., 'curl gd mbstring'):"
    read -ra extensions
else
    extensions=("$@")
fi

enable_extensions "$php_version" "${extensions[@]}"
Code language: PHP (php)

To create the script, run the following commands:

  • sudo nano /usr/local/bin/enable_php_extension # and paste the script shown above
  • sudo chmod a+x /usr/local/bin/enable_php_extension

Install composer

Some of the users might benefit from using composer with their website. For them, we will setup a script in /usr/local/bin which they can run to install composer. The content of the script is:

#!/bin/bash

# This script installs Composer locally in the user's home directory

# Check if the script is run by a regular user, not root
if [ "$EUID" -eq 0 ]; then 
  echo "Please run as a regular user, not as root"
  exit
fi

echo "Installing Composer for user: $(whoami)"

# Define the installation directory and the composer binary path
INSTALL_DIR=$HOME/.composer
COMPOSER_BIN=$HOME/composer

# Create the installation directory if it does not exist
mkdir -p $INSTALL_DIR

# Download Composer installer
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');"

# Verify installer SHA-384
EXPECTED_SIGNATURE="$(wget -q -O - https://composer.github.io/installer.sig)"
ACTUAL_SIGNATURE="$(php -r "echo hash_file('SHA384', 'composer-setup.php');")"

if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]
then
    >&2 echo 'ERROR: Invalid installer signature'
    rm composer-setup.php
    exit 1
fi

# Run the installer
php composer-setup.php --quiet --install-dir=$INSTALL_DIR --filename=composer
RESULT=$?

# Remove the installer
rm composer-setup.php

# Check if installation was successful
if [ $RESULT -eq 0 ]; then
    echo "Composer installed successfully in $COMPOSER_BIN"
else
    echo "Composer installation failed"
    exit 1
fi

# Update PATH in .bashrc if the bin directory is not already in PATH
if [ -f $HOME/.bashrc ] && ! grep -q "$INSTALL_DIR" $HOME/.bashrc; then
    echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> $HOME/.bashrc
    echo "Please log out and log back in or source .bashrc to update PATH."
fi

# Update PATH in .zshrc if the bin directory is not already in PATH
if [ -f $HOME/.zshrc ] && ! grep -q "$INSTALL_DIR" $HOME/.zshrc; then
    echo "export PATH=\"\$PATH:$INSTALL_DIR\"" >> $HOME/.zshrc
    echo "Please log out and log back in or source .zshrc to update PATH."
fiCode language: PHP (php)

To create the script, run the following commands:

  • sudo nano /usr/local/bin/install_composer # and paste the script shown above
  • sudo chmod a+x /usr/local/bin/install_composer

The quick way to install this script is:

  • sudo wget -O /usr/local/bin/install_composer https://raw.githubusercontent.com/cristidraghici/debian-server-bash-scripts/master/install_composer.sh && sudo chmod +x /usr/local/bin/install_composer

Install nvm

We will create the following script which will make available installing nvm for the current user:

#!/bin/bash

# This script downloads and installs the latest version of NVM (Node Version Manager) for the current user

# Check if the script is run by a regular user, not root
if [ "$EUID" -eq 0 ]; then
  echo "Please run as a regular user, not as root"
  exit
fi

echo "Installing the latest version of NVM (Node Version Manager) for user: $(whoami)"

# Fetch the latest version tag from the NVM GitHub repository
LATEST_NVM_VERSION=$(curl -s https://api.github.com/repos/nvm-sh/nvm/releases/latest | grep 'tag_name' | cut -d\" -f4)

echo "Latest version of NVM: $LATEST_NVM_VERSION"

# Download and execute the install script for the latest version
curl -o- "https://raw.githubusercontent.com/nvm-sh/nvm/${LATEST_NVM_VERSION}/install.sh" | bash

# Add nvm to .bashrc
if [ -f $HOME/.bashrc ] && ! grep -q "NVM_DIR" $HOME/.bashrc; then
  echo 'export NVM_DIR="$HOME/.nvm"' >> .bashrc
  echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm' >> .bashrc
  echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion' >> .bashrc

  echo "Please log out and log back in or source .bashrc to update PATH."
fi

# Add nvm to zshrc
if [ -f $HOME/.zshrc ] && ! grep -q "NVM_DIR" $HOME/.zshrc; then
  echo 'export NVM_DIR="$HOME/.nvm"' >> .bashrc
  echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm' >> .bashrc
  echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion' >> .bashrc

  echo "Please log out and log back in or source .zshrc to update PATH."
fi

echo

echo "NVM installation complete."
Code language: PHP (php)

To create the script, run the following commands:

  • sudo nano /usr/local/bin/install_nvm # and paste the script shown above
  • sudo chmod a+x /usr/local/bin/install_nvm

The quick way to install this script is:

  • sudo wget -O /usr/local/bin/install_nvm https://raw.githubusercontent.com/cristidraghici/debian-server-bash-scripts/master/install_nvm.sh && sudo chmod +x /usr/local/bin/install_nvm