3

I have an Ansible playbook I use to manage our sudoers files across our environment. We like to keep a minimal sudoers file at /etc/sudoers, then anything we want to add gets put into separate files under /etc/sudoers.d.

My Ansible playbook contains the following task for pushing these files:

- name: copy sudoers files
  copy:
    src: "{{ item }}"
    dest: "/etc/sudoers.d/{{ item }}"
    backup: yes
    owner: root
    group: root
    mode: 0440
    validate: /usr/sbin/visudo -cf %s
  with_items:
    - admins
    - apache
    - monitor

The task contains a validate clause to make sure the file is valid before committing the file, and this has generally worked well. However, today I ran into a problem where an update broke sudo. The file passed the validation step, but contained a User_Alias with the same name as a User_Alias in the main /etc/sudoers file. Any attempt to run sudo after that resulted in a parse error.

My question is this - how do I test updates to my sudoers files from Ansible that can catch errors like this? Once the file is in place, the error can be caught by running visudo -c, but putting this in as the validation step doesn't work. Ansible requires the %s placeholder, and even if it didn't, the validation is done before copying the file into place so that visudo -c wouldn't catch it.

  • @techraf, thanks for the information, this might prove useful. But it won't work in Ansible because according to the documentation on docs.ansible.com for the validate tag: "The command is passed securely so shell features like expansion and pipes won't work." – virtex Mar 13 '18 at 19:40
  • @virtex make a helper shell script that takes {{ item }} and %s as parameters; it can dump /etc/sudoers and all files from /etc/sudoers.d/* except {{ item }} (old file), add content of %s (new file) and feed to visudo. – Konstantin Suvorov Mar 13 '18 at 20:01
  • @Konstantin, this is similar to what I will likely end up doing. I'll need to do the verification as a separate task in case I'm updating more than one file, but I can copy /etc/sudoers.d to /etc/sudoers.stage.d and upload my files there, then have the helper script swap the two directories and perform the validation, then swap them back if it fails. Thanks. – virtex Mar 14 '18 at 13:17
1

Have you tried this:

- copy:
    src: '{{ item }}'
    dest: '/etc/sudoers.d/{{ item }}'
    owner: root
    group: root
    mode: 0440
    validate: 'bash -c "cat /etc/sudoers %s | visudo -cf-"'

It works for me.

0

I got it working. Here's what I did. First I added a set of Ansible tasks to create a staging directory at /etc/sudoers.stage.d and copy the contents of /etc/sudoers.d into it. I then upload the files to this staging area and, if any of them are changed, run a custom script to activate them.

This is what the logic in the playbook looks like now

- name: delete staging area
  file:
    path: "/etc/sudoers.stage.d"
    state: absent
  changed_when: false

- name: copy /etc/sudoers.d to staging area
  shell: "cp -rp /etc/sudoers.d /etc/sudoers.stage.d"
  changed_when: false

- name: stage sudoers files
  copy:
    src: "{{item}}"
    dest: "/etc/sudoers.stage.d/{{item}}"
    backup: yes
    owner: root
    group: root
    mode: 0440
    validate: /usr/sbin/visudo -cf %s
  with_items:
    - admins
    - apache
    - monitor
  register: sudoers_d

- block:
  - name: push out activate script
    copy:
      src: activate_sudoers.sh
      dest: /usr/local/bin/activate_sudoers.sh
      owner: root
      group: root
      mode: 0700

  - name: activate change
    shell: /bin/sh /usr/local/bin/activate_sudoers.sh /etc/sudoers.stage.d

  when: sudoers_d.changed

and here's what the activate_sudoers.sh script looks like.

#!/bin/sh

function usage {
    echo "Usage: $0 <stage directory>" >&2
    exit 1
}

function abort {
    echo "*** Error detected" >&2
    [ "$#" -gt 0 ] && echo "***" $@ >&2
    exit 1
}

PATH=/usr/bin:/bin:/usr/sbin:/sbin
export PATH

test $# -eq 1 || usage
test -d "$1" || abort "Stage directory $1: missing or not a directory"
test -d /etc/sudoers.old.d && rm -rf /etc/sudoers.old.d
test -d /etc/sudoers.old.d && abort "Failed to remove /etc/sudoers.old.d"

mv /etc/sudoers.d /etc/sudoers.old.d \
  && mv "$1" /etc/sudoers.d \
  && visudo -c

if [ $? -eq 0 ]; then
    # Success - clean up
    rm -rf /etc/sudoers.old.d
    exit 0
else
    # Failure - roll back
    rm -rf /etc/sudoers.d
    mv /etc/sudoers.old.d /etc/sudoers.d
    abort "sudoers update failed"
fi

It's a bit longer and more complex than I had hoped, but it gets the job done. Hopefully this will be useful to anyone else running into the same problem.

Your Answer

By clicking "Post Your Answer", you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.