impermanence: use systemd/fileSystems for the crypt mounts, instead of 3rd-party impermanence

This commit is contained in:
colin 2022-12-28 14:06:58 +00:00
parent 760f2ac66d
commit e6919dd16f

View File

@ -10,7 +10,9 @@ let
# taken from sops-nix code: checks if any secrets are needed to create /etc/shadow
secretsForUsers = (lib.filterAttrs (_: v: v.neededForUsers) config.sops.secrets) != {};
persist-base = "/nix/persist";
encrypted-clear-on-boot-base = "/var/lib/impermanence/cleared-on-boot";
encrypted-clear-on-boot-base = "/mnt/crypt/clearedonboot";
encrypted-clear-on-boot-store = "/nix/persist/crypt/clearedonboot";
encrypted-clear-on-boot-key = "/mnt/crypt/clearedonboot.key"; # TODO: move this to /tmp, but that requires tmp be mounted first?
home-dir-defaults = {
user = "colin";
group = "users";
@ -24,6 +26,15 @@ let
relativeTo = "";
};
# turn a path into a name suitable for systemd
clean-name = path: let
dashes = builtins.replaceStrings ["/"] ["-"] path;
startswith = builtins.substring 0 1 dashes;
in if startswith == "-"
then substring 1 255 dashes
else dashes
;
dir-options = defaults: types.submodule {
options = {
encryptedClearOnBoot = mkOption {
@ -52,8 +63,13 @@ let
if isString opt then
ingest-dir-option defaults { directory = opt; }
else
{
rec {
encryptedClearOnBoot = opt.encryptedClearOnBoot or false;
srcDevice = if encryptedClearOnBoot
then encrypted-clear-on-boot-base
else persist-base
;
srcPath = "${srcDevice}${directory}";
directory = defaults.relativeTo + opt.directory;
user = opt.user or defaults.user;
group = opt.group or defaults.group;
@ -73,6 +89,8 @@ let
"/var/lib/machines" # maybe not needed, but would be painful to add a VM and forget.
];
ingested-dirs = ingested-home-dirs ++ ingested-sys-dirs ++ ingested-default-dirs;
ingested-crypt-dirs = builtins.filter (o: o.encryptedClearOnBoot) ingested-dirs;
ingested-plain-dirs = builtins.filter (o: !o.encryptedClearOnBoot) ingested-dirs;
in
{
options = {
@ -88,7 +106,7 @@ in
sane.impermanence.encrypted-clear-on-boot = mkOption {
default = builtins.any (opt: opt.encryptedClearOnBoot) ingested-dirs;
type = types.bool;
description = "define /nix/persist/crypt/cleared-on-boot to be an encrypted filesystem which is unreadable after power-off";
description = "define ${encrypted-clear-on-boot-base} to be an encrypted filesystem which is unreadable after power-off";
};
sane.impermanence.home-dirs = mkOption {
default = [];
@ -124,56 +142,99 @@ in
# note: `boot.initrd.availableKernelModules` ALSO isn't enough: idk why.
boot.initrd.kernelModules = [ "fuse" ];
system.activationScripts.mountEncryptedClearedOnBoot =
system.activationScripts.prepareEncryptedClearedOnBoot =
let
pass-template = "/tmp/encrypted-clear-on-boot.XXXXXXXX";
tmpdir = "/tmp/impermanence";
script = pkgs.writeShellApplication {
name = "mountEncryptedClearedOnBoot";
runtimeInputs = with pkgs; [ fuse gocryptfs ];
name = "prepareEncryptedClearedOnBoot";
runtimeInputs = with pkgs; [ gocryptfs ];
text = ''
backing="$1"
mountpt="$2"
if ! test -e "$mountpt"/init
passfile="$2"
if ! test -e "$passfile"
then
mkdir -p "$backing" "$mountpt" ${tmpdir}
tmpdir=$(dirname "$passfile")
mkdir -p "$backing" "$tmpdir"
# if the key doesn't exist, it's probably not mounted => delete the backing dir
rm -rf "''${backing:?}"/*
passfile=$(mktemp ${pass-template})
# generate key. we can "safely" keep it around for the lifetime of this boot
dd if=/dev/random bs=128 count=1 | base64 --wrap=0 > "$passfile"
# initialize the crypt store
gocryptfs -quiet -passfile "$passfile" -init "$backing"
mount.fuse "gocryptfs#$backing" "$mountpt" -o nodev,nosuid,allow_other,passfile="$passfile"
# mount -t fuse.gocryptfs -o passfile="$passfile" "$backing" "$mountpt"
# gocryptfs -quiet -passfile "$passfile" "$backing" "$mountpt"
rm "$passfile"
unset passfile
touch "$mountpt"/init
fi
'';
};
in {
deps = [ "modprobe" ];
text = ''${script}/bin/mountEncryptedClearedOnBoot /nix/persist/crypt/cleared-on-boot "${encrypted-clear-on-boot-base}"'';
text = ''${script}/bin/prepareEncryptedClearedOnBoot ${encrypted-clear-on-boot-store} ${encrypted-clear-on-boot-key}'';
};
system.activationScripts.createPersistentStorageDirs.deps = [ "mountEncryptedClearedOnBoot" ];
fileSystems."${encrypted-clear-on-boot-base}" = {
device = encrypted-clear-on-boot-store;
fsType = "fuse.gocryptfs";
options = [
"nodev"
"nosuid"
"allow_other"
"passfile=${encrypted-clear-on-boot-key}"
"defaults"
];
noCheck = true;
};
environment.systemPackages = [ pkgs.gocryptfs ]; # fuse needs to find gocryptfs
system.activationScripts.createPersistentStorageDirs.deps = [ "prepareEncryptedClearedOnBoot" ];
})
({
# XXX: why is this necessary?
sane.image.extraDirectories = [ "/nix/persist/var/log" ];
environment.persistence = lib.mkMerge (builtins.map (opt:
let
base = if opt.encryptedClearOnBoot
then encrypted-clear-on-boot-base
else persist-base
;
in {
"${base}".directories = [
{ inherit (opt) directory user group mode; }
];
}
) ingested-dirs);
environment.persistence."${persist-base}".directories = builtins.map (opt: {
inherit (opt) directory user group mode;
}) ingested-plain-dirs;
fileSystems = listToAttrs (builtins.map
(opt: rec {
name = opt.directory;
value = {
device = "${encrypted-clear-on-boot-base}${opt.directory}";
options = [
"bind"
"x-systemd.requires=mnt-crypt-clearedonboot.mount"
"x-systemd.after=impermanence-perms-${clean-name name}.service"
# `wants` doesn't seem to make it to the service file here :-(
"x-systemd.wants=impermanence-perms-${clean-name name}.service"
];
# fsType = "bind";
noCheck = true;
};
})
ingested-crypt-dirs
);
# create services which ensure the source directories exist and have correct ownership/perms before mounting
systemd.services = listToAttrs (builtins.map
(opt: rec {
name = "impermanence-perms-${clean-name opt.directory}";
value = {
description = "prepare permissions for ${opt.directory}";
serviceConfig = {
ExecStart = let
srcPath = "${opt.srcPath}";
in pkgs.writeShellScript "prepare-${name}" ''
mkdir -p ${srcPath}
chown ${opt.user}:${opt.group} ${srcPath}
chmod ${opt.mode} ${srcPath}
'';
Type = "oneshot";
};
after = [ "mnt-crypt-clearedonboot.mount" ];
wants = [ "mnt-crypt-clearedonboot.mount" ];
wantedBy = [ "${clean-name opt.directory}.mount" ];
};
})
ingested-crypt-dirs
);
# for each edge in a mount path, impermanence gives that target directory the same permissions
# as the matching folder in /nix/persist.