Unprivileged Linux Containers in Slackware 15

Published: 2022-02-08 | Last Updated: 2022-02-17 | ~33 Minute Read

Table of Contents

Context

We have taken a look at setting up containers in past posts, however all containers in those posts have been running under the root user, which is not ideal.

From a security standpoint this means that if for some reason the processes running inside the container were to leak out to the host those processes would have root access on the host.

We will take a look at using unprivileged Linux containers on a Slackware 15.0 system.

Foreword

The latest stable Slackware version 15.0 has been recently released and I thought it would be a great opportunity to update some of the work in pasts posts to include both the latest version of Slackware as well as LXC.

In past LXC related posts the version being used was the older lxc-2.0.11. In Slackware 15.0 the provided LXC version is lxc-4.0.11 which is the latest available from upstream as of this writing.

Initial research

There are already amazing guides online on how to run unprivileged LXC containers on Slackware, however they are aimed towards the older (pre-4.x) versions of LXC.

They are however still very valid and I used them heavily as reference for this writing. I’ve listed them here for the readers' reference as well:

I ran into some issues during this process and found the following linuxquestions (LQ) threads on the topic useful:

I also created a thread myself since I was running into a problem, which turned out to be a very silly oversight on my end.

These links should provide the reader with some background on this process, it’s not actually that difficult but there are a lot of moving pieces.

Environment

I performed these steps in a fresh full installation of Slackware 15.0 in a VM with the following specs:

root@slack15:~# uname -a
Linux slack15.slack.lan 5.15.19 #1 SMP PREEMPT Wed Feb 2 01:50:51 CST 2022 x86_64 Intel Xeon E3-12xx v2 (Ivy Bridge) GenuineIntel GNU/Linux
lxcuser@slack15:~$ nproc
2
root@slack15:~# free -h
               total        used        free      shared  buff/cache   available
Mem:           1.9Gi       199Mi        63Mi       1.0Mi       1.7Gi       1.6Gi
Swap:             0B          0B          0B

lxcuser@slack15:~$ ls /var/log/packages/ | grep -v SBo | wc -l
1590

I installed two packages from slackbuilds that are not related to LXC, hence the grep -v above.

Initial steps

I followed part one in Chris' guide in order to setup the environment for unprivileged containers. Below are my notes of the process:

Step 1

From the guide’s Step 1. Setting up /etc/cgconfig.conf section:

root@slack15:~# cat /etc/cgconfig.conf
#
#  Copyright IBM Corporation. 2007
#
#  Authors:     Balbir Singh <balbir@linux.vnet.ibm.com>
#  This program is free software; you can redistribute it and/or modify it
#  under the terms of version 2.1 of the GNU Lesser General Public License
#  as published by the Free Software Foundation.
#
#  This program is distributed in the hope that it would be useful, but
#  WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
#
#group daemons/www {
#       perm {
#               task {
#                       uid = root;
#                       gid = webmaster;
#               }
#               admin {
#                       uid = root;
#                       gid = root;
#               }
#       }
#       cpu {
#               cpu.shares = 1000;
#       }
#}
#
#group daemons/ftp {
#       perm {
#               task {
#                       uid = root;
#                       gid = ftpmaster;
#               }
#               admin {
#                       uid = root;
#                       gid = root;
#               }
#       }
#       cpu {
#               cpu.shares = 500;
#       }
#}
#
#mount {
#       cpu = /mnt/cgroups/cpu;
#       cpuacct = /mnt/cgroups/cpuacct;
#}
group lxc { 
    perm {
        task {
            uid = lxcuser;
            gid = lxc;
        }
        admin {
          uid = lxcuser;
          gid = lxc;
        }
    }

  cpuset {
      cgroup.clone_children = 1;
      cpuset.mems = 0;
      cpuset.cpus = 0,1;
  }
  cpu {}
  cpuacct {}
  blkio {}
  memory { memory.use_hierarchy = 1; }
  devices {}
  freezer {}
  net_cls {}
  perf_event {}
  net_prio {}
  pids {}
}

Note that the user that you specify in the task and admin sections must already exist before running the cgconfigparser -l /etc/cgconfig.conf command. I was running into a very non-descriptive error which was fixed by creating the lxcuser.

The order in which to follow the steps in Chris' guide then should be Step 3. Setting up the user > Step 1. Setting up /etc/cgconfig.conf. This order should minimize the possibility of this error causing unwanted problems.

The error:

Error: failed to parse file /etc/cgconfig.conf
/usr/sbin/cgconfigparser; error loading /etc/cgconfig.conf: Have multiple paths for the same namespace
Failed to parse  /etc/cgconfig.conf

Step 2

From the guide’s Step 2. Setting up /etc/cgrules.conf section:

root@slack15:~# cat /etc/cgrules.conf
# /etc/cgrules.conf
#The format of this file is described in cgrules.conf(5)
#manual page.
#
# Example:
#<user>         <controllers>   <destination>
#@student       cpu,memory      usergroup/student/
#peter          cpu             test1/
#%              memory          test2/
lxcuser         *               lxc
# End of file

Step 3

From the guide’s Step 3. Setting up the user section:

I used the root user for most of the setup, so my commands here will have some differences from what is shown in the guide.

Create the non-privileged user:

root@slack15:~# groupadd lxc
root@slack15:~# useradd -g lxc -c "lxc user" -s /bin/bash -d /home/lxcuser lxcuser

Set the subordiate id ranges:

root@slack15:~# /usr/sbin/usermod --add-subuids 100000-165536 lxcuser
root@slack15:~# /usr/sbin/usermod --add-subgids 100000-165536 lxcuser

All done, check configuration

At this point part 1 of the guide is complete and a sanity check before moving on is a good idea.

Starting the rc.cgconfig and rc.cgred services in the specified order:

root@slack15:~# /etc/rc.d/rc.cgconfig start
root@slack15:~# /etc/rc.d/rc.cgred start

Output of the lscgroup command:

lxcuser@slack15:~$ lscgroup | grep "[^]]*/lxc"
cpuset:/lxc
cpu:/lxc
cpuacct:/lxc
blkio:/lxc
memory:/lxc
devices:/lxc
freezer:/lxc
net_cls:/lxc
perf_event:/lxc
net_prio:/lxc
pids:/lxc

Check subordinate id ranges:

lxcuser@slack15:~$ cat /etc/subuid /etc/subgid | grep lxcuser
lxcuser:100000:65537
lxcuser:100000:65537

Make the rc.cgconfig and rc.cgred services start at system boot time:

root@slack15:~# cat /etc/rc.d/rc.local
#!/bin/bash
#
# /etc/rc.d/rc.local:  Local system initialization script.
#
# Put any local startup commands in here.  Also, if you have
# anything that needs to be run at shutdown time you can
# make an /etc/rc.d/rc.local_shutdown script and put those
# commands in there.
# Start libcgroup services
if [ -x /etc/rc.d/rc.cgconfig -a -x /etc/rc.d/rc.cgred -a -d /sys/fs/cgroup ]; then
  echo "Starting libcgroup services"
  /etc/rc.d/rc.cgconfig start
  /etc/rc.d/rc.cgred start
fi

Checkpoint

This is where my configuration begins to change a bit from Chris' guide due to the newer LXC version and its updated configuration syntax. Note that you will get errors if you use the syntax provided in Chris' guide when using lxc-4.0.11.

In the introduction of part 2 in Chris' guide, two methods for creating containers are mentioned, the classic and modern method, however it seems like only the classic is described in detail so that’s what I will be showing here as well.

In the classic method we create a privileged container then convert that container to be unprivileged.

We will create a Slackware 15.0 based container with its networking ready to be used. Chris separates this into a part 3 in his guide, I’ll include it in the following sections.

Host networking

DHCP will be used for this post, we will create a network bridge on the host that containers will be able to use. Containers created this way will not be able to communicate outside the host without additional configuration.

Check the default configuration for the network bridge that will be created and edit it as you see fit. The relevant section in the /usr/libexec/lxc/lxc-net file:

# These can be overridden in /etc/default/lxc
#   or in /etc/default/lxc-net

USE_LXC_BRIDGE="true"
LXC_BRIDGE="lxcbr0"
LXC_BRIDGE_MAC="00:16:3e:00:00:00"
LXC_ADDR="10.0.3.1"
LXC_NETMASK="255.255.255.0"
LXC_NETWORK="10.0.3.0/24"
LXC_DHCP_RANGE="10.0.3.2,10.0.3.254"
LXC_DHCP_MAX="253"
LXC_DHCP_CONFILE=""
LXC_DHCP_PING="true"
LXC_DOMAIN=""
LXC_USE_NFT="true"

LXC_IPV6_ADDR=""
LXC_IPV6_MASK=""
LXC_IPV6_NETWORK=""
LXC_IPV6_NAT="false"

As the comment above states you can set custom values for your network bridge by creating the /etc/default/lxc-net with your preferred configuration.

I will use the default values for the network bridge, now simply edit the /etc/default/lxc-net file to contain the following:

root@slack15:~# cat /etc/default/lxc-net
USE_LXC_BRIDGE="true"

Bring up the network bridge:

/usr/libexec/lxc/lxc-net start

Check the network bridge status:

root@slack15:~# ifconfig lxcbr0
lxcbr0: flags=4099<UP,BROADCAST,MULTICAST>  mtu 1500
        inet 10.0.3.1  netmask 255.255.255.0  broadcast 10.0.3.255
        inet6 fe80::216:3eff:fe00:0  prefixlen 64  scopeid 0x20<link>
        ether 00:16:3e:00:00:00  txqueuelen 1000  (Ethernet)
        RX packets 5127  bytes 416117 (406.3 KiB)
        RX errors 0  dropped 0  overruns 0  frame 0
        TX packets 7343  bytes 73416078 (70.0 MiB)
        TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0

Make sure the bridge is brought up at system boot time:

root@slack15:~# cat /etc/rc.d/rc.local
#!/bin/bash
#
# /etc/rc.d/rc.local:  Local system initialization script.
#
# Put any local startup commands in here.  Also, if you have
# anything that needs to be run at shutdown time you can
# make an /etc/rc.d/rc.local_shutdown script and put those
# commands in there.
# Start libcgroup services
if [ -x /etc/rc.d/rc.cgconfig -a -x /etc/rc.d/rc.cgred -a -d /sys/fs/cgroup ]; then
  echo "Starting libcgroup services"
  /etc/rc.d/rc.cgconfig start
  /etc/rc.d/rc.cgred start
fi

# Start lxc-net bridge
if [ -x /usr/libexec/lxc/lxc-net ]; then
  echo "Starting lxc-net network bridge" 
  /usr/libexec/lxc/lxc-net start
fi

At this point the host is ready for unprivileged containers to be created, however the unprivileged user lxcuser is not yet allowed to create network devices on behalf of the containers that we will create in a moment.

To resolve this we need to create the /etc/lxc/lxc-usernet file with the following contents in it:

root@slack15:~/lxc# cat /etc/lxc/lxc-usernet 
lxcuser veth lxcbr0 10

The line added to the /etc/lxc/lxc-usernet file allows the lxcuser to create up to 10 veth type devices and add them to the lxcbr0 host network bridge. This file will have to be updated accordingly in case your host configuration is different from my example.

Create a privileged container

Now that we have the host configuration ready, we can move on to the container. We will be telling our lxc-create command to use a configuration file to define a few settings in our container upon creation.

The configuration file can be set to define several things however I will use a basic example. For additional details on what configuration flags can be used run man lxc.container.conf.

These are the contents of a simple configuration file called /root/lxc/default.conf:

root@slack15:~# cat /root/lxc/default.conf
lxc.net.0.type = veth
lxc.net.0.flags = up
lxc.net.0.link = lxcbr0

Note that this file can be placed in a location of your choice.

Create the container:

root@slack15:~# release=15.0 MIRROR=http://mirrors.us.kernel.org/slackware lxc-create -n c1 -t slackware -f /root/lxc/default.conf

Once the container has been created we can check its current status:

root@slack15:~# lxc-ls -f
NAME   STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED 
c1     STOPPED 0         -      -    -    false        

This shows us that the container is currently privileged, but was created successfully.

We should confirm that the container starts successfully:

root@slack15:~# lxc-start -n c1
root@slack15:~# lxc-ls -f
NAME   STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED 
c1     RUNNING 0         -      -    -    false        

Container networking

Now that we have the container started we can connect to it and update it’s network configuration.

There are two ways to connect to a LXC container, using lxc-attach and lxc-console. I prefer lxc-console since we can easily detach by pressing <Ctrl+a q>. Detaching when using lxc-attach is done by typing exit in the resulting console.

Connect to the c1 container:

root@slack15:~# lxc-console -n c1

Connected to tty 1
Type <Ctrl+a q> to exit the console, <Ctrl+a Ctrl+a> to enter Ctrl+a itself


Welcome to Linux 5.15.19 x86_64 (tty1)

c1 login: root
Password: 
Linux 5.15.19.
root@c1:~#

Once connected to the container’s console update the /etc/rc.d/rc.inet1.conf file. Update the USE_DHCP[0] field from USE_DHCP[0]="" to USE_DHCP[0]="yes", the updated section in the file should look like the following:

# IPv4 config options for eth0:
IPADDRS[0]=""
USE_DHCP[0]="yes"

Stop and start your container to confirm that the networking was setup correctly:

root@slack15:~# lxc-stop -n c1
root@slack15:~# lxc-start -n c1
root@slack15:~# lxc-ls -f
NAME   STATE   AUTOSTART GROUPS IPV4      IPV6 UNPRIVILEGED 
c1     RUNNING 0         -      10.0.3.73 -    false        

Convert privileged container to unprivileged

Now that we have successfully created a privileged container we can begin the process of converting it.

Remap uids and gids

Gather the necessary files to build the tools for remapping, from Serge Hallyn’s nsexec files:

I had initially made a mistake when downloading these files and got the wrong file by right clicking on the name of the file rather than on the far right download icon. This was suggested by a user in LQ on the thread I created.

Once we have the files correctly downloaded we can build the uidmapshift.c tool that we will need:

root@slack15:~# ls
container-userns-convert*  lxc/  uidmapshift.c
root@slack15:~# gcc -o uidmapshift uidmapshift.c
root@slack15:~# ls
container-userns-convert*  lxc/  uidmapshift*  uidmapshift.c

Edit the container-userns-convert script as suggested by Chris' guide in Step 2 - Remap uids & gids, update the uidmapshift -b /var/lib/lxc/$container/rootfs 0 $uid $range line to be ./uidmapshift -b /var/lib/lxc/$container/rootfs 0 $uid $range.

Check the current permissions of the c1 container:

root@slack15:~# ls -l /var/lib/lxc/c1/
total 8
-rw-r-----  1 root root 1602 Feb  7 01:09 config
drwxr-xr-x 21 root root 4096 Feb  7 18:34 rootfs/
root@slack15:~# ls -l /var/lib/lxc/c1/rootfs
total 84
drwxr-xr-x  2 root root  4096 Feb 13  2021 bin/
drwxr-xr-x  2 root root  4096 Oct  6  1997 boot/
drwxr-xr-x  4 root root  4096 Feb  7 01:09 dev/
drwxr-xr-x 32 root root  4096 Feb  7 18:35 etc/
drwxr-xr-x  2 root root  4096 Oct  6  1997 home/
drwxr-xr-x  7 root root  4096 Sep  3 22:08 lib/
drwxr-xr-x  6 root root 12288 Feb  7 01:09 lib64/
drwxr-xr-x 16 root root  4096 Feb  7 01:07 media/
drwxr-xr-x 10 root root  4096 Sep 25  2006 mnt/
drwxr-xr-x  2 root root  4096 Jun 10  2007 opt/
drwxr-xr-x  2 root root  4096 Oct  6  1997 proc/
drwx--x---  2 root root  4096 Feb  7 01:13 root/
drwxr-xr-x  8 root root  4096 Feb  7 18:35 run/
drwxr-xr-x  2 root root  4096 Feb  7 01:09 sbin/
drwxr-xr-x  2 root root  4096 Apr  7  2007 srv/
drwxr-xr-x  2 root root  4096 May 11  2004 sys/
drwxrwxrwt  5 root root  4096 Feb  7 08:23 tmp/
drwxr-xr-x 15 root root  4096 Feb 13  2021 usr/
drwxr-xr-x 11 root root  4096 Feb  7 01:10 var/

Remap uids and gids in the container:

root@slack15:~# ./container-userns-convert c1 100000
Container c1 has been converted

One way to confirm that the conversion was successful is to check the container permissions again, for our c1 container:

root@slack15:~# ls -l /var/lib/lxc/c1/
total 8
-rw-r-----  1 root   root   1650 Feb  8 00:59 config
drwxr-xr-x 21 100000 100000 4096 Feb  7 23:52 rootfs/
root@slack15:~# ls -l /var/lib/lxc/c1/rootfs/
total 76
drwxr-xr-x  2 100000 100000 4096 Feb 13  2021 bin/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 boot/
drwxr-xr-x  4 100000 100000 4096 Feb  7 22:53 dev/
drwxr-xr-x 31 100000 100000 4096 Feb  7 23:52 etc/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 home/
drwxr-xr-x  7 100000 100000 4096 Sep  3 22:08 lib/
drwxr-xr-x  6 100000 100000 4096 Feb  7 22:53 lib64/
drwxr-xr-x 16 100000 100000 4096 Feb  7 22:51 media/
drwxr-xr-x 10 100000 100000 4096 Sep 25  2006 mnt/
drwxr-xr-x  2 100000 100000 4096 Jun 10  2007 opt/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 proc/
drwx--x---  2 100000 100000 4096 Feb  7 23:48 root/
drwxr-xr-x  8 100000 100000 4096 Feb  7 23:52 run/
drwxr-xr-x  2 100000 100000 4096 Feb  7 22:53 sbin/
drwxr-xr-x  2 100000 100000 4096 Apr  7  2007 srv/
drwxr-xr-x  2 100000 100000 4096 May 11  2004 sys/
drwxrwxrwt  4 100000 100000 4096 Feb  7 23:34 tmp/
drwxr-xr-x 15 100000 100000 4096 Feb 13  2021 usr/
drwxr-xr-x 11 100000 100000 4096 Feb  7 23:34 var/

The remapping process was successful, now we want to make the container available to the non-privileged lxcuser user.

Assign proper ownership and permissions

Taking a look at Stéphane Graber’s guide, we can see all of the equivalent paths from privileged to unprivileged containers:

/etc/lxc/lxc.conf => ~/.config/lxc/lxc.conf
/etc/lxc/default.conf => ~/.config/lxc/default.conf
/var/lib/lxc => ~/.local/share/lxc
/var/lib/lxcsnaps => ~/.local/share/lxcsnaps
/var/cache/lxc => ~/.cache/lxc

The relevant path for us will be ~/.local/share/lxc, create it as the lxcuser and then exit back to the root user:

root@slack15:~# su - lxcuser
lxcuser@slack15:~$ mkdir -p ~/.local/share/lxc
lxcuser@slack15:~$ chmod a+x /home/lxcuser
lxcuser@slack15:~$
lxcuser@slack15:~$ exit
logout
root@slack15:~#

Copy the container into the non-privileged user’s newly created path ~/.local/share/lxc as the root user:

root@slack15:~# cp -a /var/lib/lxc/c1 /home/lxcuser/.local/share/lxc/

Now that we copied the container, let’s check its permissions:

root@slack15:~# ls -l /home/lxcuser/.local/share/lxc/
total 20
drwxrwx--- 3 root    root 4096 Feb  7 22:53 c1/
root@slack15:~# ls -l /home/lxcuser/.local/share/lxc/c1
total 8
-rw-r-----  1 root   root   1650 Feb  8 00:59 config
drwxr-xr-x 21 100000 100000 4096 Feb  7 23:52 rootfs/
root@slack15:~# ls -l /home/lxcuser/.local/share/lxc/c1/rootfs/
total 76
drwxr-xr-x  2 100000 100000 4096 Feb 13  2021 bin/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 boot/
drwxr-xr-x  4 100000 100000 4096 Feb  7 22:53 dev/
drwxr-xr-x 31 100000 100000 4096 Feb  7 23:52 etc/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 home/
drwxr-xr-x  7 100000 100000 4096 Sep  3 22:08 lib/
drwxr-xr-x  6 100000 100000 4096 Feb  7 22:53 lib64/
drwxr-xr-x 16 100000 100000 4096 Feb  7 22:51 media/
drwxr-xr-x 10 100000 100000 4096 Sep 25  2006 mnt/
drwxr-xr-x  2 100000 100000 4096 Jun 10  2007 opt/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 proc/
drwx--x---  2 100000 100000 4096 Feb  7 23:48 root/
drwxr-xr-x  8 100000 100000 4096 Feb  7 23:52 run/
drwxr-xr-x  2 100000 100000 4096 Feb  7 22:53 sbin/
drwxr-xr-x  2 100000 100000 4096 Apr  7  2007 srv/
drwxr-xr-x  2 100000 100000 4096 May 11  2004 sys/
drwxrwxrwt  4 100000 100000 4096 Feb  7 23:34 tmp/
drwxr-xr-x 15 100000 100000 4096 Feb 13  2021 usr/
drwxr-xr-x 11 100000 100000 4096 Feb  7 23:34 var/

Let’s update file permissions for /home/lxcuser/.local/share/lxc/c1 and /home/lxcuser/.local/share/lxc/c1/config to make sure the lxcuser has proper ownership.

Note that the permissions for the /home/lxcuser/.local/share/lxc/c1/rootfs directory and subdirectories inside it need to remain unchanged.

root@slack15:~# chown lxcuser:lxc /home/lxcuser/.local/share/lxc/c1 /home/lxcuser/.local/share/lxc/c1/config

Now we confirm that the permissions were set as expected:

root@slack15:~# ls -l /home/lxcuser/.local/share/lxc/
total 20
drwxrwx--- 3 lxcuser lxc 4096 Feb  7 22:53 c1/
root@slack15:~# ls -l /home/lxcuser/.local/share/lxc/c1
total 8
-rw-r-----  1 lxcuser lxc    1650 Feb  8 00:59 config
drwxr-xr-x 21  100000 100000 4096 Feb  7 23:52 rootfs/
root@slack15:~# ls -l /home/lxcuser/.local/share/lxc/c1/rootfs/
total 76
drwxr-xr-x  2 100000 100000 4096 Feb 13  2021 bin/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 boot/
drwxr-xr-x  4 100000 100000 4096 Feb  7 22:53 dev/
drwxr-xr-x 31 100000 100000 4096 Feb  7 23:52 etc/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 home/
drwxr-xr-x  7 100000 100000 4096 Sep  3 22:08 lib/
drwxr-xr-x  6 100000 100000 4096 Feb  7 22:53 lib64/
drwxr-xr-x 16 100000 100000 4096 Feb  7 22:51 media/
drwxr-xr-x 10 100000 100000 4096 Sep 25  2006 mnt/
drwxr-xr-x  2 100000 100000 4096 Jun 10  2007 opt/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 proc/
drwx--x---  2 100000 100000 4096 Feb  7 23:48 root/
drwxr-xr-x  8 100000 100000 4096 Feb  7 23:52 run/
drwxr-xr-x  2 100000 100000 4096 Feb  7 22:53 sbin/
drwxr-xr-x  2 100000 100000 4096 Apr  7  2007 srv/
drwxr-xr-x  2 100000 100000 4096 May 11  2004 sys/
drwxrwxrwt  4 100000 100000 4096 Feb  7 23:34 tmp/
drwxr-xr-x 15 100000 100000 4096 Feb 13  2021 usr/
drwxr-xr-x 11 100000 100000 4096 Feb  7 23:34 var/

Add read and execute permissions to the /home/lxcuser/.local/share/lxc/c1 directory:

root@slack15:~# chmod a+rx /home/lxcuser/.local/share/lxc/c1

And one last check:

root@slack15:~# ls -l /home/lxcuser/.local/share/lxc/
total 20
drwxrwxr-x 3 lxcuser lxc 4096 Feb  7 22:53 c1/
root@slack15:~# ls -l /home/lxcuser/.local/share/lxc/c1
total 8
-rw-r-----  1 lxcuser lxc    1650 Feb  8 00:59 config
drwxr-xr-x 21  100000 100000 4096 Feb  7 23:52 rootfs/
root@slack15:~# ls -l /home/lxcuser/.local/share/lxc/c1/rootfs/
total 76
drwxr-xr-x  2 100000 100000 4096 Feb 13  2021 bin/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 boot/
drwxr-xr-x  4 100000 100000 4096 Feb  7 22:53 dev/
drwxr-xr-x 31 100000 100000 4096 Feb  7 23:52 etc/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 home/
drwxr-xr-x  7 100000 100000 4096 Sep  3 22:08 lib/
drwxr-xr-x  6 100000 100000 4096 Feb  7 22:53 lib64/
drwxr-xr-x 16 100000 100000 4096 Feb  7 22:51 media/
drwxr-xr-x 10 100000 100000 4096 Sep 25  2006 mnt/
drwxr-xr-x  2 100000 100000 4096 Jun 10  2007 opt/
drwxr-xr-x  2 100000 100000 4096 Oct  6  1997 proc/
drwx--x---  2 100000 100000 4096 Feb  7 23:48 root/
drwxr-xr-x  8 100000 100000 4096 Feb  7 23:52 run/
drwxr-xr-x  2 100000 100000 4096 Feb  7 22:53 sbin/
drwxr-xr-x  2 100000 100000 4096 Apr  7  2007 srv/
drwxr-xr-x  2 100000 100000 4096 May 11  2004 sys/
drwxrwxrwt  4 100000 100000 4096 Feb  7 23:34 tmp/
drwxr-xr-x 15 100000 100000 4096 Feb 13  2021 usr/
drwxr-xr-x 11 100000 100000 4096 Feb  7 23:34 var/

The permissions are now set successfully.

Update unprivileged container configuration

We also need to update the /home/lxcuser/.local/share/lxc/c1/config file to reflect the unprivileged nature of this container.

From this point forward we no longer need to use the root user shell, so we switch to the lxcuser’s shell:

root@slack15:~# su - lxcuser

Update the /home/lxcuser/.local/share/lxc/c1/config file:

lxcuser@slack15:~$ cd .local/share/lxc/c1/
lxcuser@slack15:~/.local/share/lxc/c1$ vim config

The lxc.rootfs.path field seems to be duplicated in the container /home/lxcuser/.local/share/lxc/c1/config file, leave only one of the two entries.

Update the lxc.rootfs.path = dir:/var/lib/lxc/c1/rootfs entry to be lxc.rootfs.path = dir:/home/lxcuser/.local/share/lxc/c1/rootfs.

Update the lxc.mount.fstab = /var/lib/lxc/c1/rootfs/etc/fstab entry to be lxc.mount.fstab = /home/lxcuser/.local/share/lxc/c1/rootfs/etc/fstab.

Update the lines that were inserted by the uid and gid remap process with the new LXC syntax:

This uses the old LXC configuration syntax:

lxc.id_map = u 0 100000 10000
lxc.id_map = g 0 100000 10000

Update to this:

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

And finally add the following line for the sys and proc special file systems to be properly mounted on container start:

lxc.mount.auto = proc:mixed sys:ro cgroup

The full /home/lxcuser/.local/share/lxc/c1/config file should look like so:

lxcuser@slack15:~/.local/share/lxc/c1$ cat config
# Template used to create this container: /usr/share/lxc/templates/lxc-slackware
# Parameters passed to the template:
# Template script checksum (SHA-1): c33857679eb503348e352f7fa99e53356714c8a8
# For additional config options, please look at lxc.container.conf(5)

# Uncomment the following line to support nesting containers:
#lxc.include = /usr/share/lxc/config/nesting.conf
# (Be aware this has security implications)

lxc.net.0.type = veth
lxc.net.0.flags = up
lxc.net.0.link = lxcbr0

# Adding a . for LXC 4.0.x
lxc.uts.name = c1

# Adding .fstab for LXC 4.0.x
lxc.mount.fstab = /home/lxcuser/.local/share/lxc/c1/rootfs/etc/fstab

# Added .max and .path for LXC 4.0.x
lxc.tty.max = 4
lxc.pty.max = 1024
lxc.rootfs.path = dir:/home/lxcuser/.local/share/lxc/c1/rootfs

lxc.cgroup.devices.deny = a
# /dev/null and zero
lxc.cgroup.devices.allow = c 1:3 rwm
lxc.cgroup.devices.allow = c 1:5 rwm
# consoles
lxc.cgroup.devices.allow = c 5:1 rwm
lxc.cgroup.devices.allow = c 5:0 rwm
lxc.cgroup.devices.allow = c 4:0 rwm
lxc.cgroup.devices.allow = c 4:1 rwm
# /dev/{,u}random
lxc.cgroup.devices.allow = c 1:9 rwm
lxc.cgroup.devices.allow = c 1:8 rwm
lxc.cgroup.devices.allow = c 136:* rwm
lxc.cgroup.devices.allow = c 5:2 rwm
# rtc
lxc.cgroup.devices.allow = c 254:0 rwm

# we don't trust even the root user in the container, better safe than sorry.
# comment out only if you know what you're doing.
lxc.cap.drop = sys_module mknod mac_override mac_admin sys_time setfcap setpcap

# you can try also this alternative to the line above, whatever suits you better.
# lxc.cap.drop=sys_admin
lxc.idmap = u 0 100000 10000
lxc.idmap = g 0 100000 10000
lxc.mount.auto = proc:mixed sys:ro cgroup

Update container mount points

One last step is required for the conversion from privileged to unprivileged container to be complete.

Remove following lines from the /home/lxcuser/.local/share/lxc/c1/rootfs/etc/fstab file as the root user:

none /var/lib/lxc/c1/rootfs/proc    proc   defaults 0 0
none /var/lib/lxc/c1/rootfs/sys     sysfs  defaults 0 0

Exit the lxcuser shell and as root:

lxcuser@slack15:~/.local/share/lxc/c1$ exit
logout
root@slack15:~/lxc# vim /home/lxcuser/.local/share/lxc/c1/rootfs/etc/fstab 

The final file should look like so:

root@slack15:~/lxc# cat /home/lxcuser/.local/share/lxc/c1/rootfs/etc/fstab
lxcpts /var/lib/lxc/c1/rootfs/dev/pts devpts defaults,newinstance 0 0
none /dev/shm tmpfs defaults 0 0
none /run tmpfs defaults,mode=0755 0 0

Now we can go back into the lxcuser shell:

root@slack15:~/lxc# su - lxcuser
lxcuser@slack15:~$

The conversion from privileged to unprivileged container is now complete for the c1 container.

Start your new unprivileged container

Before we start our unprivileged container we need to make sure that we’re logged in as the lxcuser and check the state of our container:

lxcuser@slack15:~$ lxc-ls -f
NAME     STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED 
c1       STOPPED 0         -      -    -    true         

Excellent, now start the c1 container:

lxcuser@slack15:~$ lxc-start -n c1
lxcuser@slack15:~$ lxc-ls -f
NAME     STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
c1       RUNNING 0         -      10.0.3.164 -    true         
ds-gitea RUNNING 0         -      -          -    false        

Success! We now have an unprivileged Slackware 15.0 based container running on our Slackware 15.0 host system!

Check your work

Now that we have completed our conversion we can confirm that the processes on the host are running as an unprivileged user.

Let’s take a look at a privileged container first:

root@slack15:~/lxc# lxc-ls -f
NAME   STATE   AUTOSTART GROUPS IPV4 IPV6 UNPRIVILEGED 
test1   STOPPED 0         -      -    -    false        
root@slack15:~/lxc# lxc-start -n test1 
root@slack15:~/lxc# lxc-ls -f
NAME   STATE   AUTOSTART GROUPS IPV4      IPV6 UNPRIVILEGED 
test1  RUNNING 0         -      10.0.3.31 -    false        

Now we can check how this container is running:

root@slack15:~/lxc# ps aux | grep -E "USER|monitor"
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root      1310  0.0  0.1   8296  2864 ?        Ss   03:02   0:00 [lxc monitor] /var/lib/lxc test1

The above shows us that the test1 container is running as root, again this means that if a process leaks from the container’s namespace environment, the container process will have root level access to the host.

After we have created and started our c1 container this is what we see in the process tree:

root@slack15:~/lxc# ps aux | grep -E "USER|monitor"
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
lxcuser    741  0.0  0.1   8296  3040 ?        Ss   02:45   0:00 [lxc monitor] /home/lxcuser/.local/share/lxc c1
root      1310  0.0  0.1   8296  2864 ?        Ss   03:02   0:00 [lxc monitor] /var/lib/lxc test1

We see that the c1 container, and consequently all of it’s underlying processes, is running as the lxcuser on the host system. In case of a process leaking out of the container, the result would be a regular non privileged user on the host system.

Cleaning up

After confirming that your container works as expected and everything is in order you can delete the original container created by the root user. In our case for the c1 privileged container:

root@slack15:~# cd /var/lib/lxc/
root@slack15:/var/lib/lxc# ls
c1/
root@slack15:/var/lib/lxc# rm -rf c1/
root@slack15:/var/lib/lxc# ls

Troubleshooting

In case you run into any trouble while starting the container you can add the --logfile and --logpriority flags like so:

lxc-start -F -n c1 --logfile=c1.log --logpriority=DEBUG

From the above command the c1.log file will be created in the current directory, this file was very handy during my journey.

Automating the process

I like the idea of containers because they’re small, portable and fast to deploy. All of these traits are true for privileged containers, unprivileged containers though seem to take a little more work in Slackware. I think this is the perfect use case for a small script that can do the heavy lifting of us.

In the classic method of deploying an unprivileged container this is the current workflow:

  1. Create privileged container
  2. Convert privileged container
  3. Delete privileged container

Although it seems like “only” three steps there are many places where one can make a mistake resulting in more time spent troubleshooting than say, setting up a regular virtual machine.

This script assumes that you have successfully completed all steps in part 1 from Chris' guide and worked through the steps described in this post up until the Host networking section. The script will not change any configuration on the host.

Once you have the host ready, check the script’s help message by running it without parameters:

root@slack15:~# ./create-lxc
The create-lxc script takes the following options:
    -r  Set Slackware release to use, if not set use the default specified in /usr/share/lxc/templates/lxc-slackware
    -m  Set mirror to use, if not set use the default specified in /usr/share/lxc/templates/lxc-slackware
    -n  Set container name to use, this parameter is required
    -t  Set template to use, if not set use /usr/share/lxc/templates/lxc-slackware
    -f  Set config file to use
    -u  Set non privileged user that will own the resulting container, this parameter is required
    -g  Set user group that will be used to set the necessary permissions, this parameter is required
    -d  Create the container with a basic DHCP configuration
    -h  Print help options and exit

Creating an unprivileged container:

root@slack15:~# ./create-lxc -dr 15.0 -n test1 -u lxcuser -g lxc -f /root/lxc/default.conf

The above command will run the create-lxc script which will execute the following actions:

Once the script finishes the above tasks the user can expect the following output, as the lxcuser:

lxcuser@slack15:~$ lxc-ls -f
NAME     STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
test1    STOPPED 0         -      -          -    true         

Start the container as usual:

lxcuser@slack15:~$ lxc-start -n test1
lxcuser@slack15:~$ lxc-ls -f
NAME     STATE   AUTOSTART GROUPS IPV4       IPV6 UNPRIVILEGED 
test1    RUNNING 0         -      10.0.3.65  -    true         

You can find the source of the script in my github repository.

Conclusion

I hope this brings some clarity into how to create unprivileged containers in Slackware 15.0. I look forward to setting up some containers for my services using this technology.

Have a comment on one of my posts? Start a discussion in my public inbox by sending an email to ~grokkingnix/blog@lists.sr.ht [mailing list etiquette] [mailing list archive]


Posts from blogs I follow:

Status update, February 2022

Hello once again! Another month of free software development goes by with lots of progress in all respects. I will open with some news about godocs.io: version 1.0 of our fork of gddo has been released! Big thanks to Adnan Maolood for his work on this. I’m v…

via Drew DeVault's blog February 15, 2022
Introducing a Falkon extension RSS Finder

This weekend I decided to semi automate the process of searching for RSS feeds on websites while using Falkon web brosers. Many websites provide RSS feeds but do not provide any visible link or icon to access them (eg. many Wordpress based sites) and I ha…

via My land January 23, 2022
A warning to business owners and managers, you are a big part of the problem!

In my last couple of articles, mainly So-called modern web developers are the culprits and Is the madness ever going to end? I have written about some of the major problems with so-called modern web development and I have addressed the issues to the devel…

via unixsheikh.com January 13, 2022

Generated by openring