144 lines
5.5 KiB
Bash
Executable file
144 lines
5.5 KiB
Bash
Executable file
#!/bin/bash
|
||
###
|
||
### A stripped-down version of the 'arch-chroot' script bundled with Archlinux
|
||
###
|
||
### This version drops unused code and makes a fey key modifications,
|
||
### annotated where they occur below.
|
||
###
|
||
|
||
shopt -s extglob
|
||
|
||
die() { error "$@"; exit 1; }
|
||
|
||
chroot_add_mount() {
|
||
mount "$@" && CHROOT_ACTIVE_MOUNTS=("$2" "${CHROOT_ACTIVE_MOUNTS[@]}")
|
||
}
|
||
|
||
setup_volatile_secret_key_dir() {
|
||
if [ "$SKIP_VOLATILE_SECRET_KEY_DIR" = "true" ]; then
|
||
return
|
||
fi
|
||
|
||
# This directory stores secret GPG keys, so its contents must be kept secret
|
||
# at all costs. Anyone with access to the files in it can compromise the
|
||
# built TemplateVM and all VMs based on it.
|
||
secret_key_dir="$1/etc/pacman.d/gnupg/private-keys-v1.d" &&
|
||
|
||
# private-keys-v1.d does not exist before we create the tmpfs
|
||
mkdir -p -m 0755 -- "${secret_key_dir%/*}" &&
|
||
mkdir -p -m 0000 -- "$secret_key_dir" &&
|
||
|
||
# Create README
|
||
[[ -f "$secret_key_dir/README" ]] || cat > "$secret_key_dir/README" <<'EOF' &&
|
||
# Why is this directory immutable?
|
||
|
||
In QubesOS, a TemplateVM’s root volume is readable by all AppVMs based on it.
|
||
Therefore, it cannot be used to store secret data.
|
||
|
||
Pacman relies on the secrecy of its master key, which is normally stored in
|
||
`/etc/pacman.d/gnupg/private-keys-v1.d`. Anyone who has this key can sign
|
||
packages that Pacman will accept. Therefore, this key must not be stored on the
|
||
root volume. Furthermore, a user might (quite reasonably) assume that there is
|
||
no sensitive information on a TemplateVM’s private volume unless they have added
|
||
it explicitly. So the master key cannot be stored there either.
|
||
|
||
The only remaining option is to use an ephemeral key that is only kept in
|
||
memory. That is what QubesOS does: during the build process, a ramfs is mounted
|
||
over /etc/pacman.d/gnupg/private-keys-v1.d, so that the secret key is kept in
|
||
memory. When the ramfs is unmounted, the key is destroyed along with it.
|
||
|
||
There is one remaining problem: relying on a mount point is not fail-safe. If
|
||
the ramfs fails to mount, or if the user later runs operations like
|
||
`pacman-key --init`, a new master key will be generated. It will later be
|
||
leaked to AppVMs based on this template.
|
||
|
||
To prevent this potentially disasterous failure, QubesOS marks the directory as
|
||
immutable. This ensures that nobody (not even root) can create any files in it.
|
||
When GPG tries to write its secret key to disk, it will fail, preventing any
|
||
leakage.
|
||
|
||
P.S.: Why a ramfs and not a tmpfs? Data on a ramfs can never be paged out to
|
||
disk, which ensures that this key is never leaked to swap partitions. GPG
|
||
internally locks its memory into RAM to prevent similar problems.
|
||
EOF
|
||
# Mark private-keys-v1.d immutable, so that files (such as secret keys)
|
||
# cannot accidentally be created in it.
|
||
chattr -R +i -- "$secret_key_dir" &&
|
||
|
||
# See the README above for why this is a ramfs
|
||
chroot_add_mount pacman-privkeys "$secret_key_dir" -t ramfs -o mode=000,nosuid,noexec,nodev || exit
|
||
}
|
||
|
||
chroot_setup() {
|
||
CHROOT_ACTIVE_MOUNTS=()
|
||
[[ $(trap -p EXIT) ]] && die '(BUG): attempting to overwrite existing EXIT trap'
|
||
trap 'chroot_teardown' EXIT
|
||
|
||
# alpine-chroot drops the conditional bind mount on the chroot path, as
|
||
# it seemed to shadow mounts set up before arch-chroot was invoked
|
||
|
||
# Set the correct permissions for mount points
|
||
chmod -- 0755 "$1/dev" "$1/run" &&
|
||
chmod -- 0555 "$1/proc" "$1/sys" &&
|
||
chmod -- 1777 "$1/tmp" &&
|
||
|
||
setup_volatile_secret_key_dir &&
|
||
|
||
chroot_add_mount proc "$1/proc" -t proc -o nosuid,noexec,nodev &&
|
||
chroot_add_mount sys "$1/sys" -t sysfs -o nosuid,noexec,nodev,ro &&
|
||
# alpine-chroot will never have occasion to use efivars, so don't bother
|
||
# mounting efivarfs here
|
||
chroot_add_mount udev "$1/dev" -t devtmpfs -o mode=0755,nosuid &&
|
||
chroot_add_mount devpts "$1/dev/pts" -t devpts -o mode=0620,gid=5,nosuid,noexec &&
|
||
chroot_add_mount shm "$1/dev/shm" -t tmpfs -o mode=1777,nosuid,nodev &&
|
||
chroot_add_mount run "$1/run" -t tmpfs -o nosuid,nodev,mode=0755 &&
|
||
chroot_add_mount tmp "$1/tmp" -t tmpfs -o mode=1777,strictatime,nodev,nosuid ||
|
||
|
||
exit
|
||
if [[ -d "$APKTOOLS_CACHE_DIR" ]]; then
|
||
APKTOOLS_CACHE_MOUNT_DIR="${APKTOOLS_CACHE_MOUNT_DIR:-$1/var/cache/apk}"
|
||
mkdir -p "$APKTOOLS_CACHE_MOUNT_DIR"
|
||
# Cached qubes packages may be from old runs and throw checksum errors
|
||
chroot_add_mount "$APKTOOLS_CACHE_DIR" "$APKTOOLS_CACHE_MOUNT_DIR" --bind
|
||
fi
|
||
if [[ -d "$APKTOOLS_CUSTOM_REPO_DIR" ]]; then
|
||
mkdir -p "$1/tmp/qubes-packages-mirror-repo"
|
||
chroot_add_mount "$APKTOOLS_CUSTOM_REPO_DIR" "$1/tmp/qubes-packages-mirror-repo" --bind
|
||
fi
|
||
}
|
||
|
||
chroot_teardown() {
|
||
# alpine-chroot kills gpg-agent, started by pacman-key, which otherwise
|
||
# keeps the mounts busy and prevents unmounting
|
||
pkill gpg-agent
|
||
umount "${CHROOT_ACTIVE_MOUNTS[@]}"
|
||
unset CHROOT_ACTIVE_MOUNTS
|
||
}
|
||
|
||
usage() {
|
||
cat <<EOF
|
||
usage: ${0##*/} chroot-dir [command]
|
||
|
||
-h Print this help message
|
||
|
||
If 'command' is unspecified, ${0##*/} will launch /bin/sh.
|
||
|
||
EOF
|
||
}
|
||
|
||
if [[ -z $1 || $1 = @(-h|--help) ]]; then
|
||
usage
|
||
exit $(( $# ? 0 : 1 ))
|
||
fi
|
||
|
||
(( EUID == 0 )) || die 'This script must be run with root privileges'
|
||
chrootdir=$1
|
||
shift
|
||
|
||
[[ -d $chrootdir ]] || die "Can't create chroot on non-directory %s" "$chrootdir"
|
||
|
||
chroot_setup "$chrootdir" || die "failed to setup chroot %s" "$chrootdir"
|
||
# alpine-chroot already has /etc/resolv.conf managed by the builder
|
||
# scripts, so no need to bind to the host system's resolv.conf here
|
||
|
||
SHELL=/bin/sh unshare --fork --pid chroot "$chrootdir" "$@"
|