Ansible for IBM Z - Group home

Escalating Privileges with Ansible on z/OS

  

Have you ever wondered if you could escalate your privileges in an Ansible playbook on a z/OS managed node? Well, today were going to discuss how that can be accomplished and approach it with a few different configurations. 

You might be asking yourself, why would you need to do this? Well, what if the user running playbook (let's call them jenkins) does not have authority to create data sets with a high-level qualifier (HLQ) of SYSTEM and yet that is a requirement for the task being run. Or imagine the OMVS user profile does not have enough authority to access an APF authorized program and to do so, they would need to escalate privileges. Lastly, another popular pattern is to run playbooks under a particular service ID, where this ID is an OMVS user with escalated privileges configured to perform Ansible identified tasks. 

Playbook Tasks
For this document, we will be reusing the same Ansible tasks repeatedly to help demonstrate how privilege escalation is behaving with the various configurations we will discuss. Thus, lets review each of the tasks that will run. 

In this task, we will query the user logged onto the controller, not to be confused with the user running the Ansible playbook or being escalated. This task will query the controller, in my case, my Linux system running Ansible that I have SSH'd into with a whoami command which will return the id ddimatos. Because I want this task to run on the controller, I have delegated it to localhost (delegate_to: localhostand disabled privilege escalation for this task with become: false. We would not want to apply privilege escalation on a task running on the controller because the user suhero does not exit on the controller. 

  - name: "Query the user ID on the controller."
    command: /usr/bin/whoami
    register: result
    become: false
    delegate_to: localhost


In this task, we will print the user who is running the playbook, which is a special Ansible variable ansible_user . This user can be defined in inventory, in the playbook or adhoc through the command line interface (CLI). For example, in inventory you may have ansible_user: jenkins or using the CLI with option  -u jenkins , which is what I will be using to demonstrate the playbook. 

  - name: "Print the Ansible user ID on the controller."
    debug:
      var: ansible_user


Here we will print the users ID after the connection is made to the z/OS managed node. This will run the whoami command on the z/OS managed node before any privilege escalation.

  - name: "Query the user ID BEFORE escalated privileges on the managed z/OS node."
    command: whoami
    become: false
    register: result


Here we will print the users ID after the connection is made to the z/OS managed node. This will run the whoami command on the z/OS managed node after privilege escalation is achieved. 

  - name: "Query the user ID AFTER escalated privileges on the managed z/OS node."
    command: whoami
    become: yes
    register: result

The adhoc Command
I will be reusing the same adhoc command through Ansible's CLI so that I can clearly demonstrate how the various users and configurations are being run in the playbook. Let's examine the command:

ansible-playbook -i inventory -u jenkins zos-priv-escal-test.yaml -k

- ansible-playbook  runs Ansible playbooks executing the defined tasks on the defined managed nodes.
- -i specifies the inventory host path that will allow Ansible to connect to the managed nodes.
- -u the remote user to connect as on the managed node.
- -k initiate a prompt for the privilege escalation password.
- zos-priv-escal-test.yaml is the playbook.

An example of the prompts in the command line appear as:

$ ansible-playbook -i inventory -u jenkins zos-priv-escal-test.yaml -k
SSH password:
BECOME password[defaults to SSH password]:

SSH password will be the password for user jenkins to connect over SSH to the managed z/OS node and BECOME password is for the USS id suhero.


The Password Prompt

Unix System Services (USS) supports the switch user (su) command , and when you combine Ansible become directivesconnection variables , ansible.builtin.su plugin and the su command, you have a powerful playbook. The first thing to note is that whenever you use su in USS, you are always going to be greeted with this privilege escalation message "FSUM5019 Enter the password for <user>".

$ su suhero
FSUM5019 Enter the password for suhero:


The reason this is important to note is that we will need to instruct Ansible what greeting to watch for when we escalate our playbooks privileges. Ansible currently expects greetings such as "Password: ", thus it will need instruction which can be done several different ways. If you are content with the user you are escalating to always being the same, you can set this in ansible.cfg with key localized_prompts. Notice the localized prompt contains no trailing spaces or colon, which was resolved in this Ansible pull request.

[su_become_plugin]
localized_prompts = "FSUM5019 Enter the password for suhero"


Escalate Privileges Always
Since we are on the topic of ansible.cfg, lets look at what else we can do with ansible.cfg. If you would like your playbooks to always run with privilege escalation such is the case where maybe a service id is always interacting with the managed z/OS node, this might be an option for you. In that case, you can add the following keys and adjust as needed in both ansible.cfg and your playbook. 

 Keys:
- become - Toggles the use of privilege escalation, allowing you to ‘become’ another user after login.
- become_method - Privilege escalation method to use when become is enabled. For z/OS USS we will use su.
- become_user - The user your playbook user 'becomes' when using privilege escalation.
- become_ask_pass - Toggle to prompt for the privilege escalation password if it's not defined which we will discuss later in the document. 

With regard to privilege escalation, your ansible.cfg should look like:

[privilege_escalation]
become = True
become_method = su
become_user = suhero
become_ask_pass = True

[su_become_plugin]
localized_prompts = "FSUM5019 Enter the password for suhero"


Because this configuration will force all playbook tasks to run as the privileged user, this will conflict with tasks set to delegate_to: localhost which was explained earlier. In those tasks, I will disable privilege escalation with become: false such that my task looks like:

  - name: "Query the user ID on the controller."
    command: /usr/bin/whoami
    register: result
    become: false
    delegate_to: localhost


This is the output for the tasks when privileges are always escalated using the keys in ansible.cfg. Note, some of the output has been removed to reduce the verbosity and to aid in that, the stdout_callback = yaml has been enabled in ansible.cfg.

TASK [Print the user ID on the controller.] 
***********************************************************************************
ok: [zvm] =>
  result:
    cmd:
    - /usr/bin/whoami
    stdout: ddimatos

TASK [Print the Ansible user ID on the controller.] 
***********************************************************************************
ok: [zvm] =>
  ansible_user: jenkins

TASK [Print the user ID BEFORE escalated privileges on the managed z/OS node.] 
***********************************************************************************
ok: [zvm] =>
  result:
    cmd:
    - whoami
    stdout: JENKINS

TASK [Print user ID AFTER escalated privileges on the managed z/OS node.] 
***********************************************************************************
ok: [zvm] =>
  result:
    cmd:
    - whoami
    stdout: SUHERO


As you can see, on the controller where I have logged into Linux, my id is ddimatos. While on the controller where the playbook is running and connecting to z/OS (before any privilege escalation), the user is jenkins. When I escalate privileges and query the user, I am suhero.

Alternatives to Escalate Privileges
Now that you know how to enable privilege escalation for a z/OS managed node, lets discuss alternatives to enabling it that don't completely require ansible.cfg. Generally, when I am developing a playbook, I like to minimize the number of files I am editing and in doing so, I start off by using some Ansible directives. 

In the vars section, I can create an array with anticipated password prompts for the various escalated user id's that might be accessed. 

  vars: 
    ansible_su_prompt_l10n:
     - FSUM5019 Enter the password for suhero
     - FSUM5019 Enter the password for superson
     - FSUM5019 Enter the password for ansible

Also in the vars, can be a predefined user for privilege escalation such is the case here with superson that will correspond to the entry above FSUM5019 Enter the password for superson.

vars:
   ansible_become_user: superson

If we would like to be interactive such that we are testing various user IDs for escalation, we could interactively accept values for ansible_become_user and ansible_become_password using the Ansible vars_prompt.

  vars_prompt:
    - name: ansible_become_user
      prompt: Enter the privilege escalation user ID?

    - name: ansible_become_password
      prompt: Enter the privilege escalation password?

 

Which would appear as prompts in the terminal:

$ ansible-playbook -i inventory -u jenkins zos-priv-escal-test.yaml -k
SSH password:
Enter the privilege escalation user ID?:
Enter the privilege escalation password?:


At this point, you may prefer a more dynamic approach to the localized prompt and use Jinja2 templating to enable dynamic expressions and access to variables and facts. Note, the variable templating is happening with the variable {{ ansible_become_user }} which is set using the users input in the terminal prompt created with vars_prompt.

ansible_su_prompt_l10n: FSUM5019 Enter the password for {{ansible_become_user}}

lock

Securing Entries with Ansible Vault
You may want to encrypt and store the values for variables vaulted_become_pass and vaulted_become_user in a vault that can be read by Ansible during execution of the playbook. There are a few ways to do this, my preference is to place a vault file (crypted.yml) in a directory named host_vars/zvm alongside the playbook and have them automatically read by Ansible without the need to specify the vault in the CLI or playbook. The reason for the subdirectory zvm is because that is how I have the host identified in inventory and now Ansible knows how to correlate the inventory hosts: zvm to the path group_vars/zvm.


Typically this is what my project looks like:

<project>
├── playbook1.yml
├── playbook2yml 
│
├── ansible.cfg        
├── inventories/      
│   └── host_vars/     
│       └── zvm/
│             └── crypted.yml
│             ├── plain.yml
│   └── group_vars/       
│       └── all.yml
│   ├── inventory.yml   

Referring to the Ansible docs Keep vaulted variables safely visible you will want a file that is encrypted that looks like this before its encrypted (crypted.yml):

vaulted_become_pass: foobar
vaulted_become_user: suhero


And to aid in the visibility as described in the documentation a file remaining in clear text (plain.yml):

ansible_become_pass: "{{ vaulted_become_pass }}"
ansible_become_user: "{{ vaulted_become_user }}"


As you can see, the values in plain.yml correspond to variables in crypted.yml, ansible_become_pass <--> vaulted_become_pass and as we learned earlier, ansible_become_pass is a special variable reserved by Ansible using in privilage escalation that is now vaulted and decrypted by Ansible. Ansible will prompt for the vault password with the CLI option --ask-vault-pass , for example, ansible-playbook -i inventory -u jenkins zos-priv-escal-test.yaml -k --ask-vault-pass will result in:

$ ansible-playbook -i inventory -u jenkins zos-priv-escal-test.yaml -k --ask-vault-pass
SSH password:
Vault password:

To encrypt file crypted.yml into the vault, you can use this command which will prompt you to create a secret password, ansible-vault encrypt host_vars/zvm/crypted.yml which if you view will see its encrypted afterwards.

$ head host_vars/zvm/crypted.yml
$ANSIBLE_VAULT;1.1;AES256
33626431386237663230393365306663643637633336353131646366333364636238373131343335
6339623931363739333632323338363764373564633030300a323362373166623934323737333737


If you don't have a similar project setup and want a simpler approach to using an Ansible vault:

- you instruct Ansbile where to find the vault by using vars_files in the playbook.

  vars_files:
    - /path/to/crypted.yml


- you can instruct Ansbile where to find the vault on the command like with option --vault-password-file:

ansible-playbook -i inventory -u jenkins zos-priv-escal-test.yaml -k --ask-vault-pass --vault-password-file /path/to/crypted.yml


Putting it Together

Below is the resulting playbook considering the dynamic options discussed. 

---
- hosts: zvm
  collections:
    - ibm.ibm_zos_core
  gather_facts: no
  environment: "{{ environment_vars }}"

  vars_prompt:
    - name: ansible_become_user
      prompt: Enter the privilege escalation user ID?

    - name: ansible_become_password
      prompt: Enter the privilege escalation password?

  vars:
    ansible_become_method: su
    ansible_su_prompt_l10n: FSUM5019 Enter the password for {{ansible_become_user}}

  tasks:
  - name: "Query the user ID on the controller."
    command: /usr/bin/whoami
    register: result
    become: false
    delegate_to: localhost

  - name: "Print the user ID on the controller."
    debug:
      var: result

  - name: "Print the Ansible user ID on the controller."
    debug:
      var: ansible_user

  - name: "Query the user ID BEFORE escalated privileges on the managed z/OS node."
    command: whoami
    become: false
    register: result

  - name: "Print the user ID BEFORE escalated privileges on the managed z/OS node."
    debug:
      var: result

  - name: "Query the user ID AFTER escalated privileges on the managed z/OS node."
    command: whoami
    become: yes
    register: result

  - name: "Print user ID AFTER escalated privileges on the managed z/OS node."
    debug:
      var: result

About the Author
Demetrios Dimatos is the IBM z/OS Ansible Core Senior Technical Lead with 16 years mainframe experience and over 20 years of development experience; having led multiple products ranging from client server technologies, administration consoles, IBM Open Platform (Hadoop - HDFS, MapReduce, Yarn) and Spark, Linux and Solaris kernel development. 

Resources

IBM Ansible z/OS core on Galaxy
IBM Ansible Core Collection Repository on GitHub
IBM Ansible Core Collection on Automation Hub
Red Hat® Ansible Certified Content for IBM Z documentation