Provisioning and usage of unprivileged LXC containers via indirect login or script
As I've discovered, managing LXC containers is fairly straightforward, but when building out a system for provisioning out user maintained instances of NodeBB, it was imperative that unprivileged LXC containers were used, so that in the event of shell breakout from NodeBB followed by privilege escalation of the saas
user, the root
user in the LXC container would only be an unprivileged user on the host machine.
During the course of development, I ran into numerous blockers when it came to managing LXC containers in unexpected circumstances. Namely:
- Using LXC in a subshell is not directly supported. This usually happens under one of the following two circumstances:
- After switching users via
su
or executinglxc-*
commands as another user viasudo
- Executing
lxc-*
commands via a program, application, or script. In my case, a Node.js application.
- After switching users via
Get familiar with LXC
Take a gander through this tutorial so you get a better feel of the basic LXC commands: lxc-start
, lxc-stop
, lxc-destroy
, and lxc-create
.
We're using a DigitalOcean Ubuntu 14.04 droplet, and we'll be creating Ubuntu 14.04 containers.
Start by logging into your host droplet.
Allow creation of unprivileged containers
Some prep work needs to be done so that unprivileged containers can be created:
echo "lxc.id_map = u 0 100000 65536" > ~/.config/lxc/default.conf
echo "lxc.id_map = g 0 100000 65536" >> ~/.config/lxc/default.conf
echo "lxc.network.type = veth" >> ~/.config/lxc/default.conf
echo "lxc.network.link = lxcbr0" >> ~/.config/lxc/default.conf
# As root...
echo "saas veth lxcbr0 10" >> /etc/lxc/lxc-usernet
Distilled from the guide provided by Stéphane Graber's blog: https://www.stgraber.org/2014/01/17/lxc-1-0-unprivileged-containers/
Now you'll be able to use all of the lxc-*
commands as a non-root user. Nice!
lxc-*
commands
Allow your script to use I spent most of a day trying to figure out why my unprivileged user couldn't use lxc-*
commands, even though I ran through the steps above.
As it turns out, this was because I was logging into root
via ssh, and then switching to the unprivileged user (in my case saas
) by running su - saas
. This issue contains lots of cgroups-type information that went way over my head. Long story short, when you log in via ssh or directly via console, you are granted a cgroup, but not when you switch users afterwards, so the subshell I was using could not start or list LXC containers. I'd get fun little errors like:
lxc: utils.c: mkdir_p: 193 Permission denied - failed to create directory '/run/user/0/lock/'
lxc: lxclock.c: lxclock: 249 Error opening /tmp/1000/lxc//home/saas/.local/share/lxc/foobar
447: error creating container foobar
and
lxc_container: cgmanager.c: lxc_cgmanager_create: 299 call to cgmanager_create_sync failed: invalid request
lxc_container: cgmanager.c: lxc_cgmanager_create: 301 Failed to create hugetlb:foobar
lxc_container: cgmanager.c: cgm_create: 646 Error creating cgroup hugetlb:foobar
lxc_container: start.c: lxc_spawn: 861 failed creating cgroups
lxc_container: start.c: __lxc_start: 1080 failed to spawn 'foobar'
lxc_container: lxc_start.c: main: 342 The container failed to start.
lxc_container: lxc_start.c: main: 346 Additional information can be obtained by setting the --logfile and --logpriority options.
If you are seeing these kinds of errors, the most direct solution is to log into the user that is executing the
lxc-*
commands directly (via ssh or console). If your workflow does not allow this, continue reading.
For the first error, a helpful commenter resolved that nicely:
$ unset XDG_SESSION_ID XDG_RUNTIME_DIR
The second one was tricker, and involved installation of the cmanager-utils
package, which allowed me to call the cgm
binary, which is a neat way to grant my shell access to the cgroup:
$ sudo cgm create all $USER
$ sudo cgm chown all $USER $(id -u) $(id -g)
$ cgm movepid all $USER $$
If you're wanting to call those commands from a script, put those three lines in their own shell script, change $$
to $1
, and you can execute it from your program via exec
(or its language equivalent), and pass in your program's pid as its first (and only) argument.
In my case, when my Node.js script is run, one of the first things it does is execute this script using execFile
, passing in the current pid as its first argument:
#!/bin/bash
unset XDG_RUNTIME_DIR XDG_SESSION_ID # Most likely this line is not required, but I added it just in case.
sudo cgm create all $USER
sudo cgm chown all $USER $(id -u) $(id -g)
cgm movepid all $USER $1
Like so:
var execFile = require('child_process').execFile;
execFile(__dirname + '/scripts/cgroups-assign.sh', [process.pid], {
env: {
USER: 'myuser' // I'm not sure if process.getuid would work here, try it and let me know.
}
});
Enjoy calling lxc-*
commands from your script/program!
Thanks, this was very useful (precisely because of the
su
gotcha). You've probably seen this already, but a lot of the samecgm
magic goes into this: http://blog.lifebloodnetworks.com/?p=2118 by @nickwebha - which contains a neat and tidied-up description of how to get unprivileged containers ofn
users to autostart at boot - also a persistent side-effect of the same cgm mysteries in getting containers to do things.It's just a shame that more of this can't be collected and presented all in one place!