tudor’s website

How to manage services in FreeBSD jails with Ansible

tags: ansible, freebsd, hacks, self-hosted, tools

If you just want to see how to do it without the backstory, click here if you’re running Ansible ≥ 2.18.1, or click here if your Ansible version is older.

I didn’t write much about it yet, but a couple of months ago I scrapped my NixOS homelab setup in favour of a FreeBSD VM. I’m enjoying it so far — it’s the exact opposite of NixOS in my opinion. Instead of playing with abstractions stacked into each other like a matryoshka doll, I have to make my own little abstractions to suit my own, simpler use cases.

One of these abstractions is managing jails. They are comparable to Linux namespaces: you can use them to make something akin to (sidenote: There have been various technologies called “containers” throughout history. The one we know and love the most today, OCI Containers — or “Docker” containers — are a standardised way of packaging, distributing, and running software. It’s not just about running programs isolated. ) — little isolated systems that live on the same host. In the FreeBSD fan world, a “jail” can often mean some kind of container.

Ansible modules made for FreeBSD usually have a jail argument, for example to set rc arguments in a jail, or to install a package with pkg. The service module, however, does not. What if you want to manage services inside such a jail?

Luckily, I discovered a method that lets you do just that, without reinventing the wheel! This method works only if you run Ansible ≥ 2.18.1. If you don’t, I also discovered a hack that works around this version requirement.

Why that version specifically? Because it contains this bugfix.

The nice way (Ansible ≥ 2.18.1)

- name: Start SSHD in myjail
  ansible.builtin.service:
    arguments: "-j myjail"
    name: sshd
    state: started

When service is called with the -j flag, it will work inside a jail; see the man page.

Here, whatever is given to arguments is going to be then given to the service command, which Ansible calls behind the scenes. Ansible does something like this:

if ! service -j myjail onestatus sshd; then # check if service is already running
  service -j myjail onestart sshd # if not, start it
fi

This can be seen in Ansible’s source code.

If you want to enable a service, do not use the enabled parameter! Instead, call the sysrc module first:

- name: Enable SSHD in myjail
  community.general.sysrc:
    jail: myjail
    state: present
    name: sshd_enable
    value: "YES"

- name: Start SSHD in myjail
  ansible.builtin.service:
    arguments: "-j myjail"
    name: sshd
    state: started

The reason for this is that there is a bug in the way Ansible sets the required rc variable. Ansible will run sysrc, but without the -j parameter which lets it work in a jail (sysrc(8)).

The hack (Ansible < 2.18.1)

- name: Start SSHD in myjail
  ansible.builtin.service:
    name: "-j myjail sshd"
    state: started

Similarly, if you want to enable a service before starting it, you can use sysrc:

- name: Enable SSHD in myjail
  community.general.sysrc:
    jail: myjail
    state: present
    name: sshd_enable
    value: "YES"

- name: Start SSHD in myjail
  ansible.builtin.service:
    name: "-j myjail sshd"
    state: started

Why is this needed? Because of a bug, which was fixed in version 2.18.1. Ansible would add the value of the arguments parameter after the name of the service when calling service, which is not correct. Behind the scenes, Ansible would run:

# This is wrong!
if ! service onestatus sshd -j myjail; then # check if service is already running
  service onestart sshd -j myjail # if not, start it
fi

Here, the -j myjail argument would be passed to the init script, in this case /etc/rc.d/sshd. This is not what we want! The -j argument is supposed to be given to the service tool, as you can see in the man page.