Skip to main content
RH294

Managing Users in Ansible

By August 31, 2019September 12th, 2022No Comments

The user module in Ansible is the topic of this blog where we dive into managing Users in Ansible 2.8 running on our Red Hat Enterprise Linux 8 controller node. Rather than just running through tutorials covering elements of Ansible we are trying to build up a  picture of what you can use Ansible for. You will learn Ansible in a practical way that you can implement easily. When managing users in Ansible we can use the ansible-vault to encrypt sensitive data and task loops to create multiple users. We can also look at migrating variables to the directory ./group_vars. Are you read?, great then let’s begin managing users in Ansible.


Using Ad-Hoc Command to Manage Users in Ansible

Like all modules we can create users directly with the Ansible command. This may work for us when we do not need to set to much data and perhaps we are managing a single user in ansible.

$ ansible localhost -b -K -m user -a "user=bob state=present"
BECOME password: 
localhost | CHANGED => {
    "changed": true,
    "comment": "",
    "create_home": true,
    "group": 1004,
    "home": "/home/bob",
    "name": "bob",
    "shell": "/bin/bash",
    "state": "present",
    "system": false,
    "uid": 1004
}

 

This command is run as a standard user on Red Hat Enterprise Linux 8 who is a member of the wheel group. The wheel group can run any command as root but a password is required. The option -b allows us to escalate privileges, the -K allows for the sudo password prompt. The option -m specifies the Ansible module we want to make use of and arguments are supplied with the option -a. The arguments are space separated, we can use name or user to specify the user we are managing and the state shows that we want the account to exist on the system.  If we want more help on the user module in Ansible we can use ansible-docs. Searching for EXAMPLES is always a good idea.

$ ansible-doc user
...
- name: Add the user 'james' with a bash shell, appending the group 'admins' and 'developers' to ...
  user:
    name: james
    shell: /bin/bash
    groups: admins,developers
    append: yes

- name: Remove the user 'johnd'
  user:
    name: johnd
    state: absent
    remove: yes
...

The above is just a sample of the output and I would highly recommend reviewing the documentation. To remove the user we can use the same ansible command adjusting the arguments that we supply to the module. Remove=true is used to delete the user’s home directory and mail spool files if they exist, as with the userdel -r command.

$ ansible localhost -b -K -m user -a "user=bob remove=true state=absent"
BECOME password: 
localhost | CHANGED => {
    "changed": true,
    "force": false,
    "name": "bob",
    "remove": true,
    "state": "absent"
}

Using Playbooks when Managing Users in Ansible

In the long-term we will want to create Playbooks to manage or users. In this way the configuration is stored in files and document the desired state of the system. It is common that we create a directory to host the Playback and the associated configuration. Assuming we are in our HOME directories we create a directory for the Playbook and the for variables we will use:

$ cd ; mkdir -p users/group_vars
$ cd users

Ansible Configuration

To reduce the need of command line switches and adding to our documentation we can add the ansible.cfg file to the users directory. The configuration is search for from the current directory first.

$ vim ansible.cfg
[defaults]
inventory = ./inventory
remote_user = devops
[privilege_escalation]
become = true
become_method = sudo
become_user = root

We look for the host list, (inventory), in the current directory and the file named inventory. We connect via SSH to the remote system, managed nodes, as the user devops. I have already created that used who has sudo rights without the need of entering a password. Our current user account must be able to connect as the devops user on the managed nodes with SSH public key authentication. The public key from my account has already been added to the authorized_keys files of the devops user on those nodes. This all can be done using Ansible but running as the root account on the Managed nodes.

We can confirm the Ansible configuration detected  using the command ansible-config:

$ ansible-config view
[defaults]
inventory = ./inventory
remote_user = devops
[privilege_escalation]
become = true
become_method = sudo
become_user = root

Ansible Inventory

The inventory file, as specified by the previous configuration, should be the file inventory within the current directory. I have three hosts, two of which are RHEL 8 and 1 is Ubuntu 1804. For my setup the inventory is as follows:

$ vim inventory
[redhat]
192.168.122.[4:5]

[ubuntu]
192.168.122.6

The INI file format allows you to create groups with section headers in square brackets. We can confirm the inventory if found and read correctly using the ansible command.

$ ansible redhat --list-hosts
  hosts (2):
    192.168.122.4
    192.168.122.5
$ ansible ubuntu --list-hosts
  hosts (1):
    192.168.122.6

Variables

Although we could add variables to the inventory we have not in this case. We are going to use files based on the group names we are using. This will allow us to encrypt the files later as they will contain default passwords for new users which we will want to protect. We have already created the directory group_vars within the Playbook directory, users. Within that file we will create two files, redhat and ubuntu. Matching the group names in the inventory.

$ vim group_vars/redhat
admin_group: wheel
default_user_password: Password1
$ vim group_vars/ubuntu
admin_group: sudo
default_user_password: Password1

The admin_group variable allows us to use a single task to create administrators on either Red Hat or Ubuntu systems. This file will be encrypted using ansible-vault to protect the clear text password that we have added.

 

Simple Playbook

To look at migrating the ad-hoc commands to something a little more documented we will create the YAML file, but to begin with we will create a single user before extending it to create more once we are happy:

---                               
- name: Create New Users          
  hosts: all                                   
  tasks:                          
    - name: Create Users Task  
      user:                       
        name: bob                 
        state: present            
        password: "{{ default_user_password | password_hash('sha512', 'A512') }}"
        shell: /bin/bash          
...

 

We have added a single task, Create Users Task to a single Play, Create New Users. The password is added from the variable and hashed to SHA512. Even though we have just a single task, we will see that each Play will run another task by default to gather facts, or additional variables from the systems. Let’s run the Playbook. We run it from the Playbook directory, $HOME/users.

$ cd $HOME/users ; ansible-playbook  create-users.yml 
PLAY [Create New Users] ****************************************************************************

TASK [Gathering Facts] *****************************************************************************
ok: [192.168.122.5]
ok: [192.168.122.6]
ok: [192.168.122.4]

TASK [Create Users Task] ***************************************************************************
changed: [192.168.122.6]
changed: [192.168.122.5]
changed: [192.168.122.4]

PLAY RECAP *****************************************************************************************
192.168.122.4 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 
192.168.122.5 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 
192.168.122.6 : ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

The Play shows that two task run, the first task is Gathering Facts, using the module setup. This is not required in this Play as we do not use those variables or facts from the system hostnames, IP Addresses, disks, memory and so on. Disabling the built-in task will speed to Play and reduce CPU utilisation. We will edit the Playbook to correct this.

---                               
- name: Create New Users          
  hosts: all                      
  gather_facts: false             
  tasks:                          
    - name: Create Users Task  
      user:                       
        name: bob                 
        state: present            
        password: "{{ default_user_password | password_hash('sha512', 'A512') }}"
        shell: /bin/bash          
...

We will edit the Playbook adding to the Play the line gather_facts: false

$ ansible-playbook create-users.yml 

PLAY [Create New Users] ****************************************************************************************************************

TASK [Create Users Task] ***************************************************************************************************************
ok: [192.168.122.6]
ok: [192.168.122.4]
ok: [192.168.122.5]

PLAY RECAP *****************************************************************************************************************************
192.168.122.4              : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.122.5              : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.122.6              : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Only the single task that we created ran in this instance as we have disabled the collection of the facts which we do not use. If we do need it in the Play we can easily reverse the setting. As the user bob was already on each system in the desired state there was no need to change the account on any of the systems.

Using Ansible_Vault

When managing users in Ansible we can protect the Password with by storing the hashed password in the variable file or by encrypting the variable file. Storing the hash is OK but potentially does allow someone with access to the file to brute-force the password with tools such as Jack the Ripper. It also makes it more difficult to change the default password as we have to generate the hash. We avoid these issues by encrypting the variable files with ansible-vault.

$ ansible-vault encrypt group_vars/redhat
New Vault password:
Confirm New Vault password:
Encryption successful 
$ ansible-vault encrypt group_vars/ubuntu
New Vault password:
Confirm New Vault password:
Encryption successful

The files are now encrypted and we need to enter the password to access them. If we simply cat the file we will see something similar to this:

$ cat group_vars/redhat 
$ANSIBLE_VAULT;1.1;AES256
63316339663366333563633936333934313361336465656430386135613664643735643564613936
6233626130346233663938646130623366303165653735360a616261353430653339346438646430
37343030666337363162626630626439373936616533356334323031306630373164326264333337
3432616462313862610a323734343466303831383137343238313132623730633661633162386662
38616136326139313461343231376461633139663534343161396561636161396132336139656636
62393464396330303338383836313230643363393937363566343939663164306137363563363935
626563326335363465656538663434343765

We can use the sub-command view or edit to access the files, entering the password to open the file:

$ ansible-vault view group_vars/redhat
Vault password: 
admin_group: wheel
default_user_password: Password1

We do not need to change the Playbook but we need to specify the password when we execute the Playbook:

$ ansible-playbook --vault-id @prompt create-users.yml 
Vault password (default): 
PLAY [Create New Users] ****************************************************************************************************************

TASK [Create Users Task] ***************************************************************************************************************
ok: [192.168.122.6]
ok: [192.168.122.4]
ok: [192.168.122.5]

PLAY RECAP *****************************************************************************************************************************
192.168.122.4              : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.122.5              : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.122.6              : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

As we can see the Playbook executes in the same way and as we have the user created nothing is required to be changed. If we edit the task ensuring the user is an administrator, using the variable admin_group, changes will need to be made.

$ vim create-users.yml
---                               
- name: Create New Users          
  hosts: all                      
  gather_facts: false             
  tasks:                          
    - name: Create Users Task 
      user:                       
        name: bob                 
        state: present            
        password: "{{ default_user_password | password_hash('sha512', 'A512') }}"
        shell: /bin/bash          
        groups: "{{ admin_group }}"                                                                                                     
        append: true              
...

When we execute the Playbook now, we will see that the user is changed on each system, adding the user to the correct admin group on each managed nodes. This is where managing users in Ansible start to become very powerful.

$ ansible-playbook --vault-id @prompt create-users.yml 
Vault password (default): 
PLAY [Create New Users] ****************************************************************************************************************

TASK [Create Users Task] ***************************************************************************************************************
changed: [192.168.122.6]
changed: [192.168.122.4]
changed: [192.168.122.5]

PLAY RECAP *****************************************************************************************************************************
192.168.122.4              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.122.5              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.122.6              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Using Loops in Tasks

When managing users in Ansible we could have a task to create each user we need or, more efficiently, we can use loops. A loop is created in the task so at the same level as the module used in the task.

$ vim create-users.yml 
---
- name: Create New Users
  hosts: all
  gather_facts: false
  tasks:
   - name: Create Users Task
      user:
        name: "{{ item }}"
        state: present
        password: "{{ default_user_password | password_hash('sha512', 'A512') }}"
        shell: /bin/bash
        groups: "{{ admin_group }}"
        append: true
      loop:
        - bob
        - wendy
        - lofty
        - dizzy
...                        

The list of users to provision are crated as a YAML list below the key loop: . The user name now become “{{ item }}”, instructing Ansible to iterate through the loop. Again, we can execute this:

$ ansible-playbook --vault-id @prompt create-users.yml 
Vault password (default): 
PLAY [Create New Users] ****************************************************************************************************************

TASK [Create Users Task] ***************************************************************************************************************
ok: [192.168.122.6] => (item=bob)
ok: [192.168.122.5] => (item=bob)
ok: [192.168.122.4] => (item=bob)
changed: [192.168.122.6] => (item=wendy)
changed: [192.168.122.6] => (item=lofty)
changed: [192.168.122.5] => (item=wendy)
changed: [192.168.122.4] => (item=wendy)
changed: [192.168.122.6] => (item=dizzy)
changed: [192.168.122.5] => (item=lofty)
changed: [192.168.122.4] => (item=lofty)
changed: [192.168.122.5] => (item=dizzy)
changed: [192.168.122.4] => (item=dizzy)

PLAY RECAP *****************************************************************************************************************************

192.168.122.4              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.122.5              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.122.6              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

Very quick, managing users in Ansible, we have been able to provision the additional 3 users on each system without the need of touching the existing bob account.

Deleting Accounts

When we have finished with these test accounts on our system we can remove the accounts and associated home directories. We create the new Playbook, delete_users.xml

$ vim delete_users.yml
---                      
- name: Clean User Accounts
  hosts: all             
  gather_facts: false                                                                                                                  
  become: true           
  tasks:                                          
    - name: Delete Users
      user:              
        name: "{{ item }}"
        state: absent
        remove: true
      loop:              
        - bob            
        - wendy          
        - lofty          
        - dizzy          
...    

We can execute this across our systems:

$ ansible-playbook --vault-id @prompt delete_users.yml 
Vault password (default): 
PLAY [Clean User Accounts] *************************************************************************************************************

TASK [Delete Users] ********************************************************************************************************************
changed: [192.168.122.6] => (item=bob)
changed: [192.168.122.6] => (item=wendy)
changed: [192.168.122.5] => (item=bob)
changed: [192.168.122.4] => (item=bob)
changed: [192.168.122.6] => (item=lofty)
changed: [192.168.122.6] => (item=dizzy)
changed: [192.168.122.5] => (item=wendy)
changed: [192.168.122.4] => (item=wendy)
changed: [192.168.122.4] => (item=lofty)
changed: [192.168.122.5] => (item=lofty)
changed: [192.168.122.4] => (item=dizzy)
changed: [192.168.122.5] => (item=dizzy)

PLAY RECAP *****************************************************************************************************************************
192.168.122.4              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.122.5              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
192.168.122.6              : ok=1    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

 

The video follows: