/mnt/persist/private: split waiting on the keyfile out of the mount process

This commit is contained in:
2024-08-06 02:03:43 +00:00
parent 809c3af7fa
commit 020e5f8c6e
4 changed files with 64 additions and 102 deletions

View File

@@ -1,13 +1,3 @@
# TODO: this can be simplified.
# mounting /mnt/persist/private consists of two steps:
# - way for a password to appear at /run/gocryptfs/private.key
# - try mounting the filesystem, using that password
# and retry if that fails.
# because fs mounts lack a retry capability, that whole thing is encapsulated inside one step,
# and then there's dancing to ensure that `test -L /mnt/persist/private` doesn't hang before the store has been mounted.
# if i can break these apart, the implementation should look a lot less confusing.
#
# i can probably use `echo $passwd | gocryptfs-xray -dumpmasterkey /nix/persist/private/gocryptfs.conf` for the above
{ config, lib, pkgs, sane-lib, utils, ... }:
let
@@ -17,34 +7,18 @@ let
in
lib.mkIf config.sane.persist.enable
{
sane.programs."mount.fuse3.gocryptfs-private" = {
sane.programs."provision-private-key" = {
packageUnwrapped = pkgs.static-nix-shell.mkBash {
pname = "mount.fuse3.gocryptfs-private";
pname = "provision-private-key";
srcRoot = ./.;
pkgs = [
"coreutils-full"
"gocryptfs"
"inotify-tools"
"libfuse-sane"
];
};
# sandbox.method = "landlock"; #< trickier than it looks to enable sandboxing of any kind here.
sandbox.autodetectCliPaths = "existing";
sandbox.capabilities = [
"sys_admin"
"chown"
"dac_override"
"dac_read_search"
"fowner"
"lease"
"mknod"
"setgid"
"setuid"
];
sandbox.extraPaths = [
"/dev/fuse"
"/run/gocryptfs"
];
suggestedPrograms = [ "gocryptfs-private" ];
sandbox.method = "bwrap";
sandbox.autodetectCliPaths = "parent";
};
sane.programs.gocryptfs-private = {
packageUnwrapped = pkgs.static-nix-shell.mkBash {
@@ -91,7 +65,7 @@ lib.mkIf config.sane.persist.enable
fileSystems."${origin}" = {
device = "gocryptfs-private#${backing}";
fsType = "fuse3.gocryptfs-private";
fsType = "fuse3.sane";
options = [
# "auto"
"nofail"
@@ -117,7 +91,7 @@ lib.mkIf config.sane.persist.enable
wantedBy = [ "local-fs.target" ];
mount.depends = [
config.sane.fs."${backing}".unit
config.sane.fs."/run/gocryptfs".unit
config.sane.fs."/run/gocryptfs/private.key".unit
];
# unitConfig.DefaultDependencies = "no";
mount.mountConfig.TimeoutSec = "infinity";
@@ -154,9 +128,15 @@ lib.mkIf config.sane.persist.enable
user = config.sane.defaultUser; #< must be user-writable so i can unlock it.
mode = "0770";
};
sane.fs."/run/gocryptfs/private.key".generated.command = [
"${lib.getExe config.sane.programs.provision-private-key.package}"
"/run/gocryptfs/private.key"
"${backing}/gocryptfs.conf"
];
sane.programs."mount.fuse3.gocryptfs-private".enableFor.system = true;
system.fsPackages = [ pkgs.libfuse-sane config.sane.programs."mount.fuse3.gocryptfs-private".package ];
sane.programs."gocryptfs-private".enableFor.system = true;
sane.programs."provision-private-key".enableFor.system = true;
system.fsPackages = [ pkgs.libfuse-sane ];
sane.user.services.gocryptfs-private = {
description = "wait for /mnt/persist/private to be mounted";

View File

@@ -1,4 +1,6 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p bash -p gocryptfs
exec gocryptfs --sanebox-path /run/gocryptfs/private.key "$@"
passfile=/run/gocryptfs/private.key
gocryptfs --sanebox-path "$passfile" "$@"
rm "$passfile"

View File

@@ -1,66 +0,0 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p bash -p coreutils-full -p inotify-tools -p libfuse-sane
# backing=$1
# facing=$2
mountArgs=("$@")
passdir=/run/gocryptfs
passfile="$passdir/private.key"
waitForPassfileOnce() {
local timeout=$1
if [ -f "$passfile" ]; then
return 0
else
# wait for some file to be created inside the directory.
# inotifywait returns 0 if the file was created. 1 or 2 if timeout was hit or it was interrupted by a different event.
inotifywait --timeout "$timeout" --event create "$passdir"
return 1 #< maybe it was created; we'll pick that up immediately, on next check
fi
}
waitForPassfile() {
# there's a race condition between testing the path and starting `inotifywait`.
# therefore, use a retry loop. exponential backoff to decrease the impact of the race condition,
# especially near the start of boot to allow for quick reboots even if/when i hit the race.
for timeout in 4 4 8 8 8 8 16 16 16 16 16 16 16 16; do
if waitForPassfileOnce "$timeout"; then
return 0
fi
done
while true; do
if waitForPassfileOnce 30; then
return 0
fi
done
}
tryOpenStore() {
# try to open the store (blocking), if it fails, then delete the passfile because the user probably entered the wrong password
echo "mounting with ${mountArgs[*]}"
# gocryptfs will unlock the store, and *then* fork into the background.
# so when it returns, the files are either immediately accessible, or the mount failed (likely due to a bad password
# if ! SANEBOX_APPEND="--sanebox-path $passfile" gocryptfs "${mountArgs[@]}"; then
# echo "failed mount (transient failure)"
# rm -f "$passfile"
# return 1
# fi
#
# the actual `mount` operation blocks fs operations until it succeeds or fails.
# hence, we don't want to be inside `mount` while waiting for the password, else we block the world.
# instead, do this funky recursive thing, where we call ourself after the password is provided.
# TODO: better would be to move the password procurement out of the mount altogether, into some .unit file,
# but that's tricky because the unit file wouldn't have a great way of validating the password.
if ! mount.fuse3.sane "${mountArgs[@]}"; then
echo "failed mount (transient failure)"
rm -f "$passfile"
return 1
fi
}
waitForPassfile
while ! tryOpenStore; do
waitForPassfile
done
echo "mounted"
# mount is complete (successful), and backgrounded.
# remove the passfile even on successful mount, for vague safety reasons (particularly if the user were to explicitly unmount the private store).
rm -f "$passfile"

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p bash -p coreutils-full -p gocryptfs -p inotify-tools
passfile="$1" # e.g. /run/gocryptfs/private.key
conffile="$2" # e.g. /nix/persist/private/gocryptfs.conf
passdir=$(dirname "$passfile")
waitForPassfileOnce() {
local timeout=$1
if [ -f "$passfile" ]; then
return 0
else
# wait for some file to be created inside the directory.
# inotifywait returns 0 if the file was created. 1 or 2 if timeout was hit or it was interrupted by a different event.
inotifywait --timeout "$timeout" --event create "$passdir"
return 1 #< maybe it was created; we'll pick that up immediately, on next check
fi
}
waitForPassfile() {
# there's a race condition between testing the path and starting `inotifywait`.
# therefore, use a retry loop. exponential backoff to decrease the impact of the race condition,
# especially near the start of boot to allow for quick reboots even if/when i hit the race.
for timeout in 4 4 8 8 8 8 16 16 16 16 16 16 16 16; do
if waitForPassfileOnce "$timeout"; then
return 0
fi
done
while true; do
if waitForPassfileOnce 30; then
return 0
fi
done
}
validatePassword() {
if ! cat "$passfile" | gocryptfs-xray -dumpmasterkey "$conffile" > /dev/null; then
echo "failed key validation"
rm -f "$passfile"
return 1
fi
}
waitForPassfile
while ! validatePassword; do
waitForPassfile
done
echo "key provisioned"