zur Übersicht: Linux: meine Software und Konfigurationstipps

automatische Verschlüsselung von /tmp bei jedem Booten

19.04.2005 (aktualisiert: 30.08.2006)

Problem

Es gibt viele gute – private wie geschäftliche – Gründe, seine Daten zu verschlüsseln. Dies ist unter Linux nicht schwer; SuSE und sicher auch andere Distributionen stellen komfortable Möglichkeiten der Nutzung verschlüsselter Laufwerke für die eigenen Daten zur Verfügung. Nur in seltenen Fällen sind von dieser Verschlüsselung auch die Laufwerke mit den Systemverzeichnissen (/usr, /var usw.) betroffen.

Sicherheitslücke

Sicherheit ist aber erst dann gegeben, wenn alle Laufwerke, auf denen sensible Daten landen können, verschlüsselt sind. Ein Risiko besteht hier in erster Linie durch Anwendungsprogramme (also solche mit Userrechten). Diese können nicht in Systemverzeichnisse schreiben (insbesondere an /var ist hier zu denken) – mit Ausnahme von /tmp. Gänzlich ungeplant und unkontrollierbar können Daten auf dem Swaplaufwerk landen. Dieses lässt sich allerdings durch entsprechende Änderung des fstab-Eintrags bequem bei jedem Booten mit einer Zufallspassphrase verschlüsseln:

/dev/hda3 swap swap loop=/dev/loop7,encryption=twofish 0 0

Auch hier gilt die unten erläuterte Problematik der Kernelmodule. Daher habe ich in der boot.local noch swapoff -a;swapon -a stehen, damit die Verschlüsselung auch wirklich durchgeführt wird. Im Erfolgsfall gibt swapon (bzw. das davon aufgerufenemkswap die Meldung Swapbereich Version 1 wird angelegt, Größe 518180 KBytes aus.

Ärgerlich ist, dass – im Gegensatz zu mount ein konkretes loop device angegeben werden muss, das Stichwort loop alleine reicht nicht. Außerdem greift die Verschlüsselung absurderweise nur bei swapon -a, nicht aber bei etwa swapon /dev/hda3.

Lösung

Diese Möglichkeit, die für Swaplaufwerke schon besteht, habe ich auf /tmp erweitert.

Dieses Bootscript (Shellscript) sucht in fstab nach einem Eintrag mit Mountpunt /tmp und den Mountoptionen noauto, loop=/dev/loop<n> und encryption=<cipher>. Wenn so ein Eintrag gefunden wird, setzt das Script das angegebene loop device mit der angegebenen Verschlüsselungsart und einer Zufallspassphrase auf das entsprechende Volume, formatiert das loop device und mountet es anschließend auf /tmp.

/var/tmp nicht vergessen

Sinnvollerweise linkt man /var/tmp irgendwo in /tmp, da man ansonsten nur das halbe Problem gelöst hat.

kein Laufwerk mehr frei

Wenn man diese Möglichkeit nutzen will, aber kein Laufwerk mehr frei hat, kann man einen Trick anwenden. loop devices nehmen nicht nur block devices als Ziel, sondern sind auch mit Dateien zufrieden. Man kann also einfach eine entsprechend große Datei anlegen. Eine 200 MiB große Datei mit dem Namen tmploopdevfile legt man im Rootverzeichnis mit folgendem Kommando an:

dd if=/dev/zero of=/tmploopdevfile bs=1M count=200

In fstab wird dann einfach statt beispielsweise /dev/hda6 der Pfad /tmploopdevfile eingetragen.

Alternativen

Inzwischen weiß ich, dass es auch einfacher geht. In meiner man page für losetup findet sich folgender Hinweis:

-H phash

Hash function random does not ask for password but sets up random keys and attempts to put loop to multi-key mode. When random/1777 hash type is used as mount option for mount program, mount program will create new file system on the loop device and construct initial permissions of file system root directory from octal digits that follow the slash character.

WARNING! DO NOT USE RANDOM HASH TYPE ON PARTITION WITH EXISTING IMPORTANT DATA ON IT. RANDOM HASH TYPE WILL DESTROY YOUR DATA.

In der Tat, das ist der Weg

Nach der desaströsen Installation von SuSE 10.1 habe ich dies mal ausprobiert, und siehe da – es funktioniert. Man muss natürlich dafür sorgen, dass die benötigten Kernelmodule (in meinem Fall cryptoloop und twofish) rechtzeitig geladen werden. Das heißt, dass man sie entweder in die initrd packt oder aber /tmp nicht direkt über fstab mounten lässt. Ich habe das Problem für mich folgendermaßen gelöst. Mein fstab-Eintrag:

/dev/hda5 /tmp reiserfs noauto,defaults,loop,encryption=twofish256,phash=random/1777 0 0

Das wird kombiniert mit folgendem Eintrag in meiner /etc/init.d/boot.local:

awk '{if ($2=="/tmp") {mp="found";exit(0);}};END {if(mp=="found") exit(0); else exit (1);}' /proc/mounts ||
{ echo "Mounting /tmp with randomly encrypted file system (via fstab, phash=random)"
        mount /tmp; }
mkdir -p /tmp/.ICE-unix /tmp/var
chown root:root /tmp/.ICE-unix
chmod 1777 /tmp/.ICE-unix /tmp/var

Der awk-Aufruf erfolgt nur der Ordnung halber, im Prinzip reicht mount /tmp aus. Die Folgezeilen sind nur nötig, weil SuSE blöd ist und es nicht gebacken kriegt, diese Verzeichnisse richtig anzulegen, wenn sie fehlen. Wenn man sich dann als User einloggt, ist Ende. Das Verzeichnis lege ich an, weil mein /var/tmp darauf verlinkt.

Wenn die benötigten Kernelmodule in der initrd stünden, könnte man sich das noauto in der fstab und den boot.local-Eintrag sparen.

Download


#! /bin/bash
#
# This is a boot script for SuSE Linux and it probably has to be 
# adapted to other distributions. Look for "# SuSE specific [...]" lines 
# for "rc_failed", "rc_status" and "rc_exit". The former can be deleted 
# (they set and show the status), rc_exit has to be replaced by exit.
#
# CHANGELOG
# Version 1.2
#	- Line length limited
#	- The volume given in fstab need not be a block device but can 
#		be a regular file, too.
#	- Deleted unsupported arguments (like reload)
#	- mount -f only on success of mount
#	- suppress mount output
# Version 1.1, 19.04.2005, Hauke Laging
#	- Bugfix: chmod auf /tmp (rwxrwxrwt)
# Version 1.0.1, 23.02.2005, Hauke Laging
# Version 1.0, 15.01.2005, Hauke Laging, http://www.hauke-laging.de/ 
#	software@hauke-laging.de
#
#    This program is free software; you can redistribute it and/or modify 
#    it under the terms of the GNU General Public License as published by 
#    the Free Software Foundation; either version 2 of the License, or 
#    (at your option) any later version. 
# 
### BEGIN INIT INFO
# Provides:          boot.mount_tmp
# Required-Start:    boot.localfs
# Should-Start:
# Required-Stop:
# Should-Stop:
# Default-Start:     B
# Default-Stop:      
# Short-Description: Mount /tmp on randomly encrypted volume
# Description:       Look for a /tmp entry with loop and noauto options 
#	in /etc/fstab, set up the loop device with an random
#	passphrase, create the file system specified in 
#	/etc/fstab on the loop device and mount it to /tmp.
#	This makes sure that any information in /tmp is securely
#	lost after rebooting. This closes a kind of security gap for 
#	users who encrypt their data volumes and the swap file.
### END INIT INFO
# 

# Check for missing binaries (stale symlinks should not happen)
# Note: Special treatment of stop for LSB conformance
# debug
for prog in awk losetup grep mount
	do
	type $prog &>/dev/null || { echo "'$prog' not installed"; 
	if [ "$1" = "stop" ]; then exit 0;
	else exit 5; fi; }
done

mount_point="/tmp"
shopt -s extglob

# SuSE specific - BEGIN
. /etc/rc.status
# Reset status of this service
rc_reset
# SuSE specific - END

case "$1" in
    start)
	echo -n "Mounting ${mount_point} on a randomly encrypted volume "
	mount_point_tmp="${mount_point//\//}"
	test 0 -lt ${#mount_point_tmp} && rm -rf "${mount_point}/"*
	
	# Check for fstab entry
	#	- must be a device (not e.g. tmpfs)
	#	- mount point must match exactly (^$)
	#	- loop device must be given in mount options (must match /dev/loop[0-9]*)
	#	- encryption must be given in mount options
	
	# check if mount point is already in use
	awk '$2 == "'"${mount_point}"'" {found=1};END{if(found==1)exit(0);else exit(1)}' /proc/mounts &&
		{ echo "Mount point '$mount_point' is already in use (according to /proc/mounts); aborting";
		rc_failed;rc_exit; }

	
	# get and check the (volume) device
	device="$(awk '$1 ~ "^/dev/" && $2 ~ "^'"${mount_point}"'/?$" && $4 ~ "loop=/dev/loop" && $4 ~ '\
'"encryption=" {print NR "_" $1;matched++};END{if(matched==0) exit(1); else if(matched==1) '\
'exit(0);else exit(2)}' /etc/fstab)";rc_awk=$?
	fstab_line="${device%%_*}"
	device="${device#*_}"
	test $rc_awk -eq 1 && { rc_status -s;rc_exit; }
	test $rc_awk -gt 1 && { echo "Several matches for '${mount_point}' in /etc/fstab; aborting.";
		rc_failed;rc_exit; }
	test -b "$device" || test -f "$device" || 
		{ echo "'$device' is neither a valid block device nor a regular file; aborting";
		rc_failed;rc_exit; }
	
	# get and check the loop device
	loop_device="$(awk 'NR == '"${fstab_line}"' {pos=match($4,"(,|^)loop=/dev/loop[0-9]*(,|$)");'\
'if(pos==0) exit(1); else {loop=substr($4,RSTART+5,RLENGTH-5);gsub(",","",loop);sub("^[^l]","",loop);'\
'print loop}}' /etc/fstab)";rc_awk=$?
	test $rc_awk -ne 0 && { echo "loop device could not be found in /etc/fstab; aborting.";
		rc_failed;rc_exit; }
	test -b "$loop_device" || { echo "'$loop_device' is not a valid block device; aborting";
		rc_failed;rc_exit; }
	losetup "$loop_device" &>/dev/null;rc_losetup=$?
	test "$rc_losetup" -eq 0 && { echo "'$loop_device' is already in use; aborting";
		rc_failed;rc_exit; }
	test "$rc_losetup" -eq 2 && { echo "'$loop_device' is not a valid loop device; aborting";
		rc_failed;rc_exit; }
	
	# get and check/prepare the encryption
	encryption="$(awk 'NR == '"${fstab_line}"' {pos=match($4,"(,|^)encryption=[^,]*(,|$)");'\
'if(pos==0) exit(1); else {enc=substr($4,RSTART+11,RLENGTH-11);gsub(",","",enc);sub("^[^e]","",enc);print enc}}' \
/etc/fstab)";rc_awk=$?
	test $rc_awk -ne 0 && { echo "encryption type could not be found in /etc/fstab; aborting.";
		rc_failed;rc_exit; }
# SuSE specific - BEGIN
	case "$encryption" in
		twofish*([0-9]))
			crypto_modules=(cryptoloop loop_fish2 twofish)
			for module in "${crypto_modules[@]}"
				do
				modprobe "$module" || { echo "modprobe '${module}' failed"; 
				if [ "$1" = "stop" ]; then exit 0;
				else rc_failed;rc_exit; fi; }
			done
		;;
		*)
			echo "${warn}Encryption type '${encryption}' is not supported."
			echo "Setting up the loop device may fail due to this.${norm}"
		;;
	esac
# SuSE specific - END
	
	# get and check the file system
	filesystem="$(awk 'NR == '"${fstab_line}"' {print $3}' /etc/fstab)";rc_awk=$?
	grep -q "$filesystem" /proc/filesystems || 
		{ echo "'$filesystem' is not currently supported by kernel (according to /proc/filesystems); aborting";
		rc_failed;rc_exit; }
	case "$filesystem" in
		ext[23])
			type mke2fs &>/dev/null || 
				{ echo "The file system builder for '${filesystem}' could not be found; aborting.";
				rc_failed;rc_exit; }
		;;
		reiserfs)
			type mkreiserfs &>/dev/null || 
				{ echo "The file system builder for '${filesystem}' could not be found; aborting.";
				rc_failed;rc_exit; }
		;;
		reiser4)
			type mkfs.reiser4 &>/dev/null || 
				{ echo "The file system builder for '${filesystem}' could not be found; aborting.";
				rc_failed;rc_exit; }
		;;
		*)
			echo "The file system '${filesystem}' is not supported by this script; aborting."
			rc_failed
			rc_exit
		;;
	esac
	
	# get and prepare the mount options
	# strip off options that are not useful
	patterns=(user users auto noauto loop="${loop_device}" encryption="${encryption}" loop)
	patterns_count=${#patterns[*]}
	awk_patterns=
	for((i=0;i<patterns_count;i++))
		do
		if [ 0 -ne "$i" ]
			then
			awk_patterns="${awk_patterns};"
		fi
		awk_patterns="${awk_patterns}pat[$i]=\"${patterns[$i]}\""
	done
	mopts="$(awk 'BEGIN{matches=0;'"${awk_patterns}"'};NR == 11 {FS=",";$0=$4;'\
'for(i=1;i<=NF;i++)for(j=0;j<2;j++){if($i==pat[j]){result[matches]=$i;matches++;break}}};'\
'END{for(i=0;i<matches;i++){if(i!=0)printf ",";printf result[i]};print ""}' /etc/fstab)";rc_awk=$?

	# setting up the loop device
	passphrase="${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}"\
"${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}${RANDOM}"
	test -z "$passphrase" && { echo "passphrase could not be generated; aborting.";
		rc_failed;rc_exit; }
	echo "$passphrase" | losetup -p 0 -e "$encryption" -C 1 "$loop_device" "$device" &>/dev/null;rc_losetup=$?
	test 0 -ne "$rc_losetup" && { echo "losetup failed; aborting.";
		rc_failed;rc_exit; }
	
	# create the file system
	case "$filesystem" in
		ext2)
			mke2fs "$loop_device" &>/dev/null;rc_mkfs=$?
		;;
		ext3)
			mke2fs -j "$loop_device" &>/dev/null;rc_mkfs=$?
		;;
		reiserfs)
			mkreiserfs -q "$loop_device" &>/dev/null;rc_mkfs=$?
		;;
		reiser4)
			mkfs.reiser4 --quiet "$loop_device" &>/dev/null;rc_mkfs=$?
		;;
		*)
			echo "The file system '${filesystem}' is not supported by this script; aborting."
			rc_failed
			rc_exit
		;;
	esac
	
	# mount the file system to /tmp but don't write to /etc/mtab as this
	#	wouldn't work due to the option "loop"
	test -n "$mopts" && use_mopts="-o ${mopts}" || use_mopts=
	mount -t "$filesystem" -n $use_mopts "$loop_device" "$mount_point" &>/dev/null;rc_mount=$?
	if [ 0 -ne "$rc_mount" ]
		then
		echo "mount failed; aborting"
		losetup -d "$loop_device"
		rc_failed
		rc_exit
	else
		# create the /etc/mtab entry, show the real device and the loop device as mount option
		nec_mtab_mopts="loop=${loop_device},encryption=${encryption}"
		test -n "$mopts" && mtab_mopts="${mopts},${nec_mtab_mopts}" || mtab_mopts="${nec_mtab_mopts}"
		mount -t "$filesystem" -f -o "$mtab_mopts" "$device" "$mount_point"
		mkdir /tmp/.ICE-unix
		chmod -R 777 /tmp
		chmod -R o+t /tmp
	fi
	
	# Remember status and be verbose
	rc_status -v
	;;
    stop)
	echo -n "Unmounting randomly encrypted '${mount_point}' "
	umount "${mount_point}"

	# Remember status and be verbose
	rc_status -v
	;;
    restart)
	## Stop the service and regardless of whether it was
	## running or not, start it again.
	$0 stop
	$0 start

	# Remember status and be quiet
	rc_status
	;;
    status)
	echo -n "Checking for encrypted volume for ${mount_point} "
	encryption_active=yes
	while true # use a pseudo loop so that we can leave it from everywhere like a function
		do
		# check if mount point is already in use
		awk '$2 == "'"${mount_point}"'" {found=1};END{if(found==1)exit(0);else exit(1)}' /proc/mounts ||
			{ encryption_active=no;break; }
		device="$(awk '$1 ~ "^/dev/" && $2 ~ "^'"${mount_point}"'/?$" {print $1;matched++};'\
'END{if(matched==0) exit(1); else if(matched==1) exit(0);else exit(2)}' /proc/mounts)" ||
			{ encryption_active=no;break; }
		losetup "$device" &>/dev/null || 
			{ encryption_active=no;break; }
		losetup -a | grep -qE "^${device}.*encryption" ||
			{ encryption_active=no;break; }
		break
	done

	# Return value is slightly different for the status command:
	# 0 - service up and running
	# 3 - service not running (unused)
	
	test yes = "$encryption_active" || bash -c "exit 3"
	
	# NOTE: rc_status knows that we called this init script with
	# "status" option and adapts its messages accordingly.
	rc_status -v
	;;
    *)
	echo "Usage: $0 {start|stop|status}"
	exit 1
	;;
esac
rc_exit