From 174a96eefcf13e1c64e2123e59f42476c34c22d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20D=C3=A9siles?= <1536672+cdesiles@users.noreply.github.com> Date: Tue, 18 Aug 2026 21:52:49 +0200 Subject: [PATCH] security: always restore sudoers after makepkg, drop dead paru block The archlinux and zfs roles temporarily write `%wheel ALL=(ALL) NOPASSWD: ALL` to /etc/sudoers so makepkg/paru can run unattended, then restore it in a following task. Since the restore was a plain sibling task, any failure in between aborted the play and left the host with passwordless root for the whole wheel group. Move the restore into `always:` so it runs on failure too, and narrow the window in yay.yml to just the makepkg call (dependency install, directory setup and git clone no longer run inside it). This is not hypothetical: a provisioning run of omega failed on paru's git clone and left the sudoers hack in place. The clone failed because the deprecated compile-from-source paru block, which was commented out in tree, had been re-enabled. It fed paru_git_repo into the git module, but that variable now holds a GitHub owner/repo slug for the releases API rather than a clone URL. Remove the block outright, along with the orphaned paru_src_path default; paru is installed from the release binary. Also fix the ansible-lint violations this surfaced so roles/archlinux passes the production profile: explicit `become` next to `become_user`, a pinned branch on the yay clone, and `changed_when` on the command/shell tasks. --- roles/archlinux/defaults/main.yml | 3 +- roles/archlinux/tasks/pacman.yml | 1 + roles/archlinux/tasks/paru.yml | 61 ++----------------------------- roles/archlinux/tasks/yay.yml | 52 +++++++++++++++----------- roles/zfs/tasks/install.yml | 8 +++- 5 files changed, 43 insertions(+), 82 deletions(-) diff --git a/roles/archlinux/defaults/main.yml b/roles/archlinux/defaults/main.yml index caea001..3e19b87 100644 --- a/roles/archlinux/defaults/main.yml +++ b/roles/archlinux/defaults/main.yml @@ -2,6 +2,5 @@ arch_locale: en_US.UTF-8 yay_src_path: /opt/yay yay_git_repo: https://aur.archlinux.org/yay.git -paru_git_repo: Morganamilo/paru -paru_src_path: /opt/paru +paru_git_repo: Morganamilo/paru # GitHub owner/repo slug, used for the releases API os_arch: x86_64 # or aarch64 diff --git a/roles/archlinux/tasks/pacman.yml b/roles/archlinux/tasks/pacman.yml index 281c24f..2b11290 100644 --- a/roles/archlinux/tasks/pacman.yml +++ b/roles/archlinux/tasks/pacman.yml @@ -34,6 +34,7 @@ reflector --latest 20 --protocol https --sort rate --save /etc/pacman.d/mirrorlist when: mirrorlist_bak.stat.exists is false or (mirrorlist.stat.exists and (ansible_date_time.epoch | int - mirrorlist.stat.mtime) > 604800) + changed_when: true - name: Configure pacman to output colors ansible.builtin.lineinfile: diff --git a/roles/archlinux/tasks/paru.yml b/roles/archlinux/tasks/paru.yml index b575787..fa5ee43 100644 --- a/roles/archlinux/tasks/paru.yml +++ b/roles/archlinux/tasks/paru.yml @@ -6,53 +6,6 @@ - name: Install paru when: not paru.stat.exists - ## - ## Deprecated version with compilation - ## - # - name: Install paru - # block: - # - name: Install build dependencies - # package: - # name: - # - base-devel - # - git - # state: present - - # - name: Disable sudo password prompt (makepkg sudoers hack) - # lineinfile: - # dest: /etc/sudoers - # state: present - # regexp: "^#?%wheel" - # line: "%wheel ALL=(ALL) NOPASSWD: ALL" - # validate: /usr/sbin/visudo -cf %s - - # - name: Create paru sources dir - # file: - # path: "{{ paru_src_path }}" - # state: directory - # owner: "{{ ansible_user }}" - - # - name: Clone git sources - # become: false - # git: - # repo: "{{ paru_git_repo }}" - # dest: "{{ paru_src_path }}" - - # # note: this only works because SUDOERS password prompt is disabled - # - name: Build and install - # become: false - # command: - # chdir: "{{ paru_src_path }}" - # cmd: "makepkg -si -f --noconfirm" - - # - name: Restore sudo with password prompt - # lineinfile: - # dest: /etc/sudoers - # state: present - # regexp: "^#?%wheel" - # line: "%wheel ALL=(ALL:ALL) ALL" - # validate: /usr/sbin/visudo -cf %s - # when: not paru.stat.exists block: - name: Get the last github release ansible.builtin.uri: @@ -60,10 +13,6 @@ return_content: true register: paru_release - - name: Extract tag_name - ansible.builtin.set_fact: - paru_version: "{{ (paru_release.json.tag_name | regex_replace('^v', '')) }}" - - name: Get the binary URL ({{ os_arch }}) ansible.builtin.set_fact: paru_url: "{{ item.browser_download_url }}" @@ -91,12 +40,10 @@ remote_src: true mode: "0755" - - name: Ensure permissions - ansible.builtin.file: - path: /usr/bin/paru - mode: "0755" - - name: Cleanup ansible.builtin.file: - path: "/tmp/paru-{{ os_arch }}.tar.zst" + path: "{{ item }}" state: absent + loop: + - "/tmp/paru-{{ os_arch }}.tar.zst" + - /tmp/paru diff --git a/roles/archlinux/tasks/yay.yml b/roles/archlinux/tasks/yay.yml index 04c03e4..0b4f523 100644 --- a/roles/archlinux/tasks/yay.yml +++ b/roles/archlinux/tasks/yay.yml @@ -14,14 +14,6 @@ - git state: present - - name: Disable sudo password prompt (makepkg sudoers hack) - ansible.builtin.lineinfile: - dest: /etc/sudoers - state: present - regexp: "^#?%wheel" - line: "%wheel ALL=(ALL) NOPASSWD: ALL" - validate: /usr/sbin/visudo -cf %s - - name: Clean up stale yay sources dir ansible.builtin.file: path: "{{ yay_src_path }}" @@ -35,22 +27,40 @@ mode: "0755" - name: Clone git sources + become: true become_user: "{{ ansible_user }}" ansible.builtin.git: repo: "{{ yay_git_repo }}" dest: "{{ yay_src_path }}" + version: master - # note: this only works because SUDOERS password prompt is disabled - - name: Build and install - become_user: "{{ ansible_user }}" - ansible.builtin.command: - chdir: "{{ yay_src_path }}" - cmd: "makepkg -si -f --noconfirm" + # makepkg refuses to run as root and shells out to sudo to install the + # built package, so `--noconfirm` needs a passwordless sudo window. + # Keep that window as small as possible and always close it: the restore + # lives in `always` so it runs even when the build fails, which otherwise + # leaves the host with passwordless root for the whole wheel group. + - name: Build and install yay + block: + - name: Disable sudo password prompt (makepkg sudoers hack) + ansible.builtin.lineinfile: + dest: /etc/sudoers + state: present + regexp: "^#?%wheel" + line: "%wheel ALL=(ALL) NOPASSWD: ALL" + validate: /usr/sbin/visudo -cf %s - - name: Restore sudo with password prompt - ansible.builtin.lineinfile: - dest: /etc/sudoers - state: present - regexp: "^#?%wheel" - line: "%wheel ALL=(ALL:ALL) ALL" - validate: /usr/sbin/visudo -cf %s + - name: Build and install + become: true + become_user: "{{ ansible_user }}" + ansible.builtin.command: + chdir: "{{ yay_src_path }}" + cmd: "makepkg -si -f --noconfirm" + changed_when: true + always: + - name: Restore sudo with password prompt + ansible.builtin.lineinfile: + dest: /etc/sudoers + state: present + regexp: "^#?%wheel" + line: "%wheel ALL=(ALL:ALL) ALL" + validate: /usr/sbin/visudo -cf %s diff --git a/roles/zfs/tasks/install.yml b/roles/zfs/tasks/install.yml index 24649c6..6d8a292 100644 --- a/roles/zfs/tasks/install.yml +++ b/roles/zfs/tasks/install.yml @@ -5,6 +5,8 @@ changed_when: false failed_when: false +# The restore lives in `always` so it runs even when paru fails. Otherwise a +# failed build leaves the host with passwordless root for the whole wheel group. - name: Install zfs when: zfs_dkms_installed.stderr | length > 0 block: @@ -24,7 +26,8 @@ become: false ansible.builtin.command: cmd: "paru -S --noconfirm zfs-dkms zfs-utils" - + changed_when: true + always: - name: Restore SUDOERS password prompt after paru no_log: true ansible.builtin.lineinfile: @@ -65,7 +68,8 @@ become: false ansible.builtin.command: cmd: "paru -S --noconfirm zrepl" - + changed_when: true + always: - name: Restore SUDOERS password prompt after paru no_log: true ansible.builtin.lineinfile: