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.