Deploy a darkbot or a simple generic service with SaltStack (part 1)

SaltStack is one of the main configuration as code software. Written in Python, it uses Jinja templates. It can be easily used to deploy a service, like here a C application.

In this post, we’ll walk through the different steps to deploy the service and allow it to be managed by relevant trusted users, without root access.


A darkbot is an IRC talking bot developed in 1996 by Jason Hamilton. It’s basically a knowledge base with entries allowing for wildcards, and replies.

At Nasqueron, we use a darkbot to store factoids about server configurations or development projects. It’s hosted on Eglide, a free culture shell hosting service.

How a simple generic service work?

  • The service is a non forking application
  • The application runs on a dedicated user
  • A service manager like systemd can take care of the start/stop operations
  • Some users should have capabilities to maintain this application, without root access


In this first post of the series, will take care of the user/group/sudo logic.

Step 1 — Create a group account

First, you need to create a group and include the users allowed to control the service.

As you’ll have several of these groups (one per team, or one per family of services), something generic to create groups is needed.

As SaltStack uses Jinja2 templates, we can provide arbitrary data structures and iterate with a Python for loop:

This defines a pillar value shellgroups, as a dictionary containing two groups, nasqueron-irc and another-group. Each groups is itself a data structure with key/values (gid, description, members).

The property keys are totally arbitrary. As UNIX groups don’t have a GECOS-like or any field to describe them, the description here won’t be exploited. If we’d deploy on a Windows machine, we’d been able to use it.

We now need to read this pillar value, create the group, fill the members:

group.present will create the group if needed, and check it has the properties we want to control (name, system, gid and members here).

iteritems is a method of Python dictionaries to get an iterator.

We don’t need to iterate to members, group.present is happy with the full list.

Notice we name our group_{{group}}. We could have used directly {{group}}, and simplify like this:

This sample skip name as the state name (here {{group}}) is used when that field is omitted. But each state name must be unique, so to avoid any conflict, we prefer to prepend it with group. It eases future maintenance avoiding to track a queer bug born of states naming conflicts.

The advantage of this technique is you can centralize the groups in one reference file and track who has access to what. A configuration as code is a documentation per se, always up to date, as the repo is documentation first.

Step 2 — Create an user account

To create regular users account, we use a similar solution than for the groups, using a generic data pillar file too.

For a service, it’s more convenient to store it directly with the other service’s states:

Choose where you wish to deploy applications, /opt/, /srv/, /usr/local/. If the application doesn’t have a hierarchy of folders to store config etc. but use the unix hierarchy, you can use /nonexistent as home directory.

Step 3 — sudo capabilities

sudo doesn’t only allow to run command as root, it also allows to run command under another arbitrary user account.

Here we’ll allow members of the nasqueron-irc group to control the odderon user account.

Coupled with SSH keys, it means you don’t need to save a password for it.

That’s as simple as deploy a file in the right place, ie the /etc/sudoers.d folder. Softwares use more and more flexible configuration directories, it’s convenient here to compose services instead to manage one centralized file.

If you deploy on other OSes like FreeBSD too, think to adjust paths:

What are we deploying? We instruct salt through the source parameter to copy roles/shellserver/odderon/files/odderon.sudoers into …etc/sudoers.d/odderon (name). The template parameter allows to use a template format, and Salt will take care to pass the file through the template engine before to copy.

Capabilities could be as simple as “any user member of the nasqueron-irc group can run any command under odderon account”:

%nasqueron-irc ALL=(odderon) NOPASSWD: ALL

That allows to run commands with this syntax: sudo -u odderon whoami.

But then, we want here to run a service, and generally services are handled by a services manager, like systemd, running as root:

It’s important to provide the exact and full command to run to avoid unauthorized privilege escalation. If an account member of this group is compromised, all it could do as root is to restart the bot, not every service on the system.

Document UIDs and GIDs

There is a drawback to not centralize the groups and users in one same file: we need to avoid UID and GID conflicts and track what we assign. We suggest to create UIDs and GIDs file in your repository, the FreeBSD ports collection does that with success.

Next steps

Now we have an user account ready to run the service, and sudo capabilities allowing a team to control it, we’ll see in a next post how to ship software, how to provide a systemd unit and how to provide the configuration, including the secrets. We’ll also see the different deployment strategies, through packages or fetching and building the application locally.

Go to the part 2 of the walkthrough