Skip to content

Instantly share code, notes, and snippets.

@nanobeep
Last active March 31, 2024 07:00
Show Gist options
  • Save nanobeep/3b3d614a709086ff832a to your computer and use it in GitHub Desktop.
Save nanobeep/3b3d614a709086ff832a to your computer and use it in GitHub Desktop.

(Note: I wrote this up quickly and without a lot of research, so there are probably inaccuracies. However, I wanted to put this out there in case it helps someone else hitting this issue. Github gists like this unfortunately don't have comment notifications, so if you want me to send me a comment, use my email matt@nanobeep.com and not the comments.)

Problem: Can't use sudo command-limiting in Ansible

The ability to limit sudo users to only be able to execute certain commands doesn't work with Ansible (without a workaround).

This isn't a problem if you're running Ansible as a super-user like root, but if you are allowing others to run Ansible on your systems in order to do things like application deploys, then you need a way to limit their access to the system for basic security.

For example, a line in /etc/sudoers like this:

some-non-root-user ALL=(ALL) NOPASSWD: /some/root/command

That line allows some-not-root-user to run the /some/root/command as if they were a super-user like root.

Why doesn't it work?

Ansible sends Python code to be executed on the targeted servers. Since Ansible is running Python code and generally not executing system commands directly, you can't limit system commands with sudo and expect them to work with Ansible. While you could theoretically limit the sudo user to be able to run Python as root, that would defeat the purpose of command-limiting the user since Python can run arbitrary system commands.

A workaround that sometimes works, is to use Ansible's raw module which passes a command through to the system without the Python wrapper (I'm probably describing that wrong, but that's conceptually what happens). However, the raw module can not be used reliably since Ansible added functionality to track the success of a raw command when it is successful.

From what I understand, the raw module used to run a command directly on the system like /bin/command --options, but now it prepends a "success tracking" echo command to the command like echo BECOME-SUCCESS-sjsscfneygqfcntttkcomefpxnbkzumb; /bin/command --options.

Some versions of sudo might allow this to work if you also add echo to the commands the sudo user can run, but other versions of sudo apparently don't allow multiple commands to be run like this.

Workaround

Use another shell to invoke your sudo command.

This essentially isolates your sudo command from getting altered in a breaking way by Ansible for non-root sudo command-limited users.

- shell: sh -c "sudo /some/root/command"
  become: yes
  become_user: some-non-root-user
  become_method: sudo

Since this is a bit of a hack, you'll need to test it for your particular scenario. In my testing, I could still get adequate exit codes, standard output, etc with this method, but your mileage may vary.

Note that you can set become_method = sudo in your Ansible config file so it is used by default and then be unnecessary in the task declaration.

Alternative non-sudo method

Another pattern you can use to bypass the "sudo command limiting" issue is to use cron to monitor something like the existance of a file, then execute a workflow when the monitoring is triggered.

For example, you could set up a crontab entry to execute this deploy script every minute.

The deploy script would do something like:

# psuedocode

if /tmp/deploy.txt exists
  
  if deploy playbook is already running
    exit
  end
  
  delete /tmp/deploy.txt
  
  run deploy playbook

else
  exit
end

This allows non-privileged users to safely trigger deploys.

@nanobeep
Copy link
Author

nanobeep commented May 1, 2015

Also, the 'become_' parameters are fairly new and have replaced the now-deprecated 'sudo_' parameters. For more info, check out the new docs on 'become': http://docs.ansible.com/become.html

@bcoca
Copy link

bcoca commented May 1, 2015

nice write up, want to submit it to ansible docs?

@skug
Copy link

skug commented Dec 9, 2015

Short note, perhaps also worth a try: I was able to use a passwordless user entry in sudoers that is restricted to a command: Used the shell module and set the become parameters. It worked without the need to wrap the shell command with sh -c "...":

- name: correct write permissions
  become: yes
  become_method: sudo
  become_user: deployer
  shell: setfacl ...

@bbaassssiiee
Copy link

I would be more comfortable if some-non-root-user could only execute ansible/python and no interactive shell

@johnculviner
Copy link

the thing is @bbaassssiiee is you give
sudo python
might as well give
sudo sh
since you could write a stdin/out tty emulator with python (im sure they are out there)

@johnculviner
Copy link

This wasn't working for me so I ended up just using the raw module
ex:
raw: sudo my_command

👍

@John9570
Copy link

Has this been fixed yet, or is this still a limitation of ansible?

@jwarnier
Copy link

I believe the right solution would be to use PolicyKit to allow the user to restart the service, if using SystemD, that is.
Though I did not test it successfully so far.

@Delarius13
Copy link

I know this is an old issue, but for people who end up here looking for a way to use very limited sudoers rules and ansible - one way of achieving this is the following:
Via whatever method you want (ansible/ssh) - put an executable script somewhere on the host(s) that reads like this (some of this might not be fully needed but this is what I tested):

#!/bin/bash
echo $1 | sudo -S $2


Then you can call this via ansible and the raw module with a task. Note you should use an ansible-vault to provide the password but that's easy enough and covered elsewhere - so for this I'll just key in the password (which is called mysudopassword):

raw: /path/to/sudoscript "mysudopassword" "the sudo command I need to execute"

So in my use case, I want to stop httpd and use an ansible vault called secret (with one variable called my_pass) to provide the password - my simple playbook looks like this:

‐‐‐
‐ name: httpd‐off
hosts: myservername
vars_files:
‐ ~/Documents/secret
tasks:
‐ name: Httpd OFF
raw: /home/test/sudoscript "{{ my_pass }}" "systemctl stop httpd"

If you do this with ansible-vault providing the password, you'll find that nowhere does this password get logged, but yet it gives you the ability to run the same sudo commands that you could run when you were directly logged into the host. This can be done entirely in userspace without root permissions (apart from granting the initial sudo rules.)

Hope this helps someone,
Del

@darshitpp
Copy link

I'd rather not run it using raw and echo the password. I'd suggest using ansible.builtin.expect module.

- name: Stop service
  ansible.builtin.expect:
    command: "sudo systemctl stop service"
    responses:
      (?i)password: "{{ pass }}"
  no_log: true

It eliminates the need to even create the executable script.

Thanks @Delarius13 for the idea! I have documented my approach on my blog post

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment