Write a play that changes the root password hash and encrypts the file it is stored in

change root password using Ansible

In this article, we will examine a play used to change the root password using Ansible. It will also encrypt the file the password is stored in–using ansible-vault on your localhost.

Table of Contents

Red Hat Ansible Automation: Objectives

The encrypted password files will be stored under the password directory and saved off to a backup using a timestamp. The timestamp will indicate the last time the play was run. Both your password directory [passwdDir] and vault key [vaultPass] are variables that need to be defined.

As you can see, lines 10 and 11 show that my password directory is a hidden folder called ~/.secret-yaml-files my vault key file is also hidden in ~/.vault_key. Both files reside in my home directory as represented by the “~/” symbol before the file.

Red Hat Ansible Automation: The Breakdown

Lets take a look at the cycle_passwd.yaml file.  We will break down each line to gain a better understanding of what the play is doing (below):

				
					--- 
- name: Cycle root password and store it using ansible-vault
  hosts: all 
  become: false 
  
  # Generate and store root password in password file (secret.yaml) 
  vars: 
    rootPass: "{{ lookup('password', '{{ passwdDir }}/secret.yaml chars=ascii_letters,digits,punctuation') }}" 
    newPass: "{{ lookup('password', '{{ passwdDir }}/secret1.yaml chars=ascii_letters,digits,punctuation') }}" 
    passwdDir: ~/.secret-yaml-files 
    vaultPass: ~/.vault_key 
    runningfromHost: localhost 
    
  tasks: 
  # Check if password file exists 
  - name: Check if secret.yaml file exists 
    stat: 
      path: "{{ passwdDir }}/secret.yaml" 
    register: stat_secrets
    
  # Encrypt password file 
  - name: Encrypt password file using ansible-vault on {{ runningfromHost }} 
    shell: ansible-vault encrypt {{ passwdDir }}/secret.yaml --vault-password-file {{ vaultPass }} 
    when: 
      - stat_secrets.stat.exists|bool == true 
    run_once: true 
    delegate_to: "{{ runningfromHost }}" 
    
  # View decrypted password file 
  - name: Allow temporary password file decryption for password hashing on {{ runningfromHost }} 
    shell: ansible-vault view {{ passwdDir }}/secret.yaml --vault-password-file {{ vaultPass }} > {{ passwdDir }}/secret1.yaml 
    when: 
      - stat_secrets.stat.exists|bool == true 
    run_once: true 
    delegate_to: "{{ runningfromHost }}" 
  
  # Update root password hash 
  - name: Update root password hash 
    user: 
      name: root 
      #update_password: always 
      password: "{{ newPass | password_hash('sha512') }}"
    become: yes 
    
  - debug: msg: "{{ newPass }}" 
  
  # Get Timestamp from the OS 
  - name: Get timestamp from the system 
    shell: "date +%Y-%m-%d-%H.%M.%S" 
    register: tstamp 
    run_once: true 
    delegate_to: "{{ runningfromHost }}" 
    
  # Backup the password file 
  - name: Save password file to backup using timestamp
    command: mv {{ passwdDir }}/secret.yaml {{ passwdDir }}/secret.yaml-{{ tstamp.stdout }} 
    when: 
      - stat_secrets.stat.exists|bool == true 
    run_once: true 
    delegate_to: "{{ runningfromHost }}" 
    
  # Clean up the password dir contents 
  - name: Clean up the contents of the password file directory 
    file: 
      state: absent 
      path: "{{ passwdDir }}/secret1.yaml" 
    run_once: true 
    delegate_to: "{{ runningfromHost }}"
				
			

Ansible Headers

When creating an Ansible playbook and writing to a yaml file, you need to start with the three (—) dashes. Ansible is very picky about syntax and spacing so make sure you start your playbook properly or your playbook will fail to run.

Next, let’s title our playbook beginning with the single (-) dash and the name: variable. In this case, our playbook will reset or cycle the root password and store it securely using ansible-vault.

The hosts: variable should always be set to all. Why? When running the ansible-playbook command, you can specify which hosts or host group in your inventory file, you want your playbook to run on.

The become: variable is where we indicate whether our playbook will require root user privileges to run successfully.  In this instance, we are setting this variable to false because the majority of the playbook tasks don’t require root user access to run. In fact, only one section of the playbook requires root privileges and that involves updating the root password hash.

Obviously, you need to become root in order to make that type of a change.

				
					--- 
- name: Cycle root password and store it using ansible-vault
  hosts: all 
  become: false 
				
			

Declaring variables in your Ansible playbook

Using the vars: variable we can declare all of the variables we need to run this play. The variables are listed (in order) as follows:

  • rootPass: This variable will store the root password as generated by the playbook or entered by the user into the secret.yaml file.
    • The secret.yaml file is where the passwords will be stored. The playbook employs the ansible-vault utility to encrypt the secret.yaml file using the provided key (see the vaultPass section).
    • The chars=ascii_letters,digits,punctuation section refers to how the password should be generated. From a security compliance standpoint, the more complexity you add to your password generation, the more secure it will be. Here’s an example of a password that meets the ascii_letters, digits, and punctuation criteria (below).
				
					$ cat ~/.secret.yaml
Tas$%134$eag!&
				
			
  • newPass: This variable will store the newly-generated root password  to replace the existing one.
  • passwdDir: This variable stores the location of the hidden directory where your password files (secret.yaml) will be stored. Although, it is not compulsory, you want this directory to be hidden because of the sensitive contents stored therein. In this case, our hidden directory is called ~/.secret-yaml-files. The (~/) is simply shorthand for home directory and the (.) indicates it is hidden.
  • vaultPass: This variable stores the key file (in plain text). You need the key file to decrypt any files encrypted using ansible-vault and this key (e.g. the secret.yaml files). Obviously, this file should also be hidden.
  • runningfromHost: This variable is self-explanatory. We want to run this playbook from our localhost. This will ensure all files are stored locally or from the host assigned to this variable. 
				
					  # Generate and store root password in password file (secret.yaml) 
  vars: 
    rootPass: "{{ lookup('password', '{{ passwdDir }}/secret.yaml chars=ascii_letters,digits,punctuation') }}" 
    newPass: "{{ lookup('password', '{{ passwdDir }}/secret1.yaml chars=ascii_letters,digits,punctuation') }}" 
    passwdDir: ~/.secret-yaml-files 
    vaultPass: ~/.vault_key 
    runningfromHost: localhost 
				
			

Ansible Playbook Tasks

Now that we’ve established where we want our playbook to run and declared the variables we need, let’s breakdown each task.

Checking for an existing password file

In this section, we want to first check and see if a preferred root password is present. This specific task checks for the presence of a secret.yaml file. Then, it registers the outcome (true or false) of that check to the variable stat_secrets. Also, if a secret.yaml file already exists or is provided by the user, the playbook will go with the password provided in that file instead of generating a random one.

				
					  tasks: 
  # Check if password file exists 
  - name: Check if secret.yaml file exists 
    stat: 
      path: "{{ passwdDir }}/secret.yaml" 
    register: stat_secrets
				
			

Encrypting and Decrypting the password file

The next set of tasks use the ansible-vault utility to encrypt and decrypt the password (secret.yaml) file. The decrypt section is done temporarily (in secret1.yaml) to allow the password to be stored in plain text for use in changing or updating the root password hash.

The password needs to be in plain text and can’t be encrypted–because it is encrypted again using the SHA-512 algorithm as part of that (root password hash update) process.

Also, the encrypt and decrypt tasks only need to run once so we set the run_once: variable to true.

The delegate_to: variable simply states where we want these tasks executed. In this case, our runningfromHost variable is set to localhost. Therefore, all of the associated encrypt and decrypt tasks will run from the localhost.

				
					  # Encrypt password file 
  - name: Encrypt password file using ansible-vault on {{ runningfromHost }} 
    shell: ansible-vault encrypt {{ passwdDir }}/secret.yaml --vault-password-file {{ vaultPass }} 
    when: 
      - stat_secrets.stat.exists|bool == true 
    run_once: true 
    delegate_to: "{{ runningfromHost }}" 
    
  # View decrypted password file 
  - name: Allow temporary password file decryption for password hashing on {{ runningfromHost }} 
    shell: ansible-vault view {{ passwdDir }}/secret.yaml --vault-password-file {{ vaultPass }} > {{ passwdDir }}/secret1.yaml 
    when: 
      - stat_secrets.stat.exists|bool == true 
    run_once: true 
    delegate_to: "{{ runningfromHost }}" 
				
			

Updating the root password hash

Now we can proceed with updating the root password hash. The root password will be generated using SHA-512 and it will be stored in the newPass variable. Also, updating the root password hash requires root user privileges. Therefore, we will assigned the true value to the become: variable.

The debug: section is used to check if everything is running as expected. In this instance, it displays the new root password to your terminal screen. You can test the validity of the displayed password by trying to log onto a host using it–after the playbook has finished running. 

Upon a successful login, you have just confirmed the root password change works as expected. Therefore, the debug: and msg: lines can be removed from your playbook… or you can leave it. It’s up to you.

				
					  # Update root password hash 
  - name: Update root password hash 
    user: 
      name: root 
      #update_password: always 
      password: "{{ newPass | password_hash('sha512') }}"
    become: yes 
    
  - debug: msg: "{{ newPass }}" 
				
			

Backing up the old passwords

These next set of tasks backup the old root password file. This can come in handy if for some reason, the playbook fails to run on some machines and you need the old password for a manual intervention.

When copying a file to a backup, we’re essentially creating a copy of the same file with a new name. For example secret.yaml copied to a secret.yaml.backup. However, we need to account for each time we run the playbook. In other words, if we ran the playbook three times for example, the secret.yaml.backup file would be overwritten each time.

There would be no way of knowing that a root password was generated three separate times and the initial root password that existed prior to running the play, would be lost. This could be detrimental if for instance, password-aging restrictions are set on your machines prohibiting multiple root password changes within a given period of time.

To resolve this issue, we generate a timestamp from the system using the shell: variable. The shell variable allows the playbook to execute regular Linux shell commands.  The “date +%Y-%m-%d-%H.%M.%S” command will generate a timestamp that includes the current date down to the hours, minutes, and seconds. It will then register that value to the tstamp variable.

The next task block uses the tstamp variable to create the backup password file. Essentially, the backup password file will be stored as password-file-timestamp or /secret.yaml-{{ tstamp.stdout }} using the proper Ansible syntax. This task will run only when the secret.yaml file exists as specified in the (-stat_secrets.stat.exists|bool == true).

Furthermore, it will only need to run once on our localhost. Therefore, we set the run_once: and delegate_to: variables accordingly.

				
					  # Get Timestamp from the OS 
  - name: Get timestamp from the system 
    shell: "date +%Y-%m-%d-%H.%M.%S" 
    register: tstamp 
    run_once: true 
    delegate_to: "{{ runningfromHost }}" 
    
  # Backup the password file 
  - name: Save password file to backup using timestamp
    command: mv {{ passwdDir }}/secret.yaml {{ passwdDir }}/secret.yaml-{{ tstamp.stdout }} 
    when: 
      - stat_secrets.stat.exists|bool == true 
    run_once: true 
    delegate_to: "{{ runningfromHost }}" 
				
			

Clean ups and reviews

Before we conclude, we need to account for something. Remember the secret1.yaml file? This was the plain-text file generated earlier on in the play using ansible-vault for decryption. Ensure this file is deleted as part of the clean up process (using the state: variable set to absent). This is also a task that runs once on our localhost as indicated in the follow-up lines.

				
					  # Clean up the password dir contents 
  - name: Clean up the contents of the password file directory 
    file: 
      state: absent 
      path: "{{ passwdDir }}/secret1.yaml" 
    run_once: true 
    delegate_to: "{{ runningfromHost }}"
				
			

Executing The Playbook

Before we execute this play, we need to determine what system or systems this play will run on. In order to better illustrate this, we’ll examine a sample inventory file.

A Sample Inventory File

The file below (inventory.yaml) is what we’re going to execute our playbook against. For the purposes of this demonstration, we will execute our playbook against the test systems in the inventory file.

				
					all:
  children:
    testing:
       hosts:
         vm1.test.infotechys.com
         vm2.test.infotechys.com
         vm3.test.infotechys.com
    development:
       hosts:
         vm1.dev.infotechys.com
         vm2.dev.infotechys.com
         vm3.dev.infotechys.com
         vm4.dev.infotechys.com
         vm5.dev.infotechys.com
         vm6.dev.infotechys.com
    dev-db:
       hosts:
         db1.dev.infotechys.com
         db2.dev.infotechys.com
         db3.dev.infotechys.com
    production:
       hosts:
         vm1.prod.infotechys.com
         vm2.prod.infotechys.com
         vm3.prod.infotechys.com
         vm4.prod.infotechys.com
				
			

Running a play on select hosts

Using the above inventory file (inventory.yaml), we can execute the play against the test machines only by running the following command:

				
					$ ansible-playbook -i ~/inventory.yaml -l 'all:&testing' cycle_passwd.yaml --vault-password-file ~/.vault_key -K
				
			

As you can see, the ‘all:&testing’ option in the above command tells Ansible to only execute the play on the hosts in the inventory.yaml file, listed under testing: only. You can also list the test hosts in the inventory file by using the –list-hosts flag (below):

				
					$ ansible-playbook -i ~/inventory.yaml -l 'all:&testing' cycle_passwd.yaml --list-hosts
				
			

Conclusion

Copy and paste this play into a yaml file and give it a test run. Did it run successfully? Do you know of a better or cleaner way to write this play? Let us know in the comments section. Cheers and happy coding!!

Related Posts

Leave a Reply

Your email address will not be published. Required fields are marked *