diff --git a/harden b/harden index 99f3372..035b715 100755 --- a/harden +++ b/harden @@ -19,7 +19,7 @@ trap 'printf "Error on line %d with signal %s" "$LINENO" "$?"' ERR # Exit on err 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##*/}" +##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 @@ -74,8 +74,12 @@ function install_packages { 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 + echo 'debconf debconf/frontend select Noninteractive' | sudo debconf-set-selections + export DEBIAN_FRONTEND=noninteractive + export NEEDRESTART_MODE=a + export DEBIAN_PRIORITY=critical + sudo -E apt update -y && sudo apt upgrade -y # Update the package list and upgrade the packages + sudo -E apt install -y "$@" # Install the packages passed as arguments ;; "dnf") # If the package manager is dnf sudo dnf upgrade -y # Update the package list @@ -98,7 +102,7 @@ function install_packages { # 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 + local dependencies=(fuse-overlayfs dbus-user-session uidmap slirp4netns docker-compose htop curl git sudo vim ssh 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 @@ -132,7 +136,7 @@ function hardenSSH { # 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 \ + sudo sed -i \ -e 's/^#AllowGroups.*/AllowGroups sudo/' \ -e 's/^#Port.*/Port 22100/' \ -e 's/^#ClientAliveInterval.*/ClientAliveInterval 300/' \ @@ -142,6 +146,7 @@ function hardenSSH { -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 @@ -156,16 +161,20 @@ function getCorrectFirewall { 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 + echo '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 # 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 + sudo dnf install firewalld -y # Install firewalld printf "%s" "firewalld" # Output firewalld ;; "openSUSE Leap") # If the distribution is OpenSUSE - zypper install firewalld -y # Install firewalld + sudo zypper install firewalld -y # Install firewalld printf "%s" "firewalld" # Output firewalld ;; *) @@ -183,20 +192,24 @@ function getCorrectKernelSecurityModule { 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 + sudo 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 + echo '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 # 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 + sudo 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 + sudo zypper install -t pattern apparmor -y # Install apparmor printf "%s" "apparmor" # Output apparmor ;; *) @@ -209,20 +222,18 @@ function getCorrectKernelSecurityModule { } function firewallInit { - local firewall - firewall="$(getCorrectFirewall)" # Get the correct firewall - case "$firewall" in + getCorrectFirewall # Get the correct firewall installed + # Determine if ufw or firewalld is installed + 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 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" @@ -232,8 +243,9 @@ function firewallInit { } function kernelSecurityModuleInit { - local kernelSecurityModule - kernelSecurityModule="$(getCorrectKernelSecurityModule)" # Get the correct kernel security module + 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 @@ -241,11 +253,12 @@ function kernelSecurityModuleInit { ;; 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 +## 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 + sudo sed -i 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config # Set selinux to enforcing + ## 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" @@ -256,13 +269,23 @@ function kernelSecurityModuleInit { function dockerInit { # Add user to docker group to avoid using sudo when running docker commands - sudo usermod -aG docker "$USER" + ##sudo usermod -aG docker "$USER" + # Set up rootless docker + sudo runuser - secdep -c 'curl -fsSL https://get.docker.com/rootless | sh' + sudo runuser - secdep -c 'export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock' + sudo runuser - secdep -c 'export PATH=/home/$USER/bin:$PATH' + sudo runuser - secdep -c 'printf "%s\n" "export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock" >> "$HOME/.bashrc"' + sudo runuser - secdep -c 'printf "%s\n" "export PATH=/home/$USER/bin:$PATH" >> "$HOME/.bashrc"' + sudo runuser - secdep -c 'systemctl --user enable --now docker.service' + sudo runuser - secdep -c 'sudo setcap cap_net_bind_service=ep "$(which rootlesskit)"' + sudo runuser - secdep -c 'systemctl --user restart docker.service' # Create a new docker network to dissalow communication between containers - sudo docker network create --driver bridge -o "com.docker.network.bridge.enable_icc"="false" dockerNetworkNoICC + ##sudo docker network create --driver bridge -o "com.docker.network.bridge.enable_icc"="false" dockerNetworkNoICC # Get all arguments passed to the function and store them in the dockerImages array local dockerImages=("$@") # Check if there is a docker-compose.yml file in the user's home directory - [[ -f "$HOME/docker-compose.yml" ]] && docker-compose -f "$HOME/docker-compose.yml" up -d # If there is, run it + sudo runuser - secdep -c '[[ -f "$HOME/docker-compose.yml" ]] && docker-compose -f "$HOME/docker-compose.yml" up -d' # If there is, run it + # sudo runuser - secdep -c '[[ -f "$HOME/docker-compose.yml" ]] && docker-compose -f "$HOME/docker-compose.yml" up -d' || return 0 # If there is, run it # Check if the dockerImages array is empty and return 0 if it is [[ "${#dockerImages[@]}" -eq 0 ]] && return 0 # Loop through the dockerImages array @@ -270,49 +293,53 @@ function dockerInit { 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 + # with the restart 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 + # The --cap-add NET_BIND_SERVICE option will add the NET_BIND_SERVICE capability to the docker image # The --read-only option will mount the docker image as read-only # The --tmpfs /opt option will mount the /opt directory as a tmpfs # The --network dockerNetworkNoICC option will connect the docker image to the dockerNetworkNoICC network + # The -v /:/host option will enable the docker rootless mode # # The --user secdep option will run the docker image as the secdep user to prevent privilege escalation - docker run -d --restart always --name "$dockerImage" --security-opt=no-new-privileges --cap-drop all --cap-add NET_ADMIN --read-only --tmpfs /opt --network dockerNetworkNoICC "$dockerImage" + /home/secdep/bin/docker run -d --restart always --name "$dockerImage" --security-opt=no-new-privileges --cap-drop all --cap-add NET_BIND_SERVICE --read-only --tmpfs /opt -v /:/host "$dockerImage" + ##docker run -d --restart always --name "$dockerImage" --security-opt=no-new-privileges --cap-drop all --cap-add NET_BIND_SERVICE --read-only --tmpfs /opt -v /:/host --network dockerNetworkNoICC "$dockerImage" #docker run -d --restart always --name "$dockerImage" --security-opt=no-new-privileges --cap-drop all --cap-add NET_ADMIN --user secdep "$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 -} +# 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 -} +# 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 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' @@ -360,7 +387,26 @@ 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 +#sudo systemctl restart sshd +} + +function restartServices { + for service in "${services[@]}"; do + sudo systemctl restart "$service" + done + # command -v ufw >/dev/null 2>&1 && currentFirewall="ufw" || currentFirewall="firewalld" + whereis ufw | grep -q /ufw && currentFirewall="ufw" || currentFirewall="firewalld" + # For ufw + # Enable the firewall + # Enable and start the firewall on boot + [[ "$currentFirewall" == "ufw" ]] && sudo ufw enable && sudo systemctl enable --now ufw + # For firewalld + # Reload the firewall + [[ "$currentFirewall" == "firewalld" ]] && sudo firewall-cmd --reload + sudo systemctl disable --now docker + # Make sure docker is disabled after + # installing docker-compose, to make sure + # only rootless docker is used } # The main function will call the check_dependencies function and exit if it fails. @@ -371,14 +417,16 @@ function main { 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 +## I should probably delete this one 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 +## [[ $# -gt 0 ]] && dockerInit "$@" || exit 1 + dockerInit "$@" || exit 1 + restartServices || exit 1 printf "%s" "Script finished" # Output message to the user } diff --git a/secdep.py b/secdep.py index 87e4e23..1d28353 100755 --- a/secdep.py +++ b/secdep.py @@ -1127,6 +1127,9 @@ def create_node(provider, name=None, location=None, size=None, image=None, confi sudo chown secdep:secdep /home/secdep -R sudo chmod 700 /home/secdep /home/secdep/.ssh sudo chmod 600 /home/secdep/.ssh/authorized_keys''' + ## Last two lines don't work + ## sudo printf "%s\n" "secdep ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/secdepRules" + ## sudo chmod 0440 "/etc/sudoers.d/secdepRules"''' deploy = ScriptDeployment(script=SCRIPT, name="initialization.sh", delete=True) if args.deploy: actualDeployScript = ScriptFileDeployment(script_file=SECDEP_DEPLOY_SCRIPT, args=args.deploy, name="harden", delete=True) @@ -1268,6 +1271,9 @@ def create_node(provider, name=None, location=None, size=None, image=None, confi sudo chown secdep:secdep /home/secdep -R sudo chmod 700 /home/secdep /home/secdep/.ssh sudo chmod 600 /home/secdep/.ssh/authorized_keys''' + ## Last two lines don't work + ## sudo printf "%s\n" "secdep ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/secdepRules" + ## sudo chmod 0440 "/etc/sudoers.d/secdepRules"''' deploy = ScriptDeployment(script=SCRIPT, name="initialization.sh", delete=True) if args.deploy: actualDeployScript = ScriptFileDeployment(script_file=SECDEP_DEPLOY_SCRIPT, args=args.deploy, name="harden", delete=True) @@ -1324,7 +1330,10 @@ def list_all_nodes(provider, filterIn=None, awsRegion=None): if len(azureNodes) > 0: for node in azureNodes: nodes.append(node) - console.print("[bold white]Loading %s%%...[/bold white]" % (int((2/providers_quantity)*100))) + if providers_quantity < 3: + console.print("[bold white]Loading %s%%...[/bold white]" % (int(((2/providers_quantity)/2)*100))) + else: + console.print("[bold white]Loading %s%%...[/bold white]" % (int((2/providers_quantity)*100))) else: console.print("Skipping azure", style="bold red") if SECDEP_AWS_ACCESS_KEY != "": @@ -1338,7 +1347,10 @@ def list_all_nodes(provider, filterIn=None, awsRegion=None): if len(awsNodes) > 0: for node in awsNodes: nodes.append(node) - console.print("[bold white]Loading %s%%...[/bold white]" % (int((3/providers_quantity)*100))) + if providers_quantity < 3: + console.print("[bold white]Loading %s%%...[/bold white]" % (int((providers_quantity/providers_quantity)*100))) + else: + console.print("[bold white]Loading %s%%...[/bold white]" % (int((3/providers_quantity)*100))) else: console.print("Skipping aws", style="bold red") status.stop() @@ -1350,7 +1362,7 @@ def list_all_nodes(provider, filterIn=None, awsRegion=None): if len(gceNodes) > 0: for node in gceNodes: nodes.append(node) - console.print("[bold white]Loading %s%%...[/bold white]" % (int((1/providers_quantity)*100))) + console.print("[bold white]Loading 100%...[/bold white]") else: console.print("Skipping gce", style="bold red") status.stop() @@ -1362,7 +1374,7 @@ def list_all_nodes(provider, filterIn=None, awsRegion=None): if len(azureNodes) > 0: for node in azureNodes: nodes.append(node) - console.print("[bold white]Loading %s%%...[/bold white]" % (int((2/providers_quantity)*100))) + console.print("[bold white]Loading 100%...[/bold white]") else: console.print("Skipping azure", style="bold red") status.stop() @@ -1390,7 +1402,7 @@ def list_all_nodes(provider, filterIn=None, awsRegion=None): if len(awsNodes) > 0: for node in awsNodes: nodes.append(node) - console.print("[bold white]Loading %s%%...[/bold white]" % (int((3/providers_quantity)*100))) + console.print("[bold white]Loading 100%...[/bold white]") else: console.print("Skipping aws", style="bold red") status.stop()