This site is permanently under construction. Expect the worst.

Using sudoedit in Ansible

Imagine you are at work. After years of arguing you finally get people to use systemd to start services at boot instead of simply hoping the servers never go down. You even convinced the admins to give you a sudoers rule akin to:

merlin ALL=(root) sudoedit /etc/systemd/system/merlin-*, \
  /usr/bin/systemctl start merlin-*, \
  /usr/bin/systemctl stop merlin-*, \
  /usr/bin/systemctl restart merlin-*, \
  /usr/bin/systemctl enable merlin-*, \
  /usr/bin/systemctl disable merlin-*, \
  /usr/bin/systemctl status merlin-*, \
  /usr/bin/systemctl daemon-reload

So far so good, you can now edit the service files and even start/stop them. Good luck manually editing the service files on your fleet of hundreds of servers.

Fortunately, you thought ahead and started using Ansible years ago. But how do you get Ansible scripts (which you cannot run as root) to edit your service files? After all, you only have the rights to sudoedit instead of proper write permissions on the files.

You can solve this by overriding EDITOR:

---
- name: Create temporary service file
  ansible.builtin.tempfile:
    state: file
  register: util_service_temp_file

- name: Template the service file
  ansible.builtin.template:
    src: "{{ service_template }}.j2"
    dest: "{{ util_service_temp_file.path }}"
    # https://www.freedesktop.org/software/systemd/man/latest/systemd-analyze.html
    validate: 'systemd-analyze verify %s:{{ service_name }}'
    mode: '0700'

- name: Install all service files via sudoedit stub
  ansible.builtin.command:
    cmd: "sudoedit /etc/systemd/system/{{ service_name }}"
  environment:
    EDITOR: sh -c "cat \$SOURCE > \$0"
    SOURCE: "{{ util_service_temp_file.path }}"

- name: Reload systemd service files
  # We have to do it via command as otherwise we will not pass the sudoers rules
  ansible.builtin.command:
    cmd: "sudo systemctl daemon-reload"

This works. Why though?

What does sudoedit even do?

To understand what’s going on let’s take a look at sudoedit since it’s load-bearing in this post.

When you run

EDITOR=nano sudoedit /root/file.txt

sudoedit does three things:

  1. Copy the requested file (as root) to a temporary location
  2. Open your desired EDITOR (as the invoking user) pointing to that location ($EDITOR $1)
  3. After the EDITOR process has ended, check if the temporary file has been modified and if so, copy it back (as root) to the original location, overriding the old file

Using this to our advantage

Usually, the requested editor is just this - a text editor like nano, vim, helix. It doesn’t have to be though. In my first iterations it was a simple bash file:

#!/usr/bin/env bash
# edit.sh

cat /tmp/new-service-file > $1

This fits nicely into the usual sudoedit flow. sudoedit copies the file to some temporary location and runs edit.sh with the temporary path as its only argument. edit.sh just writes our desired contents into the temporary file, which doesn’t need any root privileges at that point. After edit.sh has exited, sudoedit helpfully copies the edited file back to its original location.

Now, just package that up into Ansible, make the service file a template, replace the separate EDITOR script with a call to sh -c, and add some usual systemd logic like reloading the service files after you’re done.

Just like this we can deploy our hundreds of service files without SSHing into every single server.