feat(configuration): generic FDE with systemd-cryptenroll, clevis fallback, and configurable initramfs
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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="
|
||||
|
||||
98
roles/configuration/tasks/encryption/initramfs_detect.yml
Normal file
98
roles/configuration/tasks/encryption/initramfs_detect.yml
Normal file
@@ -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-<kver> (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 }}"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user