Introduction
Changing user passwords across multiple Linux servers is a common administrative task that Ansible automates securely. The ansible.builtin.user module requires passwords in hashed form (not plaintext), which Ansible's password_hash filter handles automatically. This article covers basic password changes, Vault-encrypted passwords, bulk updates, password policies, and idempotent management.
Basic Password Change
---
- name: Change user password
hosts: all
become: true
vars:
myuser: "example"
mypassword: "SecureP@ss2026!"
tasks:
- name: Update password
ansible.builtin.user:
name: "{{ myuser }}"
state: present
password: "{{ mypassword | password_hash('sha512') }}"
Execution
$ ansible-playbook -i inventory change_password.yml
PLAY [Change user password] *******************
TASK [Update password] ************************
changed: [demo.example.com]
PLAY RECAP ************************************
demo.example.com : ok=2 changed=1 unreachable=0 failed=0
Verify
$ sshpass -p 'SecureP@ss2026!' ssh example@demo.example.com whoami
example
Understanding password_hash
Linux stores passwords as hashes in /etc/shadow. Ansible requires pre-hashed passwords — the password_hash filter generates them:
# SHA-512 (recommended — default on most Linux distros)
password: "{{ 'mypassword' | password_hash('sha512') }}"
# SHA-256
password: "{{ 'mypassword' | password_hash('sha256') }}"
# With explicit salt (for idempotency)
password: "{{ 'mypassword' | password_hash('sha512', 'mysalt') }}"
# With rounds parameter
password: "{{ 'mypassword' | password_hash('sha512', 65534 | random(seed=inventory_hostname) | string) }}"
The Idempotency Problem
Without a fixed salt, password_hash generates a random salt each run, causing changed every time:
# ❌ Changes every run (random salt)
password: "{{ mypassword | password_hash('sha512') }}"
# ✅ Idempotent (fixed salt per host)
password: "{{ mypassword | password_hash('sha512', inventory_hostname_short) }}"
Or use update_password: on_create to only set the password when creating the user:
- name: Set password only on creation
ansible.builtin.user:
name: deploy
password: "{{ mypassword | password_hash('sha512') }}"
update_password: on_create
Secure Passwords with Ansible Vault
Never store plaintext passwords in playbooks. Use Ansible Vault:
Create Encrypted Variable
# Encrypt a password string
ansible-vault encrypt_string 'SecureP@ss2026!' --name 'user_password'
# Output:
user_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
3832653363...
Use in Playbook
---
- name: Change password (vault-encrypted)
hosts: all
become: true
vars:
user_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
3832653363616437...
tasks:
- name: Update password
ansible.builtin.user:
name: deploy
password: "{{ user_password | password_hash('sha512', inventory_hostname_short) }}"
no_log: true
Use Vault File
# vars/passwords.yml (encrypted with ansible-vault encrypt)
deploy_password: "SecureP@ss2026!"
admin_password: "Adm1n#P@ss!"
---
- name: Change passwords from vault
hosts: all
become: true
vars_files:
- vars/passwords.yml
tasks:
- name: Update deploy user
ansible.builtin.user:
name: deploy
password: "{{ deploy_password | password_hash('sha512', inventory_hostname_short) }}"
no_log: true
# Run with vault password
ansible-playbook site.yml --ask-vault-pass
Bulk Password Changes
Multiple Users from a List
---
- name: Update passwords for multiple users
hosts: all
become: true
vars:
users:
- name: deploy
password: "{{ vault_deploy_pass }}"
- name: admin
password: "{{ vault_admin_pass }}"
- name: monitoring
password: "{{ vault_monitoring_pass }}"
tasks:
- name: Update all user passwords
ansible.builtin.user:
name: "{{ item.name }}"
password: "{{ item.password | password_hash('sha512', inventory_hostname_short) }}"
loop: "{{ users }}"
no_log: true
Random Password Generation
---
- name: Generate and set random passwords
hosts: all
become: true
tasks:
- name: Generate random password
ansible.builtin.set_fact:
new_password: "{{ lookup('password', '/dev/null length=20 chars=ascii_letters,digits,punctuation') }}"
no_log: true
- name: Set password
ansible.builtin.user:
name: deploy
password: "{{ new_password | password_hash('sha512') }}"
no_log: true
- name: Save password locally
ansible.builtin.copy:
content: "{{ inventory_hostname }}: {{ new_password }}"
dest: "/tmp/passwords/{{ inventory_hostname }}.txt"
mode: "0600"
delegate_to: localhost
no_log: true
Password Expiry and Policies
Force Password Change on Next Login
- name: Create user with expired password
ansible.builtin.user:
name: newuser
password: "{{ temp_password | password_hash('sha512') }}"
password_expire_max: 90
password_expire_min: 1
- name: Force password change on next login
ansible.builtin.command: chage -d 0 newuser
changed_when: true
Set Password Expiry
- name: Set password to expire in 90 days
ansible.builtin.user:
name: deploy
password_expire_max: 90
password_expire_min: 7
# Or use chage for more control
- name: Configure password aging
ansible.builtin.command: >
chage --maxdays 90 --mindays 7 --warndays 14 deploy
changed_when: true
Lock and Unlock Accounts
# Lock account (disable login)
- name: Lock user account
ansible.builtin.user:
name: old_employee
password_lock: true
# Unlock account
- name: Unlock user account
ansible.builtin.user:
name: returning_employee
password_lock: false
Platform Differences
| Platform | Hash Format | Notes |
|---|---|---|
| Linux (RHEL, Ubuntu, SUSE) | SHA-512 via password_hash('sha512') | Standard |
| macOS | Cleartext accepted | Uses dscl internally |
| FreeBSD | SHA-512 | Same as Linux |
| Windows | Use ansible.windows.win_user | Accepts cleartext |
Windows Password Change
- name: Change Windows user password
ansible.windows.win_user:
name: Administrator
password: "{{ vault_win_password }}"
state: present
Common Mistakes
1. Plaintext Password (No Hash)
# ❌ WRONG — stores literal string in /etc/shadow
password: "mypassword"
# ✅ RIGHT — hashes the password
password: "{{ 'mypassword' | password_hash('sha512') }}"
2. Missing no_log
# ❌ Password visible in ansible output
- ansible.builtin.user:
name: deploy
password: "{{ pass | password_hash('sha512') }}"
# ✅ Hidden from output
- ansible.builtin.user:
name: deploy
password: "{{ pass | password_hash('sha512') }}"
no_log: true
3. Missing become
# ❌ Fails — can't modify /etc/shadow without root
- ansible.builtin.user:
name: deploy
password: "{{ pass | password_hash('sha512') }}"
# ✅ Needs privilege escalation
- ansible.builtin.user:
name: deploy
password: "{{ pass | password_hash('sha512') }}"
become: true
Complete Production Playbook
---
- name: Rotate service account passwords
hosts: all
become: true
vars_files:
- vars/passwords.yml # vault-encrypted
vars:
service_accounts:
- name: deploy
password: "{{ vault_deploy_pass }}"
groups: ['sudo']
shell: /bin/bash
- name: monitoring
password: "{{ vault_monitoring_pass }}"
groups: ['monitoring']
shell: /usr/sbin/nologin
tasks:
- name: Update service account passwords
ansible.builtin.user:
name: "{{ item.name }}"
password: "{{ item.password | password_hash('sha512', inventory_hostname_short) }}"
groups: "{{ item.groups }}"
shell: "{{ item.shell }}"
update_password: always
loop: "{{ service_accounts }}"
no_log: true
- name: Verify accounts exist
ansible.builtin.command: "id {{ item.name }}"
loop: "{{ service_accounts }}"
changed_when: false
Related Articles
- Ansible user Module Guide
- Ansible Vault Guide
- Ansible win_user Unhandled Exception
- Ansible Variables Guide
Conclusion
Change Linux user passwords with Ansible using ansible.builtin.user and the password_hash('sha512') filter. Always use Ansible Vault for password storage, no_log: true to hide passwords from output, and update_password: on_create for idempotent initial setup. For consistent behavior across runs, provide a fixed salt to password_hash based on the hostname.