In this post, we will create a jail template on TrueNAS Core. The template will have everything we need to create Ansible-managed jails. In particular, we will install a base set of packages, add a dedicated user account, enable key-based SSH authentication, and configure sudo.

In the following, I assume you already have a running TrueNAS instance with networking and pools configured. I also assume that the “basic” jail configuration is set up. “Basic” means that you should have a pool for your jails. If not, go to the management web interface, click “Jails”, and choose the pool on which you want to store your jails. TrueNAS uses iocage to manage its jails. After configuring the jail pool, you should see that iocage created its datasets on the selected pool. My pool is called “jails”:

root@truenas[~]# zfs list -r jails
NAME                     USED  AVAIL     REFER  MOUNTPOINT
jails                   1.26M  13.1G       96K  /mnt/jails
jails/iocage             688K  13.1G      112K  /mnt/jails/iocage
jails/iocage/download     96K  13.1G       96K  /mnt/jails/iocage/download
jails/iocage/images       96K  13.1G       96K  /mnt/jails/iocage/images
jails/iocage/jails        96K  13.1G       96K  /mnt/jails/iocage/jails
jails/iocage/log          96K  13.1G       96K  /mnt/jails/iocage/log
jails/iocage/releases     96K  13.1G       96K  /mnt/jails/iocage/releases
jails/iocage/templates    96K  13.1G       96K  /mnt/jails/iocage/templates

In this example, I’m running TrueNAS-13.0-U3, which is based on FreeBSD 13.1. Thus, we will also build our jail template based on FreeBSD 13.1. If you did not use the web interface to set up any jails before, fetch this release with iocage:

root@truenas[~]# iocage fetch --release 13.1-RELEASE

Now that we have the correct release, creating a template starts by creating a regular jail first:

root@truenas[~]# iocage create --name ansible-managed --release 13.1-RELEASE vnet=on ip4_addr='vnet0|10.250.1.11/24' defaultrouter=10.250.1.1 ip6=disable

This command creates a new jail named ansible-managed based on the release 13.1. The jail will use the virtual network stack (vnet); the virtual network interface vnet0 within the jail will get the IPv4 address 10.250.1.11/24. Finally, the default gateway will be 10.250.1.1, and IPv6 will be disabled. Have a look at the iocage documentation for more configuration options.

Side note: If you are doing this with VirtualBox, make sure that the interface that you are using for your jails has the promiscuous mode enabled (“Allow VMs” or “Allow All”). Otherwise, your jail cannot talk to anything outside of its VM. Check this under machine settings » Network » Adapter N » Advanced » Promiscuous Mode.

Now, we can configure our jail and make changes we want in our template. Let’s enter the jail:

root@truenas[~]# iocage console ansible-managed --force

If you did not run the command “iocage update” before, the first thing to do is to bootstrap pkg:

root@ansible-managed:~ # pkg bootstrap -y && pkg update

For Ansible-managed jails, we need Python and sudo. I also want bash:

root@ansible-managed:~ # pkg install -y bash python3 sudo

After that, we allow members of the wheel group to run any command with sudo:

root@ansible-managed:~ # echo '%wheel ALL=(ALL:ALL) ALL' > /usr/local/etc/sudoers.d/wheel

On systems that I manage alone, I typically add a dedicated ansible user:

root@ansible-managed:~ # pw useradd -n ansible -u 900 -G wheel -m -s /usr/local/bin/bash
root@ansible-managed:~ # passwd ansible

The next step is to add the public SSH key to the user’s authorized_keys file. Doing so enables us to log in without having to use password-based authentication.

root@ansible-managed:~ # mkdir -p -m 0700 /home/ansible/.ssh
root@ansible-managed:~ # set ssh_key='ssh-ed25519 AAAAC3…yourkey…4uUzFoJE you@some-system'
root@ansible-managed:~ # echo $ssh_key > /home/ansible/.ssh/authorized_keys
root@ansible-managed:~ # chmod 0600 /home/ansible/.ssh/authorized_keys
root@ansible-managed:~ # chown -R ansible:ansible /home/ansible/.ssh/

Moreover, we deploy a default sshd_config file for our jails. If required, replace the config during the first Ansible run. Yet, for most use cases, it should be fine.

root@ansible-managed:~ # cat <<EOF >> /etc/ssh/sshd_config
AddressFamily inet 
ListenAddress 0.0.0.0

SyslogFacility AUTH
LogLevel INFO

LoginGraceTime 1m
PermitRootLogin no
StrictModes yes
MaxAuthTries 3
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
HostbasedAuthentication no
IgnoreRhosts yes
PasswordAuthentication no
PermitEmptyPasswords no
KbdInteractiveAuthentication yes
KerberosAuthentication no
GSSAPIAuthentication no
UsePAM yes

AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
GatewayPorts no
X11Forwarding no
TCPKeepAlive yes
PermitUserEnvironment no
Compression yes

Subsystem sftp /usr/libexec/sftp-server
EOF

That is already everything required for a minimal jail that can be managed with Ansible. Of course, we could add more fine-tuning and hardening, but that is something for a different day.

The last thing to do is to convert the jail into a template. For this, we have to exit the jail and shut it down.

root@ansible-managed:~ # logout
root@truenas[~]# iocage stop ansible-managed
root@truenas[~]# iocage set template=yes ansible-managed
ansible-managed converted to a template.
root@truenas[~]# iocage set notes="template for Ansible managed jails" ansible-managed
notes: none -> template for Ansible managed jails

After that, you can verify that the jail is a template by running:

root@truenas[~]# iocage list -t
+------+-----------------+-------+--------------+-------------+
| JID  |      NAME       | STATE |   RELEASE    |     IP4     |
+======+=================+=======+==============+=============+
| None | ansible-managed | down  | 13.1-RELEASE | 10.250.1.11 |
+------+-----------------+-------+--------------+-------------+

You can also check that the files belonging to the template are no longer present under /mnt/jails/iocage/jails but /mnt/jails/iocage/templates.

Finally, here is a simple script to create a template like the one described above. Some steps differ a little bit, but the result is the same.

#!/bin/bash -eu

jail='ansible-managed'
release='13.1-RELEASE'
ssh_key='CHANGE_ME_ENTER_PUBLIC_SSH_KEY_HERE'

jail_exec="iocage exec $jail"
jail_root="/mnt/jails/iocage/jails/$jail/root"


iocage create --name $jail --release $release \
  notes='template for Ansible managed jails' \
  vnet=on \
  defaultrouter=10.250.1.1 \
  ip4_addr='vnet0|10.250.1.11/24' \
  ip6=disable

iocage start $jail
$jail_exec "pkg bootstrap -y && pkg update"
$jail_exec "pkg install -y bash python3 sudo"
$jail_exec "pw useradd -n ansible -u 900 -G wheel -m -s /usr/local/bin/bash"
echo '[*] Setting password for user ansible'
$jail_exec "passwd ansible"
mkdir -p -m 0700 $jail_root/home/ansible/.ssh
echo $ssh_key > $jail_root/home/ansible/.ssh/authorized_keys
$jail_exec "chmod 0600 /home/ansible/.ssh/authorized_keys"
$jail_exec "chown -R ansible:ansible /home/ansible/.ssh/"
echo '%wheel ALL=(ALL:ALL) ALL' > $jail_root/usr/local/etc/sudoers.d/wheel
mv $jail_root/etc/ssh/sshd_config $jail_root/etc/ssh/sshd_config.orig
cat <<EOF >> $jail_root/etc/ssh/sshd_config
AddressFamily inet 
ListenAddress 0.0.0.0

SyslogFacility AUTH
LogLevel INFO

LoginGraceTime 1m
PermitRootLogin no
StrictModes yes
MaxAuthTries 3
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
HostbasedAuthentication no
IgnoreRhosts yes
PasswordAuthentication no
PermitEmptyPasswords no
KbdInteractiveAuthentication yes
KerberosAuthentication no
GSSAPIAuthentication no
UsePAM yes

AllowAgentForwarding no
AllowTcpForwarding no
PermitTunnel no
GatewayPorts no
X11Forwarding no
TCPKeepAlive yes
PermitUserEnvironment no
Compression yes

Subsystem sftp /usr/libexec/sftp-server
EOF

$jail_exec "sysrc sshd_enable='YES'"
iocage stop $jail
iocage set template=yes $jail

That’s it. Now, we can create jails based on the template:

root@truenas[~]# iocage create \
                   -n jx01 \
                   -t ansible-managed \
                   vnet=on \
                   ip4_addr='vnet0|192.168.56.110/24' \
                   ip6=disable

Of course, you have to adjust the jail parameters to your specific needs.