qubes-builder-alpine/scripts/alpine-chroot
2022-03-04 14:33:13 -05:00

144 lines
5.5 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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 TemplateVMs 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 TemplateVMs 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" "$@"