I recently discovered that my previous configuration for per-host sudo passwords, where I set ansible_become_pass to a lookup based on inventory_hostname, fails for delegated tasks. This is because inventory_hostname remains set to the original host, not the delegated host where the command is run, so the wrong password is used.

The original configuration was to set the password as an inventory variable:

ansible_become_password: "{{ lookup('community.hashi_vault.vault_read', 'kv/hosts/' + inventory_hostname + '/users/ansible').data.password }}"

To get around this, I changed to a task that sets the variable using ansible.builtin.set_fact on the host after looking up the password (delegated to localhost, as that should be able to do the vault lookup). This little test playbook demonstrates it working (and also demonstrates only setting it if the password is in the vault, which neatly deals with my current issue of some hosts being setup this way and some not):

---
- hosts: all
  gather_facts: true
  tasks:
    - name: Ansible sudo password is retrieved from vault, if known
      delegate_to: localhost
      community.hashi_vault.vault_read:
        # So many things can determine the remote username (
        # ansible_user variable, SSH_DEFAULT_USER environment
        # variable, .ssh/config, etc. etc.) it's safer to user the
        # discovered fact.
        path: kv/hosts/{{ inventory_hostname }}/users/{{ ansible_facts.user_id }}
      register: sudo_pass
      # No password in vault is fine - will just not set it.
      failed_when: false
    - name: sudo password is set for host, if found in the vault
      ansible.builtin.set_fact:
        ansible_become_password: '{{ sudo_pass.data.data.password }}'
      when: "'data' in sudo_pass"
    - become: true
      ansible.builtin.command: whoami
      register: whoami_output
    - ansible.builtin.debug: var=whoami_output
...

To use, I just copied the relevant (first two) tasks into my main site.yaml file - I added it to my existing play (which already uses facts) that groups hosts by domain:

- hosts: all:!dummy
  tags: always  # Always add extra groups and lookup sudo password
  tasks:
    - name: Group hosts by domain (mainly for environment detection)
      ansible.builtin.group_by:
        key: domain_{{ ansible_facts.domain | replace('.', '_') }}
    - name: Ansible sudo password is retrieved from vault, if known
      delegate_to: localhost
      community.hashi_vault.vault_read:
        # So many things can determine the remote username (
        # ansible_user variable, SSH_DEFAULT_USER environment
        # variable, .ssh/config, etc. etc.) it's safer to user the
        # discovered fact.
        path: kv/hosts/{{ inventory_hostname }}/users/{{ ansible_facts.user_id }}
      register: sudo_pass
      # No password in vault is fine - will just not set it.
      failed_when: false
    - name: sudo password is set for host, if found in the vault
      ansible.builtin.set_fact:
        ansible_become_password: '{{ sudo_pass.data.data.password }}'
      when: "'data' in sudo_pass"

I also added it to my bootstrap, reinstall and install updates playbooks. The tasks are identical except in boostrap.yaml, which is hardcoded to the ansible user as the play it is in is hardcoded to use root and su due to the user still being setup. New passwords are set on remote host and Ansible user can sudo are existing tasks in bootstrap.yaml:

- name: New passwords are set on remote host
  become: true
  ansible.builtin.user:
    name: "{{ item }}"
    password: "{{ lookup('community.hashi_vault.vault_read', 'kv/hosts/' + inventory_hostname + '/users/' + item).data.password | ansible.builtin.password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}"
  loop:
    - root
    - ansible
- name: New ansible user sudo password is retrieved from vault
  delegate_to: localhost
  community.hashi_vault.vault_read:
    path: kv/hosts/{{ inventory_hostname }}/users/ansible
  register: sudo_pass
- name: New sudo password is set for host
  ansible.builtin.set_fact:
    ansible_become_password: '{{ sudo_pass.data.data.password }}'
- name: Ansible user can sudo
  become: true
  ansible.builtin.user:
    name: ansible
    append: true
    groups:
      - sudo