Skip to content

Instantly share code, notes, and snippets.

@CertainLach
Created February 27, 2026 15:46
Show Gist options
  • Select an option

  • Save CertainLach/c2ea61168705c2c48a014cf7e2706d63 to your computer and use it in GitHub Desktop.

Select an option

Save CertainLach/c2ea61168705c2c48a014cf7e2706d63 to your computer and use it in GitHub Desktop.
NixOS configuration for btrfs + luks2 sdImage builder
# Copy of https://github.com/NixOS/nixpkgs/blob/8647f7d06de5f1e5f5e34a740bdd94d5af09917c/nixos/lib/make-btrfs-fs.nix
# with LUKS2 encryption added on top.
# Includes this patch: https://github.com/NixOS/nixpkgs/pull/434122
{
stdenv,
buildPackages,
storePaths,
populateImageCommands ? "",
volumeLabel,
uuid ? "44444444-4444-4444-8888-888888888888",
luksVolumeLabel ? "NIXOS_LUKS",
luksUuid ? "44444444-4444-4444-9999-888888888888",
btrfs-progs,
libfaketime,
util-linux,
cryptsetup,
qemu-utils,
luksPassphrase ? "changeme",
# Ignored for luks
compressImage ? false,
}:
let
sdClosureInfo = buildPackages.closureInfo { rootPaths = storePaths; };
in
stdenv.mkDerivation {
name = "btrfs-luks-fs.img";
nativeBuildInputs = [
btrfs-progs
libfaketime
util-linux
cryptsetup
qemu-utils
];
buildCommand = ''
set -x
(
mkdir -p ./files
${populateImageCommands}
)
mkdir -p ./rootImage/nix/store
xargs -I % cp -a --reflink=auto % -t ./rootImage/nix/store/ < ${sdClosureInfo}/store-paths
(
GLOBIGNORE=".:.."
shopt -u dotglob
for f in ./files/*; do
cp -a --reflink=auto -t ./rootImage/ "$f"
done
)
cp ${sdClosureInfo}/registration ./rootImage/nix-path-registration
needed=$(du -sb ./rootImage | awk '{print $1}')
# I had troubles resizing image created with btrfs --shrink. It should work in theory, but with smaller images and
# less built-in services
size=$(( needed * 120 / 100 + 512 * 1024 * 1024 ))
truncate -s $size unencrypted.img
unshare --map-root-user faketime -f "1970-01-01 00:00:01" mkfs.btrfs -L ${volumeLabel} -U ${uuid} -r ./rootImage unencrypted.img
if ! btrfs check $img; then
echo "--- 'btrfs check' failed for BTRFS image ---"
return 1
fi
# Cryptsetup doesn't have commands for encryption of raw disk image, it is only possible to do without sandboxing.
# Qemu-img on the other hand can convert disk image to LUKS1.
# Should I add the necessary code to cryptsetup instead?..
# Or should I add luks2 support to qemu-img?..
qemu-img convert -f raw -O luks \
--object secret,id=sec0,data=${luksPassphrase} \
-o key-secret=sec0 \
unencrypted.img luks1.img
# But because qemu-img produces LUKS1... How it can be converted to LUKS2? Manually, I hate this code too.
# Extract headers and master key from luks1, to reconstruct luks2 later.
LUKS1_DUMP=$(cryptsetup luksDump luks1.img)
L1_OFF=$(echo "$LUKS1_DUMP" | awk '/Payload offset/{print $3}')
MK_BITS=$(echo "$LUKS1_DUMP" | awk '/MK bits/{print $3}')
CIPHER_NAME=$(echo "$LUKS1_DUMP" | awk '/Cipher name/{print $3}')
CIPHER_MODE=$(echo "$LUKS1_DUMP" | awk '/Cipher mode/{print $3}')
ENC_SIZE=$(( $(stat -c %s luks1.img) - L1_OFF * 512 ))
echo -n "${luksPassphrase}" | cryptsetup luksDump \
--dump-volume-key --volume-key-file ./master-key \
--batch-mode --key-file - luks1.img
# And with dumped LUKS1 parameters, we can create a LUKS2 image with the same encryption params, and copy the data.
# Note that 512 is used for sector size, qemu-img does not support other sector sizes
L2_OFF=32768
truncate -s $(( L2_OFF * 512 + ENC_SIZE )) $out
echo -n "${luksPassphrase}" | cryptsetup luksFormat \
--type luks2 --batch-mode \
--volume-key-file ./master-key --key-size "$MK_BITS" \
--cipher "$CIPHER_NAME-$CIPHER_MODE" \
--sector-size 512 \
--label ${luksVolumeLabel} --uuid ${luksUuid} \
--offset $L2_OFF \
--key-file - $out
dd if=luks1.img of=$out bs=512 skip=$L1_OFF seek=$L2_OFF conv=notrunc
'';
}
{
# Only relevant configuration is included, you should still add the necessary modules so your system can
# be built into an ext4 sdCard image, and then this configuration can be added on top
sdImage.rootFilesystemCreator = ./btrfs-luks2.nix;
boot.initrd.luks.devices.cryptroot = {
device = "/dev/disk/by-uuid/44444444-4444-4444-9999-888888888888";
};
# sdImage module defines root as ext4 with extra options, mkForce is used to drop everything that is not relevant for btrfs
fileSystems."/" = lib.mkForce {
device = "/dev/mapper/cryptroot";
fsType = "btrfs";
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment