Unprivileged Linux Containers in Slackware 15
Published: 2022-02-08 | Last Updated: 2022-02-17 | ~33 Minute Read
Table of Contents
- Context
- Foreword
- Initial research
- Environment
- Initial steps
- Checkpoint
- Host networking
- Create a privileged container
- Convert a privileged container to unprivileged
- Start your new unprivileged container
- Check your work
- Cleaning up
- Troubleshooting
- Automating the process
- Conclusion
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:
- Chris Willing’s Slackware specific guide to Unprivileged containers in Slackware
- Stéphane Graber’s general LXC Introduction to unprivileged containers
I ran into some issues during this process and found the following linuxquestions (LQ) threads on the topic useful:
- General LXC configuration on Slackware thread
cgconfig.conf
andrc.cgconfig
configuration related thread- General LXC configuration and permission related thread
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.
uidmapshift
compilation errors thread
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:
- Create privileged container
- Convert privileged container
- 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:
- Create a privileged container based on Slackware
15.0
- Container’s name will be
test1
- Use the default
slackpkg
mirror defined in/usr/share/lxc/templates/lxc-slackware
- The
/root/lxc/default.conf
file will be used as a custom LXC configuration file - A basic DHCP configuration will be set in the container i.e.
USE_DHCP[0]="yes"
- Once the
test1
privileged container is created, convert it to unprivileged and transfer it to thelxcuser
’s environment - Download and build tools required for
uid
andgid
remapping if necessary, search path is/tmp/create-lxc-tmpdir
- Remap
uid
s andgid
s - Ownership of the relevant container files will be granted to the
lxcuser
user and thelxc
group. - Update
test1
configuration from privileged to unprivileged - Update
test1
mount point configuration - The original
test1
privileged container will then be deleted.
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.