#3483 Issue closed: LUKS password not protected with { ... ; } 2>>/dev/$SECRET_OUTPUT_DEV

Labels: bug, fixed / solved / done

jsmeix opened issue at 2025-06-26 12:48:

ReaR version

master

Describe the ReaR bug in detail

See
https://github.com/rear/rear/issues/3474#issuecomment-2912653292
which reads (excerpt):

From what I see in the code in
https://github.com/rear/rear/blob/master/usr/share/rear/layout/prepare/GNU/Linux/160_include_luks_code.sh#L91
it seems there is support for some predefined password value.

As far as I understand the code that predefined password value
can be specified in the disklayout.conf file
in the crypt entries, see
https://github.com/rear/rear/blob/master/doc/user-guide/06-layout-configuration.adoc#luks-devices

LUKS Devices

crypt /dev/mapper/<name> ... [password=<password>] ...

The current code at
https://github.com/rear/rear/blob/master/usr/share/rear/layout/prepare/GNU/Linux/160_include_luks_code.sh#L91
is (excerpts)

    for option in $options ; do
        # $option is of the form keyword=value and
        # we assume keyword has no '=' character but value could be anything that may have a '=' character
        # so we split keyword=value at the leftmost '=' character so that
        # e.g. keyword=foo=bar gets split into key="keyword" and value="foo=bar":
        key=${option%%=*}
        value=${option#*=}
        # The "cryptseup luksFormat" command does not require any of the type, cipher, key-size, hash, uuid option values
        # because if omitted a cryptseup default value is used so we treat those values as optional.
        # Using plain test to ensure the value is a single non empty and non blank word
        # without quoting because test " " would return zero exit code
        # cf. "Beware of the emptiness" in https://github.com/rear/rear/wiki/Coding-Style
        case "$key" in
            (type)
                test $value && cryptsetup_options+=" --type $value"
                ;;
            (cipher)
                test $value && cryptsetup_options+=" --cipher $value"
                ;;
            (key_size)
                test $value && cryptsetup_options+=" --key-size $value"
                ;;
            (hash)
                test $value && cryptsetup_options+=" --hash $value"
                ;;
            (uuid)
                test $value && cryptsetup_options+=" --uuid $value"
                ;;
            (keyfile)
                test $value && keyfile=$value
                ;;
            (password)
                test $value && password=$value
                ;;
            (*)
                LogPrintError "Skipping unsupported LUKS cryptsetup option '$key' in 'crypt $target_device $source_device' entry in $LAYOUT_FILE"
                ;;
        esac
    done
...
    elif [ -n "$password" ] ; then
        echo "echo \"$password\" | cryptsetup luksFormat --batch-mode $cryptsetup_options $source_device"
        echo "echo \"$password\" | cryptsetup luksOpen $source_device $mapping_name"
...
    for option in $options ; do
        # $option is of the form keyword=value and
        # we assume keyword has no '=' character but value could be anything that may have a '=' character
        # so we split keyword=value at the leftmost '=' character so that
        # e.g. keyword=foo=bar gets split into key="keyword" and value="foo=bar":
        key=${option%%=*}
        value=${option#*=}
        case "$key" in
            (keyfile)
                test $value && keyfile=$value
                ;;
            (password)
                test $value && password=$value
                ;;
        esac
    done
...
    elif [ -n "$password" ] ; then
        echo "echo \"$password\" | cryptsetup luksOpen $source_device $mapping_name"

It should be something like

    { for option in $options ; do
        # $option is of the form keyword=value and
        # we assume keyword has no '=' character but value could be anything that may have a '=' character
        # so we split keyword=value at the leftmost '=' character so that
        # e.g. keyword=foo=bar gets split into key="keyword" and value="foo=bar":
        key=${option%%=*}
        value=${option#*=}
        # The "cryptseup luksFormat" command does not require any of the type, cipher, key-size, hash, uuid option values
        # because if omitted a cryptseup default value is used so we treat those values as optional.
        # Using plain test to ensure the value is a single non empty and non blank word
        # without quoting because test " " would return zero exit code
        # cf. "Beware of the emptiness" in https://github.com/rear/rear/wiki/Coding-Style
        case "$key" in
            (type)
                test $value && cryptsetup_options+=" --type $value"
                ;;
            (cipher)
                test $value && cryptsetup_options+=" --cipher $value"
                ;;
            (key_size)
                test $value && cryptsetup_options+=" --key-size $value"
                ;;
            (hash)
                test $value && cryptsetup_options+=" --hash $value"
                ;;
            (uuid)
                test $value && cryptsetup_options+=" --uuid $value"
                ;;
            (keyfile)
                test $value && keyfile=$value
                ;;
            (password)
                test $value && password=$value
                ;;
            (*)
                LogPrintError "Skipping unsupported LUKS cryptsetup option '$key' in 'crypt $target_device $source_device' entry in $LAYOUT_FILE"
                ;;
        esac
      done
    } 2>>/dev/$SECRET_OUTPUT_DEV
...
    elif { [ -n "$password" ] ; then
           echo "echo \"$password\" | cryptsetup luksFormat --batch-mode $cryptsetup_options $source_device"
           echo "echo \"$password\" | cryptsetup luksOpen $source_device $mapping_name"
         } 2>>/dev/$SECRET_OUTPUT_DEV
...
    { for option in $options ; do
        # $option is of the form keyword=value and
        # we assume keyword has no '=' character but value could be anything that may have a '=' character
        # so we split keyword=value at the leftmost '=' character so that
        # e.g. keyword=foo=bar gets split into key="keyword" and value="foo=bar":
        key=${option%%=*}
        value=${option#*=}
        case "$key" in
            (keyfile)
                test $value && keyfile=$value
                ;;
            (password)
                test $value && password=$value
                ;;
        esac
      done
    } 2>>/dev/$SECRET_OUTPUT_DEV
...
    elif { [ -n "$password" ] ; then
           echo "echo \"$password\" | cryptsetup luksOpen $source_device $mapping_name"
         } 2>>/dev/$SECRET_OUTPUT_DEV

jsmeix commented at 2025-06-26 13:25:

https://github.com/rear/rear/pull/3430
should be merged first to avoid possible conflicts in
/usr/share/rear/layout/prepare/GNU/Linux/160_include_luks_code.sh

jsmeix commented at 2025-07-01 10:58:

Ugh!
This issue is bigger than expected.

I tested "rear -D recover" with this original test VM:

# parted -s /dev/vda unit MiB print
Model: Virtio Block Device (virtblk)
Disk /dev/vda: 15360MiB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags: pmbr_boot
Number  Start     End       Size      File system  Name  Flags
 1      1.00MiB   9.00MiB   8.00MiB                      bios_grub
 2      9.00MiB   13312MiB  13303MiB                     legacy_boot
 3      13312MiB  15360MiB  2048MiB                      swap

# lsblk -ipo NAME,KNAME,TYPE,FSTYPE,SIZE,MOUNTPOINTS /dev/vda
NAME                    KNAME     TYPE  FSTYPE      SIZE MOUNTPOINTS
/dev/vda                /dev/vda  disk               15G 
|-/dev/vda1             /dev/vda1 part                8M 
|-/dev/vda2             /dev/vda2 part  crypto_LUKS  13G 
| `-/dev/mapper/cr_root /dev/dm-0 crypt ext4         13G /
`-/dev/vda3             /dev/vda3 part  crypto_LUKS   2G 
  `-/dev/mapper/cr_swap /dev/dm-1 crypt swap          2G [SWAP]

On a replacement VM with same disk type and size
I booted the ReaR recovery system and therein
I manually added 'password=johannes' in disklayout.conf like

crypt /dev/mapper/cr_root /dev/vda2 type=luks1 ... password=johannes
crypt /dev/mapper/cr_swap /dev/vda3 type=luks1 ... password=johannes

Additionally I needed in etc/rear/local.conf

LUKS_CRYPTSETUP_OPTIONS+=" --force-password"

because of the weak LUKS password 'johannes'
that was used only for this test here.

Then I did "rear -D recover"
where the LUKS setup worked without manual interaction:

RESCUE localhost:~ # rear -D recover
...
Start system layout restoration.
Disk '/dev/vda': creating 'gpt' partition table
Disk '/dev/vda': creating partition number 1 with name ''vda1''
Disk '/dev/vda': creating partition number 2 with name ''vda2''
Disk '/dev/vda': creating partition number 3 with name ''vda3''
Creating LUKS volume cr_root on /dev/vda2
Creating filesystem of type ext4 with mount point / on /dev/mapper/cr_root.
Mounting filesystem /
Creating LUKS volume cr_swap on /dev/vda3
Creating swap on /dev/mapper/cr_swap
Disk layout created.
...

BUT:

The LUKS password 'johannes' appears at more places than expected
in the "rear -D recover" debugscript mode log file:

RESCUE localhost:~ # egrep '+ source |johannes' /var/log/rear/rear-localhost.log | grep -B1 johannes

+ builtin source /usr/share/rear/layout/prepare/default/510_list_dependencies.sh
+++ echo '/dev/mapper/cr_root /dev/vda2 type=luks1 cipher=aes-xts-plain64 key_size=512 hash=sha256 uuid=b37bfd65-b8cd-42cb-b2a7-b3ae772bc2c5 password=johannes'
+++ echo '/dev/mapper/cr_root /dev/vda2 type=luks1 cipher=aes-xts-plain64 key_size=512 hash=sha256 uuid=b37bfd65-b8cd-42cb-b2a7-b3ae772bc2c5 password=johannes'
+++ echo '/dev/mapper/cr_swap /dev/vda3 type=luks1 cipher=aes-xts-plain64 key_size=512 hash=sha256 uuid=84838219-dfbe-449b-95df-28f3e177de94 password=johannes'
+++ echo '/dev/mapper/cr_swap /dev/vda3 type=luks1 cipher=aes-xts-plain64 key_size=512 hash=sha256 uuid=84838219-dfbe-449b-95df-28f3e177de94 password=johannes'

--

+ builtin source /usr/share/rear/layout/prepare/default/540_generate_device_code.sh
++ value=johannes
++ test johannes
++ password=johannes
++ '[' -n johannes ']'
++ echo 'echo "johannes" | cryptsetup luksFormat --batch-mode  --type luks1 --cipher aes-xts-plain64 --key-size 512 --hash sha256 --uuid b37bfd65-b8cd-42cb-b2a7-b3ae772bc2c5 --iter-time 2000 --use-random --force-password /dev/vda2'
++ echo 'echo "johannes" | cryptsetup luksOpen /dev/vda2 cr_root'
++ value=johannes
++ test johannes
++ password=johannes
++ '[' -n johannes ']'
++ echo 'echo "johannes" | cryptsetup luksFormat --batch-mode  --type luks1 --cipher aes-xts-plain64 --key-size 512 --hash sha256 --uuid 84838219-dfbe-449b-95df-28f3e177de94 --iter-time 2000 --use-random --force-password /dev/vda3'
++ echo 'echo "johannes" | cryptsetup luksOpen /dev/vda3 cr_swap'

--

++ builtin source /var/lib/rear/layout/diskrestore.sh
+++ echo johannes
+++ echo johannes
+++ echo johannes
+++ echo johannes

So things need to be also fixed in the
generate_layout_dependencies() function in
usr/share/rear/lib/layout-functions.sh
and in
usr/share/rear/layout/prepare/GNU/Linux/160_include_luks_code.sh
where a proper fix in 160_include_luks_code.sh
will avoid that the password gets logged during
source /var/lib/rear/layout/diskrestore.sh
because diskrestore.sh does set -x on its own
and it contains those code parts

# Create /dev/mapper/cr_root (crypt)
LogPrint "Creating LUKS volume cr_root on /dev/vda2"
echo "johannes" | cryptsetup luksFormat --batch-mode  --type luks1 --cipher aes-xts-plain64 --key-size 512 --hash sha256 --uuid b37bfd65-b8cd-42cb-b2a7-b3ae772bc2c5 --iter-time 
2000 --use-random --force-password /dev/vda2
echo "johannes" | cryptsetup luksOpen /dev/vda2 cr_root

...

# Create /dev/mapper/cr_swap (crypt)
LogPrint "Creating LUKS volume cr_swap on /dev/vda3"
echo "johannes" | cryptsetup luksFormat --batch-mode  --type luks1 --cipher aes-xts-plain64 --key-size 512 --hash sha256 --uuid 84838219-dfbe-449b-95df-28f3e177de94 --iter-time 2000 --use-random --force-password /dev/vda3
echo "johannes" | cryptsetup luksOpen /dev/vda3 cr_swap

jsmeix commented at 2025-07-01 11:10:

Even more Ugh!

In the rebooted recreated system I find the LUKS password 'johannes' in

/var/log/rear/recover/layout/diskrestore.sh
/var/log/rear/recover/layout/disklayout.conf
/var/log/rear/recover/layout/disklayout.conf.20250701120719.recover.796.orig
/var/log/rear/recover/rear-localhost.log

At least /var/log/rear/recover/ has reasonably secure permissions

# ls -ld /var/log/rear/recover/
drwx------ 5 root root 4096 Jul  1 12:08 /var/log/rear/recover/

jsmeix commented at 2025-07-03 14:00:

I keep this issue open to continue with further fixes
to protect the LUKS password against leaking out
as far as possible with reasonable effort.

jsmeix commented at 2025-07-03 14:15:

The next fix is for

echo "$password" | cryptsetup ...

see
https://github.com/rear/rear/pull/3485#issuecomment-3027807100
which reads (excerpt)

My immediate idea is to use
-------------------------------------------------------------
COMMAND ... <<< "$password"
-------------------------------------------------------------

On simple commandline with COMMAND 'cat' that seems to work:
-------------------------------------------------------------
# password="my password"

# ( set -x ; echo "$password" | cat -n )
+ echo 'my password'
+ cat -n
     1  my password

# ( set -x ; cat -n <<< "$password" )
+ cat -n
     1  my password
-------------------------------------------------------------
In this test cat -n intentionally shows the password value
to prove that COMMAND got the password value as input.
The point of this test is to avoid that the COMMAND input
gets shown in any case with 'set -x'.

This works as expected with adapted code in
layout/prepare/GNU/Linux/160_include_luks_code.sh

    elif { test "$password" ; } 2>>/dev/$SECRET_OUTPUT_DEV ; then
        { echo "{ cryptsetup luksFormat --batch-mode $cryptsetup_options $source_device <<< \"$password\" ; } 2>/dev/null"
          echo "{ cryptsetup luksOpen $source_device $mapping_name <<< \"$password\" ; } 2>/dev/null"
        } 2>>/dev/$SECRET_OUTPUT_DEV
...
    elif { test "$password" ; } 2>>/dev/$SECRET_OUTPUT_DEV ; then
        { echo "{ cryptsetup luksOpen $source_device $mapping_name <<< \"$password\" ; } 2>/dev/null"
        } 2>>/dev/$SECRET_OUTPUT_DEV

which avoids that the cryptsetup password input
gets shown by 'set -x' in the ReaR log file
because

RESCUE localhost:~ # rear -D recover
...
Start system layout restoration.
Disk '/dev/vda': creating 'gpt' partition table
Disk '/dev/vda': creating partition number 1 with name ''vda1''
Disk '/dev/vda': creating partition number 2 with name ''vda2''
Disk '/dev/vda': creating partition number 3 with name ''vda3''
Creating LUKS volume cr_root on /dev/vda2
Creating filesystem of type ext4 with mount point / on /dev/mapper/cr_root.
Mounting filesystem /
Creating LUKS volume cr_swap on /dev/vda3
Creating swap on /dev/mapper/cr_swap
Disk layout created.
...

RESCUE localhost:~ # grep johannes /var/log/rear/rear-localhost.log
[no output]

BUT

RESCUE localhost:~ # grep johannes /root/rear.github.master/var/lib/rear/layout/diskrestore.sh

{ cryptsetup luksFormat ... /dev/vda2 <<< "johannes" ; } 2>/dev/null
{ cryptsetup luksOpen /dev/vda2 cr_root <<< "johannes" ; } 2>/dev/null
{ cryptsetup luksFormat ... /dev/vda3 <<< "johannes" ; } 2>/dev/null
{ cryptsetup luksOpen /dev/vda3 cr_swap <<< "johannes" ; } 2>/dev/null

so the cryptsetup password input is still there
in the diskrestore.sh script.

So

cryptsetup ... <<< "$password"

is a step forward into the right direction
but likely not yet what is possible with reasonable effort.

My next idea how to improve things with reasonable effort
is using a temporary file which contains the password value
to avoid that a password variable needs to be evaluated like

# echo "my password" >password_file

# ( set -x ; cat -n <password_file )
+ cat -n
     1  my password

jsmeix commented at 2025-07-03 16:05:

https://github.com/rear/rear/pull/3489
implements
https://github.com/rear/rear/issues/3483#issuecomment-3032444609
and - by the way - that makes the code
simpler and more straightforward because those awkward
{ ... ; } 2>>/dev/$SECRET_OUTPUT_DEV "code wrappers"
are less often needed - i.e. as soon as the secret vaule
is stored in a file which is sufficiently securely located
further things get simpler and more straightforward.

jsmeix commented at 2025-07-03 16:21:

The next thing I will think about is
to remove the LUKS password value from disklayout.conf
after it was read from there and stored in a file
during "rear recover", for example via something like

sed -e '/^crypt/s/password=.*/password=XXX/'

which replaces password=johannes by password=XXX
so it is still visible that some LUKS password was set
during "rear recover" but its secret value got removed.

jsmeix commented at 2025-07-07 14:40:

With the changes in
https://github.com/rear/rear/pull/3490
all works well for me.

jsmeix commented at 2025-07-11 13:19:

With
https://github.com/rear/rear/pull/3490
merged this issue should be sufficiently fixed.


[Export of Github issue for rear/rear.]