How to Implement Nested Loops with Fileglob Patterns in Ansible for SSH Key Management


4 views

When working with Ansible's with_nested and fileglob lookup, you might encounter an issue where the lookup plugin doesn't evaluate as expected within nested loops. Instead of getting a list of files, you get the literal lookup string. Here's the problematic code:

- name: copy authorized keys
  authorized_key: user={{ item.0.username }} key={{ item.1 }}
  with_nested:
    - users
    - "lookup('fileglob', 'public_keys/*')"

The fundamental problem lies in how Ansible processes the nested loop structure. The fileglob lookup needs to be evaluated before being used in the nested loop context. When you include the lookup directly in the with_nested structure, it gets treated as a string rather than being executed.

The most reliable approach is to first evaluate the fileglob pattern and store the results in a variable, then use that variable in your nested loop:

- name: Find all public key files
  set_fact:
    public_key_files: "{{ lookup('fileglob', 'public_keys/*', wantlist=True) }}"

- name: Copy authorized keys to users
  authorized_key:
    user: "{{ item.0.username }}"
    key: "{{ lookup('file', item.1) }}"
  with_nested:
    - "{{ users }}"
    - "{{ public_key_files }}"

Another option is to use the with_fileglob lookup combined with with_items for the users:

- name: Copy authorized keys with fileglob
  authorized_key:
    user: "{{ user_item.username }}"
    key: "{{ lookup('file', file_item) }}"
  loop: "{{ users | product(lookup('fileglob', 'public_keys/*', wantlist=True)) | list }}"
  loop_control:
    loop_var: item
  vars:
    user_item: "{{ item[0] }}"
    file_item: "{{ item[1] }}"

When implementing this solution, consider these important aspects:

  • Directory Structure: Ensure your public key files are organized properly. A common pattern is public_keys/username/keyfile
  • Permission Management: The destination .ssh/authorized_keys file needs proper permissions (600) and ownership
  • Duplicate Keys: Consider adding a unique comment to each key to prevent duplicates

Here's a full working example with error handling:

- name: Ensure .ssh directory exists
  file:
    path: "/home/{{ item.username }}/.ssh"
    state: directory
    mode: 0700
    owner: "{{ item.username }}"
  loop: "{{ users }}"

- name: Get list of public key files
  set_fact:
    pubkey_files: "{{ lookup('fileglob', 'public_keys/*.pub', wantlist=True) }}"

- name: Deploy authorized keys
  authorized_key:
    user: "{{ user_item.username }}"
    key: "{{ lookup('file', key_file) }}"
    exclusive: no
    state: present
  loop: "{{ users | product(pubkey_files) | list }}"
  loop_control:
    loop_var: item
  vars:
    user_item: "{{ item[0] }}"
    key_file: "{{ item[1] }}"
  when: key_file is defined

When managing server configurations with Ansible, a common requirement is deploying SSH authorized keys for multiple users. The challenge arises when you need to:

  • Apply multiple public keys to multiple user accounts
  • Dynamically discover available public key files
  • Maintain a clean, maintainable playbook structure

The initial approach using with_nested and lookup('fileglob') fails because:

- name: Problematic implementation
  authorized_key: user={{ item.0.username }} key={{ item.1 }}
  with_nested:
    - users
    - lookup('fileglob', 'public_keys/*')

This results in the literal string being passed rather than evaluated file paths, because the lookup plugin isn't properly interpreted in this context.

Here's the correct way to implement this:

- name: Deploy SSH authorized keys
  authorized_key:
    user: "{{ item.0.username }}"
    key: "{{ lookup('file', item.1) }}"
  with_nested:
    - "{{ users }}"
    - "{{ lookup('fileglob', 'public_keys/*', wantlist=True) }}"
  vars:
    ansible_become: "{{ item.0.username == 'root' }}"

For better readability with many keys:

- name: Process each user
  include_tasks: process_user_keys.yml
  loop: "{{ users }}"
  vars:
    current_user: "{{ item }}"
    
# process_user_keys.yml
- name: Add each key for current user
  authorized_key:
    user: "{{ current_user.username }}"
    key: "{{ lookup('file', item) }}"
  loop: "{{ lookup('fileglob', 'public_keys/*', wantlist=True) }}"
  • Always use wantlist=True with fileglob to ensure proper looping
  • Consider key file naming conventions (e.g., username_keyname.pub)
  • Handle file permissions properly when dealing with root vs regular users

For environments with many users and keys:

- name: Cache public key files
  set_fact:
    all_public_keys: "{{ lookup('fileglob', 'public_keys/*', wantlist=True) }}"

- name: Batch process keys
  authorized_key:
    user: "{{ user_item.username }}"
    key: "{{ lookup('file', key_item) }}"
  loop: "{{ users | product(all_public_keys) | list }}"
  vars:
    user_item: "{{ item.0 }}"
    key_item: "{{ item.1 }}"