145 lines
5.5 KiB
Text
145 lines
5.5 KiB
Text
|
#!/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" "$@"
|