I have a LUKS setup on my laptop. Grub runs the bootloader from GPT and then initrd asks me for the decryption password. Then Gnome loads and I need to log in.
I’d like to have a setup similar to windows and macos where the disk is encrypted and your password is used both to decrypt the disk and to log you in.
I wonder if that’s achievable? I know that the loginwindow code in OS X was loathed by their developers and they didn’t want to make any changes to it, so I assume that an early GUI with credentials capture is a somewhat hard problem.
I also wonder about how to implement the login, perhaps something akin to ssh-agent with a PAM plugin allowing a single login shortly after decryption?
I have a TPM module too, not sure if that would be useful here.
I enabled auto-logon in the display manager, because if someone can unlock my LUKS container there’s not much the session manager can do to prevent unauthorized access.
@emmanuelrosa’s suggestion is the best interim, but don’t be tempted to not set a password/have no lock screen, since that makes your system susceptible to run-time attacks.
One way of achieving that is to have the disk unencrypted (or encrypted with keys in TPM) and then encrypt the actually sensitive data (your home directory) with a separate key (your user login).
TPM decryption isn’t a think in NixOS yet AFAICT and kinda requires a verified boot to be truly effective (only really protects against a stolen disk without; a stolen machine would still expose system data).
It is indeed possible to achieve a single password authentication flow and I have been doing it for years on my Gentoo system. But, I am working on migrating to NixOS and trying to figure out how to achieve this same experience right now.
The way I solved this on Gentoo is by using Dracut as my initrd system and using systemd with systemd-cryptsetup for managing LUKS partitions. It uses the systemd-ask-password under the hood for password prompting which has an option --keyname that is set to cryptsetup when it is invoked by the systemd-cryptsetup. The passphrase will be cached in the kernel keyring and accessible for 2.5 minutes, then expire. I then use pam_gdm PAM module to try and read the stored password from keyring and inject it into the PAM stack.
For this to work my user password has to be synchronized with my LUKS passphrase. I enable autologin and when the system is cold-booting, I get prompted for LUKS passphrase once and then go all the way to my desktop and even get my GNOME Keyring automatically unlocked.
This of course has a bunch of security implications, but I am comfortable with the trade-off this presents (not everyone is or should be based on their risk/threat profile).
I believe systemd is now available in stage1 in NixOS, so I hope to get something like this working on NixOS as well.
It is, but not by default. It is as simple as enabling boot.initrd.systemd.enable then using a single systemd-cryptenroll command to enroll the root disk.
Here’s what I did: I put the root filesystem (actually /nix because I used impermanence, but that doesn’t matter) on one luks partition, and /home/aleksana on another luks partition.
When booting, systemd’s initrd will try to unlock the root partition. If I use systemd-cryptenroll to make TPM generate a luks key and put it into a slot of the root partition, as long as the status of the device hardware, BIOS, and secure boot state (These are called TPM pcrs, you can read systemd-cryptenroll’s manpages for more information) does not change when booting again, initrd will automatically unlock the root partition, otherwise I would need to enter the password.
Then I set the luks password of the home directory to be the same as my account password, so that pam_mount will help me unlock the partition when logging in (pam_mount also supports encryption methods such as zfs and fscrypt).
In this way, I only entered the password once during the boot process, and if the initrd asked me to enter the password, there must be something wrong. For example, if I upgrade the BIOS, or change the status of secure boot, or use another hard disk but keep the data unchanged, the key calculated by TPM will change, causing the root partition to fail to unlock. I recently discovered that changing the hard drive from one m2 slot to another also causes the keys calculated by the TPM to change.
Unfortunately, home-manager will have problems with pam_mount, because home-manager wants the user’s directory to appear as soon as possible. I use a hack that converts the activation script from a systemd service to a systemd-user service so that it runs after login.
This is the idea of my current setup. If you need more details, I can expand on that.
For this to work my user password has to be synchronized with my LUKS passphrase. I enable autologin and when the system is cold-booting, I get prompted for LUKS passphrase once and then go all the way to my desktop and even get my GNOME Keyring automatically unlocked.
Did you manage to get this workiong in NixOS?
With autoLogin enabled I still have the keyring locked at startup even though the same password is used for LUKS + user password + keyring.
I did not, unfortunately. I had to take an extended break from all of this and just recently returned to my system set-up. I have decided to go with ZFS on my system and the problem still unsolved there as well. I will probably end-up manually using systemd-ask-password --keyname cryptsetup in the boot.initrd.postDeviceCommands and feeding that to zfs load-key, or something like that.
Yes I did and it required quite a bit of set-up to work. Here’s the gist of the idea for you:
boot = {
initrd = {
systemd = {
enable = true;
# Disablet the `zfs-import` service provided by NixOS
services.zfs-import-my_pool.enable = false;
extraBin = {
# We need keyctl inside of initrd to be able to cache the key
keyctl = "${pkgs.keyutils}/bin/keyctl";
};
services.load-encryption-key =
let
# This is a PARTUUID of the drive that hosts my encrypted root
nvme = "dev-disk-by\\x2dpartuuid-01234567\\x2d89ab\\x2dcdef\\x2d0123\\x2d456789abcdef.device";
in
{
bindsTo = [ nvme ];
after = [
nvme
"systemd-modules-load.service"
"systemd-ask-password-console.service"
];
before = [
"sysroot.mount"
"sysroot-nix-store.mount"
"zfs-import.target"
"shutdown.target"
];
requiredBy = [
"sysroot.mount"
"sysroot-nix-store.mount"
"zfs-import.target"
];
conflicts = [ "shutdown.target" ];
unitConfig.DefaultDependencies = false;
serviceConfig = {
RemainAfterExit = true;
Type = "oneshot";
KeyringMode = "inherit";
};
script = ''
# Import the pool
${config.boot.zfs.package}/bin/zpool import my_pool
for _ in $(seq 5); do
# Store the entered passphrase in the Kernel keyring, use `cryptsetup` as the name because that's what `pam_gdm.so` expects
KEYID=$(${config.boot.initrd.systemd.package}/bin/systemd-ask-password -n --no-tty "Enter the ZFS encryption key" | keyctl padd user cryptsetup @u)
# Set the key expiration — I don't want the key to stay accessible longer than needed
keyctl timeout "$KEYID" 20 || true
# Feed the key to the `zfs load-key`
if keyctl pipe "$KEYID" | ${config.boot.zfs.package}/bin/zfs load-key -a ; then
exit 0
fi
done
echo >&2 "Failed to load ZFS encryption keys"
exit 1
'';
};
};
};
The reason why I am using keyctl here is to make sure the key actually expires. There is no way to do this using systemd-ask-password in systemd before version 257 (24.11 is on 256).
It will be greatly simplified with systemd 257+, because they introduced a SYSTEMD_ASK_PASSWORD_KEYRING_TIMEOUT_SEC that can be set to control how long the key will stay alive, and so script can be simplified to something like:
for _ in $(seq 5); do
if ${config.boot.initrd.systemd.package}/bin/systemd-ask-password -n --no-tty --keyname=cryptsetup --id=zfs:my_pool "Enter the ZFS encryption key" |
${config.boot.zfs.package}/bin/zfs load-key -a; then
exit 0
fi
echo "Invalid key, try again"
done
exit 1
If you don’t use a display manager you can also use this option:
services.getty.autologinOnce
If enabled the automatic login will only happen in the first tty
once per boot. This can be useful to avoid retyping the account
password on systems with full disk encrypted.
Oh I actually got keyring unlocking working myself in the meantime, using a different approach.
I patched the zfs module, adding just --keyname=cryptsetup to the code that asks for your password makes it work. I also added one more step before the while loop, which is to reuse a cached password if you have multiple pools with the same keyname or if you already unlocked something else (like /boot) using LUKS with the same password.
For anyone who wants to do the same, this is how I did it:
The patch:
diff --git a/nixos/modules/tasks/filesystems/zfs.nix b/nixos/modules/tasks/filesystems/zfs.nix
index 741c8fe3fb7e..17473b599456 100644
--- a/nixos/modules/tasks/filesystems/zfs.nix
+++ b/nixos/modules/tasks/filesystems/zfs.nix
@@ -168,22 +168,24 @@ let
{
if [[ "$ks" != unavailable ]]; then
continue
fi
case "$kl" in
none )
;;
prompt )
tries=3
success=false
+ ${systemd}/bin/systemd-ask-password --keyname=cryptsetup --accept-cached --timeout=${toString cfgZfs.passwordTimeout} "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds" \
+ && success=true
while [[ $success != true ]] && [[ $tries -gt 0 ]]; do
- ${systemd}/bin/systemd-ask-password --timeout=${toString cfgZfs.passwordTimeout} "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds" \
+ ${systemd}/bin/systemd-ask-password --keyname=cryptsetup --timeout=${toString cfgZfs.passwordTimeout} "Enter key for $ds:" | ${cfgZfs.package}/sbin/zfs load-key "$ds" \
&& success=true \
|| tries=$((tries - 1))
done
[[ $success = true ]]
;;
* )
${cfgZfs.package}/sbin/zfs load-key "$ds"
;;
esac
} < /dev/null # To protect while read ds kl in case anything reads stdin
I put the patch file in /((my nixos configuration path))/patches/zfs-add-cryptsetup-keyname.patch, and this is how I applied it (add this to configuration.nix, “inputs” is from the main flake.nix, and “applyPatches” has to come from nixpkgs in your flake, not from your pkgs in configuration.nix):
Also, make sure systemd initrd is enabled for this to work.
boot.initrd.systemd.enable = true;
Final note: patching a “normal package” is much easier than patching a nixos module… All you have to do to patch a normal program is to add the patchfile path to its patches attribute via overrideAttrs