diff --git a/README.md b/README.md index 050ba76..0d038d4 100644 --- a/README.md +++ b/README.md @@ -37,11 +37,14 @@ That's where SecDep comes in 💪. With SecDep, you can manage your virtual mach - [x] Fail2ban installation and configuration - [x] Kernel Security Module installation (AppArmor or SELinux) - [x] Docker Rootless installation - - [ ] gVisor installation and integration with Docker Rootless + - [x] gVisor installation and integration with Docker Rootless + - [x] CronJob to update the system periodically + - [x] CronJob to allow or disallow docker ports - [x] Docker deployment during hardening - [x] Single docker-compose file deployment - [x] Pulling of multiple docker images - [x] Automatic portainer deployment + - [x] Automatic watchtower deployment # Prerequisites 📋 diff --git a/harden b/harden index cb61662..fe9b9a7 100755 --- a/harden +++ b/harden @@ -383,10 +383,13 @@ EOF # which will be installed and run on port 9443 by default to make it easier to manage docker # url to follow after the installation is complete: https://vps_ip:9443 # the https:// part is important as portainer will not work without it - # For portainer, we will be using the --runtime=runc option to run it with runc because + # For portainer (and watchtower), we will be using the --runtime=runc option to run it with runc because # it doesn't work with runsc as it is not exposing the docker socket to the container # but containers downloaded from it will still use runsc sudo -E runuser - secdep -c 'docker run --runtime=runc -d -p 8000:8000 -p 9443:9443 --name=portainer --restart=always -v /run/user/$UID/docker.sock:/var/run/docker.sock -v portainer_data:/data portainer/portainer-ce' + # Watchtower is a docker image that will automatically update all the other docker images + # that are installed and running so we don't have to do it manually + sudo -E runuser - secdep -c 'docker run --runtime=runc -d --name watchtower --restart=always -v /run/user/$UID/docker.sock:/var/run/docker.sock containrrr/watchtower --cleanup --interval 3600' # Check if the dockerImages array is empty and return 0 if it is [[ "${#dockerImages[@]}" -eq 0 ]] && return 0 # Loop through the dockerImages array @@ -486,6 +489,168 @@ EOF sudo at now + 1 minute <<< "bash /root/delete_users.sh" } +function dynamicDockerPortsCronjob { + sudo mkdir -p /root/bin + cat << 'TOHERE' | sudo tee /root/bin/dynamic_docker_ports_cronjob.sh +#!/usr/bin/env bash +# Get the current ports used by docker +CURRENT_DOCKER_PORTS_CMD="docker ps --format '{{.Ports}}' | rev | cut -d'/' -f2 | sed 's@^[^0-9]*\([0-9]\+\).*@\1@' | rev | sort -u | tr '\n' ' ')" +CURRENT_DOCKER_PORTS="$(sudo machinectl shell secdep@ /bin/bash -c "$CURRENT_DOCKER_PORTS_CMD")" +# Get the current ports allowed by the firewall +CURRENT_FIREWALL_PORTS_FIREWALLD_CMD="sudo firewall-cmd --list-ports | tr '\n' ' '" +CURRENT_FIREWALL_PORTS_UFW_CMD="sudo ufw status numbered | awk '{print \$3}' | sed '/^[[:space:]]*$/d' | \grep -Eow '[[:digit:]]+' | sort -u | tr '\n' ' '" +# Determine if ufw or firewalld is currently used +whereis ufw | grep -q /ufw && currentFirewall="ufw" || currentFirewall="firewalld" +# Find which ports are not allowed by the firewall but are used by docker +case "$currentFirewall" in + ufw) + NEW_PORTS="$(comm -23 <(printf "%s" "$CURRENT_DOCKER_PORTS") <(printf "%s" "$CURRENT_FIREWALL_PORTS_UFW_CMD"))" + ;; + firewalld) + NEW_PORTS="$(comm -23 <(printf "%s" "$CURRENT_DOCKER_PORTS") <(printf "%s" "$CURRENT_FIREWALL_PORTS_FIREWALLD_CMD"))" + ;; + *) + printf "%s" "Unsupported firewall" + exit 1 + ;; +esac +# Loop through the ports in the NEW_PORTS variable if it is not empty +if [[ -n "$NEW_PORTS" ]]; then + for port in $NEW_PORTS; do + # Allow the port in the firewall + case "$currentFirewall" in + ufw) + sudo ufw allow "$port"/tcp + ;; + firewalld) + sudo firewall-cmd --permanent --add-port="$port"/tcp + ;; + *) + printf "%s" "Unsupported firewall" + exit 1 + ;; + esac + done +fi +# Find which ports are not used by docker but are allowed by the firewall +case "$currentFirewall" in + ufw) + OLD_PORTS="$(comm -23 <(printf "%s" "$CURRENT_FIREWALL_PORTS_UFW_CMD") <(printf "%s" "$CURRENT_DOCKER_PORTS"))" + ;; + firewalld) + OLD_PORTS="$(comm -23 <(printf "%s" "$CURRENT_FIREWALL_PORTS_FIREWALLD_CMD") <(printf "%s" "$CURRENT_DOCKER_PORTS"))" + ;; + *) + printf "%s" "Unsupported firewall" + exit 1 + ;; +esac +# Loop through the ports in the OLD_PORTS variable if it is not empty +if [[ -n "$OLD_PORTS" ]]; then + for port in $OLD_PORTS; do + # Deny the port in the firewall + case "$currentFirewall" in + ufw) + sudo ufw deny "$port"/tcp + ;; + firewalld) + sudo firewall-cmd --permanent --remove-port="$port"/tcp + ;; + *) + printf "%s" "Unsupported firewall" + exit 1 + ;; + esac + done +fi +TOHERE +cat << TOHERE | sudo tee -a /var/spool/cron/crontabs/root +# Every 30 minutes check if there are any new ports used by docker and allow them in the firewall +*/30 * * * * /root/bin/dynamic_docker_ports_cronjob.sh +TOHERE +sudo chmod +x /root/bin/dynamic_docker_ports_cronjob.sh +sudo systemctl restart cron +} + +function automaticUpdatesCronjob { + sudo mkdir -p /root/bin + cat << 'TOHERE' | sudo tee /root/bin/automatic_updates_cronjob.sh +#!/usr/bin/env bash +function get_distro { + if [[ -e /etc/os-release ]] && [[ -r /etc/os-release ]]; then # Check if file exists and is readable + . /etc/os-release # Source the file + printf "%s" "$NAME" # Output the distribution's name + else # If the file does not exist or is not readable + printf "%s" "File os-release not found or not readable" # Output error message + exit 1 # Exit with error code 1 + fi +} + +# The get_package_manager function will take the output of the get_distro function and determine +# which is the package manager used for the most popular server distros and exit if it is not found. +function get_package_manager { + local distro # Declare distro as a local variable + distro="$(get_distro)" # Get the distribution name + case "$distro" in # Use case to check for the distribution name + "Ubuntu" | "Debian GNU/Linux") # If the distribution is Ubuntu or Debian + printf "%s" "apt" # Output apt + ;; + "CentOS Linux" | "Fedora" | "Red Hat Enterprise Linux Server") # If the distribution is CentOS, Fedora or RHEL + printf "%s" "dnf" # Output dnf + ;; + "openSUSE Leap") # If the distribution is OpenSUSE + printf "%s" "zypper" # Output zypper + ;; + *) + # If the distribution is none of the above, output unsupported distribution + # and exit with error code 1 + printf "%s" "Unsupported distribution" + exit 1 # Exit with error code 1 + ;; + esac +} + +# The update_system function will take the output of the get_package_manager function and update the system +# It will also check if the package manager is known and exit if it is not. +function install_packages { + local package_manager # Declare package_manager as a local variable + package_manager="$(get_package_manager)" # Get the package manager + case "$package_manager" in # Use case to check for the package manager + "apt") # If the package manager is apt + printf "%s" 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + export DEBIAN_PRIORITY=critical + # Running sudo with -E will preserve the environment variables set in the script + sudo -E apt update -y && sudo apt upgrade -y # Update the package list and upgrade the packages + sudo -E apt install -y + ;; + "dnf") # If the package manager is dnf + sudo dnf upgrade -y # Update the package list + sudo dnf install -y + ;; + "zypper") # If the package manager is zypper + sudo zypper update -y # Update the package list + sudo zypper install -y + ;; + *) + # If the package manager is not one of the above, output unsupported package manager + # and exit with error code 1 + printf "%s" "Unsupported package manager" + exit 1 # Exit with error code 1 + ;; + esac +} +update_system +TOHERE +cat << TOHERE | sudo tee -a /var/spool/cron/crontabs/root +# Every day at 4:00 AM update the system +0 4 * * * /root/bin/automatic_updates_cronjob.sh +TOHERE +sudo chmod +x /root/bin/automatic_updates_cronjob.sh +sudo systemctl restart cron +} + # The main function will call the declared functions in order and exit if any of them fails. # It will also pass any arguments passed to the script to the dockerInit function. # Then it will output a message to the user and reboot the system in 2 minutes. @@ -504,9 +669,13 @@ function main { # Call the dockerInit function with the arguments passed to the script dockerInit "$@" || exit 1 # Initialize docker and exit if it fails printf "%s" "Docker Rootless, docker-compose and gVisor installed and configured" - printf "%s" "Portainer along with any specified docker images from the command line or a docker-compose.yml file installed" + printf "%s" "Portainer and Watchtower along with any specified docker images from the command line or a docker-compose.yml file installed" enableServices || exit 1 # Enable the services that need to be restarted and the firewall printf "%s" "Services restarted and firewall enabled" + dynamicDockerPortsCronjob || exit 1 # Allow the ports used by docker in the firewall + printf "%s" "CronJob to allow the ports used by docker in the firewall installed" + automaticUpdatesCronjob || exit 1 # Install a cronjob to update the system periodically + printf "%s" "CronJob to update the system installed" deleteRemainingUsers || exit 1 # Delete possible remaining users printf "%s" "Any unnecessary users deleted" printf "%s" "$SCRIPT_NAME script finished" # Output message to the user