diff --git a/roles/configuration/tasks/encryption.yml b/roles/configuration/tasks/encryption.yml index c5064b1..6226cd0 100644 --- a/roles/configuration/tasks/encryption.yml +++ b/roles/configuration/tasks/encryption.yml @@ -36,6 +36,12 @@ configuration_luks_tpm2_device: "{{ system_cfg.luks.tpm2.device }}" configuration_luks_tpm2_pcrs: "{{ luks_tpm2_pcrs }}" configuration_luks_keyfile_path: "/etc/cryptsetup-keys.d/{{ system_cfg.luks.mapper }}.key" + configuration_luks_tpm2_token_lib: >- + {{ + '/usr/lib/x86_64-linux-gnu/cryptsetup/libcryptsetup-token-systemd-tpm2.so' + if os_family == 'Debian' + else '/usr/lib64/cryptsetup/libcryptsetup-token-systemd-tpm2.so' + }} - name: Validate LUKS UUID is available ansible.builtin.assert: @@ -51,8 +57,13 @@ fail_msg: system.luks.passphrase must be set for LUKS auto-decrypt. no_log: true - - name: Enroll TPM2 for LUKS - when: configuration_luks_auto_method == 'tpm2' + - name: Detect TPM2 unlock method + ansible.builtin.include_tasks: encryption/initramfs_detect.yml + + - name: Enroll TPM2 via systemd-cryptenroll + when: + - configuration_luks_auto_method == 'tpm2' + - _tpm2_method | default('systemd-cryptenroll') == 'systemd-cryptenroll' ansible.builtin.include_tasks: encryption/tpm2.yml - name: Configure LUKS keyfile auto-decrypt @@ -78,7 +89,7 @@ }} luks_tpm2_option_list: >- {{ - (configuration_luks_auto_method == 'tpm2') + (configuration_luks_auto_method == 'tpm2' and (_tpm2_method | default('systemd-cryptenroll')) == 'systemd-cryptenroll') | ternary( ['tpm2-device=' + configuration_luks_tpm2_device] + (['tpm2-pcrs=' + configuration_luks_tpm2_pcrs] @@ -122,16 +133,16 @@ path: /mnt{{ configuration_luks_keyfile_path }} state: absent + - name: Configure initramfs for LUKS + ansible.builtin.include_tasks: encryption/initramfs.yml + - name: Configure crypttab ansible.builtin.include_tasks: encryption/crypttab.yml - - name: Configure initramfs - ansible.builtin.include_tasks: encryption/initramfs.yml - - - name: Configure dracut - when: os_family == 'RedHat' + - name: Configure dracut for LUKS + when: _initramfs_generator | default('') == 'dracut' ansible.builtin.include_tasks: encryption/dracut.yml - name: Configure GRUB for LUKS - when: not os_family == 'RedHat' + when: _initramfs_generator | default('') != 'dracut' or os_family != 'RedHat' ansible.builtin.include_tasks: encryption/grub.yml diff --git a/roles/configuration/tasks/encryption/dracut.yml b/roles/configuration/tasks/encryption/dracut.yml index c89fd82..f0bcd53 100644 --- a/roles/configuration/tasks/encryption/dracut.yml +++ b/roles/configuration/tasks/encryption/dracut.yml @@ -9,48 +9,58 @@ ansible.builtin.copy: dest: /mnt/etc/dracut.conf.d/crypt.conf content: | - add_dracutmodules+=" crypt " - {% if configuration_luks_keyfile_in_use %} + add_dracutmodules+=" crypt systemd " + {% if configuration_luks_keyfile_in_use | default(false) %} install_items+=" {{ configuration_luks_keyfile_path }} " {% endif %} + {% if configuration_luks_auto_method == 'tpm2' %} + install_items+=" {{ configuration_luks_tpm2_token_lib | default('') }} " + {% endif %} mode: "0644" -- name: Read kernel cmdline defaults +# --- Kernel cmdline: write rd.luks.* args for dracut --- +- name: Ensure kernel cmdline directory exists + ansible.builtin.file: + path: /mnt/etc/kernel + state: directory + mode: "0755" + +- name: Read existing kernel cmdline ansible.builtin.slurp: src: /mnt/etc/kernel/cmdline - register: configuration_kernel_cmdline_slurp + register: _kernel_cmdline_slurp + failed_when: false - name: Build kernel cmdline with LUKS args vars: - kernel_cmdline_current: >- - {{ configuration_kernel_cmdline_slurp.content | b64decode | trim }} - kernel_cmdline_list: >- + _cmdline_current: >- + {{ (_kernel_cmdline_slurp.content | default('') | b64decode | default('')) | trim }} + _cmdline_list: >- + {{ _cmdline_current.split() if _cmdline_current | length > 0 else [] }} + _cmdline_filtered: >- {{ - kernel_cmdline_current.split() - if kernel_cmdline_current | length > 0 else [] - }} - kernel_cmdline_filtered: >- - {{ - kernel_cmdline_list + _cmdline_list | reject('match', '^rd\\.luks\\.(name|options|key)=' ~ configuration_luks_uuid ~ '=') | list }} - kernel_cmdline_new: >- + _cmdline_new: >- {{ - (kernel_cmdline_filtered + configuration_luks_kernel_args.split()) + (_cmdline_filtered + configuration_luks_kernel_args.split()) | unique | join(' ') }} ansible.builtin.set_fact: - configuration_kernel_cmdline_new: "{{ kernel_cmdline_new }}" + _dracut_kernel_cmdline: "{{ _cmdline_new }}" - name: Write kernel cmdline with LUKS args ansible.builtin.copy: dest: /mnt/etc/kernel/cmdline mode: "0644" - content: "{{ configuration_kernel_cmdline_new }}\n" + content: "{{ _dracut_kernel_cmdline }}\n" +# --- BLS entries: RedHat-specific --- - name: Update BLS entries with LUKS kernel cmdline + when: os_family == 'RedHat' vars: - _bls_cmdline: "{{ configuration_kernel_cmdline_new }}" + _bls_cmdline: "{{ _dracut_kernel_cmdline }}" ansible.builtin.include_tasks: ../_bls_update.yml diff --git a/roles/configuration/tasks/encryption/initramfs.yml b/roles/configuration/tasks/encryption/initramfs.yml index 43548af..80ba6ef 100644 --- a/roles/configuration/tasks/encryption/initramfs.yml +++ b/roles/configuration/tasks/encryption/initramfs.yml @@ -1,8 +1,94 @@ --- -- name: Ensure keyfile pattern for initramfs-tools +# Initramfs configuration for LUKS auto-unlock. +# Runs AFTER Build LUKS parameters (so configuration_luks_keyfile_in_use is set). +# _initramfs_generator and _tpm2_method are set by initramfs_detect.yml. + +# --- clevis: install and bind TPM2 --- +- name: Install clevis in target system when: - - os_family == 'Debian' - - configuration_luks_keyfile_in_use + - configuration_luks_auto_method == 'tpm2' + - _tpm2_method | default('') == 'clevis' + ansible.builtin.command: >- + {{ chroot_command }} apt install -y clevis clevis-luks clevis-tpm2 clevis-initramfs tpm2-tools + register: _clevis_install_result + changed_when: _clevis_install_result.rc == 0 + +- name: Install clevis on installer for LUKS binding + when: + - configuration_luks_auto_method == 'tpm2' + - _tpm2_method | default('') == 'clevis' + community.general.pacman: + name: + - clevis + - tpm2-tools + state: present + retries: 3 + delay: 5 + +- name: Create clevis passphrase file + when: + - configuration_luks_auto_method == 'tpm2' + - _tpm2_method | default('') == 'clevis' + ansible.builtin.copy: + dest: /mnt/root/.luks-enroll-key + content: "{{ configuration_luks_passphrase }}" + mode: "0600" + no_log: true + +- name: Ensure TPM device accessible for clevis + when: + - configuration_luks_auto_method == 'tpm2' + - _tpm2_method | default('') == 'clevis' + ansible.builtin.shell: >- + ls /mnt/dev/tpmrm0 2>/dev/null + || (ls /dev/tpmrm0 && cp -a /dev/tpmrm0 /mnt/dev/tpmrm0) + changed_when: false + failed_when: false + +- name: Bind LUKS to TPM2 via clevis + when: + - configuration_luks_auto_method == 'tpm2' + - _tpm2_method | default('') == 'clevis' + vars: + _clevis_config: >- + {{ + '{"pcr_ids":"' + configuration_luks_tpm2_pcrs + '"}' + if configuration_luks_tpm2_pcrs | length > 0 + else '{}' + }} + ansible.builtin.command: >- + clevis luks bind -f -k /mnt/root/.luks-enroll-key + -d {{ configuration_luks_device }} tpm2 '{{ _clevis_config }}' + register: _clevis_bind_result + changed_when: _clevis_bind_result.rc == 0 + failed_when: false + + # Initramfs regeneration is handled by the bootloader task which runs after + # encryption configuration. Clevis hooks are included automatically by + # update-initramfs when clevis-initramfs is installed. + +- name: Remove clevis passphrase file + when: + - configuration_luks_auto_method == 'tpm2' + - _tpm2_method | default('') == 'clevis' + ansible.builtin.file: + path: /mnt/root/.luks-enroll-key + state: absent + +- name: Report clevis binding result + when: + - configuration_luks_auto_method == 'tpm2' + - _tpm2_method | default('') == 'clevis' + ansible.builtin.debug: + msg: >- + {{ 'Clevis TPM2 binding succeeded' if (_clevis_bind_result.rc | default(1)) == 0 + else 'Clevis TPM2 binding failed: ' + (_clevis_bind_result.stderr | default('unknown')) + '. System will require passphrase at boot.' }} + +# --- initramfs-tools: keyfile support (non-TPM2) --- +- name: Configure initramfs-tools keyfile pattern + when: + - _initramfs_generator | default('') == 'initramfs-tools' + - configuration_luks_keyfile_in_use | default(false) | bool ansible.builtin.lineinfile: path: /mnt/etc/cryptsetup-initramfs/conf-hook regexp: "^KEYFILE_PATTERN=" @@ -10,8 +96,9 @@ create: true mode: "0644" +# --- mkinitcpio: systemd + sd-encrypt hooks --- - name: Configure mkinitcpio hooks for LUKS - when: os == 'archlinux' + when: _initramfs_generator | default('') == 'mkinitcpio' ansible.builtin.lineinfile: path: /mnt/etc/mkinitcpio.conf regexp: "^HOOKS=" @@ -20,13 +107,13 @@ block sd-encrypt{{ ' lvm2' if system_cfg.filesystem != 'btrfs' else '' }} filesystems fsck) - name: Read mkinitcpio configuration - when: os == 'archlinux' + when: _initramfs_generator | default('') == 'mkinitcpio' ansible.builtin.slurp: src: /mnt/etc/mkinitcpio.conf register: configuration_mkinitcpio_slurp - name: Build mkinitcpio FILES list - when: os == 'archlinux' + when: _initramfs_generator | default('') == 'mkinitcpio' vars: mkinitcpio_files_list: >- {{ @@ -42,7 +129,7 @@ {{ ( (mkinitcpio_files_list + [configuration_luks_keyfile_path]) - if configuration_luks_keyfile_in_use + if (configuration_luks_keyfile_in_use | default(false)) else ( mkinitcpio_files_list | reject('equalto', configuration_luks_keyfile_path) @@ -55,7 +142,7 @@ configuration_mkinitcpio_files_list_new: "{{ mkinitcpio_files_list_new }}" - name: Configure mkinitcpio FILES list - when: os == 'archlinux' + when: _initramfs_generator | default('') == 'mkinitcpio' ansible.builtin.lineinfile: path: /mnt/etc/mkinitcpio.conf regexp: "^FILES=" diff --git a/roles/configuration/tasks/encryption/initramfs_detect.yml b/roles/configuration/tasks/encryption/initramfs_detect.yml new file mode 100644 index 0000000..c289c4b --- /dev/null +++ b/roles/configuration/tasks/encryption/initramfs_detect.yml @@ -0,0 +1,98 @@ +--- +# Resolve initramfs generator and TPM2 unlock method. +# Sets _initramfs_generator and _tpm2_method facts. +# +# Generator detection: derived from the platform's initramfs_cmd +# (dracut → dracut, mkinitcpio → mkinitcpio, else → initramfs-tools) +# TPM2 method: systemd-cryptenroll when generator supports tpm2-device, +# clevis fallback otherwise. Non-native dracut installed automatically. + +- name: Resolve initramfs generator + vars: + _user_generator: "{{ system_cfg.features.initramfs.generator | default('') }}" + _native_generator: >- + {{ + 'dracut' if _configuration_platform.initramfs_cmd is search('dracut') + else ('mkinitcpio' if _configuration_platform.initramfs_cmd is search('mkinitcpio') + else 'initramfs-tools') + }} + ansible.builtin.set_fact: + _initramfs_generator: >- + {{ _user_generator if _user_generator | length > 0 else _native_generator }} + _initramfs_native_generator: "{{ _native_generator }}" + +# --- Install non-native dracut if overridden or needed --- +- name: Install dracut in chroot when not native + when: + - _initramfs_generator == 'dracut' + - _initramfs_native_generator != 'dracut' + ansible.builtin.shell: >- + {{ chroot_command }} sh -c ' + command -v apt >/dev/null 2>&1 && apt install -y dracut || + command -v pacman >/dev/null 2>&1 && pacman -S --noconfirm dracut || + command -v dnf >/dev/null 2>&1 && dnf install -y dracut + ' + register: _dracut_install_result + changed_when: _dracut_install_result.rc == 0 + failed_when: false + +- name: Override initramfs command to dracut + when: + - _initramfs_generator == 'dracut' + - _initramfs_native_generator != 'dracut' + vars: + # Generate dracut initramfs with output name matching what GRUB expects: + # mkinitcpio native: /boot/initramfs-linux.img (Arch convention) + # initramfs-tools native: /boot/initrd.img- (Debian convention) + _dracut_cmd: >- + {{ + 'bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initramfs-linux.img $kver; done"' + if _initramfs_native_generator == 'mkinitcpio' + else 'bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initrd.img-$kver $kver; done"' + }} + ansible.builtin.set_fact: + _configuration_platform: >- + {{ _configuration_platform | combine({'initramfs_cmd': _dracut_cmd}) }} + +# --- TPM2 method detection --- +- name: Probe dracut for TPM2 module support + when: + - configuration_luks_auto_method == 'tpm2' + - _initramfs_generator != 'mkinitcpio' + ansible.builtin.command: "{{ chroot_command }} dracut --list-modules" + register: _dracut_modules_check + changed_when: false + failed_when: false + +- name: Resolve TPM2 unlock method + when: configuration_luks_auto_method == 'tpm2' + vars: + # mkinitcpio sd-encrypt supports tpm2-device natively + # dracut with tpm2-tss module supports tpm2-device natively + # everything else needs clevis + _supports_tpm2_native: >- + {{ + _initramfs_generator == 'mkinitcpio' + or ('tpm2-tss' in (_dracut_modules_check.stdout | default(''))) + }} + ansible.builtin.set_fact: + _tpm2_method: "{{ 'systemd-cryptenroll' if _supports_tpm2_native | bool else 'clevis' }}" + +# --- Auto-upgrade to dracut when tpm2-tss available but generator isn't dracut --- +- name: Switch to dracut for TPM2 support + when: + - configuration_luks_auto_method == 'tpm2' + - _tpm2_method == 'systemd-cryptenroll' + - _initramfs_generator not in ['dracut', 'mkinitcpio'] + vars: + _dracut_cmd: >- + bash -c "for kver in /lib/modules/*/; do kver=$(basename $kver); dracut --force /boot/initrd.img-$kver $kver; done" + ansible.builtin.set_fact: + _initramfs_generator: dracut + _configuration_platform: >- + {{ _configuration_platform | combine({'initramfs_cmd': _dracut_cmd}) }} + +- name: Report TPM2 configuration + when: configuration_luks_auto_method == 'tpm2' + ansible.builtin.debug: + msg: "TPM2 unlock: {{ _tpm2_method | default('none') }} | initramfs: {{ _initramfs_generator }}" diff --git a/roles/configuration/tasks/encryption/tpm2.yml b/roles/configuration/tasks/encryption/tpm2.yml index 0b7a7e5..66320a7 100644 --- a/roles/configuration/tasks/encryption/tpm2.yml +++ b/roles/configuration/tasks/encryption/tpm2.yml @@ -1,26 +1,35 @@ --- +# TPM2 enrollment via systemd-cryptenroll. +# Works with dracut and mkinitcpio (sd-encrypt). The user-set passphrase +# remains as a backup unlock method — no auto-generated keyfiles. - name: Enroll TPM2 for LUKS block: - # Tempfile in chroot /tmp — accessible by both chroot and host commands - name: Create temporary passphrase file for TPM2 enrollment ansible.builtin.tempfile: - path: /mnt/tmp + path: /mnt/root prefix: luks-passphrase- state: file - register: configuration_luks_tpm2_passphrase_tempfile + register: _tpm2_passphrase_tempfile - - name: Write passphrase into temporary file for TPM2 enrollment + - name: Write passphrase into temporary file ansible.builtin.copy: - dest: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}" + dest: "{{ _tpm2_passphrase_tempfile.path }}" content: "{{ configuration_luks_passphrase }}" owner: root group: root mode: "0600" no_log: true - - name: Enroll TPM2 token + - name: Ensure TPM device is accessible in chroot + ansible.builtin.shell: >- + ls /mnt/dev/tpmrm0 2>/dev/null + || (ls /dev/tpmrm0 && cp -a /dev/tpmrm0 /mnt/dev/tpmrm0) + changed_when: false + failed_when: false + + - name: Enroll TPM2 token via systemd-cryptenroll vars: - configuration_luks_enroll_args: >- + _enroll_args: >- {{ [ '/usr/bin/systemd-cryptenroll', @@ -28,69 +37,28 @@ '--tpm2-with-pin=false', '--wipe-slot=tpm2', '--unlock-key-file=' + ( - configuration_luks_tpm2_passphrase_tempfile.path - | regex_replace('^/mnt', '') + _tpm2_passphrase_tempfile.path | regex_replace('^/mnt', '') ) ] + (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs] if configuration_luks_tpm2_pcrs | length > 0 else []) + [configuration_luks_device] }} - configuration_luks_enroll_chroot_cmd: >- - {{ chroot_command }} {{ configuration_luks_enroll_args | join(' ') }} - ansible.builtin.command: "{{ configuration_luks_enroll_chroot_cmd }}" - register: configuration_luks_tpm2_enroll_chroot - changed_when: configuration_luks_tpm2_enroll_chroot.rc == 0 - failed_when: false + ansible.builtin.command: "{{ chroot_command }} {{ _enroll_args | join(' ') }}" + register: _tpm2_enroll_result + changed_when: _tpm2_enroll_result.rc == 0 - - name: Retry TPM2 enrollment in installer environment - when: - - (configuration_luks_tpm2_enroll_chroot.rc | default(1)) != 0 - vars: - configuration_luks_enroll_args: >- - {{ - [ - '/usr/bin/systemd-cryptenroll', - '--tpm2-device=' + configuration_luks_tpm2_device, - '--tpm2-with-pin=false', - '--wipe-slot=tpm2', - '--unlock-key-file=' + configuration_luks_tpm2_passphrase_tempfile.path - ] - + (['--tpm2-pcrs=' + configuration_luks_tpm2_pcrs] - if configuration_luks_tpm2_pcrs | length > 0 else []) - + [configuration_luks_device] - }} - ansible.builtin.command: - argv: "{{ configuration_luks_enroll_args }}" - register: configuration_luks_tpm2_enroll_host - changed_when: configuration_luks_tpm2_enroll_host.rc == 0 - failed_when: false - - - name: Validate TPM2 enrollment succeeded - ansible.builtin.assert: - that: - - >- - (configuration_luks_tpm2_enroll_chroot.rc | default(1)) == 0 - or (configuration_luks_tpm2_enroll_host.rc | default(1)) == 0 - fail_msg: >- - TPM2 enrollment failed. - chroot rc={{ configuration_luks_tpm2_enroll_chroot.rc | default('n/a') }}, - host rc={{ configuration_luks_tpm2_enroll_host.rc | default('n/a') }}, - chroot stderr={{ configuration_luks_tpm2_enroll_chroot.stderr | default('') }}, - host stderr={{ configuration_luks_tpm2_enroll_host.stderr | default('') }} rescue: - - name: Warn about TPM2 enrollment failure + - name: TPM2 enrollment failed ansible.builtin.debug: msg: >- - WARNING: TPM2 enrollment failed — falling back to keyfile auto-decrypt. - The system will use a keyfile instead of TPM2 for automatic LUKS unlock. + TPM2 enrollment failed: {{ _tpm2_enroll_result.stderr | default('unknown') }}. + The system will require the passphrase for LUKS unlock on boot. + TPM2 can be enrolled post-deployment via: systemd-cryptenroll --tpm2-device=auto {{ configuration_luks_device }} - - name: Fallback to keyfile auto-decrypt - ansible.builtin.set_fact: - configuration_luks_auto_method: keyfile always: - - name: Remove TPM2 enrollment passphrase file - when: configuration_luks_tpm2_passphrase_tempfile.path is defined + - name: Remove temporary passphrase file + when: _tpm2_passphrase_tempfile.path is defined ansible.builtin.file: - path: "{{ configuration_luks_tpm2_passphrase_tempfile.path }}" + path: "{{ _tpm2_passphrase_tempfile.path }}" state: absent