#!/usr/bin/env bash # Ensure bash path is found # shellcheck source=/etc/os-release # use shellcheck to declare which file to source # Using set to make the script safer set -e # Exit on error set -u # Exit on undefined variable # set -x # Print commands for debugging set -a # Export all variables set -C # Disable overwriting of files set -o pipefail # Exit on pipe error # set -euxaCo pipefail # All options # Use the built-in trap command to catch error lines and signal numbers trap 'printf "Error on line %d with signal %s" "$LINENO" "$?"' ERR # Exit on error # Also use trap to catch interrupt signals and exit cleanly with a message to the user and a return code trap 'printf "Interrupted on line %d with signal %s" "$LINENO" "$?"' INT SIGHUP SIGINT SIGTERM # Get script name using parameter expansion to not spawn a new subprocess SCRIPT_NAME="${0##*/}" # We will be using printf instead of echo because it is more standardised. # Also we will be using the test command's functionality as # [[ because like this it constitutes a keyword and not a command. # Functions will be defined as function "name" {body}" to make them # more clear and () will not be used since using the keyword function renders them redundant # We are taking for granted that the os-release file is in /etc as # it has become a standard in most GNU/Linux distributions using systemd. # We'll be using the -e flag just in case it is actually a symlink to another location. # The get_distro function will use short if statements to check for the os-release file existence and readability. # Then it will source it and output the distribution's name or exit in case of failure of either case. 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 install_packages function will take the output of the get_package_manager function and install any # package passed as an argument to it. 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 > /dev/null 2>&1 # Update the package list and upgrade the packages sudo -E apt install -y "$@" > /dev/null 2>&1 # Install the packages passed as arguments ;; "dnf") # If the package manager is dnf sudo dnf upgrade -y > /dev/null 2>&1 # Update the package list sudo dnf install -y "$@" > /dev/null 2>&1 # Install the packages passed as arguments ;; "zypper") # If the package manager is zypper sudo zypper update -y > /dev/null 2>&1 # Update the package list sudo zypper install -y "$@" > /dev/null 2>&1 # Install the packages passed as arguments ;; *) # 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 } # The check_dependencies function will check if the dependencies defined in a local array are not installed # and store the ones that are indeed absent in another local array. # Then it will install the packages that are missing by invoking the install_packages function. function check_dependencies { # systemd-container is for machinectl local dependencies=(fuse-overlayfs dbus-user-session uidmap slirp4netns systemd-container at cron htop curl git sudo vim ssh wget fail2ban) # Declare dependencies as a local array #> in the future we should see what to do with name differences between distros if any <# local missing_dependencies=() # Declare missing_dependencies as a local array for dependency in "${dependencies[@]}"; do # Loop through the dependencies array # If the dependency is not installed, add it to the missing_dependencies array ! command -v "$dependency" &> /dev/null && missing_dependencies+=("$dependency") done # If the missing_dependencies array is not empty, install the packages [[ ${#missing_dependencies[@]} -ne 0 ]] && install_packages "${missing_dependencies[@]}" } # Global array of the service names to be restarted services=() # The hardenSSH function will use sed to modify the sshd_config file to have the following settings: # - Allow ssh access to users in the sudo group only # - Change the port to 22100 if it is available # - Configure idle timeout to 5 minutes # - Limit the number of authentication attempts to 3 # - Disable root login # - Disable empty passwords # - Disable ssh protocol 1 # - Disable password authentication and only allow public key authentication # - Disable X11 forwarding for security reasons (X11 forwarding is not needed for ssh) # - Disable agent forwarding to prevent ssh-agent hijacking # Then it will store the sshd service name in the services array. function hardenSSH { # Check if the sshd_config file exists and is readable # If it is, then modify it using sed and restart the sshd service # If it is not, then output an error message and exit with error code 1 # The -i flag is used to modify the file in place # We split the sed command into multiple lines for readability purposes # and to avoid calling it multiple times if [[ -e /etc/ssh/sshd_config ]] && [[ -r /etc/ssh/sshd_config ]]; then sudo sed -i \ -e 's/^#AllowGroups.*/AllowGroups sudo/' \ -e 's/^#Port.*/Port 22100/' \ -e 's/^#ClientAliveInterval.*/ClientAliveInterval 300/' \ -e 's/^#ClientAliveCountMax.*/ClientAliveCountMax 3/' \ -e 's/^#PermitRootLogin.*/PermitRootLogin no/' \ -e 's/^#PermitEmptyPasswords.*/PermitEmptyPasswords no/' \ -e 's/^#Protocol.*/Protocol 2/' \ -e 's/^#PasswordAuthentication.*/PasswordAuthentication no/' \ -e 's/^#X11Forwarding.*/X11Forwarding no/' \ -e 's/^X11Forwarding.*/X11Forwarding no/' \ -e 's/^#AllowAgentForwarding.*/AllowAgentForwarding no/' \ /etc/ssh/sshd_config services+=("sshd") # Add sshd to the services array else printf "%s" "File sshd_config not found or not readable" exit 1 fi } function getCorrectFirewall { 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" 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections export DEBIAN_FRONTEND=noninteractive export NEEDRESTART_MODE=a export DEBIAN_PRIORITY=critical sudo -E apt install ufw -y > /dev/null 2>&1 # Install ufw printf "%s" "ufw" # Output ufw ;; "CentOS Linux" | "Fedora" | "Red Hat Enterprise Linux Server") # If the distribution is CentOS, Fedora or RHEL sudo dnf install firewalld -y > /dev/null 2>&1 # Install firewalld printf "%s" "firewalld" # Output firewalld ;; "openSUSE Leap") # If the distribution is OpenSUSE sudo zypper install firewalld -y > /dev/null 2>&1 # Install firewalld printf "%s" "firewalld" # Output firewalld ;; *) # 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 } function getCorrectKernelSecurityModule { 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") # If the distribution is Ubuntu sudo apt install apparmor-profiles apparmor-utils apparmor-profiles-extra -y > /dev/null 2>&1 # Install apparmor printf "%s" "apparmor" # Output apparmor ;; "Debian GNU/Linux") # If the distribution is Debian printf "%s" 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections export DEBIAN_FRONTEND=noninteractive export NEEDRESTART_MODE=a export DEBIAN_PRIORITY=critical sudo -E apt install apparmor apparmor-profiles apparmor-profiles-extra apparmor-utils auditd python3-apparmor -y > /dev/null 2>&1 # Install apparmor printf "%s" "apparmor" # Output apparmor ;; "CentOS Linux" | "Fedora" | "Red Hat Enterprise Linux Server") # If the distribution is CentOS, Fedora or RHEL sudo dnf install selinux container-selinux -y > /dev/null 2>&1 # Install selinux printf "%s" "selinux" # Output selinux ;; "openSUSE Leap") # If the distribution is OpenSUSE sudo zypper install -t pattern apparmor -y > /dev/null 2>&1 # Install apparmor printf "%s" "apparmor" # Output apparmor ;; *) # 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 } function firewallInit { getCorrectFirewall # Get the correct firewall installed # Determine if ufw or firewalld is installed # We are not using command -v because we may be checking from a user that doesn't have the command in his path whereis ufw | grep -q /ufw && currentFirewall="ufw" || currentFirewall="firewalld" case "$currentFirewall" in ufw) sudo ufw default allow outgoing # Allow outgoing connections sudo ufw default deny incoming # Deny incoming connections sudo ufw allow 22100/tcp # Allow ssh connections on port 22100 sudo ufw allow 8000/tcp # Allow portainer connections on port 8000 sudo ufw allow 9443/tcp # Allow portainer connections on port 9443 sudo ufw allow 8080/tcp # Allow watchtower connections on port 8080 ;; firewalld) sudo systemctl enable firewalld # Enable the firewall on boot sudo firewall-cmd --permanent --add-port=22100/tcp # Allow ssh connections on port 22100 sudo firewall-cmd --permanent --add-port=8000/tcp # Allow portainer connections on port 8000 sudo firewall-cmd --permanent --add-port=9443/tcp # Allow portainer connections on port 9443 sudo firewall-cmd --permanent --add-port=8080/tcp # Allow watchtower connections on port 8080 ;; *) printf "%s" "Unsupported firewall" exit 1 ;; esac } function kernelSecurityModuleInit { getCorrectKernelSecurityModule # Get the correct kernel security module installed # Determine if apparmor or selinux is installed whereis apparmor | grep -q /apparmor && kernelSecurityModule="apparmor" || kernelSecurityModule="selinux" case "$kernelSecurityModule" in apparmor) sudo systemctl enable --now apparmor # Enable the kernel security module on boot and start it sudo aa-enforce /etc/apparmor.d/* # Enforce all apparmor profiles ;; selinux) sudo systemctl enable --now selinux # Enable the kernel security module on boot and start it sudo setenforce 1 # Enforce selinux sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config # Set selinux to enforcing ;; *) printf "%s" "Unsupported kernel security module" exit 1 ;; esac } function dockerInit { # Set up rootless docker # Enable linger for the secdep user sudo loginctl enable-linger secdep # Enable dbus for the secdep user sudo machinectl shell secdep@ /bin/bash -c "systemctl --user enable --now dbus" # Install rootless docker sudo machinectl shell secdep@ /bin/bash -c "curl -fsSL https://get.docker.com/rootless | sh" # Add important environment variables to the secdep user's .bashrc sudo su secdep << 'EOF' printf "%s\n" "export PATH=/home/$USER/bin:$PATH" >> "$HOME/.bashrc" printf "%s\n" "export DOCKER_HOST=unix:///run/user/$UID/docker.sock" >> "$HOME/.bashrc" EOF # Enable the user to bind to ports below 1024 sudo setcap cap_net_bind_service=ep /home/secdep/bin/rootlesskit # Restart docker sudo machinectl shell secdep@ /bin/bash -c "systemctl --user restart docker" # Get all arguments passed to the function and store them in the dockerImages array local dockerImages=("$@") # Using -f instead of -e to check if the file exists AND that it is a regular file [[ -f /root/docker-compose.yml ]] && sudo mv /root/docker-compose.yml /home/secdep/docker-compose.yml [[ -f /home/admin/docker-compose.yml ]] && sudo mv /home/admin/docker-compose.yml /home/secdep/docker-compose.yml [[ -f /home/ec2-user/docker-compose.yml ]] && sudo mv /home/ec2-user/docker-compose.yml /home/secdep/docker-compose.yml [[ -f /home/centos/docker-compose.yml ]] && sudo mv /home/centos/docker-compose.yml /home/secdep/docker-compose.yml [[ -f /home/fedora/docker-compose.yml ]] && sudo mv /home/fedora/docker-compose.yml /home/secdep/docker-compose.yml [[ -f /home/ubuntu/docker-compose.yml ]] && sudo mv /home/ubuntu/docker-compose.yml /home/secdep/docker-compose.yml # Since FileDeployment does not work and we used ScriptFileDeployment which might make the file owned by another user # we need to make sure the file is owned by the secdep user. # When using [[ -f /home/secdep/docker-compose.yml ]] && sudo chown secdep:secdep /home/secdep/docker-compose.yml # it doesn't get executed somehow so we'll send the "no such file or directory" error to /dev/null sudo chown secdep:secdep /home/secdep/docker-compose.yml > /dev/null 2>&1 # Since FileDeployment does not work and we used ScriptFileDeployment which automatically makes the file executable # we need to make sure the file is not executable. # when using [[ -f /home/secdep/docker-compose.yml ]] && sudo chmod -x /home/secdep/docker-compose.yml # it doesn't get executed somehow so we'll send the "no such file or directory" error to /dev/null sudo chmod -x /home/secdep/docker-compose.yml > /dev/null 2>&1 sudo machinectl shell secdep@ /bin/bash -c 'curl -SL https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-linux-x86_64 -o /home/secdep/bin/docker-compose' sudo machinectl shell secdep@ /bin/bash -c 'chmod +x /home/secdep/bin/docker-compose' # Install gVisor (runsc) and containerd-shim-runsc-v1 ( set -e ARCH=$(uname -m) URL=https://storage.googleapis.com/gvisor/releases/release/latest/${ARCH} wget -q "${URL}"/runsc "${URL}"/runsc.sha512 \ "${URL}"/containerd-shim-runsc-v1 "${URL}"/containerd-shim-runsc-v1.sha512 sha512sum -c runsc.sha512 \ -c containerd-shim-runsc-v1.sha512 rm -f -- *.sha512 chmod a+rx runsc containerd-shim-runsc-v1 sudo mv runsc containerd-shim-runsc-v1 /home/secdep/bin sudo chown secdep:secdep /home/secdep/bin/runsc sudo chown secdep:secdep /home/secdep/bin/containerd-shim-runsc-v1 ) # Enable gVisor for docker by default while hardening the docker daemon.json file # The no-new-privileges option will prevent the docker daemon from gaining new privileges # The runsc option --network=host and --ignore-cgroups=true will help with running runsc # with rootless docker without much hassle sudo runuser - secdep -c 'mkdir -p /home/secdep/.config/docker' # "icc": false to disable inter-container communication, does not work without br_netfilter module loaded # and it is sometimes prohibited by the hosting provider # so we'll leave it out of the daemon config DOCKERD_CONFIG=$(cat <<'EOF' { "no-new-privileges": true, "selinux-enabled": false, "default-runtime": "runsc", "runtimes": { "runsc": { "path": "/home/secdep/bin/runsc", "runtimeArgs": [ "--network=host", "--ignore-cgroups=true" ] } } } EOF ) sudo runuser - secdep -c "printf '%s\n' '$DOCKERD_CONFIG' > /home/secdep/.config/docker/daemon.json" # Restart docker to apply the changes sudo machinectl shell secdep@ /bin/bash -c "systemctl --user restart docker" whereis apparmor | grep -q /apparmor && kernelSecurityModule="apparmor" || kernelSecurityModule="selinux" if [[ "$kernelSecurityModule" == "selinux" ]]; then sudo chcon -Rt svirt_sandbox_file_t /home/secdep/.config/docker sudo chcon -Rt svirt_sandbox_file_t /home/secdep/bin/runsc setsebool -P container_manage_cgroup true sudo runuser - secdep -c 'sed -i "s/\"selinux-enabled\": false/\"selinux-enabled\": true/" /home/secdep/.config/docker/daemon.json' fi sudo machinectl shell secdep@ "$(which bash)" -c '[[ -f "$HOME/docker-compose.yml" ]] && DOCKER_HOST=unix:///run/user/$UID/docker.sock /home/secdep/bin/docker-compose -f /home/secdep/docker-compose.yml up -d' # Read the docker-compose.yml file for port mappings to add to the firewall CMD_PORTS="cat /home/secdep/docker-compose.yml | sed '/^[[:space:]]*$/d' | grep -A1 ports | grep '[0-9]:[0-9]' | rev | cut -d':' -f1 | rev | grep -Eow '[[:digit:]]+' | tr '\n' ' '" sudo -E runuser - secdep -c "$CMD_PORTS" > /dev/null 2>&1 && PORTS="$(sudo -E runuser - secdep -c "$CMD_PORTS")" || PORTS="" # Loop through the ports in the PORTS variable if [[ -n "$PORTS" ]]; then for port in "${PORTS[@]}"; do # Using "${PORTS[@]}" instead of "$PORTS" as the latter will only return the first port # 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 # Portainer is a docker image that provides a web interface for docker # 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 (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 # Note: If a new install of Portainer is not configured within 5 minutes, # it shuts down internally for security reasons but that does not matter since # we reboot after the script is done. 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 # The dockerImages array contains all the docker images to install and run for dockerImage in "${dockerImages[@]}"; do # If dockerImage contains a ":" character (if it is a docker image with a tag) # then store the docker image name in the dockerImageName variable [[ "$dockerImage" == *":"* ]] && dockerImageName="${dockerImage%:*}" || dockerImageName="$dockerImage" # Same goes for "/" [[ "$dockerImageName" == *"/"* ]] && dockerImageName="${dockerImageName%/*}" CMD="docker pull $dockerImage" printf "%s\n" "Downloaded $dockerImageName docker image" sudo -E runuser - secdep -c "$CMD" done } # The configureFail2ban function will configure fail2ban to ban ip addresses that try to brute force the ssh port (22100) function configureFail2ban { FAIL2BAN_LOCAL=$(cat <<'EOF' [Definition] allowipv6 = auto EOF ) printf "%s\n" "$FAIL2BAN_LOCAL" | sudo tee /etc/fail2ban/fail2ban.local > /dev/null 2>&1 FAIL2BAN_SSH_JAIL_LOCAL=$(cat <<'EOF' [sshd] enabled = true filter = sshd banaction = iptables backend = systemd maxretry = 3 # 3 failed attempts in 600 seconds = 10 minutes findtime = 1d bantime = 1d ignoreip = 127.0.0.1/8 EOF ) FAIL2BAN_JAIL_LOCAL=$(cat <<'EOF' [DEFAULT] bantime = 1d EOF ) printf "%s\n" "$FAIL2BAN_JAIL_LOCAL" | sudo tee /etc/fail2ban/jail.local > /dev/null 2>&1 sudo rm -f /etc/fail2ban/jail.d/* printf "%s\n" "$FAIL2BAN_SSH_JAIL_LOCAL" | sudo tee /etc/fail2ban/jail.d/sshd.local > /dev/null 2>&1 FAIL2BAN_FILTER=$(cat <<'EOF' [Definition] failregex = ^.*DROP_.*SRC= DST=.*$ journalmatch = _TRANSPORT=kernel EOF ) printf "%s\n" "$FAIL2BAN_FILTER" | sudo tee /etc/fail2ban/filter.d/fwdrop.local > /dev/null 2>&1 sudo systemctl enable --now fail2ban printf "%s\n" "LogLevel VERBOSE" | sudo tee -a /etc/ssh/sshd_config > /dev/null 2>&1 } function enableServices { # Loop through the services array which in the future could contain more services for service in "${services[@]}"; do sudo systemctl restart "$service" done whereis ufw | grep -q /ufw && currentFirewall="ufw" || currentFirewall="firewalld" # With the if block it doesn't error out at firewalld check so we do it this way # For ufw if [[ "$currentFirewall" == "ufw" ]]; then # Enable the firewall sudo ufw --force enable # Enable and start the firewall on boot sudo systemctl enable --now ufw # For firewalld elif [[ "$currentFirewall" == "firewalld" ]]; then # Reload the firewall sudo firewall-cmd --reload else printf "%s" "Somehow there is no firewall installed" exit 1 fi } # If we are using aws to make the vps there is a default user different from the one we want to use. # Also for the same reason, this script could be run by one of those users so we are using at to delete them # after 2 minutes. If it was 1 minute it could fail if its xx:59 because at doesn't actually wait for 60 seconds # but it waits for the next minute to come. Also the user would not be deleted without at because the script would # be running as the user that we want to delete. function deleteRemainingUsers { # In case atd wasn't running sudo systemctl enable --now atd # Set the correct timezone for Greece before using at sudo timedatectl set-timezone Europe/Athens # Delete possible remaining users cat << EOF | sudo tee /root/delete_users.sh > /dev/null 2>&1 [[ -d /home/admin ]] && sudo userdel -r admin && sudo groupdel admin [[ -d /home/ec2-user ]] && sudo userdel -r ec2-user && sudo groupdel ec2-user [[ -d /home/centos ]] && sudo userdel -r centos && sudo groupdel centos [[ -d /home/fedora ]] && sudo userdel -r fedora && sudo groupdel fedora [[ -d /home/ubuntu ]] && sudo userdel -r ubuntu && sudo groupdel ubuntu sudo rm -f /root/delete_users.sh EOF # Restart atd after setting the timezone sudo systemctl restart atd # Use at as root because if it is run as one of the users above it will fail sudo at now + 2 minute <<< "bash /root/delete_users.sh" } # It is not entirely possible to dynamically add and remove firewall rules for docker ports, # especially when using portainer so we will use a cronjob to check every 30 minutes if there are # any changes to be made to the firewall rules. function dynamicDockerPortsCronjob { # Part of the code responsible for the comparison # of the arrays was taken from: # https://stackoverflow.com/questions/2312762/compare-difference-of-two-arrays-in-bash sudo mkdir -p /root/bin cat << 'TOHERE' | sudo tee /root/bin/dynamic_docker_ports_cronjob.sh > /dev/null 2>&1 #!/usr/bin/env bash # Get the current ports used by docker CURRENT_DOCKER_PORTS="$(DOCKER_HOST=unix:///run/user/"$(id -u secdep)"/docker.sock /home/secdep/bin/docker ps --format '{{.Ports}}' | tr ' ' '\n' | rev | cut -d'/' -f2 | sed 's@^[^0-9]*\([0-9]\+\).*@\1@' | rev | sort -u | tr '\n' ' ')" # 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 | grep -i allow | 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) l2=" ${CURRENT_FIREWALL_PORTS_UFW_CMD[*]} " # add framing blanks for item in ${CURRENT_DOCKER_PORTS[@]}; do if ! [[ $l2 =~ $item ]] ; then # use $item as regexp NEW_PORTS+=("$item") fi done ;; firewalld) l2=" ${CURRENT_FIREWALL_PORTS_FIREWALLD_CMD[*]} " # add framing blanks for item in ${CURRENT_DOCKER_PORTS[@]}; do if ! [[ $l2 =~ $item ]] ; then # use $item as regexp NEW_PORTS+=("$item") fi done ;; *) 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) l2=" ${CURRENT_DOCKER_PORTS[*]} " # add framing blanks for item in ${CURRENT_FIREWALL_PORTS_UFW_CMD[@]}; do if ! [[ $l2 =~ $item ]] ; then # use $item as regexp OLD_PORTS+=("$item") fi done ;; firewalld) l2=" ${CURRENT_DOCKER_PORTS[*]} " # add framing blanks for item in ${CURRENT_FIREWALL_PORTS_FIREWALLD_CMD[@]}; do if ! [[ $l2 =~ $item ]] ; then # use $item as regexp OLD_PORTS+=("$item") fi done ;; *) 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 sudo ufw allow 22100/tcp if [[ "$currentFirewall" == "firewalld" ]]; then sudo firewall-cmd --reload else sudo ufw reload fi TOHERE # Every 30 minutes check if there are any new ports used by docker and allow them in the firewall cat << TOHERE | sudo tee -a /var/spool/cron/crontabs/root > /dev/null 2>&1 */30 * * * * /root/bin/dynamic_docker_ports_cronjob.sh TOHERE sudo chmod +x /root/bin/dynamic_docker_ports_cronjob.sh sudo systemctl restart cron } # Even though there is a package called cron-apt, it is only available for debian and ubuntu # while this script is supposed to work on all the major server distros function automaticUpdatesCronjob { sudo mkdir -p /root/bin cat << 'TOHERE' | sudo tee /root/bin/automatic_updates_cronjob.sh > /dev/null 2>&1 #!/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 update_system { 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 # Every day at 4:00 AM update the system cat << TOHERE | sudo tee -a /var/spool/cron/crontabs/root > /dev/null 2>&1 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. function main { printf "%s\n" "$SCRIPT_NAME script started" check_dependencies || exit 1 # Check dependencies and exit if it fails printf "%s\n" "Dependencies installed" hardenSSH || exit 1 # Harden ssh and exit if it fails printf "%s\n" "SSH hardened" firewallInit || exit 1 # Initialize the firewall and exit if it fails printf "%s\n" "Firewall initialized" kernelSecurityModuleInit || exit 1 # Initialize the kernel security module and exit if it fails printf "%s\n" "Kernel security module initialized" configureFail2ban || exit 1 # Initialize fail2ban and exit if it fails printf "%s\n" "Fail2ban configured" # Call the dockerInit function with the arguments passed to the script dockerInit "$@" || exit 1 # Initialize docker and exit if it fails printf "%s\n" "Docker Rootless, docker-compose and gVisor installed and configured" printf "%s\n" "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\n" "Services restarted and firewall enabled" dynamicDockerPortsCronjob || exit 1 # Allow the ports used by docker in the firewall printf "%s\n" "CronJob to adjust the ports used by docker and the firewall installed" automaticUpdatesCronjob || exit 1 # Install a cronjob to update the system periodically printf "%s\n" "CronJob to update the system installed" # If the username is not secdep, delete the remaining users [[ "$USER" != "secdep" ]] && deleteRemainingUsers || exit 1 # Delete possible remaining users printf "%s\n" "Any unnecessary users deleted" printf "%s\n" "$SCRIPT_NAME script finished" # Output message to the user printf "%s\n" "System will reboot momentarily" # Output message to the user # Reboot the system in 3 minutes with the shutdown command so that login before the reboot is not possible # If the username is not secdep, reboot the system in 1 minute # We reboot just in case there are any updates that need to be applied # It was not the original intention of the script to reboot the system but it is better to be safe than sorry # We also wait (for 1 or 3 minutes depending on the user running it) so that the script can finish as we # want to see the exit code. if [[ "$USER" != "secdep" ]]; then sudo shutdown -r +3 else sudo shutdown -r +1 fi } # Check if the user is root if [[ "$EUID" -eq 0 ]]; then printf "%s\n" "Which distro let you login as root?" exit 1 else # Call the main function main "$@" fi exit 0 # The right and proper way to exit a script