#!/bin/bash

## Copyright (C) 2019 - 2025 ENCRYPTED SUPPORT LLC <adrelanos@whonix.org>
## See the file COPYING for copying conditions.

## features:
## - nodev,nosuid where appropriate
## - optional noexec for most except /home
## - optional noexec for all including /home
## - idempotent (script can be safely re-run)
## - can be run from:
##   - systemd
##   - dracut
##   - manually from command line
## - can safely handle non-existing folders
## - error handling
## - log output:
##   - shows each and every command executed
##   - shows old mount options prior running remount-secure
##   - shows new mount options after running remount-secure

## noexec in /tmp and/or /home can break some malware but also legitimate
## applications.

## https://www.kicksecure.com/wiki/Noexec
## https://www.kicksecure.com/wiki/Dev/remount-secure
## https://forums.whonix.org/t/re-mount-home-and-other-with-noexec-and-nosuid-among-other-useful-mount-options-for-better-security/7707

#set -x
set -e
set -o pipefail
set -o nounset

init() {
   if test -o xtrace ; then
      output_command=true
   else
      output_command=echo
   fi

   $output_command "$0: INFO: START"

   ## dracut does not have id. Saving space in initial ramdisk.
   if command -v id &>/dev/null ; then
      if [ "$(id -u)" != "0" ]; then
         $output_command "ERROR: must be run as root! sudo $0"
         exit 1
      fi
   fi

   mkdir --parents "/run/remount-secure"
   exit_code=0

   ## dracut sets NEWROOT=/sysroot
   [[ -v NEWROOT ]] || NEWROOT=""
   if [ "$NEWROOT" = "" ]; then
      $output_command "INFO: dracut detected: no"
   else
      $output_command "INFO: dracut detected: yes - NEWROOT: '$NEWROOT'"
   fi

   ## Debugging.
   #echo "ls -la /root/"
   #ls -la / || true
   #echo "ls -la /sysroot/"
   #ls -la /sysroot/ || true
   #echo "env"
   #env || true
}

parse_options() {
   ## Thanks to:
   ## https://mywiki.wooledge.org/BashFAQ/035

   while :
   do
       case ${1:-} in
           0)
               $output_command "WARNING: Not using remount-secure."
               exit 0
               shift
               ;;
           1)
               $output_command "INFO: level 1/3 (low)"
               most_noexec_maybe=""
               home_noexec_maybe=""
               parsed=true
               shift
               ;;
           2)
               $output_command "INFO: level 2/3 (medium)"
               most_noexec_maybe=",noexec"
               home_noexec_maybe=""
               parsed=true
               shift
               ;;
           3)
               $output_command "INFO: level 3/3 (high)"
               most_noexec_maybe=",noexec"
               home_noexec_maybe=",noexec"
               parsed=true
               shift
               ;;
           --force)
               $output_command "INFO: --force"
               option_force=true
               shift
               ;;
           --)
               shift
               break
               ;;
           -*)
               echo "ERROR: unknown option: $1" >&2
               exit 1
               ;;
           *)
               break
               ;;
       esac
   done

   [[ -v option_force ]] || option_force=""
   [[ -v parsed ]] || parsed=false
   [[ -v home_noexec_maybe ]] || home_noexec_maybe=""
   [[ -v most_noexec_maybe ]] || most_noexec_maybe=""

   $output_command "INFO: using nosuid,nodev: yes"

   if [ "$home_noexec_maybe" = "" ]; then
      $output_command "INFO: using noexec for all: no"
   else
      $output_command "INFO: using noexec for all: yes"
      return 0
   fi

   if [ "$most_noexec_maybe" = "" ]; then
      $output_command "INFO: using noexec for most: no"
   else
      $output_command "INFO: using noexec for most (not all): yes"
      return 0
   fi

   if [ "$parsed" = "true" ]; then
      return 0
   fi

   $output_command "ERROR: syntax error. use either:
$0 0
$0 1
$0 2
$0 3"

   exit 1
}

preparation() {
   ## Debugging.
   #$output_command "INFO: 'findmnt --list' output at the START."
   #$output_command "$(findmnt --list)"
   #$output_command ""
   true
}

remount_secure() {
   $output_command ""

   ## ${FUNCNAME[1]} is the name of the calling function. I.e. the function
   ## which called this function.
   status_file_name="${FUNCNAME[1]}"
   ## example status_file_name:
   ## _home
   status_file_full_path="/run/remount-secure/${status_file_name}"
   ## example status_file_full_path:
   ## /run/remount-secure/_home

   old_mount_options="$(findmnt --noheadings --output options -- "$mount_folder")" || true
   ## example old_mount_options:
   ## rw,nosuid,nodev,relatime,discard

   $output_command "INFO: '$mount_folder' old_mount_options: '$old_mount_options'"

   if printf '%s\n' "$old_mount_options" | grep "$intended_mount_options" >/dev/null 2>/dev/null ; then
      $output_command "INFO: '$mount_folder' has already intended mount options. ('$intended_mount_options')"
      return 0
   fi

   ## When this package is upgraded, the systemd unit will run again.
   ## If the user meanwhile manually relaxed mount options, this should not be undone.

   if [ ! "$option_force" == "true" ]; then
      if [ -e "$status_file_full_path" ]; then
         $output_command "INFO: '$mount_folder' already remounted earlier. Not remounting again. Use --force if this is what you want."
         return 0
      fi
   fi

   if ! test -d "$mount_folder" ; then
      ## For example /boot/efi does not always exist on all systems.
      $output_command "INFO: '$mount_folder' folder exists: no"
      return 0
   fi
   $output_command "INFO: '$mount_folder' folder exists: yes"

   if findmnt --noheadings "$mount_folder" >/dev/null ; then
      $output_command "INFO: '$mount_folder' already mounted, therefore using remount."
      $output_command INFO: Executing: mount --make-private --options "remount,${intended_mount_options}" "$mount_folder"
      mount --make-private --options "remount,${intended_mount_options}" "$mount_folder" || exit_code=100
   else
      $output_command "INFO: '$mount_folder' not yet mounted, therefore using mount bind."
      $output_command INFO: Executing: mount --make-private --options "$intended_mount_options" --bind "$mount_folder" "$mount_folder"
      mount --make-private --options "$intended_mount_options" --bind "$mount_folder" "$mount_folder" || exit_code=101
   fi

   new_mount_options="$(findmnt --noheadings --output options -- "$mount_folder")" || true
   $output_command "INFO: '$mount_folder' new_mount_options: '$new_mount_options'"

   touch "$status_file_full_path"
}

_boot() {
   mount_folder="$NEWROOT/boot"
   ## https://lists.freedesktop.org/archives/systemd-devel/2015-February/028456.html
   intended_mount_options="nosuid,nodev,noexec"
   remount_secure
}

_boot_efi() {
   ## TODO: new, test
   mount_folder="$NEWROOT/boot/efi"
   intended_mount_options="nosuid,nodev,noexec"
   remount_secure
}

_run() {
   mount_folder="/run"
   ## https://lists.freedesktop.org/archives/systemd-devel/2015-February/028456.html
   intended_mount_options="nosuid,nodev${most_noexec_maybe}"
   remount_secure
}

_dev() {
   mount_folder="/dev"
   ## /dev should be nosuid,noexec as per:
   ## https://bugs.launchpad.net/ubuntu/+source/systemd/+bug/1991975
   intended_mount_options="nosuid,noexec"
   remount_secure
}

_dev_shm() {
   mount_folder="/dev/shm"
   intended_mount_options="nosuid,nodev${most_noexec_maybe}"
   remount_secure
}

_sys() {
   ## TODO: new, test
   mount_folder="/sys"
   intended_mount_options="nosuid,nodev,noexec"
   remount_secure
}

_tmp() {
   mount_folder="$NEWROOT/tmp"
   intended_mount_options="nosuid,nodev${most_noexec_maybe}"
   remount_secure
}

_var_tmp() {
   mount_folder="$NEWROOT/var/tmp"
   intended_mount_options="nosuid,nodev${most_noexec_maybe}"
   remount_secure
}

_var_log() {
   mount_folder="$NEWROOT/var/log"
   intended_mount_options="nosuid,nodev,noexec"
   remount_secure
}

_var() {
   mount_folder="$NEWROOT/var"
   ## noexec: Not possible. Reason:
   ## Debian stores executable maintainer scripts in /var/lib/dpkg/info folder.
   intended_mount_options="nosuid,nodev"
   remount_secure
}

_usr() {
   ## TODO: new, test
   mount_folder="$NEWROOT/usr"
   intended_mount_options="nodev"
   remount_secure
}

_home() {
   mount_folder="$NEWROOT/home"
   intended_mount_options="nosuid,nodev${home_noexec_maybe}"
   remount_secure
}

_root() {
   ## TODO: new, test
   mount_folder="$NEWROOT/root"
   intended_mount_options="nosuid,nodev${home_noexec_maybe}"
   remount_secure
}

_srv() {
   ## TODO: new, test
   mount_folder="$NEWROOT/srv"
   intended_mount_options="nosuid,nodev${most_noexec_maybe}"
   remount_secure
}

_media() {
   ## TODO: new, test
   mount_folder="$NEWROOT/media"
   intended_mount_options="nosuid,nodev${most_noexec_maybe}"
   remount_secure
}

_mnt() {
   ## TODO: new, test
   mount_folder="$NEWROOT/mnt"
   intended_mount_options="nosuid,nodev${most_noexec_maybe}"
   remount_secure
}

_opt() {
   ## TODO: new, test
   mount_folder="$NEWROOT/opt"
   ## Allow /opt exec as usually optional binaries are placed there such as Firefox
   ## when manually installed from tarball.
   intended_mount_options="nosuid,nodev"
   remount_secure
}

_etc() {
   ## TODO: new, test
   ## /etc cannot be noexec because various executables are there. To find, run:
   ## sudo find /etc -executable
   mount_folder="$NEWROOT/etc"
   intended_mount_options="nosuid,nodev"
   remount_secure
}

end() {
   ## Debugging.
   #$output_command "INFO: 'findmnt --list' output at the END."
   #$output_command "$(findmnt --list)"

   $output_command ""
   $output_command "INFO: exit_code: $exit_code"
   $output_command "$0: INFO: END"
   exit $exit_code
}

main() {
   init
   parse_options "$@"
   preparation

   _boot
   _boot_efi
   _run
   _dev
   _dev_shm
   _tmp
   _var_tmp
   _var_log
   _var
   _usr
   _home
   _root
   _srv
   _media
   _mnt
   _opt
   _etc

   end
}

## TODO: see also hidepid /usr/lib/systemd/system/proc-hidepid.service
#mount --options defaults,nosuid,nodev,noexec,remount,subset=pid /proc

main "$@"
