Running unprivileged LXC containers on Fedora

2 Dec 2021 6 minutes

LXC containers are great - they are an easy way to set up a clean development or testing environment, lightweight, and simple to maintain. Unfortunately, running unprivileged containers (i.e. not running them as root, which is inconvenient and generally not a good idea) doesn’t work out-of-the box on Fedora (but to be fair, the same goes for Debian and friends).

At the end of this guide you should be able to run unprivileged containers without needing to mess with SELinux (which strangely the Wiki suggests you need to disable) with the option to add bind mounts in your container to share files between your home folder and the container’s filesystem.

Some basics first

Before we continue we just need to clear up some basics. By running containers in unprivileged mode, it means that you will never be making use of root privileges on your host machine. They will be started as your normal user account - for this to work LXC will make use of something called UID namespaces. Basically, inside the container, UID 0 (which is root) will be mapped to some arbitrary UID on the host (usually way up in the thousands). This has the cool benefit that if something were to break out of the container, it will be running as some arbitrary, unprivileged UID on the host and probably won’t be able to do much. The other cool thing is that this allows your unprivileged normal user account to start, stop and manage containers without ever touching sudo.

Installation

I’m writing this guide on Fedora 34, but it should also work on other versions. Firstly, install LXC and its templates (templates makes creating containers much easier):

sudo dnf install lxc lxc-templates

Setting up the networking

On Fedora 35, the default LXC bridge’s name was changed to lxcbr0 - so be sure to change virbr0 to lxcbr0 in all of the configuration options below.

Next you’ll need to allow your user access to the network bridge created by the LXC networking service. To do so, run the following:

# allow user to access network bridge
# see the note regarding F35 above
sudo tee /etc/lxc/lxc-usernet <<EOF
$USER veth virbr0 10
EOF

# restart the lxc network service
sudo systemctl restart lxc-net

With these two commands you’ve allowed your user account to create 10 network bridges on the default LXC network device virbr0. Without this your containers won’t be able to attach to the network bridge and they won’t have network connectivity.

Updating the firewall rules on F35

This step is only required if you’re running Fedora 35 or newer. By default, firewalld will block your containers from obtaining DHCP addresses. To fix this you’ll need to add the LXC bridge to firewalld’s trusted zone.

sudo firewall-cmd --zone=trusted --change-interface=lxcbr0 --permanent
sudo firewall-cmd --reload

Configuring LXC

Finally we also need to tell LXC to use the correct network interface, and to set up the correct UID and GID mappings. You can add the following to the system-wide LXC config (/etc/lxc/default.conf) however I prefer using the one in my home folder so that I can check it into my dotfiles.

Add the following to ~/.config/lxc/default.conf (you might need to create the folders):

# default networking
lxc.net.0.type = veth
# remember to update this to lxcbr0 on F35 and above
lxc.net.0.link = virbr0
lxc.net.0.flags = up
lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx

# uid and gid mapping - all the ids, except 1000, are mapped to an
# unpriviledged range on the host. 1000 is kept so that filesharing through
# bind mounts is possible
lxc.idmap = u 0 100000 1000
lxc.idmap = u 1000 1000 1
lxc.idmap = u 1001 101000 64536

lxc.idmap = g 0 100000 1000
lxc.idmap = g 1000 1000 1
lxc.idmap = g 1001 101000 64536

One thing to note is the UID mapping ranges. All the container’s UIDs, except for 1000, is mapped to UIDs >100000. However UID 1000 is mapped to 1000 on the host - this is to allow read-write bind mounts between your home folder and the container’s default user (not root! A normal user account that you created - see the next section). Your account should have an UID of 1000 by default. If not, you’ll need to slightly adjust the above to fit your account’s UID (man lxc.container.conf).

Creating the container

At this point you should be able to create and run a container. To do so run the following:

# here because the keyserver used by LXC no longer exists
export DOWNLOAD_KEYSERVER=keys.openpgp.org
# create the container - doesn't need to be fedora
lxc-create -t download -n my_container -- -d fedora -r 34 -a amd64

If everything went well you should be presented with something like the following:

Setting up the GPG keyring
Downloading the image index
Downloading the rootfs
Downloading the metadata
The image cache is now ready
Unpacking the rootfs

---
You just created a Fedora 34 x86_64 (20210805_20:33) container.

Start the container, and create a normal user account to use:

# start the container
lxc-start my_container
# attach to it
lxc-attach my_container

## we're now inside the container as the root user

# create a normal user and set a password
useradd -mG wheel -s /usr/bin/bash my_user
passwd my_user
# log in as the normal user
su my_user
# create a folder in their home folder
mkdir ~/test
# log out of the normal user
exit
# exit the container
exit

## we're now back on the host

# stop the container
lxc-stop my_container

We created the test folder so that we can set up a bind-mount, as explained in the next section.

Bind-mounting folders from the host to the container

Using bind-mounts comes in very handy, for example mounting your code directory (which is on the host) to inside the container. Since the UID mappings have already been set up you just need to add the following to your container config file in ~/.local/share/lxc/my_container/config:

lxc.mount.entry = /home/kobus/test home/my_user/test none bind 0 0

A few things to note here:

  • The first part of the line is the absolute path to the folder on the host system (my username is kobus, be sure to adjust it accordingly).
  • The second part is the path relative to the container’s rootfs - i.e. just make sure to omit the leading slash /.
  • You can add mounting options instead of the none. For instance, this can be used to make the mount read-only.

Close and save the file, and that’s it! If you start your container again the bind mount should be up, allowing you to easily share files between your host and container.


9fee12d 0.92.2
© 2022 Kobus van Schoor