#!/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 sudo apt update # Update the package list sudo apt install -y "$@" # Install the packages passed as arguments ;; "dnf") # If the package manager is dnf sudo dnf upgrade -y # Update the package list sudo dnf install -y "$@" # Install the packages passed as arguments ;; "zypper") # If the package manager is zypper sudo zypper update -y # Update the package list sudo zypper install -y "$@" # 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 { local dependencies=(curl git sudo vim ssh docker-ce docker.io docker docker-compose wget fail2ban) # Declare dependencies as a local array #> 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 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/^#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 apt install ufw -y # Install ufw printf "%s" "ufw" # Output ufw ;; "CentOS Linux" | "Fedora" | "Red Hat Enterprise Linux Server") # If the distribution is CentOS, Fedora or RHEL dnf install firewalld -y # Install firewalld printf "%s" "firewalld" # Output firewalld ;; "openSUSE Leap") # If the distribution is OpenSUSE zypper install firewalld -y # 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 apt install apparmor-profiles apparmor-utils apparmor-profiles-extra -y # Install apparmor printf "%s" "apparmor" # Output apparmor ;; "Debian GNU/Linux") # If the distribution is Debian apt install apparmor apparmor-utils auditd python3-apparmor -y # Install apparmor printf "%s" "apparmor" # Output apparmor ;; "CentOS Linux" | "Fedora" | "Red Hat Enterprise Linux Server") # If the distribution is CentOS, Fedora or RHEL dnf install selinux container-selinux -y # Install selinux printf "%s" "selinux" # Output selinux ;; "openSUSE Leap") # If the distribution is OpenSUSE zypper install -t pattern apparmor -y # 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 { local firewall firewall="$(getCorrectFirewall)" # Get the correct firewall case "$firewall" 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 enable # Enable the firewall sudo systemctl enable --now ufw # Enable and start the firewall on boot ;; firewalld) sudo systemctl enable --now firewalld # Enable the firewall on boot and start it sudo firewall-cmd --permanent --add-port=22100/tcp # Allow ssh connections on port 22100 sudo firewall-cmd --reload # Reload the firewall ;; *) printf "%s" "Unsupported firewall" exit 1 ;; esac } function kernelSecurityModuleInit { local kernelSecurityModule kernelSecurityModule="$(getCorrectKernelSecurityModule)" # Get the correct kernel security module 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 printf "%s" "{\"selinux-enabled\":true}" | sudo tee /etc/docker/daemon.json # Enable selinux in docker sudo setenforce 1 # Enforce selinux sudo systemctl restart docker # Restart docker sudo restorecon -Rv /var/lib/docker # Restore the selinux context of the docker directory sudo restorecon -Rv /usr/bin # Restore the selinux context of the docker directory ;; *) printf "%s" "Unsupported kernel security module" exit 1 ;; esac } function dockerInit { # Add user to docker group to avoid using sudo when running docker commands sudo usermod -aG docker "$USER" # Get all arguments passed to the function and store them in the dockerImages array local dockerImages=("$@") # 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 # No need to pull the docker image as the run command will do it automatically # Run the docker image in the background, # with the restar always option and the name of the docker image # The --security-opt=no-new-privileges option will prevent the docker image from gaining new privileges # The --cap-drop all option will drop all capabilities from the docker image # The --cap-add NET_ADMIN option will add the NET_ADMIN capability to the docker image docker run -d --restart always --name "$dockerImage" --security-opt=no-new-privileges --cap-drop all --cap-add NET_ADMIN "$dockerImage" done } # The apparmorConfig function will set up and configure apparmor with sane defaults. function apparmorConfig { # Create a new apparmor profile for the docker daemon sudo aa-genprof docker # Enable the apparmor profile for the docker daemon sudo aa-enforce docker # Reload the apparmor profiles sudo systemctl reload apparmor } # The selinuxConfig function will set up and configure selinux with sane defaults. function selinuxConfig { # Set the selinux boolean to allow docker to use the network sudo setsebool -P docker_connect_any 1 } # This function will create a new apparmor profile for every docker image installed on the system. function apparmorProfiles { # Get all the docker images installed on the system and store them in the dockerImages array local dockerImages=("$(docker images --format "{{.Repository}}")") # Loop through the dockerImages array for dockerImage in "${dockerImages[@]}"; do # Create a new apparmor profile for the docker image sudo aa-genprof "$dockerImage" # Enable the apparmor profile for the docker image sudo aa-enforce "$dockerImage" done # Reload the apparmor profiles sudo systemctl reload apparmor } function configureFail2ban { FAIL2BAN_LOCAL=$(cat <<'EOF' [Definition] logtarget = /var/log/fail2ban/fail2ban.log allowipv6 = auto EOF ) printf "%s" "$FAIL2BAN_LOCAL" | sudo tee /etc/fail2ban/fail2ban.local FAIL2BAN_SSH_JAIL_LOCAL=$(cat <<'EOF' [sshd] enabled = true filter = sshd banaction = ufw backend = systemd maxretry = 3 # 3 failed attempts in 600 seconds = 10 minutes findtime = 1d bantime = 1d ignoreip = 127.0.0.1/8 EOF ) printf "%s" "$FAIL2BAN_SSH_JAIL_LOCAL" | sudo tee /etc/fail2ban/jail.local/sshd.local FAIL2BAN_FILTER=$(cat <<'EOF' [Definition] failregex = ^.*DROP_.*SRC= DST=.*$ journalmatch = _TRANSPORT=kernel EOF ) printf "%s" "$FAIL2BAN_FILTER" | sudo tee /etc/fail2ban/filter.d/fwdrop.local HARDEN_FAIL2BAN_SERVICE=$(cat <<'EOF' [Service] PrivateDevices=yes PrivateTmp=yes ProtectHome=read-only ProtectSystem=strict ReadWritePaths=-/var/run/fail2ban ReadWritePaths=-/var/lib/fail2ban ReadWritePaths=-/var/log/fail2ban ReadWritePaths=-/var/spool/postfix/maildrop ReadWritePaths=/run/xtables.lock CapabilityBoundingSet=CAP_AUDIT_READ CAP_DAC_READ_SEARCH CAP_NET_ADMIN CAP_NET_RAW EOF ) printf "%s" "$HARDEN_FAIL2BAN_SERVICE" | sudo tee /etc/systemd/system/fail2ban.service.d/override.conf sudo systemctl enable --now fail2ban printf "%s" "LogLevel VERBOSE" | sudo tee -a /etc/ssh/sshd_config sudo systemctl restart sshd } # The main function will call the check_dependencies function and exit if it fails. # It will also output a message to the user to let them know that the script has finished. function main { check_dependencies || exit 1 # Check dependencies and exit if it fails hardenSSH || exit 1 # Harden ssh and exit if it fails firewallInit || exit 1 # Initialize the firewall and exit if it fails kernelSecurityModuleInit || exit 1 # Initialize the kernel security module and exit if it fails configureFail2ban || exit 1 # Initialize fail2ban and exit if it fails dockerInit || exit 1 # Initialize docker and exit if it fails apparmorConfig # Configure apparmor apparmorProfiles # Create apparmor profiles for all docker images selinuxConfig # Configure selinux # If number of arguments is greater than 0 # Call the dockerInit function with the arguments passed to the script # Else exit with error code 1 [[ $# -gt 0 ]] && dockerInit "$@" || exit 1 printf "%s" "Script finished" # Output message to the user } # # The am_i_root function will check if the user is root and exit if they are not. # function am_i_root { # if [[ $EUID -ne 0 ]]; then # Check if the user is root # printf "%s" "Please run as root" # Output message to the user # exit 1 # Exit with error code 1 # fi # } # Call the main function main "$@" exit 0 # The right and proper way to exit a script