Ansible privilege escalation with expect when you don't have root shell privileges

The default Ansible privilege escalation mechanism requires broad sudo privileges. If your production environment gives you sudo access but bars you from getting a root shell, you are out of luck. As, the doc says - you cannot expect Ansible to work when sudo commands are restricted.

Privilege escalation permissions have to be general. Ansible does not always use a specific command to do something but runs modules (code) from a temporary file name which changes every time. If you have /sbin/service or /bin/chmod as the allowed commands this will fail with ansible as those paths won’t match with the temporary file that ansible creates to run the module.

Our company, for good reasons, has these restrictions. Root shells bypass audit logs and can let really bad things happen by mistake. Even if I am working on my personal hosts, I try to make it a habit of not working in a root shell. The convenience is just not worth the risk.

So, anyway, ansible is pretty useless for anything involving root user on our systems.

However, if we look at the actual problem in more detail, the issue is clearly the way sudo is invoked.

Without become ansible essentially runs the following on the remote host:

sh -c "/usr/bin/python /path/to/ansible/compiled/python/script"

With become however, ansible runs something like this:

sh -c "sudo -u root /bin/sh -c 'echo BECOME-SUCCESS-oqgdgdpwngxeakkmhtvcxzvhnwtsxzzm ; /usr/bin/python  /path/to/ansible/compiled/python/script"

It is that echo statement, which is possibly being used by ansible to distinguish between a privilege escalation failure and a task failure, which is bringing in that requirement to run a shell within sudo. Unfortunately, this is something the ansible team has decided to stick with. So to me, one way out was to stop relying on ansible to run sudo, and run sudo myself in an ordinary command and figure out a way to supply a password to the inevitable prompt.

I tried to see if I can use that old trick of using expect to supply the password. Ansible has an expect module. Unfortunately, it requires the python library pexpect, which performs similar functionality in pure python, to be pexpect >= 3.3 which is not available in RHEL7 hosts in our production environment.

$ sudo yum list pexpect
...
Available Packages
pexpect.noarch           2.3-11.el7         release-rhel-x86_64-workstation-7-r03

$ cat /etc/redhat-release
Red Hat Enterprise Linux Workstation release 7.3 (Maipo)

So I was left to use the regular expect command, available in most server environments for eons, to do my job. I had to read up a bit of TCL to find my way around. That and this stackoverflow answer helped me construct this proof-of-concept playbook which does the job.

- hosts: all
  gather_facts: no

  vars:
      sudo_cmd: id

  vars_prompt:
      - name: sudo_password
        msg: Enter sudo password
        private: yes

  tasks:

      # 
      - name: "run sudo"
        shell: |
            set timeout 10
            spawn sudo {{ sudo_cmd }}
            expect {
                "sudo*password*: " { send "{{sudo_password}}\n" }
            }
            expect eof
            lassign [wait] pid spawnid os_error_flag ret_code

            if {$os_error_flag != 0} {
                exit $os_error_flag
            }
            exit $ret_code
        args:
            executable: /usr/bin/expect
        no_log: True

This script will prompt for the sudo password once at the beginning.

You can then run your sudo command like this.

ansible-playbook -v  --limit somehost sudo.yml -e "sudo_cmd=ls /root"

Of course, one gotcha is that in many stock deployments, the expect package is not installed and you might have to install it first.

The usual security risks of having secrets passed into shell commands in ansible apply, of course. The no_log parameter is especially important, without which ansible will leak your password to syslog on the remote host.

It is a hack, I know. If there is a better way of doing it, I would be really interested to know. I couldn’t find anything on ansible forums and documentation for my particular needs.

 
comments powered by Disqus