unprivileged lxc

# Today I tried setting up an lxc container on my Debian, that I can start
# without root access. Here are the problems I had to solve...

# First some config as root.

# I want to use the lxc bridge:

sudo -s
MY_USER_NAME=neels
MY_USER_ID=1000

cat >> /etc/lxc/default.conf <<END
lxc.network.type = veth
lxc.network.0.link = lxcbr0
lxc.network.0.flags = up
lxc.network.0.hwaddr = 00:16:3e:xx:xx:xx
END

# (Got this from the archlinux wiki, but had to change lxc.net.* to lxc.network.*)

# We also need to define IP addresses for lxcbr0.
# You certainly already have a /etc/default/lxc-net file, can just edit that.

cat >> /etc/default/lxc-net <<END
USE_LXC_BRIDGE="true"
LXC_BRIDGE="lxcbr0"
LXC_ADDR="10.23.42.1"
LXC_NETMASK="255.255.255.0"
LXC_NETWORK="10.23.42.0/24"
LXC_DHCP_RANGE="10.23.42.2,10.23.42.254"
LXC_DHCP_MAX="253"
END

# I need to allow my user to use the lxc net, 2 is the amount of interfaces
# allowed:

echo $MY_USER_NAME veth lxcbr0 2 > /etc/lxc/lxc-usernet

# To manage subusers (IIUC), there needs to be some quota allowance:

cat > /etc/systemd/system/user-${MY_USER_ID}.slice <<END
[Unit]
Description=$MY_USER_ID user.slice

[Slice]
MemoryAccounting=true
MemoryLimit=3000M
END

systemctl daemon-reload

# And we have to enable this feature here:

echo "kernel.unprivileged_userns_clone=1" > /etc/sysctl.d/80-lxc-userns.conf
sysctl --system

# It appears that some of these changes need an apparmor restart.

systemctl restart apparmor

# The remaining config is as your own user, exit the sudo.

exit

# And actually the user.slice stuff may need a re-login of your current user.
# So log out completely and log back in now.

# There needs to be config specific to the user in

mkdir -p ~/.config/lxc

# Normal users cannot write to /var/lib/, so configure a custom container
# storage:

MY_LXC_PATH=$HOME/lxc
mkdir -p $MY_LXC_PATH
echo lxc.lxcpath = $MY_LXC_PATH > ~/.config/lxc/lxc.conf

# Make sure there is an /etc/subuid and /etc/subgid for your user, for me it
# was already configured like this:

cat /etc/subuid
my_user_name:100000:65536

cat /etc/subgid
my_user_name:100000:65536

# Tell lxc to use those:

cat > ~/.config/lxc/default.conf <<END
lxc.include = /etc/lxc/default.conf
lxc.id_map = u 0 100000 65536
lxc.id_map = g 0 100000 65536
END

# Finally we're in a position to create an lxc-container:

lxc-create -n foo -t download -- -d debian -r buster -a amd64

lxc-start -n foo

# If that worked, then this should give you a root prompt, where you can setup
# a user and password (the password also important for sudo later).

lxc-attach -n foo

useradd -m -s /bin/bash user
passwd user
exit

# From this point on you can login as the new user using lxc-console:

lxc-console -n foo

#login: user
#Password: *******

#$

# Obviously "nothing" is installed, so that's not much use yet.
# Exit again: as the lxc-console tells you, close it with CTRL-a q

# Even if you go in with lxc-attach, there is no network.
# Why: lxcbr0 needs NAT, and NAT needs forwarding and masquerading.
# You may need to re-do these steps after each reboot:

sudo sh -c "echo 1 > /proc/sys/net/ipv4/ip_forward"
sudo iptables -t nat -A POSTROUTING -s 10.23.42.0/24 -o eth0 -j MASQUERADE

# 10.23.42.0 matches the IP range configured in /etc/default/lxc-net.
# eth0 should be the network device that gets internet uplink.

# Now that the network is up, I want to install a bunch of stuff. I have a list
# of packages in a file. Make it available in the lxc, by copying this file
# from outside:

cp ~/lxc_packages $HOME/lxc/foo/rootfs/tmp/

# And let's go back in as root with lxc-attach.

lxc-attach -n foo

apt-get update

# Wait, still not working? Then likely /etc/resolv.conf is empty, no DNS.
# (This sometimes happened to me, I'm not sure why. Normally it's fine.)
# The typical fallback for these situations...

echo nameserver 9.9.9.9 > /etc/resolv.conf

# Once forwarding and DNS work out, install.

apt-get update
apt-get install --no-install-recommends $(cat /tmp/lxc_packages)

# Among other things, I install:
# dnsutils iptables less openssh-server openssh-client sudo

# So add the user to the sudo group, and make sure there is a password.

gpasswd -a user sudo

# Now we could leave lxc-attach for good and only attach via
# lxc-console, or even via ssh, and then do all admin via sudo.

exit

# To login via ssh, we need to know the IP address.

lxc-ls --fancy
#NAME STATE   AUTOSTART GROUPS IPV4         IPV6
#foo  RUNNING 0         -      10.23.42.175 -

ssh user@10.23.42.175
exit

# However, let's make it so that this container always gets the same IP
# address.

lxc-stop -n foo

sudo -s
echo dhcp-host=foo,10.23.42.123 >> /etc/lxc/dnsmasq.conf
echo LXC_DHCP_CONFILE=/etc/lxc/dnsmasq.conf >> /etc/default/lxc-net
systemctl restart lxc-net
exit

lxc-start -n foo
lxc-ls --fancy
#NAME STATE   AUTOSTART GROUPS IPV4         IPV6
#foo  RUNNING 0         -      10.23.42.123 -

# Which enables things like

cat >> ~/.ssh/config <<END
Host foo
Hostname 10.23.42.123
User user
END

ssh-copy-id foo
ssh foo
exit

# If I remove the container, it would be gone completely and for good,
# so I don't.

#lxc-destroy -n foo

# The big advantage of unprivileged containers is that all user- and group-ids
# get mangled, so your host system is pretty safe. And one big drawback is that
# all user- and group-ids get mangled. So even if your normal user can create,
# destroy, start and stop the container, your user can *not* write to the
# container's rootfs directly; why? Because your user ID is 1000, and even if
# inside the container it is also 1000, outside it gets mangled to 101000
# instead. So you will not be able to easily create some bit of file system
# where the user ID in the lxc container matches your outside user id.

# But if I want to conveniently share files across the file systems, I can
# (besides using scp) create a group matching the gid inside the container, and
# mount in a dir belonging to that group. Check that group id:

ls -al ~/lxc/foo/rootfs/home/
#drwxr-xr-x  4 101000 101000 4096 user/

# It will always turn out as 101000, because 100000 is the gid mangling offset
# set up above, and debian starts to assign user/group IDs starting with 1000.
# Outside, on the host system, create a name for that group and add yourself:

sudo groupadd -g 101000 lxcuser
sudo gpasswd -a $USER lxcuser

# Set sticky group permissions for that group on a shared dir:

mkdir ~/lxc/share
sudo chgrp lxcuser ~/lxc/share
sudo chmod g+rwxs ~/lxc/share
sudo setfacl -d -m group:lxcuser:rwx ~/lxc/share

# Mount this into the container.

echo lxc.mount.entry = ~/lxc/share share none bind,create=dir 0 0 >> ~/lxc/foo/config

# Restart to take effect.

lxc-stop -n foo
lxc-start -n foo

# Let me demonstrate.

touch ~/lxc/share/hello_container
ls -al ~/lxc/share
#-rw-rw-r-- 1 neels lxcuser    0 hello_container

lxc-console -n foo
#login: user

touch /share/hello_host

ls -al /share
#-rw-rw-r--  1 nobody user    0 hello_container
#-rw-rw-r--  1 user   user    0 hello_host

rm /share/hello_container
# The lxc user was allowed to remove a file created by the outside user.

# CTRL-a q  (exit lxc-console)

ls -al ~/lxc/share
#-rw-rw-r-- 1 101000 lxcuser    0 hello_host

rm ~/lxc/share/hello_host
# The outside user was allowed to remove a file created by the lxc user.

# That's all, enjoy!
# Copyright 2018 neels@hofmeyr.de
# License: CreativeCommons BY-NC-SA
login 2018-11-15 04:59