image


Предисловие


В связи с необходимостью работать в другом городе, пришлось приобрести ноутбук.
Постепенно, назрела проблема синхронизации его и стационарной машины.
Несмотря на то, что все мои проекты ведутся в гите, не весь код полностью мой, и не хочется его выкладывать на гитхаб.


Для решения этой проблемы, я начал строить свой NAS, который даст мне, ко всему прочему, дополнительные возможности.


Изучив, какие сейчас имеются ОС для решения данной задачи, я пришёл к выводу, что изо всего многообразия, наиболее развиты, широко используемы и, следовательно, проработаны, FreeNAS на основе FreeBSD и OpenMediaVault на основе Debian, созданный одним из разработчиков FreeNAS.


FreeNAS стабилен, удобен, гибок и вообще хорош, но попытавшись его поставить, вместо FreeBSD bsdinstall, я увидел совершенно урезанный инсталлятор, в котором я могу только выбрать диски и ввести пароль root: даже разметить диски нельзя.
GELI мне понравился больше cryptsetup на Linux, как и BSD-шный parted.
Попытавшись сделать root на шифрованном разделе, я понял, что эта задача нетривиальна, несмотря на то, что они уже используют root на ZFS.
Затем, пообщавшись, с сообществом FreeNAS, которые стали доказывать, что FreeNAS — не ОС, а приложение, я решил установить OMV.


К тому же, Debian — моя основная ОС и с Linux дела обещали быть проще...


Выяснилось, что не совсем. Задача создания такой конфигурации, как у меня, совсем не тривиальна. Потому, я решил написать данную статью.


Существующие ресурсы


Руководство проекта ZFS for Linux — основной документ, без которого мне пришлось бы разбираться гораздо дольше.
Его необходимо хотя бы кратко посмотреть.


Выполнив большую часть работы вручную, я наткнулся на скрипт выполняющий сходную задачу, но без организации зеркала.


Ну и, всё же упомяну здесь man по cryptsetup, руководство Debian по шифрованию, статьи и документацию по ZFS (там есть ссылки на оригинальную документацию).


Много данных по ZFS есть на форуме FreeNAS.


И уже в процессе написания данной статьи, выяснилось, что ничто не ново под Луной


Вот ещё одна статья


Однако, моя несколько схема другая, и она имеет, как свои недостатки, так и преимущества.


Схема организации диска


Система установлена на двух SSD: Micron и Samsung PRO (в дальнейшем, я буду к ним обращаться).


На каждой SSD такая схема разбиения:


  • part_boot — раздел с загрузчиком. Размер = 1 GB.
  • part_system — раздел с системой. Размер = 32 GB (Рекомендованный размер: 16 GB * 2).
  • part_slog — раздел с SLOG. Размер = 5 GB.

part_system и part_slog зашифрованы в XTS режиме.


В целом, выглядит это так:


SSD1: [part_boot] -> [ext4] <---> SSD2
SSD1: [part_system] -> [crypto_xts] -> [zfs_mirror] <---> SSD2
SSD1: [part_slog] -> [crypto_xts] -> [zfs_zil_mirror] <---> SSD2

ZIL и корень системы дублируются на вторую SSD средствами ZFS.


Подготовка


  1. Загрузить ISO Debian
  2. Установить образ на USB flash.

Установка


Дальнейшие действия во много заимствованы из руководства и даны с некоторыми моими пояснениями.
Предполагается, что все команды выполняются от root.


Добавить contrib в sources.list, что требуется для установки ZFS


echo "deb http://ftp.debian.org/debian stretch main contrib" > /etc/apt/sources.list && apt-get update


Установить debootstrap и zfs-dkms


apt-get install debootstrap linux-headers-$(uname -r) zfs-dkms


Установить cryptsetup


apt-get install cryptsetup


Забить SSD случайными данными


dd if=/dev/urandom bs=4M oflag=direct of=/dev/disk/by-id/ata-Samsung_SSD_850_PRO && blkdiscard /dev/disk/by-id/ata-Samsung_SSD_850_PRO
dd if=/dev/urandom bs=4M oflag=direct of=/dev/disk/by-id/ata-Micron_1100 && blkdiscard /dev/disk/by-id/ata-Micron_1100


Это займёт порядка 30 минут для SSD размером 250 ГБ.
Вызов blkdiscard пометит все блоки на SSD, как неиспользуемые.


Создать разделы для загрузки


sgdisk -a1 -n1:34:2047 -t1:EF02 -n2:0:+1G -t2:8300 /dev/disk/by-id/ata-Samsung_SSD_850_PRO
sgdisk -a1 -n1:34:2047 -t1:EF02 -n2:0:+1G -t2:8300 /dev/disk/by-id/ata-Micron_1100


Сюда, впоследствии будет установлен GRUB.
Создание первого маленького раздела обязательно: без него не загрузится.


Создать системные разделы


sgdisk -n3::+32G -t3:8300 -c3:part_system1 /dev/disk/by-id/ata-Samsung_SSD_850_PRO
sgdisk -n3::+32G -t3:8300 -c3:part_system2 /dev/disk/by-id/ata-Micron_1100


Создать разделы SLOG


sgdisk -n4::+5G -t4:8300 -c4:part_slog1 /dev/disk/by-id/ata-Samsung_SSD_850_PRO && sgdisk -n4::+5G -t4:8300 -c4:part_slog2 /dev/disk/by-id/ata-Micron_1100


В этой статье, они никак не используются.


Создать шифрованные корневые разделы


Стоит это делать руководствуясь документом от Debian, если вы не очень хорошо знаете cryptsetup.
Здесь используется LUKS, что несколько упрощает работу с шифрованием.


cryptsetup --cipher aes-xts-plain64 --key-size 512 --verify-passphrase --hash sha512 --use-random -v luksFormat /dev/disk/by-id/ata-Samsung_SSD_850_PRO-part3
cryptsetup --cipher aes-xts-plain64 --key-size 512 --verify-passphrase --hash sha512 --use-random -v luksFormat /dev/disk/by-id/ata-Micron_1100-part3


Теперь надо открыть разделы:


cryptsetup luksOpen /dev/disk/by-id/ata-Samsung_SSD_850_PRO-part3 root_crypt1
cryptsetup luksOpen /dev/disk/by-id/ata-Micron_1100-part3 root_crypt2


После ввода корректного пароля, будут созданы новые блочные устройства, которые используются для шифрования.
На них разместится корневой пул ZFS.


Создать корневой ZFS pool


  modprobe zfs
  zpool create -o ashift=12     -O atime=off -O canmount=off -O compression=lz4 -O normalization=formD     -O mountpoint=/ -R /mnt     rpool mirror /dev/disk/by-id/dm-name-root_crypt*

Теперь есть пул, в который входят оба шифрованных раздела, включенные зеркально.


Создать наборы данных для корневой ФС


zfs create -o canmount=off -o mountpoint=none rpool/ROOT
zfs create -o canmount=noauto -o mountpoint=/ rpool/ROOT/debian


Сделать корень загрузочным


zfs mount rpool/ROOT/debian
zpool set bootfs=rpool/ROOT/debian rpool


Создать прочие наборы данных


zfs create -o setuid=off rpool/home
zfs create -o mountpoint=/root rpool/home/root
zfs create -o canmount=off -o setuid=off -o exec=off rpool/var
zfs create -o com.sun:auto-snapshot=false rpool/var/cache
zfs create rpool/var/log
zfs create rpool/var/spool
zfs create -o com.sun:auto-snapshot=false -o exec=on rpool/var/tmp


На данном этапе, структура ФС готова.


Установить минимальную систему


chmod 1777 /mnt/var/tmp
debootstrap stretch /mnt && zfs set devices=off rpool


Сконфигурировать систему


echo nas > /mnt/etc/hostname.
echo "rpool/ROOT/debian / zfs defaults,noatime 0 0" > /mnt/etc/fstab
echo "tmpfs /tmp tmpfs nosuid,nodev 0 0" >> /mnt/etc/fstab


Здесь /tmp монтируется, как tmpfs, а / будет на ZFS.
Затем, надо исправить /mnt/etc/hosts. В него требуется добавить имя хоста "nas".
И добавить настройки сетевых интерфейсов в /mnt/etc/network/interfaces.d (обязательно):


  • iface_name — имя активного интерфейса в ip addr show.
  • echo "auto $iface_name" > /mnt/etc/network/interfaces.d/$iface_name
  • echo "iface $iface_name inet dhcp" >> /mnt/etc/network/interfaces.d/$iface_name

Это важный пункт! Если этого не сделать, после перезагрузки вы окажетесь без сети и без установленных пакетов. Не сделаете — сеть всё-равно придётся поднимать вручную.


Переключиться в установленную систему


mount --rbind /dev /mnt/dev && mount --rbind /proc /mnt/proc && mount --rbind /sys /mnt/sys
chroot /mnt /bin/bash --login
mount /tmp


Далее, вся работа идёт в установленной системе.


Настроить APT и установить нужные пакеты


  • Установить пакет HTTPS транспорта: apt-get install apt-transport-https
    Опционально, но я предпочитаю работать через HTTPS.
  • Добавить репозиторий contrib:
    cat > /etc/apt/sources.list << EOF
    deb https://deb.debian.org/debian stretch main contrib
    deb-src https://deb.debian.org/debian stretch main contrib
    EOF
  • apt-get update.
  • Установить необходимые пакеты:
    • apt-get install locales && dpkg-reconfigure locales
    • dpkg-reconfigure tzdata
    • apt-get install bash-completion man gdisk linux-headers-$(uname -r) linux-image-amd64
    • apt-get install cryptsetup zfs-dkms zfs-initramfs

Сборка модуля ZFS займёт ощутимое время.


Настроить cryptsetup


Безусловно включить в initramfs:
sed -i 's/#CRYPTSETUP=/CRYPTSETUP=y/' /etc/cryptsetup-initramfs/conf-hook


Затем, надо исправить хук cryptroot в /usr/share/initramfs-tools/hooks/cryptroot и заменить скрипт в /etc/initrmafs-tools/scripts/local-top/cryptroot.
Существуют две проблемы с пакетом cryptsetup в Debian Stretch:


  • Он не работает с ZFS.
  • При загрузке, он не кэширует вводимый пароль.

Баг на ZFS уже давно заведён, туда отправлены и мои доработки.
На кэширование пароля я завёл баг и отправил фикс, так что, возможно он уже будет исправлен.


Тем не менее, я привожу полные скрипты и диффы:


/usr/share/initramfs-tools/hooks/cryptroot
#!/bin/sh

PREREQ=""

prereqs()
{
    echo "$PREREQ"
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

. /usr/share/initramfs-tools/hook-functions

# get_fs_devices() - determine source device(s) of mount point from /etc/fstab
#
# expected arguments:
# * fstab mount point (full path)
#
# This function searches for the first entry from /etc/fstab which has the
# given mountpoint set. It returns the canonical_device() of corresponding
# source device (first field in /etc/fstab entry).
# In case of btrfs, canoncial_device() of all btrfs source devices (possibly
# more than one) are returned.
#
get_fs_devices() {
    local device mount type options dump pass
    local wantmount="$1"

    if [ ! -r /etc/fstab ]; then
        return 1
    fi

    grep -s '^[^#]' /etc/fstab |     while read device mount type options dump pass; do
        if [ "$mount" = "$wantmount" ]; then
            local devices
            if [ "$type" = "btrfs" ]; then
                for dev in $(btrfs filesystem show $(canonical_device "$device" --no-simplify) 2>/dev/null | sed -r -e 's/.*devid .+ path (.+)/\1/;tx;d;:x') ; do
                    devices="${devices:+$devices }$(canonical_device "$dev")"
                done
                      elif [ "$type" = "zfs" ]; then
                              zpool="$(echo "$device"|sed 's#^/dev/zvol/##;s#\([^/]*\).*#\1#')"
                              for ss in $(zpool list -Pv "$zpool"); do
                                      cdev=$(canonical_device "$ss" 2>/dev/null) || continue
                                      devices="${devices:+$devices }$cdev"
                              done || return 0
            else
                devices=$(canonical_device "$device") || return 0
            fi
            printf '%s' "$devices"
            return
        fi
    done
}

# get_resume_devices() - determine devices used for system suspend/hibernate
#
# expected arguments:
# * none
#
# This function searches well known places for devices that are used for system
# suspension and/or hibernation. It returns canonical_device() of any detected
# devices and prints a warning if more than one device is detected.
#
get_resume_devices() {
    local device opt count dupe candidates devices derived
    candidates=""

    # First, get a list of potential resume devices

    # uswsusp
    if [ -e /etc/uswsusp.conf ]; then
        device=$(sed -rn 's/^resume device[[:space:]]*[:=][[:space:]]*// p' /etc/uswsusp.conf)
        if [ -n "$device" ]; then
            candidates="${candidates:+$candidates }$device"
        fi
    fi

    # uswsusp - again...
    if [ -e /etc/suspend.conf ]; then
        device=$(sed -rn 's/^resume device[[:space:]]*[:=][[:space:]]*// p' /etc/suspend.conf)
        if [ -n "$device" ]; then
            candidates="${candidates:+$candidates }$device"
        fi
    fi

    # regular swsusp
    for opt in $(cat /proc/cmdline); do
        case $opt in
            resume=*)
                device="${opt#resume=}"
                candidates="${candidates:+$candidates }$device"
                ;;
        esac
    done

    # initramfs-tools >=0.129
    device="${RESUME:-auto}"
    if [ "$device" != none ]; then
        if [ "$device" = auto ]; then
            # next line from /usr/share/initramfs-tools/hooks/resume
            device="$(grep ^/dev/ /proc/swaps | sort -rnk3 | head -n 1 | cut -d " " -f 1)"
            if [ -n "$device" ]; then
                device="UUID=$(blkid -s UUID -o value "$device" || true)"
            fi
        fi
        candidates="${candidates:+$candidates }$device"
    fi

    # Now check the sanity of all candidates
    devices=""
    count=0
    for device in $candidates; do
        # Remove quotes around device candidate
        device=$(printf '%s' "$device" | sed -r -e 's/^"(.*)"\s*$/\1/' -e "s/^'(.*)'\s*$/\1/")

        # Weed out clever defaults
        if [ "$device" = "<path_to_resume_device_file>" ]; then
            continue
        fi

        # Detect devices required by decrypt_derived
        derived=$(get_derived_device "$device")
        if [ -n "$derived" ]; then
            devices="${devices:+$devices }$derived"
        fi

        device=$(canonical_device "$device") || return 0

        # Weed out duplicates
        dupe=0
        for opt in $devices; do
            if [ "$device" = "$opt" ]; then
                dupe=1
            fi
        done
        if [ $dupe -eq 1 ]; then
            continue
        fi

        # This device seems ok
        devices="${devices:+$devices }$device"
        count=$(( $count + 1 ))
    done

    if [ $count -gt 1 ]; then
        echo "cryptsetup: WARNING: found more than one resume device candidate:" >&2
        for device in $devices; do
            echo "                     $device" >&2
        done
    fi

    if [ $count -gt 0 ]; then
        printf '%s' "$devices"
    fi

    return 0
}

# get_initramfs_devices() - determine devices with explicit 'initramfs' option
#
# expected arguments:
# * none
#
# This function processes entries from /etc/crypttab with the 'initramfs'
# option set. For each processed device, potential get_derived_device()
# devices are determined. The canonical_device() of each detected device
# is returned.
#
get_initramfs_devices() {
    local device opt count dupe target source key options candidates devices derived

    candidates="$(grep -s '^[^#]' /etc/crypttab |     while read target source key options; do
        if printf '%s' "$options" | grep -Eq "^(.*,)?initramfs(,.*)?$"; then
            echo " /dev/mapper/$target"
        fi
    done;)"

    devices=""
    count=0
    for device in $candidates; do
        # Detect devices required by decrypt_derived
        derived=$(get_derived_device "$device")
        if [ -n "$derived" ]; then
            devices="${devices:+$devices }$derived"
        fi

        device=$(canonical_device "$device") || return 0

        # Weed out duplicates
        dupe=0
        for opt in $devices; do
            if [ "$device" = "$opt" ]; then
                dupe=1
            fi
        done
        if [ $dupe -eq 1 ]; then
            continue
        fi

        # This device seems ok
        devices="${devices:+$devices }$device"
        count=$(( $count + 1 ))
    done

    if [ $count -gt 0 ]; then
        printf '%s' "$devices"
    fi

    return 0
}

# get_derived_device() - determine dependency devices for decrypt_derived
#
# expected arguments:
# * crypttab target device name (either <name> or /dev/mapper/<name>)
#
# This function takes a target device name and checks whether this device has
# the decrypt_derived keyscript set in /etc/crypttab. If true, the dependency
# device required for the decrypt_derived keyscript is detected and its
# canonical_device() returned if it's not listed in $rootdevs.
#
get_derived_device() {
    local device derived
    device="$1"

    derived="$( awk -vtarget="${device#/dev/mapper/}"         '$1 == target && $4 ~ /^(.*,)?keyscript=([^,]*\/)?decrypt_derived(,.*)?$/ {print $3; exit}'         /etc/crypttab )"
    if [ -n "$derived" ]; then
        if node_is_in_crypttab "$derived"; then
            derived=$(canonical_device "/dev/mapper/$derived") || return 0
            if ! printf '%s' "$rootdevs" | tr ' ' '\n' | grep -Fxq "$derived"; then
                printf '%s' "$derived"
            fi
        else
            echo "cryptsetup: WARNING: decrypt_derived device $derived not found in crypttab" >&2
        fi
    fi
}

# node_is_in_crypttab() - test whether a device is configured in /etc/crypttab
#
# expected arguments:
# * crypttab target device names (without /dev/mapper/ prefix)
#
# This function takes a target device name and fails if it is not
# configured in /etc/crypttab.
#
node_is_in_crypttab() {
    [ -f /etc/crypttab ] || return 1
    sed -n '/^[^#]/ s/\s.*//p' /etc/crypttab | grep -Fxq "$1"
}

# node_or_pv_is_in_crypttab() - test whether devices are configured in /etc/crypttab
#
# expected arguments:
# * crypttab target device names (without /dev/mapper/ prefix), or LVM
#   logical volume device-mapper name (format <VG>-<LV>)
#
# This function fails unless every argument is either a target device
# name configured in /etc/crypttab, or an LVM logical volume
# device-mapper name (format <VG>-<LV>) with only parents devices (PVs)
# configured in /etc/crypttab.
#
node_or_pv_is_in_crypttab() {
    local node lvmnodes lvmnode
    for node in "$@"; do
        if ! node_is_in_crypttab "$node"; then
            lvmnodes="$(get_lvm_deps "$node" --assert-crypt)" || return 1
            [ "$lvmnodes" ] || return 1
            for lvmnode in $lvmnodes; do
                node_is_in_crypttab "$lvmnode" || return 1
            done
        fi
    done
    return 0
}

# get_lvm_deps() - determine the parent devices (PVs) of a LVM logical volume
#
# expected arguments:
# * LVM logical volume device-mapper name (format <VG>-<LV>)
# * optional options to the function
#
# This function takes a LVM logical volume name and determines the corresponding
# crypted physical volumes (PVs). It returns the name of the underlying
# device-mapper crypt devices (without /dev/mapper).
# If option '--assert-crypt' is given as second argument, then the
# function fails unless all PVs are dm-crypt devices.
#
get_lvm_deps() {
    local node opt deps maj min depnode
    node="$1"
    opt="${2:-}"

    if [ -z "$node" ]; then
        echo "cryptsetup: WARNING: get_lvm_deps - invalid arguments" >&2
        return 1
    fi

    if ! deps=$(vgs --noheadings -o pv_name $(dmsetup --noheadings splitname $node | cut -d':' -f1) 2>/dev/null); then
        # $node is not a LVM node, stopping here
        [ "$opt" != '--assert-crypt' ] && return 0 || return 1
    fi

    # We should now have a list of physical volumes for the VG
    for dep in $deps; do
        depnode=$(dmsetup info -c --noheadings -o name "$dep" 2>/dev/null)
        if [ -z "$depnode" ]; then
            [ "$opt" != '--assert-crypt' ] && continue || return 1
        fi
        if [ "$(dmsetup table "$depnode" 2>/dev/null | cut -d' ' -f3)" != "crypt" ]; then
            get_lvm_deps "$depnode" $opt || return 1
            continue
        fi
        printf '%s\n' "$depnode"
    done

    return 0
}

# get_device_opts() - determine and set options for a crypttab target device
#
# expected arguments:
# * crypttab target device name (without /dev/mapper/ prefix)
# * optional extra options
#
# This function determines options for a crypttab target device and sets them
# accordingly. In order to detect the options, it parses the corresponding
# /etc/crypttab entry and takes optional extra options as second argument.
# Some sanity checks are done on the corresponding source device and configured
# options.
# After everything is processed, the options are saved in '$OPTIONS' for later
# access by parent functions.
#
get_device_opts() {
    local target source link extraopts rootopts opt key
    target="$1"
    extraopts="$2"
    KEYSCRIPT=""
    KEYFILE="" # key file to copy to the initramfs image
    CRYPTHEADER=""
    OPTIONS=""

    if [ -z "$target" ]; then
        echo "cryptsetup: WARNING: get_device_opts - invalid arguments" >&2
        return 1
    fi

    opt="$( awk -vtarget="$target" '$1 == target {gsub(/[ \t]+/," "); print; exit}' /etc/crypttab )"
    source=$( printf '%s' "$opt" | cut -d " " -f2 )
    key=$( printf '%s' "$opt" | cut -d " " -f3 )
    rootopts=$( printf '%s' "$opt" | cut -d " " -f4- )

    if [ -z "$opt" ] || [ -z "$source" ] || [ -z "$key" ] || [ -z "$rootopts" ]; then
        echo "cryptsetup: WARNING: invalid line in /etc/crypttab for $target - $opt" >&2
        return 1
    fi

    # Sanity checks for $source
    if [ -h "$source" ]; then
        link=$(readlink -nqe "$source")
        if [ -z "$link" ]; then
            echo "cryptsetup: WARNING: $source is a dangling symlink" >&2
            return 1
        fi

        if [ "$link" != "${link#/dev/mapper/}" ]; then
            echo "cryptsetup: NOTE: using $link instead of $source for $target" >&2
            source="$link"
        fi
    fi

    if [ "UUID=${source#UUID=}" = "$source" -a ! \( -b "/dev/disk/by-uuid/${source#UUID=}" -o -b "/dev/disk/by-partuuid/${source#UUID=}" \) ] || [ "UUID=${source#UUID=}" != "$source" -a ! -b "$source" ]; then
        echo "cryptsetup: WARNING: Invalid source device $source" >&2
    fi

    # Sanity checks for $key
    if [ "$key" = "/dev/random" ] || [ "$key" = "/dev/urandom" ]; then
        echo "cryptsetup: WARNING: target $target has a random key, skipped" >&2
        return 1
    fi

    if [ -n "$extraopts" ]; then
        rootopts="$extraopts,$rootopts"
    fi

    # We have all the basic options, let's go trough them
    OPTIONS="target=$target,source=$source"
    local IFS_BCK="$IFS"
    local IFS=", "
    unset HASH_FOUND
    unset LUKS_FOUND
    for opt in $rootopts; do
        case $opt in
            cipher=*)
                OPTIONS="$OPTIONS,$opt"
                ;;
            size=*)
                OPTIONS="$OPTIONS,$opt"
                ;;
            hash=*)
                OPTIONS="$OPTIONS,$opt"
                HASH_FOUND=1
                ;;
            tries=*)
                OPTIONS="$OPTIONS,$opt"
                ;;
            discard)
                OPTIONS="$OPTIONS,$opt"
                ;;
            luks)
                LUKS_FOUND=1
                ;;
            header=*)
                opt="${opt#header=}"
                if [ ! -e "$opt" ]; then
                    echo "cryptsetup: WARNING: target $target has an invalid header, skipped" >&2
                    return 1
                fi
                CRYPTHEADER="$opt"
                OPTIONS="$OPTIONS,header=$CRYPTHEADER"
                ;;
            tcrypt)
                OPTIONS="$OPTIONS,$opt"
                ;;
            keyscript=*)
                opt="${opt#keyscript=}"
                if [ ! -x "/lib/cryptsetup/scripts/$opt" ] && [ ! -x "$opt" ]; then
                    echo "cryptsetup: WARNING: target $target has an invalid keyscript, skipped" >&2
                    return 1
                fi
                KEYSCRIPT="$opt"
                OPTIONS="$OPTIONS,keyscript=/lib/cryptsetup/scripts/$(basename "$opt")"
                ;;
            keyslot=*)
                OPTIONS="$OPTIONS,$opt"
                ;;
            veracrypt)
                OPTIONS="$OPTIONS,$opt"
                ;;
            lvm=*)
                OPTIONS="$OPTIONS,$opt"
                ;;
            rootdev)
                OPTIONS="$OPTIONS,$opt"
                ;;
            resumedev)
                OPTIONS="$OPTIONS,$opt"
                ;;
            *)
                # Presumably a non-supported option
                ;;
        esac
    done
    IFS="$IFS_BCK"

    # Warn for missing hash option, unless we have a LUKS partition
    if [ -z "$HASH_FOUND" ] && [ -z "$LUKS_FOUND" ]; then
        echo "WARNING: Option hash missing in crypttab for target $target, assuming ripemd160." >&2
        echo "         If this is wrong, this initramfs image will not boot." >&2
        echo "         Please read /usr/share/doc/cryptsetup/README.initramfs.gz and add" >&2
        echo "         the correct hash option to your /etc/crypttab."  >&2
    fi

    # Warn that header only applies to a LUKS partition currently
    if [ -n "$CRYPTHEADER" ] && [ -z "$LUKS_FOUND" ]; then
        echo "WARNING: Option LUKS missing in crypttab for target $target." >&2
        echo "         Headers are only supported for LUKS devices." >&2
    fi

    # If keyscript is set, the "key" is just an argument to the script
    if [ "$key" != "none" ] && [ -z "$KEYSCRIPT" ]; then
        case "$key" in
            $KEYFILE_PATTERN)
                KEYFILE="$key"
                key="/cryptroot-keyfiles/${target}.key"
                ;;
            *)
                key=$(readlink -e "$key")
                # test whether $target is a root device (or parent of the root device)
                if printf '%s' "$OPTIONS" | grep -Eq '^(.*,)?rootdev(,.*)?$'; then
                    echo "cryptsetup: WARNING: root target $target uses a key file, skipped" >&2
                    return 1
                # test whether a) key file is not on root fs
                #           or b) root fs is not encrypted
                elif [ "$(stat -c %m -- "$key" 2>/dev/null)" != / ] || ! node_or_pv_is_in_crypttab $rootdevs; then
                    echo "cryptsetup: WARNING: $target's key file $key is not on an encrypted root FS, skipped" >&2
                    return 1
                fi
                if printf '%s' "$OPTIONS" | grep -Eq '^(.*,)?resumedev(,.*)?$'; then
                    # we'll be able to decrypt the device, but won't be able to use it for resuming
                    echo "cryptsetup: WARNING: resume device $source uses a key file" >&2
                fi
                # prepend "/root" (to be substituted by the real root FS
                # mountpoint "$rootmnt" in the boot script) to the
                # absolute filename
                key="/root$key"
                ;;
        esac
        OPTIONS="$OPTIONS,keyscript=cat"
    fi
    OPTIONS="$OPTIONS,key=$key"
}

# get_device_modules() - determine required crypto kernel modules for device
#
# expected arguments:
# * crypttab target device name (without /dev/mapper/ prefix)
#
# This function determines the required crypto kernel modules for cipher,
# block cipher and optionally ivhash of the target device and returns them.
#
get_device_modules() {
    local node value cipher blockcipher ivhash
    node="$1"

    # Check the ciphers used by the active root mapping
    value=$(dmsetup table "$node" | cut -d " " -f4)
    cipher=$(echo "$value" | cut -d ":" -f1 | cut -d "-" -f1)
    blockcipher=$(echo "$value" | cut -d ":" -f1 | cut -d "-" -f2)
    ivhash=$(echo "$value" | cut -d ":" -s -f2)

    if [ -n "$cipher" ]; then
        echo "$cipher"
    else
        return 1
    fi

    if [ -n "$blockcipher" ] && [ "$blockcipher" != "plain" ]; then
        echo "$blockcipher"
    fi

    if [ -n "$ivhash" ] && [ "$ivhash" != "plain" ]; then
        echo "$ivhash"
    fi
    return 0
}

# canonical_device() - determine the 
#
# expected arguments:
# * device (either full path or LABEL=<x> or UUID=<y>)
# * optional options to the function
#
# This function takes a device as argument and determines the corresponding
# canonical device name.
# If option '--no-simplify' is given as second argument, then the origin device
# path after unraveling LABEL= and UUID= format and following symlinks is
# returned.
# If no option is given, the device is further unraveled and depending on the
# device path, either the corresponding device-mapper path (as found in
# /dev/mapper/) or the the corresponding disk symlink (as found in
# /dev/disk/by-*/) is returned.
#
canonical_device() {
    local dev altdev original
    dev="$1"
    opt="$2"

    if [ "${dev#LABEL=}" != "$dev" ]; then
        altdev="${dev#LABEL=}"
        dev="/dev/disk/by-label/$(printf '%s' "$altdev" | sed 's,/,\\x2f,g')"
    elif [ "${dev#UUID=}" != "$dev" ]; then
        altdev="${dev#UUID=}"
        dev="/dev/disk/by-uuid/$altdev"
    fi

    original="$dev"
    if [ -h "$dev" ]; then
        dev=$(readlink -e "$dev")
    fi

    if [ "$opt" = "--no-simplify" ]; then
        printf '%s' "$dev"
        return 0
    fi

    if [ "x${dev%/dev/dm-*}" = "x" ]; then
        # try to detect corresponding symlink in /dev/mapper/
        for dmdev in /dev/mapper/*; do
            if [ "$(readlink -e "$dmdev")" = "$dev" ]; then
                dev="$dmdev"
            fi
        done
    fi

    altdev="${dev#/dev/mapper/}"
    if [ "$altdev" != "$dev" ]; then
        printf '%s' "$altdev"
        return 0
    elif [ "x${original%/dev/disk/by-*/*}" = "x" ]; then
        # support crypttab UUID/LABEL entries
        # this is a /dev/disk/by-*/ path so return just the 'basename'
        echo "${original##/dev/disk/by-*/}"
        return 0
    fi

    echo "cryptsetup: WARNING: failed to detect canonical device of $original" >&2
    return 1
}

# add_device() - Process a given device and add to /conf/conf.d/cryptroot
#
# expected arguments:
# * device name (either crypttab target device name without /dev/mapper/ prefix
#                    or LVM device-mapper name in format '<VG>:<LV>')
#
# This function takes a device name, does all required processing and adds the
# result with all device options to /conf/conf.d/cryptroot in the initramfs.
# Additionally, it returns required kernel modules.
#
add_device() {
    local node nodes lvmnodes opts lastopts i count
    nodes="$1"
    opts=""     # Applied to all nodes
    lastopts="" # Applied to last node

    if [ -z "$nodes" ]; then
        return 0
    fi

    # Flag root and resume devices
    if printf '%s' "$rootdevs" | tr ' ' '\n' | grep -Fxq "$nodes"; then
        opts="${opts:+$opts,}rootdev"
    fi
    if printf '%s' "$resumedevs" | tr ' ' '\n' | grep -Fxq "$nodes"; then
        opts="${opts:+$opts,}resumedev"
    fi

    # Check that it is a node under /dev/mapper/
    # nodes=$(canonical_device "$nodes") || return 0

    # Can we find this node in crypttab
    if ! node_is_in_crypttab "$nodes"; then
        # dm node but not in crypttab, is it a lvm device backed by dm-crypt nodes?
        lvmnodes=$(get_lvm_deps "$nodes") || return 1

        # not backed by any dm-crypt nodes; stop here
        if [ -z "$lvmnodes" ]; then
            return 0
        fi

        # It is a lvm device!
        opts="${opts:+$opts,}lvm=$nodes"
        nodes="$lvmnodes"
    fi

    # Prepare to setup each node
    count=$(printf '%s' "$nodes" | wc -w)
    i=1
    for node in $nodes; do
        # Prepare the additional options
        if [ $i -eq $count ]; then
            if [ -n "$lastopts" ]; then
                opts="${opts:+$opts,}$lastopts"
            fi
        fi

        # Get crypttab root options
        if ! get_device_opts "$node" "$opts"; then
            continue
        fi
        printf '%s\n' "$OPTIONS" >>"$DESTDIR/conf/conf.d/cryptroot"

        # If we have a keyscript, make sure it is included
        if [ -n "$KEYSCRIPT" ]; then
            if [ ! -d "$DESTDIR/lib/cryptsetup/scripts" ]; then
                mkdir -p "$DESTDIR/lib/cryptsetup/scripts"
            fi

            if [ -e "/lib/cryptsetup/scripts/$KEYSCRIPT" ]; then
                copy_exec "/lib/cryptsetup/scripts/$KEYSCRIPT" /lib/cryptsetup/scripts >&2
            elif [ -e "$KEYSCRIPT" ]; then
                copy_exec "$KEYSCRIPT" /lib/cryptsetup/scripts >&2
            elif KSTYPE="$(type "$KEYSCRIPT" 2>&1)"; then
                if [ -x "${KSTYPE#"$KEYSCRIPT" is }" ]; then
                    copy_exec "${KSTYPE#"$KEYSCRIPT" is }" /lib/cryptsetup/scripts >&2
                fi
            else
                echo "cryptsetup: WARNING: failed to find keyscript $KEYSCRIPT" >&2
                continue
            fi
        elif [ -n "$KEYFILE" ]; then
            case "$KEYFILE" in
                $KEYFILE_PATTERN)
                    mkdir -pm0700 "$DESTDIR/cryptroot-keyfiles"
                    cp --preserve=all "$KEYFILE" "$DESTDIR/cryptroot-keyfiles/${node}.key"
                    ;;
            esac
        fi

        # If we have a LUKS header, make sure it is included
        # TODO: make it configurable to include the LUKS header into initramfs
        # disabled for now due to security reasons
        if [ -n "$CRYPTHEADER" ]; then
            if [ ! -d "$DESTDIR/conf/conf.d/cryptheader" ]; then
                mkdir -p "$DESTDIR/conf/conf.d/cryptheader"
            fi

            #if [ -e "$CONFDIR/conf.d/cryptheader/$CRYPTHEADER" ]; then
            #   copy_exec "$CONFDIR/conf.d/cryptheader/$CRYPTHEADER" /conf/conf.d/cryptheader >&2
            #elif [ -e "$CRYPTHEADER" ]; then
            #   copy_exec "$CRYPTHEADER" /conf/conf.d/cryptheader >&2
            #else
            #   echo "cryptsetup: WARNING: failed to find LUKS header $CRYPTHEADER" >&2
            #   continue
            #fi
        fi

        # Calculate needed modules
        modules=$(get_device_modules $node | sort | uniq)
        if [ -z "$modules" ]; then
            echo "cryptsetup: WARNING: failed to determine cipher modules to load for $node" >&2
            continue
        fi
        echo dm_mod
        echo dm_crypt
        echo "$modules"
        # Load hardware aes module
        if cpu_has_aesni; then
            echo aesni
        fi
        i=$(( $i + 1 ))
    done

    return 0
}

# cpu_has_aesni() - Detect whether the host CPU has AES-NI support
#
# expected arguments:
# * none
#
# This functions returns true when the host CPU has AES-NI support.
#
cpu_has_aesni() {
    return $(grep -q "^flags\s*:\s*.*aes" /proc/cpuinfo)
}

# add_crypto_modules() - determine kernel module path and add to initramfs
#
# expected arguments:
# * kernel module name
#
# This function takes a kernel module name, determines the corresponding path
# and runs manual_add_modules() from initramfs hook functions to add the module
# to the initramfs.
#
add_crypto_modules() {
    local mod file altmod found genericfound
    mod="$1"
    found=""
    genericfound=""

    if [ -z "$mod" ]; then
        return 1
    fi

    # We have several potential sources of modules (in order of preference):
    #
    #   a) /lib/modules/$VERSION/kernel/arch/$ARCH/crypto/$mod-$specific.ko
    #   b) /lib/modules/$VERSION/kernel/crypto/$mod_generic.ko
    #   c) /lib/modules/$VERSION/kernel/crypto/$mod.ko
    #
    # and (currently ignored):
    #
    #   d) /lib/modules/$VERSION/kernel/drivers/crypto/$specific-$mod.ko

    for file in $(find "$MODULESDIR/kernel/arch/" -name "$mod-*.ko" 2>/dev/null); do
        altmod="${file##*/}"
        altmod="${altmod%.ko}"
        manual_add_modules "$altmod"
        found="yes"
    done

    for file in $(find "$MODULESDIR/kernel/crypto/" -name "${mod}_generic.ko" 2>/dev/null); do
        altmod="${file##*/}"
        altmod="${altmod%.ko}"
        manual_add_modules "$altmod"
        found="yes"
        genericfound="yes"
    done

    if [ -z "$genericfound" ]; then
        for file in $(find "$MODULESDIR/kernel/crypto/" -name "${mod}.ko" 2>/dev/null); do
            altmod="${file##*/}"
            altmod="${altmod%.ko}"
            manual_add_modules "$altmod"
            found="yes"
        done
    fi

    if [ -z "$found" ]; then
        return 1
    fi

    return 0
}

#
# Begin real processing
#

setup="no"
rootdevs=""
usrdevs=""
resumedevs=""

# XXX Backward compatibility: remove once Stretch has been promoted stable
for v in CRYPTSETUP KEYFILE_PATTERN; do
    if eval [ "\${$v+x}" ]; then
        echo "WARNING: Setting $v in /etc/initramfs-tools/initramfs.conf"              "is deprecated and will stop working in the future."              "Use /etc/cryptsetup-initramfs/conf-hook instead." >&2
    fi
done

# Load the hook's config
if [ -f "/etc/cryptsetup-initramfs/conf-hook" ]; then
    . /etc/cryptsetup-initramfs/conf-hook
fi

# Include cryptsetup modules, regardless of _this_ machine configuration
if [ -n "$CRYPTSETUP" ] && [ "$CRYPTSETUP" != "n" ]; then
    setup="yes"
fi

if [ "$KEYFILE_PATTERN" ]; then
    setup="yes"
    case "${UMASK:-$(umask)}" in
        0[0-7]77) ;;
        *) echo "WARNING: permissive UMASK (${UMASK:-$(umask)})."                 "Private key material inside the initrd might be left unprotected." >&2
        ;;
    esac
fi

# Find the root and resume device(s)
if [ -r /etc/crypttab ]; then
    rootdevs=$(get_fs_devices /)
    if [ -z "$rootdevs" ]; then
        echo "cryptsetup: WARNING: could not determine root device from /etc/fstab" >&2
    fi
    usrdevs=$(get_fs_devices /usr)
    resumedevs=$(get_resume_devices)
    initramfsdevs=$(get_initramfs_devices)
fi

# Load the config opts and modules for each device
for dev in $rootdevs $usrdevs $resumedevs $initramfsdevs; do
    if ! modules=$(add_device "$dev"); then
        echo "cryptsetup: FAILURE: could not determine configuration for $dev" >&2
        continue
    fi

    if [ -n "$modules" ]; then
        setup="yes"
    fi

    if [ "$setup" = "no" ]; then
        continue
    fi

    if [ "$MODULES" = "most" ]; then
        archcrypto="$(find "$MODULESDIR/kernel/arch" -type d -name "crypto" 2>/dev/null)"
        if [ -n "$archcrypto" ]; then
            copy_modules_dir "${archcrypto##*${MODULESDIR}/}"
        fi
        copy_modules_dir "kernel/crypto"
    else
        for mod in $modules; do
            add_crypto_modules $mod
        done
    fi
done

# With large initramfs, we always add a basic subset of modules
if [ "$MODULES" != "dep" ] && [ "$setup" = "yes" ]; then
    for mod in aes cbc chainiv cryptomgr krng sha256 xts; do
        add_crypto_modules $mod
    done
fi

# See if we need to add the basic components
if [ "$setup" = "yes" ]; then
    for mod in dm_mod dm_crypt; do
        manual_add_modules $mod
    done

    copy_exec /sbin/cryptsetup
    copy_exec /sbin/dmsetup
    copy_exec /lib/cryptsetup/askpass

    # We need sed. Either via busybox or as standalone binary.
    if [ "$BUSYBOX" = "n" ] || [ ! -e ${BUSYBOXDIR}/busybox ]; then
        copy_exec /bin/sed
    fi
fi

exit 0

diff /usr/share/initramfs-tools/hooks/cryptroot cryptroot
45a46,51
>                       elif [ "$type" = "zfs" ]; then
>                               zpool="$(echo "$device"|sed 's#^/dev/zvol/##;s#\([^/]*\).*#\1#')"
>                               for ss in $(zpool list -Pv "$zpool"); do
>                                       cdev=$(canonical_device "$ss" 2>/dev/null) || continue
>                                       devices="${devices:+$devices }$cdev"
>                               done || return 0
375c381
<       if [ "UUID=${source#UUID=}" = "$source" -a ! -b "/dev/disk/by-uuid/${source#UUID=}" ] || [ "UUID=${source#UUID=}" != "$source" -a ! -b "$source" ]; then
---
>       if [ "UUID=${source#UUID=}" = "$source" -a ! \( -b "/dev/disk/by-uuid/${source#UUID=}" -o -b "/dev/disk/by-partuuid/${source#UUID=}" \) ] || [ "UUID=${source#UUID=}" != "$source" -a ! -b "$source" ]; then

/etc/initramfs-tools/scripts/local-top
#!/bin/sh

PREREQ="cryptroot-prepare"

#
# Standard initramfs preamble
#
prereqs()
{
    # Make sure that cryptroot is run last in local-top
    for req in $(dirname $0)/*; do
        script=${req##*/}
        if [ $script != cryptroot ]; then
            echo $script
        fi
    done
}

case $1 in
prereqs)
    prereqs
    exit 0
    ;;
esac

# source for log_*_msg() functions, see LP: #272301
. /scripts/functions

#
# Helper functions
#
message()
{
    if [ -x /bin/plymouth ] && plymouth --ping; then
        plymouth message --text="$@"
    elif [ -p /dev/.initramfs/usplash_outfifo ] && [ -x /sbin/usplash_write ]; then
        usplash_write "TEXT-URGENT $@"
    else
        echo "$@" >&2
    fi
    return 0
}

udev_settle()
{
    # Wait for udev to be ready, see https://launchpad.net/bugs/85640
    if command -v udevadm >/dev/null 2>&1; then
        udevadm settle --timeout=30
    elif command -v udevsettle >/dev/null 2>&1; then
        udevsettle --timeout=30
    fi
    return 0
}

parse_options()
{
    local cryptopts
    cryptopts="$1"

    if [ -z "$cryptopts" ]; then
        return 1
    fi

    # Defaults
    cryptcipher=aes-cbc-essiv:sha256
    cryptsize=256
    crypthash=ripemd160
    crypttarget=cryptroot
    cryptsource=""
    cryptheader=""
    cryptlvm=""
    cryptkeyscript=""
    cryptkey="" # This is only used as an argument to an eventual keyscript
    cryptkeyslot=""
    crypttries=3
    crypttcrypt=""
    cryptveracrypt=""
    cryptrootdev=""
    cryptdiscard=""
    CRYPTTAB_OPTIONS=""

    local IFS=" ,"
    for x in $cryptopts; do
        case $x in
        hash=*)
            crypthash=${x#hash=}
            ;;
        size=*)
            cryptsize=${x#size=}
            ;;
        cipher=*)
            cryptcipher=${x#cipher=}
            ;;
        target=*)
            crypttarget=${x#target=}
            export CRYPTTAB_NAME="$crypttarget"
            ;;
        source=*)
            cryptsource=${x#source=}
            if [ ${cryptsource#UUID=} != $cryptsource ]; then
                cryptsource="/dev/disk/by-uuid/${cryptsource#UUID=}"
            elif [ ${cryptsource#LABEL=} != $cryptsource ]; then
                cryptsource="/dev/disk/by-label/$(printf '%s' "${cryptsource#LABEL=}" | sed 's,/,\\x2f,g')"
            elif [ ${cryptsource#ID=} != $cryptsource ]; then
                cryptsource="/dev/disk/by-id/${cryptsource#ID=}"
            fi
            export CRYPTTAB_SOURCE="$cryptsource"
            ;;
        header=*)
            cryptheader=${x#header=}
            if [ ! -e "$cryptheader" ] && [ -e "/conf/conf.d/cryptheader/$cryptheader" ]; then
                cryptheader="/conf/conf.d/cryptheader/$cryptheader"
            fi
            export CRYPTTAB_HEADER="$cryptheader"
            ;;
        lvm=*)
            cryptlvm=${x#lvm=}
            ;;
        keyscript=*)
            cryptkeyscript=${x#keyscript=}
            ;;
        key=*)
            if [ "${x#key=}" != "none" ]; then
                cryptkey=${x#key=}
            fi
            export CRYPTTAB_KEY="$cryptkey"
            ;;
        keyslot=*)
            cryptkeyslot=${x#keyslot=}
            ;;
        tries=*)
            crypttries="${x#tries=}"
            case "$crypttries" in
              *[![:digit:].]*)
                crypttries=3
                ;;
            esac
            ;;
        tcrypt)
            crypttcrypt="yes"
            ;;
        veracrypt)
            cryptveracrypt="--veracrypt"
            ;;
        rootdev)
            cryptrootdev="yes"
            ;;
        discard)
            cryptdiscard="yes"
            ;;
        esac
        PARAM="${x%=*}"
        if [ "$PARAM" = "$x" ]; then
            VALUE="yes"
        else
            VALUE="${x#*=}"
        fi
        CRYPTTAB_OPTIONS="$CRYPTTAB_OPTIONS $PARAM"
        eval export CRYPTTAB_OPTION_$PARAM="\"$VALUE\""
    done
    export CRYPTTAB_OPTIONS

    if [ -z "$cryptsource" ]; then
        message "cryptsetup ($crypttarget): source parameter missing"
        return 1
    fi
    return 0
}

activate_vg()
{
    # Sanity checks
    if [ ! -x /sbin/lvm ]; then
        message "cryptsetup ($crypttarget): lvm is not available"
        return 1
    fi

    # Detect and activate available volume groups
    /sbin/lvm vgscan
    /sbin/lvm vgchange -a y --sysinit
    return $?
}

setup_mapping()
{
    local opts count cryptopen cryptremove NEWROOT is_luks
    opts="$1"
    is_luks=0

    if [ -z "$opts" ]; then
        return 0
    fi

    parse_options "$opts" || return 1

    if [ -z "$cryptkeyscript" ]; then
        if [ ${cryptsource#/dev/disk/by-uuid/} != $cryptsource ]; then
            # UUIDs are not very helpful
            diskname="$crypttarget"
        else
            diskname="$cryptsource ($crypttarget)"
        fi
        cryptkeyscript="/lib/cryptsetup/askpass"
        cryptkey="1Please unlock disk $diskname: "
    elif ! type "$cryptkeyscript" >/dev/null; then
        message "cryptsetup ($crypttarget): error - script \"$cryptkeyscript\" missing"
        return 1
    fi

    if [ "$cryptkeyscript" = "cat" ] && [ "${cryptkey#/root/}" != "$cryptkey" ]; then
        # skip the mapping if the root FS is not mounted yet
        sed -rn 's/^\s*[^#]\S*\s+(\S+)\s.*/\1/p' /proc/mounts | grep -Fxq "$rootmnt" || return 1
        # substitute the "/root" prefix by the real root FS mountpoint otherwise
        cryptkey="${rootmnt}/${cryptkey#/root/}"
    fi

    if [ -n "$cryptheader" ] && ! type "$cryptheader" >/dev/null; then
        message "cryptsetup ($crypttarget): error - LUKS header \"$cryptheader\" missing"
        return 1
    fi

    # The same target can be specified multiple times
    # e.g. root and resume lvs-on-lvm-on-crypto
    if [ -e "/dev/mapper/$crypttarget" ]; then
        return 0
    fi

    modprobe -q dm_crypt

    # Make sure the cryptsource device is available
    if [ ! -e $cryptsource ]; then
        activate_vg
    fi

    # If the encrypted source device hasn't shown up yet, give it a
    # little while to deal with removable devices

    # the following lines below have been taken from
    # /usr/share/initramfs-tools/scripts/local, as suggested per
    # https://launchpad.net/bugs/164044
    if [ ! -e "$cryptsource" ]; then
        log_begin_msg "Waiting for encrypted source device..."

        # Default delay is 180s
        if [ -z "${ROOTDELAY}" ]; then
            slumber=180
        else
            slumber=${ROOTDELAY}
        fi
        if [ -x /sbin/usplash_write ]; then
            /sbin/usplash_write "TIMEOUT ${slumber}" || true
        fi

        slumber=$(( ${slumber} * 10 ))
        while [ ! -e "$cryptsource" ]; do
            # retry for LVM devices every 10 seconds
            if [ ${slumber} -eq $(( ${slumber}/100*100 )) ]; then
                activate_vg
            fi

            /bin/sleep 0.1
            slumber=$(( ${slumber} - 1 ))
            [ ${slumber} -gt 0 ] || break
        done

        if [ ${slumber} -gt 0 ]; then
            log_end_msg 0
        else
            log_end_msg 1 || true
        fi
        if [ -x /sbin/usplash_write ]; then
            /sbin/usplash_write "TIMEOUT 15" || true
        fi
    fi
    udev_settle

    # We've given up, but we'll let the user fix matters if they can
    if [ ! -e "${cryptsource}" ]; then

        echo "  ALERT! ${cryptsource} does not exist."
        echo "  Check cryptopts=source= bootarg: cat /proc/cmdline"
        echo "  or missing modules, devices: cat /proc/modules; ls /dev"
        panic -r "Dropping to a shell. Will skip ${cryptsource} if you can't fix."
    fi

    if [ ! -e "${cryptsource}" ]; then
        return 1
    fi

    # Prepare commands
    cryptopen="/sbin/cryptsetup -T 1"
    if [ "$cryptdiscard" = "yes" ]; then
        cryptopen="$cryptopen --allow-discards"
    fi
    if [ -n "$cryptheader" ]; then
        cryptopen="$cryptopen --header=$cryptheader"
    fi
    if [ -n "$cryptkeyslot" ]; then
        cryptopen="$cryptopen --key-slot=$cryptkeyslot"
    fi
    if /sbin/cryptsetup isLuks ${cryptheader:-$cryptsource} >/dev/null 2>&1; then
        is_luks=1
        cryptopen="$cryptopen open --type luks $cryptsource $crypttarget --key-file=-"
    elif [ "$crypttcrypt" = "yes" ]; then
        cryptopen="$cryptopen open --type tcrypt $cryptveracrypt $cryptsource $crypttarget"
    else
        cryptopen="$cryptopen -c $cryptcipher -s $cryptsize -h $crypthash open --type plain $cryptsource $crypttarget --key-file=-"
    fi
    cryptremove="/sbin/cryptsetup remove $crypttarget"
    NEWROOT="/dev/mapper/$crypttarget"

    # Try to get a satisfactory password $crypttries times
    count=0
    while [ $crypttries -le 0 ] || [ $count -lt $crypttries ]; do
        export CRYPTTAB_TRIED="$count"
        if [ $count -gt 1 ]; then
            /bin/sleep 3
        fi

        if [ -z "$cryptkeyscript" -a "$is_luks" -eq "1" ]; then
            cryptkey="Unlocking the disk $cryptsource ($crypttarget)\nEnter passphrase: "
            if [ -x /bin/plymouth ] && plymouth --ping; then
                cryptkeyscript="plymouth ask-for-password --prompt"
                cryptkey=$(echo -e "$cryptkey")
            else
                cryptkeyscript="/lib/cryptsetup/askpass"
            fi
        fi

        if [ -n "$CACHED_PASSWORD" ]; then
            if ! crypttarget="$crypttarget" cryptsource="$cryptsource"                  echo -n "$CACHED_PASSWORD" | $cryptopen 2>/dev/null; then
                unset CACHED_PASSWORD
            fi
        fi

        count=$(( $count + 1 ))

        if [ -z "$CACHED_PASSWORD" ]; then
            CACHED_PASSWORD="`$cryptkeyscript \"$cryptkey\"`"
            if ! crypttarget="$crypttarget" cryptsource="$cryptsource"                  echo -n "$CACHED_PASSWORD" | $cryptopen; then
                message "cryptsetup: cryptsetup failed, bad password or options?"
                unset CACHED_PASSWORD
                continue
            fi
        fi

        if [ ! -e "$NEWROOT" ]; then
            message "cryptsetup ($crypttarget): unknown error setting up device mapping"
            return 1
        fi

        #FSTYPE=''
        #eval $(fstype < "$NEWROOT")
        FSTYPE="$(/sbin/blkid -s TYPE -o value "$NEWROOT")"

        # See if we need to setup lvm on the crypto device
        #if [ "$FSTYPE" = "lvm" ] || [ "$FSTYPE" = "lvm2" ]; then
        if [ "$FSTYPE" = "LVM_member" ] || [ "$FSTYPE" = "LVM2_member" ]; then
            if [ -z "$cryptlvm" ]; then
                message "cryptsetup ($crypttarget): lvm fs found but no lvm configured"
                return 1
            elif ! activate_vg; then
                # disable error message, LP: #151532
                #message "cryptsetup ($crypttarget): failed to setup lvm device"
                return 1
            fi

            # Apparently ROOT is already set in /conf/param.conf for
            # flashed kernels at least. See bugreport #759720.
            if [ -f /conf/param.conf ] && grep -q "^ROOT=" /conf/param.conf; then
                NEWROOT=$(sed -n 's/^ROOT=//p' /conf/param.conf)
            else
                NEWROOT=${cmdline_root:-/dev/mapper/$cryptlvm}
                if [ "$cryptrootdev" = "yes" ]; then
                    # required for lilo to find the root device
                    echo "ROOT=$NEWROOT" >>/conf/param.conf
                fi
            fi
            #eval $(fstype < "$NEWROOT")
            FSTYPE="$(/sbin/blkid -s TYPE -o value "$NEWROOT")"
        fi

        #if [ -z "$FSTYPE" ] || [ "$FSTYPE" = "unknown" ]; then
        if [ -z "$FSTYPE" ]; then
            message "cryptsetup ($crypttarget): unknown fstype, bad password or options?"
            udev_settle
            $cryptremove
            unset CACHED_PASSWORD
            continue
        fi

        # decrease $count by 1, apparently last try was successful.
        count=$(( $count - 1 ))

        message "cryptsetup ($crypttarget): set up successfully"

        export CACHED_PASSWORD

        break
    done

    failsleep=60 # make configurable later?

    if [ "$cryptrootdev" = "yes" ] && [ $crypttries -gt 0 ] && [ $count -ge $crypttries ]; then
        message "cryptsetup ($crypttarget): maximum number of tries exceeded"
        message "cryptsetup: going to sleep for $failsleep seconds..."
        sleep $failsleep
        exit 1
    fi

    udev_settle
    return 0
}

exit_script()
{
    CACHED_PASSWORD="`dd bs=512 if=/dev/random count=1 2>/dev/null `"
    unset CACHED_PASSWORD
    exit $1
}

#
# Begin real processing
#

# Do we have any kernel boot arguments?
cmdline_cryptopts=''
unset cmdline_root
for opt in $(cat /proc/cmdline); do
    case $opt in
    cryptopts=*)
        opt="${opt#cryptopts=}"
        if [ -n "$opt" ]; then
            if [ -n "$cmdline_cryptopts" ]; then
                cmdline_cryptopts="$cmdline_cryptopts $opt"
            else
                cmdline_cryptopts="$opt"
            fi
        fi
        ;;
    root=*)
        opt="${opt#root=}"
        case $opt in
        /*) # Absolute path given. Not lilo major/minor number.
            cmdline_root=$opt
            ;;
        *) # lilo major/minor number (See #398957). Ignore
        esac
        ;;
    esac
done

if [ -n "$cmdline_cryptopts" ]; then
    # Call setup_mapping separately for each possible cryptopts= setting
    for cryptopt in $cmdline_cryptopts; do
        setup_mapping "$cryptopt"
    done
    exit 0
fi

# Do we have any settings from the /conf/conf.d/cryptroot file?
if [ -r /conf/conf.d/cryptroot ]; then
    while read mapping <&3; do
        setup_mapping "$mapping" 3<&-
    done 3< /conf/conf.d/cryptroot
fi

exit_script 0

diff /usr/share/initramfs-tools/scripts/local-top/cryptroot cryptroot
35a36,37
>   elif [ -p /dev/.initramfs/usplash_outfifo ] && [ -x /sbin/usplash_write ]; then
>       usplash_write "TEXT-URGENT $@"
101a104,105
>           elif [ ${cryptsource#ID=} != $cryptsource ]; then
>               cryptsource="/dev/disk/by-id/${cryptsource#ID=}"
182c186
<   local opts count cryptopen cryptremove NEWROOT
---
>   local opts count cryptopen cryptremove NEWROOT is_luks
183a188
>   is_luks=0
199c204
<       cryptkey="Please unlock disk $diskname: "
---
>       cryptkey="1Please unlock disk $diskname: "
244a250,252
>       if [ -x /sbin/usplash_write ]; then
>           /sbin/usplash_write "TIMEOUT ${slumber}" || true
>       fi
262a271,273
>       if [ -x /sbin/usplash_write ]; then
>           /sbin/usplash_write "TIMEOUT 15" || true
>       fi
291a303
>       is_luks=1
304a317,337
>       if [ $count -gt 1 ]; then
>           /bin/sleep 3
>       fi
> 
>       if [ -z "$cryptkeyscript" -a "$is_luks" -eq "1" ]; then
>           cryptkey="Unlocking the disk $cryptsource ($crypttarget)\nEnter passphrase: "
>           if [ -x /bin/plymouth ] && plymouth --ping; then
>               cryptkeyscript="plymouth ask-for-password --prompt"
>               cryptkey=$(echo -e "$cryptkey")
>           else
>               cryptkeyscript="/lib/cryptsetup/askpass"
>           fi
>       fi
> 
>       if [ -n "$CACHED_PASSWORD" ]; then
>           if ! crypttarget="$crypttarget" cryptsource="$cryptsource" >                echo -n "$CACHED_PASSWORD" | $cryptopen 2>/dev/null; then
>               unset CACHED_PASSWORD
>           fi
>       fi
> 
307c340,341
<       if [ ! -e "$NEWROOT" ]; then
---
>       if [ -z "$CACHED_PASSWORD" ]; then
>           CACHED_PASSWORD="`$cryptkeyscript \"$cryptkey\"`"
309,310c343,345
<                $cryptkeyscript "$cryptkey" | $cryptopen; then
<               message "cryptsetup ($crypttarget): cryptsetup failed, bad password or options?"
---
>                echo -n "$CACHED_PASSWORD" | $cryptopen; then
>               message "cryptsetup: cryptsetup failed, bad password or options?"
>               unset CACHED_PASSWORD
355a391
>           unset CACHED_PASSWORD
362a399,401
>       
>       export CACHED_PASSWORD
>       
378a418,424
> exit_script()
> {
>   CACHED_PASSWORD="`dd bs=512 if=/dev/random count=1 2>/dev/null `"
>   unset CACHED_PASSWORD
>   exit $1
> }
> 
425c471
< exit 0
---
> exit_script 0

Дополнить crypttab:


echo "root_crypt1 /dev/disk/by-id/ata-Samsung_SSD_850_PRO-part3 none luks,discard" >> /mnt/etc/crypttab
echo "root_crypt2 /dev/disk/by-id/ata-Micron_1100-part3 none luks,discard" >> /mnt/etc/crypttab


Здесь диски указываются не по UUID, это сделано специально.
Хук cryptroot, либо поправленный мною скрипт (о чём ниже) почему-то некорректно отработали с UUID-ами и не увидели разделы.


Установить загрузчик и убедиться, что он распознаёт корневую ФС


apt-get install grub-pc
grub-probe / — должен вывести "zfs"


Настроить загрузчик и установить в загрузочную запись


echo GRUB_PRELOAD_MODULES=\"part_gpt zfs\" >> /etc/default/grub
echo GRUB_DISABLE_OS_PROBER=true >> /etc/default/grub
echo "export ZPOOL_VDEV_NAME_PATH=YES" > /etc/profile.d/grub2_zpool_fix.sh
ZPOOL_VDEV_NAME_PATH=YES update-grub
update-initramfs -u -k all


Проинициализировать загрузочные разделы


cd && tar -C / -cf boot.tar /boot
mkfs.ext4 -L boot1 -m0 /dev/disk/by-id/ata-Samsung_SSD_850_PRO-part2
mkfs.ext4 -L boot2 -m0 /dev/disk/by-id/ata-Micron_1100-part2


Установить загрузчик


mount /dev/disk/by-id/ata-Samsung_SSD_850_PRO-part2 /boot && tar -C / -xf boot.tar
update-initramfs -k all -u -t && update-grub
grub-install --bootloader-id=debian1 --recheck --no-floppy /dev/disk/by-id/ata-Samsung_SSD_850_PRO
umount /boot
mount /dev/disk/by-id/ata-Micron_1100-part2 /boot && tar -C / -xf boot.tar
update-initramfs -k all -u -t && update-grub
grub-install --bootloader-id=debian2 --recheck --no-floppy /dev/disk/by-id/ata-Micron_1100
umount /boot


Установить пароль root


passwd


Создать снимок


zfs snapshot rpool/ROOT/debian@install


Выполнить отмонтирование и перезагрузку


umount /tmp
exit
mount | grep -v zfs | tac | awk '/\/mnt/ {print $3}' | xargs -i{} umount -lf {}
zpool export rpool
reboot


Загрузиться в установленную систему и выполнить её донастройку


Проверить работоспособность пула (пул и оба диска в нём должны быть ONLINE):
zpool status -v


Установить SSH сервер.
Создать нового пользователя:
zfs create rpool/home/user
adduser user
cp -a /etc/skel/.[!.]* /home/user
chown -R user:user /home/user
usermod -a -G audio,cdrom,dip,floppy,netdev,plugdev,sudo,video user


Обновить систему


apt dist-upgrade --yes


После этого шага у вас есть Debian с корнем на шифрованном зеркале ZFS.


Предполагаемые FAQ


Не снизит ли шифрование производительность?


Снизит.
Но это не имеет большого значение по следующим причинам:


  • В большинстве новых CPU (в особенности серверных) поддерживается AES-NI.
  • Это диск с ОС, от которого не требуется сверх-быстродействия.
  • Всегда возможно использовать кэширование (preload, например).

В целом, снижение быстродействия не то что, невозможно будет заметить, а даже сложно будет измерить.


Почему используется не EFI загрузчик?


На это есть две причины:


  • Плата, которую я использую, плохо загружается с EFI.
  • Обычный /boot c grub внутри легко потом включить в зеркало ZFS, что не так просто сделать с EFI разделами.

Почему не используется ZFS шифрование?


Потому что, на данный момент оно ещё сырое и оно шифрует не всё, оставляя метаданные.


Почему /boot не на ZFS?


Потому что, пока я этого ещё не сделал. Но grub поддерживает это.


Почему загрузка идёт не с шифрованного раздела?


Grub поддерживает эту возможность, но пароль придётся вводить трижды (сначала, на каждый корневой раздел, затем для cryptroot).
И есть у этого способа ещё недостатки, о которых я не буду здесь говорить.


Почему не используется FreeBSD, ведь там же даже шифрование с корнем на ZFS "из коробки"?


Потому что, я хочу сделать NAS с WEB-интерфейсом. FreeBSD — это ОС, которую придётся допиливать, чтобы работало приложение FreeNAS.
OpenMediaVault — это пакет, который я могу поставить в Debian.


Благодарности


Хочу выразить свою благодарность, прежде всего, русскому сообществу Debian: они сильно помогли, отвечая на мои вопросы.
Сообществу FreeNAS, которое имеет огромную базу знаний по ZFS.
Людям, которые портировали ZFS на Linux.

Комментарии (13)


  1. tuffnatty
    24.03.2018 23:19

    А то, что в (стабильной ветке) ZoL не работает TRIM, вас не смущает?


    1. artiom_n Автор
      25.03.2018 23:50

      В данном случае, не сильно, поскольку:
      — SSD заполнены незначительно.
      — ZoL активно дорабатывается: шифрование впилили быстрее, чем во FreeBSD, возможно, скоро будет.
      — Собственно, хранилище у меня на обычных HDD, а ОС на зеркале из SSD.


  1. JC_IIB
    25.03.2018 00:35

    Несмотря на то, что все мои проекты ведутся в гите, не весь код полностью мой, и не хочется его выкладывать на гитхаб.


    Bitbucket дает бесплатно приватные репозитории.


    1. artiom_n Автор
      25.03.2018 23:50

      Да, я в курсе. Но приватные — не значит «свои».


  1. skymal4ik
    25.03.2018 00:49

    Интересный опыт!

    Но всё же, почему не стандартная для linux связка md+lvm+ext4? Почему для вас zfs перевешивает даже с таким не самым интуитивным инсталлом?


    1. artiom_n Автор
      25.03.2018 23:58

      Потому что в схеме выше — 4 компонента (ещё шифрование) и тяжёлые снэпшоты. Пусть даже md возможно убрать. При этом, нет сквозного контроля: люди обсуждают, лучше хранить хэши файлов рядом с ними или в отдельном каталоге.
      ZFS берёт на себя управление дисками, сквозной контроль, сжатие, создание снэпшотов, синхронизацию между разными ФС (zfs send), функционал рэйд-массивов, управление опциями монтирования, дедупликацию и так далее.
      В идеале ещё и шифрование.
      Кроме того, проблем с масштабированием не возникнет на 100%.


  1. nlykl
    25.03.2018 01:45

    Статей про ZFS становится все больше, это радует.

    Зачем вы забили SSD случайными данными перед blkdiscard, который потом все очистит?


    1. YaakovTooth
      25.03.2018 04:14

      Потому что это разные операции. :)


    1. bfuvx
      25.03.2018 18:29

      Зря минусуют предыдущего комментатора, т.к. это на самом деле разные операции для разных целей. ioctl BLKDISCARD (ATA TRIM) не гарантирует какого-то общего поведения на всех устройствах, все зависит от firmware конкретно диска. После discard'а контроллер диска может возвращать нули, прошлые данные, случайные данные, а может поведение вообще не быть четко описанным. Также есть и другие ata комманды: ATA SECURE TRIM (ioctl BLKSECDISCARD, blkdiscard -s), ATA SECURITY ERASE UNIT (hdparm --security-erase), но опять же их поведение может быть по разному реализовано в firmware различных дисков.
      Поэтому топикастер заполняет сначала весь диск случайными данными, чтобы точно нельзя было достать прошлые данные напрямую с диска. После чего делается blkdiscard уже для других целей — помечаем все блоки как свободные, чтобы вернуть показатели производительности ssd диска к изначальным. При этом мы уже не волнуемя, что с диска можно будет достать старые данные при любом поведение discard'а.
      Ну и для dd лучше поднять bs хотя бы до 1M-2M — меньше write iops на диск будет, больше по скорости можно будет разогнаться. И oflag=direct добавить, мы же хотим быть уверены, что сразу по факту все перезаписано. Ну или можно использовать специализированную утилиту — shred.


      1. artiom_n Автор
        25.03.2018 23:59

        Да, про oflag=direct, я забыл. Спасибо.


    1. artiom_n Автор
      26.03.2018 00:07

      До этого я на эти SSD уже ставил систему: у меня не всё сразу взлетело.
      И, как заметил комментатор выше, это просто гарантированное стирание.
      Про hdparm --security-erase я в курсе. Но у этого способа есть проблема: он не везде работает.
      В том числе и у меня. Некоторые платы блокируют secure erase (хотя, мои SSD его поддерживают). Для того, чтобы разблокировать, предлагалось вытащить провод «на горячую», а для этого надо открывать корпус, чего мне делать не хотелось.

      А blkdiscard действительно гарантирует лишь то, что блоки будут помечены, как неиспользуемые, хотя в моём случае «свободные» блоки и возвращают нули (специально сейчас проверил).

      Что, конечно, несколько понижает уровень криптостойкости (хотя, не критично).


  1. MrFrizzy
    25.03.2018 02:01

    при шифровании /boot раздела можно использовать специальный файлик с ключом от остальных дисков на зашифрованном /boot или внутри образа ядра;
    плюсы: пароль вводиться только для /boot раздела
    минусы: при компрометации оси пасс от дисков несколько проще украсть из файла, нежели из памяти. хотя при такой компрометации оси с дальнейшем физическим доступом к дискам, это не самая большая ваша проблема имхо…


    1. artiom_n Автор
      26.03.2018 00:14

      Как вариант. Под «образом ядра», вы имели ввиду initrd, я так понимаю?
      Небольшие минусы способа:
      — Дешифровку будет производить граб. Он делает это медленно (видимо, aes-ni не использует).
      — При подключении раздела (для обновления ядра/initrd) его надо будет расшифровать.

      Ну и пока вы пароль не введёте, консоль граба не будет доступна.