Unprivileged LXC container with libvirt

LXC is a low level but very flexible set of tools for managing containers. When used in combination with libvirt it becomes a really powerful tool. In the past LXC was rightly considered insecure because it did not support any ID mapping between the host and the container. This page describes the setup of unprivileged containers with libvirt. For more details see the LXC setup on Debian.

System setup

Allow normal users to run unprivileged containers by setting the kernel.unprivileged_userns_clone sysctl setting. Create a file /etc/sysctl.d/80-lxc-userns.conf with the following content and reload your sysctl settings: sudo sysctl --system:

kernel.unprivileged_userns_clone=1

LXC configuration

Next, find the subuids and subgids of the current user:

cat  /etc/s*id|grep $USER

Using these numbers, create the LXC config file in $XDG_CONFIG_HOME/lxc/default.conf:

lxc.include = /etc/lxc/default.conf

lxc.idmap = u 0 100000 65536
lxc.idmap = g 0 100000 65536

# "Secure" mounting
lxc.mount.auto = proc:mixed sys:ro cgroup:mixed

# Disable AppArmor confinement for containers started by non-root
# See https://discuss.linuxcontainers.org/t/unprivileged-container-wont-start-cgroups-sysvinit/6766 and
# https://discuss.linuxcontainers.org/t/cannot-use-generated-profile-apparmor-parser-not-available/4449
lxc.apparmor.profile = unconfined

Now you should be able to create a container. You can only use the download target for unprivileged containers, so create the container with the command:

lxc-create --name container-name -t download -- -d debian -r bullseye -m amd64

The container can be removed with the lxc-destroy command.

This creates a container in $XDG_DATA_HOME/lxc/container-name directory, with a sub-directory root-fs which contains the actual file system

libvirt configuration

An example libvirt definition could look like the file below. Notice the subuids and subgids must be used in the idmap tag.

<domain type='lxc'>
  <name>container-name</name>
  <memory unit='KiB'>32768</memory>
  <vcpu placement='static'>1</vcpu>
  <resource>
    <partition>/machine</partition>
  </resource>
  <os>
    <type arch='x86_64'>exe</type>
    <init>/lib/systemd/systemd</init>
  </os>
  <idmap>
    <uid start='0' target='100000' count='65536'/>
    <gid start='0' target='100000' count='65536'/>
  </idmap>
  <clock offset='utc'/>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>destroy</on_crash>
  <devices>
    <emulator>/usr/lib/libvirt/libvirt_lxc</emulator>
    <filesystem type='mount' accessmode='passthrough'>
      <source dir='/path/to/container-name/rootfs'/>
      <target dir='/'/>
    </filesystem>
    <interface type='network'>
      <source network='default'/>
    </interface>
    <console type='pty'>
      <target type='lxc' port='0'/>
    </console>
  </devices>
</domain>

Create the container with

virsh -c lxc:/// define file.xml

Before you can use the container you will need to set the root password. The traditional LXC were not practical in my setup, so I ended up with this procedure: on the host, open the root-fs/etc/passwd file and remove the x character in the second field for the root user. Now you can start the container and log in as root without password. While the container is running, open the root-fs/etc/passwd file again and insert the x character. Then you can use the passwd command in the container to set the password.