Ansible Sudoers Module
Ansible
Ansible is a configuration management tool to facilitate managing "infrastructure as code". I use it extensively to manage up my home server and other computer-based infrastructure.
For example, I can take an empty Ubuntu machine, add an entry to my Ansible inventory with the appropriate roles, then run the Ansible playbook against it to set it up as described in the Ansible code.
It certainly beats logging in to the machine, installing packages, and editing config files by hand!
Sudoers
The Sudoers file describes what is permissible to run with sudo for a particular user or group.
To run a command as the super user, a user may run sudo my-command
(sudo being short for "super user do").
For example, this line allows any member of the sudo group to execute any command (requiring the user's password to do so):
sudoers%sudo ALL=(ALL:ALL) ALL
To simplify use, the requirement of the user's password can be dropped (essential for unattended use) for an alice
user with this line:
alice ALL=(ALL:ALL) NOPASSWD:ALL
Sudo use with specific commands may also be granted - this is the case I wrote the Sudoers Ansible module for.
On Ubuntu systems (at least) instead of writing all configuration to a single Sudoers file, any file in the
/etc/sudoers.d/
directory is read for configuration.
This is very helpful as it means that syntax errors in one of these files does not break sudo!
Generally when I want to allow a user to sudo a command, I wrap the functionality into it's own script and allow the
user sudo access for that specific script alone.
If a user needs to be able to restart a service, instead of allowing the user to sudo
/bin/systemctl restart myservice
, I'll write that to /usr/local/bin/restart-myservice
and allow the user to sudo
that file.
This way, I don't need to worry about what options (and what order) may be used when writing the Sudoers rule.
I use Sensu to orchestrate checks and metric gathering across my infrastructure, and have
implemented several checks to check for available updates for installed packages.
For example, checking for available Nextcloud updates is done by executing occ update:check
and processing the output.
The sensu user should not have access to the Nextcloud files and executables, and I wouldn't want to run the Nextcloud
executables as root as that'd allow a privilege escalation for the nextcloud user.
The check script (written to /usr/local/bin/check-nextcloud-version.sh
) is as follows:
#!/bin/bash
output=$(sudo -u www-data php /var/www/html/nextcloud/occ update:check)
echo "$output"
echo "$output" | grep -qE 'updates? available'
if [ $? -eq 1 ]; then
exit 0
fi
exit 1
The script uses sudo to run occ update:check
as the www-data
user.
It then prints the output (mostly for debug purposes) and checks to see whether "update available" or
"updates available" exists in the output of that command.
If updates are available, the check script returns 1 which indicates a warning level event to Sensu.
If the script did not print the output of the command, the update check command could be completely encapsulated within
this script.
To allow this script to be executed by the monitoring group, the following should be written to a file in the
/etc/sudoers.d/
directory.
%monitoring ALL=NOPASSWD: /usr/local/bin/check-nextcloud-version.sh
The Sudoers Module
While writing Ansible plays for my personal infrastructure, there were several cases where I wanted to manage sudo access via the Sudoers feature my Ubuntu machines. The most common use case for needing sudo access is to allow a monitoring agent to run some privileged command to check or measure something that required elevated privileges to read.
The main reason I wrote this Sudoers Ansible module was due to the number of times I needed to look up the format for the Sudoers file each time I needed to use it (or at least copy it from a previous use).
Before I wrote the Sudoers module, I would use Ansible's copy module to write the file:
YAML- name: allow monitoring to sudo the check
copy:
dest: "/etc/sudoers.d/monitoring-check-nextcloud-version"
content: "%monitoring ALL=NOPASSWD:/usr/local/bin/check-nextcloud-version.sh\n"
The new line character at the end is critical - it does not work without it.
Using the module, the same functionality is achievable with this Ansbile:
YAML- name: allow monitoring to sudo the check
sudoers:
name: monitoring-check-nextcloud-version # The name of the file in the sudoers.d directory
group: monitoring # the user or group may be specified for this rule
command: /usr/local/bin/check-nextcloud-version.sh # a command or list of commands that are being granted by this rule
nopassword: true # whether a password is required when using sudo for this rule
state: present # whether this rule is to be kept or removed
My common defaults are set so you wouldn't normally set more than the name, group (or user), and command.
The Sudoers module supports several of the use cases that I've used for the Sudoers file - mostly allowing access to commands to specific users and groups. It does not currently support the aliasing features as I've actually never used them. I'm reasonably happy it'd be straight forward to add to this module though.
The module supports check mode and returns whether the invocation of the module caused a change or not.
See the repository readme for further usage examples.
Ansible Galaxy
At iWeb, We have a very similar use-case when managing sudoers files, so I decided the best way to use it in both places was to publish it as an Ansible collection in Ansible Galaxy.
This turned out to be a reasonably straight forward thing to do, however I was initially confused as to how publish a
collection that only contained modules.
When this is written into an Ansible structure, it would be added to the library
directory.
However for a collection, it should be placed in the plugins/modules
directory.
Secondly, I found the process to update the version frustrating as the galaxy.yml
config file needs to be updated with
the tagged version of the code.
I had to go back and create a new version with the correct version in the config file and re-update it.
I would have liked the version to be populated or templated from the git tag if possible.
The collection must be built and uploaded to Ansible galaxy, I'm sure I could have written a CI job to do this, but it didn't seem worth it for the low frequency that I update the module.
Recognition
This repository earned me my first two stars on GitHub, and apparently is being stored in their Arctic Archive Program.
Hopefully this module will be useful for others too.