diff --git a/roles/bootstrap/tasks/main.yml b/roles/bootstrap/tasks/main.yml index a89565a..267881d 100644 --- a/roles/bootstrap/tasks/main.yml +++ b/roles/bootstrap/tasks/main.yml @@ -49,6 +49,15 @@ bootstrap_var_key: "{{ 'bootstrap_' + (os | replace('-lts', '') | replace('-', '_')) }}" ansible.builtin.include_tasks: "{{ bootstrap_os_task_map[os] }}" +# dnf --installroot never runs anaconda, so no authselect profile is selected and +# /etc/pam.d/system-auth is missing, leaving the system unable to authenticate. +# local is the right profile: local-auth only, no pam_sss.so, still CIS-capable. +- name: Select default authselect profile for the PAM stack + when: is_authselect | bool + ansible.builtin.command: "{{ chroot_command }} authselect select local --force" + register: bootstrap_authselect_result + changed_when: bootstrap_authselect_result.rc == 0 + - name: Install hardware-matched firmware/microcode/GPU/peripheral packages when: >- (system_cfg.features.firmware.enabled | bool) diff --git a/roles/bootstrap/vars/main.yml b/roles/bootstrap/vars/main.yml index 3492c29..3eaa702 100644 --- a/roles/bootstrap/vars/main.yml +++ b/roles/bootstrap/vars/main.yml @@ -1,6 +1,6 @@ --- -# Feature-gated packages shared across all distros. -# Arch has special nftables handling and composes this differently. +# Feature-gated packages shared across all distros. Arch strips nftables from +# this and composes it differently. bootstrap_common_conditional: >- {{ ( @@ -15,12 +15,29 @@ bootstrap_common_conditional: >- ) }} -# --------------------------------------------------------------------------- -# Per-OS package definitions: base (rootfs/group install), extra (post-base), -# conditional (feature/version-gated, appended by task files). -# DNF-based distros also carry repos (dnf --repo) and use base as group names. -# --------------------------------------------------------------------------- +# Native-installer parity backfill: anaconda and the d-i "standard" task leave +# these, but install_weak_deps=False / Recommends-off minimal installs drop them. +bootstrap_el_runtime: + - NetworkManager + - authselect + - authselect-libs + - chrony + - crypto-policies + - crypto-policies-scripts + - dbus + - polkit +bootstrap_deb_runtime: + - apparmor-utils + - chrony + - libpam-pwquality + - needrestart + - network-manager + - sudo + +# Per-OS package definitions: base (rootfs/group install), extra (post-base), +# conditional (feature/version-gated, appended by task files). DNF distros also +# carry repos and use base as group names. bootstrap_rhel: repos: - "rhel{{ os_version_major }}-baseos" @@ -53,6 +70,7 @@ bootstrap_rhel: + (['python39'] if os_version_major | default('') == '8' else ['python']) + (['kernel'] if os_version_major | default('') == '10' else []) + (['zram-generator'] if os_version_major | default('') in ['9', '10'] else []) + + bootstrap_el_runtime + bootstrap_common_conditional }} @@ -87,8 +105,8 @@ bootstrap_almalinux: - zstd conditional: >- {{ - (['dbus-daemon'] if (os_version_major | default('10') | int) >= 9 else []) - + (['dhcp-client'] if (os_version_major | default('10') | int) < 10 else []) + (['dhcp-client'] if (os_version_major | default('10') | int) < 10 else []) + + bootstrap_el_runtime + bootstrap_common_conditional }} @@ -127,6 +145,7 @@ bootstrap_rocky: conditional: >- {{ (['dhcp-client'] if (os_version_major | default('9') | int) < 10 else []) + + bootstrap_el_runtime + bootstrap_common_conditional }} @@ -160,7 +179,6 @@ bootstrap_fedora: - nc - nfs-utils - nfsv4-client-utils - - polkit - ppp - python3 - ripgrep @@ -171,7 +189,7 @@ bootstrap_fedora: - zoxide - zram-generator - zstd - conditional: "{{ bootstrap_common_conditional }}" + conditional: "{{ bootstrap_el_runtime + bootstrap_common_conditional }}" bootstrap_debian: base: @@ -189,28 +207,22 @@ bootstrap_debian: - python3 - xfsprogs extra: - - apparmor-utils - bat - - chrony - curl - entr - fish - fzf - htop - jq - - libpam-pwquality - linux-image-amd64 - lrzsz - mtr - ncdu - - needrestart - net-tools - - network-manager - python-is-python3 - ripgrep - rsync - screen - - sudo - syslog-ng - tcpd - vim @@ -225,6 +237,7 @@ bootstrap_debian: + (['systemd-zram-generator'] if (os_version | string) not in ['10', '11'] else []) + (['tldr'] if (os_version | string) not in ['13', 'unstable'] else []) + (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else []) + + bootstrap_deb_runtime + bootstrap_common_conditional }} @@ -246,10 +259,8 @@ bootstrap_ubuntu: - python3 - xfsprogs extra: - - apparmor-utils - bash-completion - bat - - chrony - curl - dnsutils - duf @@ -261,20 +272,16 @@ bootstrap_ubuntu: - fzf - htop - jq - - libpam-pwquality - lrzsz - mtr - ncdu - ncurses-term - - needrestart - net-tools - - network-manager - python-is-python3 - ripgrep - rsync - screen - software-properties-common - - sudo - syslog-ng - systemd-zram-generator - tcpd @@ -288,6 +295,7 @@ bootstrap_ubuntu: conditional: >- {{ (['shim-signed'] if system_cfg.features.secure_boot.enabled | bool else []) + + bootstrap_deb_runtime + bootstrap_common_conditional }} diff --git a/roles/cis/tasks/auth.yml b/roles/cis/tasks/auth.yml index ec47e59..a414df0 100644 --- a/roles/cis/tasks/auth.yml +++ b/roles/cis/tasks/auth.yml @@ -27,7 +27,9 @@ # Non-RHEL/non-Debian distros: loop evaluates to [] (intentional skip) - name: Prevent Login to Accounts With Empty Password - when: cis_effective_rules.empty_password_login | default(false) + when: + - cis_effective_rules.empty_password_login | default(false) + - not is_authselect | bool ansible.builtin.replace: dest: "{{ item }}" regexp: "\\s*nullok" diff --git a/roles/cis/tasks/crypto.yml b/roles/cis/tasks/crypto.yml index 28bbdae..b622f68 100644 --- a/roles/cis/tasks/crypto.yml +++ b/roles/cis/tasks/crypto.yml @@ -1,11 +1,15 @@ --- # Fedora ships its own crypto-policies preset and update-crypto-policies # behaves differently; applying DEFAULT:NO-SHA1 can break package signing. +# EL10 dropped the NO-SHA1 subpolicy module (DEFAULT already disables SHA-1 +# signatures), so the modifier is set only on EL9 and below. - name: Configure System Cryptography Policy + vars: + _cis_crypto_policy: "{{ 'DEFAULT' if (os_version_major | int >= 10) else 'DEFAULT:NO-SHA1' }}" when: - cis_effective_rules.crypto_policy | default(false) - os in (os_family_rhel | difference(['fedora'])) - ansible.builtin.command: "{{ chroot_command }} /usr/bin/update-crypto-policies --set DEFAULT:NO-SHA1" + ansible.builtin.command: "{{ chroot_command }} /usr/bin/update-crypto-policies --set {{ _cis_crypto_policy }}" register: cis_crypto_policy_result changed_when: "'Setting system-wide crypto-policies to' in cis_crypto_policy_result.stdout" diff --git a/roles/cis/tasks/security_lines.yml b/roles/cis/tasks/security_lines.yml index b140411..45e040b 100644 --- a/roles/cis/tasks/security_lines.yml +++ b/roles/cis/tasks/security_lines.yml @@ -126,32 +126,45 @@ regexp: '^\s*#?\s*auth\s+required\s+pam_wheel\.so' line: auth required pam_wheel.so +# authselect wires the pam_faillock stack via the feature; deny/unlock_time live +# in faillock.conf, the supported place (pam_faillock(8) deprecates module args). +- name: Configure account lockout (authselect) + when: + - cis_effective_rules.faillock | default(false) + - is_authselect | bool + block: + - name: Enable the authselect faillock feature + ansible.builtin.command: "{{ chroot_command }} authselect enable-feature with-faillock" + register: cis_faillock_result + changed_when: cis_faillock_result.rc == 0 + + - name: Set faillock thresholds + ansible.builtin.lineinfile: + path: /mnt/etc/security/faillock.conf + regexp: "{{ item.regexp }}" + line: "{{ item.line }}" + create: true + mode: "0644" + loop: + - {regexp: '^\s*#?\s*deny\s*=', line: "deny = {{ cis_cfg.faillock_deny }}"} + - {regexp: '^\s*#?\s*unlock_time\s*=', line: "unlock_time = {{ cis_cfg.faillock_unlock_time }}"} + loop_control: + label: "{{ item.line }}" + - name: Configure account lockout - when: cis_effective_rules.faillock | default(false) + when: + - cis_effective_rules.faillock | default(false) + - not is_authselect | bool ansible.builtin.lineinfile: path: "{{ item.path }}" regexp: "{{ item.regexp }}" line: "{{ item.line }}" loop: - - path: >- - /mnt/etc/{{ - "pam.d/common-auth" - if is_debian | bool - else "authselect/system-auth" - if os == "fedora" - else "pam.d/system-auth" - }} + - path: '/mnt/etc/{{ "pam.d/common-auth" if is_debian | bool else "pam.d/system-auth" }}' regexp: '^\s*auth\s+required\s+pam_faillock\.so' line: >- auth required pam_faillock.so onerr=fail audit silent deny={{ cis_cfg.faillock_deny }} unlock_time={{ cis_cfg.faillock_unlock_time }} - - path: >- - /mnt/etc/{{ - "pam.d/common-account" - if is_debian | bool - else "authselect/system-auth" - if os == "fedora" - else "pam.d/system-auth" - }} + - path: '/mnt/etc/{{ "pam.d/common-account" if is_debian | bool else "pam.d/system-auth" }}' regexp: '^\s*account\s+required\s+pam_faillock\.so' line: account required pam_faillock.so loop_control: diff --git a/roles/global_defaults/tasks/main.yml b/roles/global_defaults/tasks/main.yml index 52ffee0..f14b10d 100644 --- a/roles/global_defaults/tasks/main.yml +++ b/roles/global_defaults/tasks/main.yml @@ -61,6 +61,12 @@ ansible.builtin.set_fact: os_version_major: "{{ (os_version | string).split('.')[0] }}" +# EL>=10 and Fedora dropped the static /etc/pam.d/system-auth shipped by pam; +# the PAM stack is generated by authselect and absent until a profile is selected. +- name: Flag authselect-managed PAM stacks + ansible.builtin.set_fact: + is_authselect: "{{ is_rhel | bool and (os_version_major | default('0') | int) >= 10 }}" + - name: Set chroot command wrapper ansible.builtin.set_fact: chroot_command: >-