In this article, we will review how we can automate a task that checks the timezone using Ansible, offering a detailed walkthrough of the steps
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.
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.
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 }}"
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
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:
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!&
# 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
Now that we’ve established where we want our playbook to run and declared the variables we need, let’s breakdown each task.
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
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 }}"
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 }}"
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 }}"
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 }}"
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.
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
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
Related Posts
In this article, we will review how we can automate a task that checks the timezone using Ansible, offering a detailed walkthrough of the steps
Installing RHEL7 or CentOS7 on a PC is fairly easy to do. In this tutorial, we will review the installation process step-by-step. Today’s focus will
In today’s Ansible series, we will learn about automating changes using Ansible. Specifically, we will automate changes to the sshd config file. Table of Contents