#!/bin/bash ## Copyright (C) 2019 - 2024 ENCRYPTED SUPPORT LP ## 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 echo "$old_mount_options" | grep --quiet "$intended_mount_options" ; 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 "$@"