Jon Ellis

The musings of an "engineer"

Jon Ellis

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:

check-nextcloud-version.sh
#!/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-check-nextcloud-version
%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.