Introduction

Managing user accounts is one of the most fundamental tasks in Linux system administration. The Ansible ansible.builtin.user module provides a declarative, idempotent way to create, modify, and remove user accounts across your entire infrastructure. This article covers everything from basic user creation to advanced scenarios including password hashing, SSH key generation, group management, and account expiration.

Module Overview

The ansible.builtin.user module is a builtin module shipped with Ansible core. It manages user accounts on Linux, macOS, FreeBSD, and SunOS systems.

Under the hood it uses:

  • Linux: useradd, usermod, userdel
  • FreeBSD: pw useradd
  • macOS: dscl create

For Windows targets, use ansible.windows.win_user instead.

Parameters Reference

ParameterTypeDescription
namestring (required)Username to create/manage
statestringpresent (default) or absent
passwordstringHashed password (use password_hash filter)
uidintegerNumeric user ID
groupstringPrimary group
groupslistAdditional groups
appendbooleanAppend to groups (true) or replace (false, default)
shellstringLogin shell (e.g., /bin/bash)
homestringHome directory path
create_homebooleanCreate home directory (true default)
commentstringGECOS field / user description
systembooleanCreate a system account
expiresfloatAccount expiry (epoch time, -1 to remove)
password_expire_minintegerMinimum days between password changes
password_expire_maxintegerMaximum days before password must change
generate_ssh_keybooleanGenerate SSH keypair
ssh_key_bitsintegerSSH key size (default: 4096)
ssh_key_filestringSSH key file path
ssh_key_typestringSSH key type (rsa, ed25519, etc.)
ssh_key_passphrasestringPassphrase for SSH key
removebooleanRemove home dir when state: absent
forcebooleanForce removal even if user is logged in
move_homebooleanMove home dir when home changes
skeletonstringSkeleton directory for home
password_lockbooleanLock the password

Basic User Creation

The simplest example — create a user with default settings:

---
- name: Create basic user
  hosts: all
  become: true
  tasks:
    - name: Create user 'deploy'
      ansible.builtin.user:
        name: deploy
        state: present

This creates the user with a home directory at /home/deploy, the default shell, and no password.

Complete User Creation Playbook

A production-ready example with password, groups, SSH key, and shell:

---
- name: User management Playbook
  hosts: all
  become: true
  tasks:
    - name: Create user with full configuration
      ansible.builtin.user:
        name: example
        password: "{{ 'MySecureP@ss!' | password_hash('sha512', 'mysecretsalt') }}"
        groups:
          - wheel
          - adm
        append: true
        state: present
        shell: /bin/bash
        system: false
        create_home: true
        home: /home/example
        comment: "Example application user"
        generate_ssh_key: true
        ssh_key_type: ed25519
        ssh_key_bits: 4096
        ssh_key_comment: "example@{{ inventory_hostname }}"
      register: user_result

    - name: Display SSH public key
      ansible.builtin.debug:
        msg: "{{ user_result.ssh_public_key }}"
      when: user_result.ssh_public_key is defined

Password Hashing

Never store plaintext passwords in playbooks. Ansible requires pre-hashed passwords:

Method 1: Inline with password_hash Filter

password: "{{ 'plaintext_password' | password_hash('sha512') }}"

Method 2: With a Salt for Idempotency

Without a fixed salt, the hash changes on every run, causing unnecessary changed status:

password: "{{ 'plaintext_password' | password_hash('sha512', 'fixed_salt_value') }}"

Method 3: Using Ansible Vault

Store the password in an encrypted vault file:

# group_vars/all/vault.yml (encrypted)
vault_user_password: "MySecureP@ss!"
# playbook
- name: Create user with vaulted password
  ansible.builtin.user:
    name: deploy
    password: "{{ vault_user_password | password_hash('sha512', 'salt') }}"

Method 4: Pre-generate the Hash

# Generate hash on command line
python3 -c "import crypt; print(crypt.crypt('MyPassword', crypt.mksalt(crypt.METHOD_SHA512)))"

# Or use mkpasswd
mkpasswd --method=sha-512

Group Management

Primary vs Additional Groups

Pay close attention to group (singular) vs groups (plural):

- name: Set primary and additional groups
  ansible.builtin.user:
    name: deploy
    group: developers       # Primary group (singular)
    groups:                  # Additional groups (plural)
      - docker
      - sudo
    append: true             # Add to these groups, don't replace existing

Warning: Without append: true, Ansible removes the user from all groups not listed in groups.

Ensure User Is in Specific Group Only

- name: Reset user groups
  ansible.builtin.user:
    name: deploy
    groups:
      - developers
    append: false  # Remove from all other secondary groups

SSH Key Generation

Automatically generate SSH keypairs for service accounts:

- name: Create service account with SSH key
  ansible.builtin.user:
    name: ansible_svc
    generate_ssh_key: true
    ssh_key_type: ed25519
    ssh_key_comment: "ansible-service-account"
    ssh_key_file: .ssh/id_ed25519
  register: svc_user

- name: Save public key for distribution
  ansible.builtin.copy:
    content: "{{ svc_user.ssh_public_key }}"
    dest: /tmp/ansible_svc_pub_key
  delegate_to: localhost

Account Expiration

Set Account Expiry Date

- name: Create temporary contractor account
  ansible.builtin.user:
    name: contractor01
    expires: "{{ ('2026-12-31' | to_datetime).strftime('%s') }}"
    comment: "Contractor - expires Dec 2026"

Set Password Expiry Policy

- name: Create user with password policy
  ansible.builtin.user:
    name: secure_user
    password: "{{ password | password_hash('sha512', 'salt') }}"
    password_expire_min: 7    # Min 7 days between changes
    password_expire_max: 90   # Must change every 90 days

Remove Account Expiry

- name: Remove account expiry
  ansible.builtin.user:
    name: contractor01
    expires: -1

System Accounts

Create system accounts for services (low UID, no login):

- name: Create application service account
  ansible.builtin.user:
    name: myapp
    system: true
    shell: /usr/sbin/nologin
    home: /opt/myapp
    create_home: true
    comment: "MyApp service account"

Removing Users

- name: Remove user and home directory
  ansible.builtin.user:
    name: old_user
    state: absent
    remove: true    # Also remove home directory
    force: true     # Remove even if user is logged in

Bulk User Management

Create multiple users from a variable list:

---
- name: Manage multiple users
  hosts: all
  become: true
  vars:
    users:
      - name: alice
        groups: [developers, docker]
        shell: /bin/bash
      - name: bob
        groups: [developers]
        shell: /bin/zsh
      - name: charlie
        groups: [ops, docker, sudo]
        shell: /bin/bash
  tasks:
    - name: Create all users
      ansible.builtin.user:
        name: "{{ item.name }}"
        groups: "{{ item.groups }}"
        shell: "{{ item.shell }}"
        append: true
        state: present
        create_home: true
      loop: "{{ users }}"

    - name: Set authorized keys
      ansible.posix.authorized_key:
        user: "{{ item.name }}"
        key: "{{ lookup('file', 'keys/' + item.name + '.pub') }}"
        state: present
      loop: "{{ users }}"
      when: lookup('file', 'keys/' + item.name + '.pub', errors='ignore')

Return Values

The user module returns useful information:

- name: Create user and capture result
  ansible.builtin.user:
    name: deploy
    generate_ssh_key: true
  register: result

- name: Show return values
  ansible.builtin.debug:
    var: result

Key return values:

  • result.name — Username
  • result.uid — Numeric UID
  • result.group — Primary GID
  • result.home — Home directory path
  • result.shell — Login shell
  • result.ssh_key_file — Path to private key
  • result.ssh_public_key — Public key content
  • result.ssh_fingerprint — Key fingerprint

Common Mistakes

1. Forgetting append: true

# ❌ This REMOVES the user from all groups except 'docker'
groups: [docker]

# ✅ This ADDS 'docker' to existing groups
groups: [docker]
append: true

2. Plaintext Passwords

# ❌ Will NOT work - Ansible expects a hash
password: "MyPassword123"

# ✅ Correct - use password_hash
password: "{{ 'MyPassword123' | password_hash('sha512', 'salt') }}"

3. Missing become

# ❌ User creation requires root privileges
- hosts: all
  tasks:
    - ansible.builtin.user:
        name: newuser

# ✅ Use become: true
- hosts: all
  become: true
  tasks:
    - ansible.builtin.user:
        name: newuser

Conclusion

The ansible.builtin.user module is essential for managing user accounts at scale. Key takeaways: always hash passwords with password_hash, use append: true when adding groups, generate SSH keys for service accounts, and leverage Ansible Vault for sensitive credentials. With proper automation, user management becomes consistent, auditable, and repeatable across your entire infrastructure.