Checking sudoers.d files with Ansible
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.
Solution 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.
Solution 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.
Solution 3:
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