When managing Linux users with Ansible, a common pitfall is assuming that removing a user from your variables file will automatically remove it from target systems. The standard user
module only creates or modifies users - it doesn't handle deletions when users are removed from your configuration.
Here's a robust approach that combines multiple techniques to maintain an exact user list:
- name: Get current system users in adm group
shell: "getent group {{ common_adm_group }} | cut -d: -f4 | tr ',' '\\n'"
register: current_users
changed_when: false
- name: Calculate users to be removed
set_fact:
users_to_remove: "{{ current_users.stdout_lines | difference(common_adm_users | map(attribute='name')) }}"
- name: Remove obsolete adm users
user:
name: "{{ item }}"
state: absent
remove: yes
loop: "{{ users_to_remove }}"
when: users_to_remove | length > 0
- name: Create/update adm users
user:
name: "{{ item.name }}"
group: "{{ common_adm_group }}"
createhome: yes
password: "!!"
update_password: always
state: present
loop: "{{ common_adm_users }}"
For a complete solution, we should also manage SSH keys and other user attributes:
- name: Ensure authorized_keys for adm users
authorized_key:
user: "{{ item.name }}"
key: "{{ item.ssh_key }}"
state: present
exclusive: yes
loop: "{{ common_adm_users }}"
when: item.ssh_key is defined
For enterprise environments, consider these enhancements:
# vars/main.yml
required_users:
- name: user1
uid: 2001
groups: [adm, docker]
ssh_key: "ssh-rsa AAAAB3Nza..."
shell: /bin/bash
- name: user2
uid: 2002
groups: [adm]
shell: /usr/sbin/nologin
# tasks/main.yml
- name: Ensure exact user list
block:
- name: Get current users in specified groups
shell: |
for group in adm docker; do
getent group $group || continue
getent group $group | cut -d: -f4 | tr ',' '\\n'
done | sort -u
register: current_users_all
changed_when: false
- name: Remove obsolete users
user:
name: "{{ item }}"
state: absent
remove: yes
loop: "{{ current_users_all.stdout_lines | difference(required_users | map(attribute='name')) }}"
when: current_users_all.stdout_lines | length > 0
- name: Create/update required users
user:
name: "{{ item.name }}"
uid: "{{ item.uid }}"
groups: "{{ item.groups | default(omit) }}"
shell: "{{ item.shell | default('/bin/bash') }}"
state: present
system: no
createhome: yes
loop: "{{ required_users }}"
The community.general.users
module provides a more declarative approach:
- name: Manage users declaratively
community.general.users:
name: "{{ item.name }}"
state: present
groups: "{{ item.groups | default(['users']) }}"
append: no
shell: "{{ item.shell | default('/bin/bash') }}"
loop: "{{ required_users }}"
- name: Purge unlisted users
community.general.users:
name: "{{ item }}"
state: absent
purge: yes
loop: "{{ current_users.stdout_lines | difference(required_users | map(attribute='name')) }}"
when: current_users.stdout_lines | difference(required_users | map(attribute='name')) | length > 0
Remember to test these playbooks with --check
mode before applying to production systems, as user deletion is a destructive operation.
When managing Linux users with Ansible, a common pitfall occurs when trying to maintain an exact set of authorized users across servers. The standard user
module creates users but doesn't handle removal when users are deleted from your configuration.
# Typical user creation task
- name: Create admin users
user:
name: "{{ item.name }}"
group: "admins"
state: present
loop: "{{ authorized_users }}"
To enforce exact user lists, we need to combine creation with removal logic. Here's a robust approach:
- name: Get current users in admin group
shell: "getent group admins | cut -d: -f4 | tr ',' '\\n'"
register: current_admin_users
changed_when: false
- name: Calculate users to remove
set_fact:
users_to_remove: "{{ current_admin_users.stdout_lines | difference(authorized_users | map(attribute='name')) }}"
- name: Remove unauthorized admin users
user:
name: "{{ item }}"
state: absent
remove: yes
loop: "{{ users_to_remove }}"
when: users_to_remove | length > 0
- name: Create/update authorized users
user:
name: "{{ item.name }}"
group: "admins"
shell: "/bin/bash"
ssh_key: "{{ item.ssh_key | default(omit) }}"
state: present
loop: "{{ authorized_users }}"
For production environments, consider these enhancements:
- name: Protect critical system users
set_fact:
users_to_remove: "{{ users_to_remove | difference(['root', 'backup']) }}"
- name: Clean up residual files
file:
path: "/home/{{ item }}"
state: absent
loop: "{{ users_to_remove }}"
when: users_to_remove | length > 0
Here's a full implementation with error handling:
- name: User management pre-checks
assert:
that:
- "'root' not in authorized_users | map(attribute='name')"
- authorized_users is defined
fail_msg: "Invalid user configuration detected"
- name: Synchronize admin users
block:
- name: Get current admins (cross-platform)
ansible.builtin.shell: |
if command -v getent >/dev/null; then
getent group admins | cut -d: -f4 | tr ',' '\\n'
elif command -v dscl >/dev/null; then # macOS
dscl . -read /Groups/admins GroupMembership | cut -d' ' -f2-
fi
register: current_admins
changed_when: false
ignore_errors: true
- name: Normalize current admin list
set_fact:
normalized_current_admins: "{{ (current_admins.stdout_lines | default([])) | flatten | map('trim') | select('match', '^[a-zA-Z]') | list }}"
- name: Calculate delta
set_fact:
users_to_remove: "{{ normalized_current_admins | difference(authorized_users | map(attribute='name') | list) }}"
users_to_add: "{{ authorized_users | map(attribute='name') | difference(normalized_current_admins) }}"
- name: Remove obsolete users
user:
name: "{{ item }}"
state: absent
remove: yes
loop: "{{ users_to_remove }}"
when: users_to_remove | length > 0
tags: [user-removal]
- name: Deploy authorized users
user:
name: "{{ item.name }}"
uid: "{{ item.uid | default(omit) }}"
group: "{{ item.group | default('admins') }}"
groups: "{{ item.groups | default(omit) }}"
append: "{{ item.append | default(true) }}"
shell: "{{ item.shell | default('/bin/bash') }}"
ssh_key: "{{ item.ssh_key | default(omit) }}"
state: present
loop: "{{ authorized_users }}"
tags: [user-deployment]
Always verify with check mode first:
ansible-playbook site.yml --limit=production --tags=user-management --check