6

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.

4
  • Cross-site dup: Visudo sanity check for the whole ecosystem of included files?
    – techraf
    Mar 13, 2018 at 19:25
  • @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, 2018 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. Mar 13, 2018 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, 2018 at 13:17

3 Answers 3

9

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
2

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.

1
  • You don't have to create a staging area, because the module says if "validate" is present, it will be validated in a temporary file before it's moved in-place. Maybe it's a new feature... Apr 3, 2023 at 14:13
1

Doing the same thing with Ansible modules only.

- name: Creating assemble directory
  file:
    path: /etc/sudoers.stage.d
    state: directory
    mode: 0600

- name: Applying sudoers.d files on assemble directory
  template:
    src: "{{ item }}.j2"
    dest: /etc/sudoers.stage.d/{{ item }}
    mode: 0400
  loop:
    - 10-wheel
    - 20-sys
  register: check

- name: Copying original sudoers to the assemble directory
  copy:
    remote_src: yes
    src: /etc/sudoers
    dest: /etc/sudoers.stage.d/99-sudoers
  when: check.changed

- name: Removing include line from 99-sudoers
  lineinfile:
    path: /etc/sudoers.stage.d/99-sudoers
    state: absent
    regex: ^#includedir /etc/sudoers.d
  when: check.changed

- name: Assembling unique config for validation
  assemble:
    src: /etc/sudoers.stage.d
    dest: /etc/sudoers.stage
    mode: 0600
    validate: visudo -cf %s
  when: check.changed

- name: Applying sudoers configurations
  template:
    src: "{{ item }}.j2"
    dest: /etc/sudoers.d/{{ item }}
    mode: 0440
  loop:
    - 10-wheel
    - 20-sys
  when: check.changed

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .