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 }}"