Namespaces in Go - User

Ed King
4 min readDec 13, 2016

--

In the previous article we saw how to create and run a process in various Linux namespaces using Go. We left with some code that runs a /bin/sh process in a new Mount, UTS, IPC, PID, Network and User namespace.

You may recall that once we added the User namespace to ns-process we no longer had to run it as the root user. This is a great feature to have as it means ns-process can be run much more securely. However, in adding the User namespace to the program, we have inadvertently introduced some less desirable behaviour.

This behaviour can be demonstrated by comparing the output of whoami from within the namespaced shell both before and after we added the User namespace, as follows.

# Git repo: https://github.com/teddyking/ns-process
# Git tag: 1.0
# Prior to adding the User namespace
$ go build
$ sudo ./ns-process
-[ns-process]- # whoami
root
-[ns-process]- # id root
uid=0(root) gid=0(root) groups=0(root)
# Git tag: 1.1
# After adding the User namespace
$ go build
$ ./ns-process
-[ns-process]- # whoami
nobody
-[ns-process]- # id nobody
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

Although we are now able to run ns-process as a non-root user, once inside the namespaced shell we have lost our root identity.

In this article we will work through a fix for this regression, and learn a little bit more about the User namespace along the way.

🗺 UID and GID mapping

The reason behind our loss of identity is that we’re missing some important configuration. It is not enough to simply add the CLONE_NEWUSER flag and expect the User namespace to be ready for use. In order to setup the namespace properly, we also need to provide what is know as a UID and a GID mapping.

💁 If you’re not interested in the theory and are eager to crack on with the Go coding, feel free to skip the rest of this section

ID mapping and how it relates to User namespaces is a huge topic in itself, and it falls mostly out of scope for this article. Having said that, there are a few things you need to know in order to understand how we’re going to fix our identity crisis. Here are the TL;DR essentials.

  • The User namespace provides isolation of UIDs and GIDs
  • There can be multiple, distinct User namespaces in use on the same host at any given time
  • Every Linux process runs in one of these User namespaces
  • User namespaces allow for the UID of a process in User namespace 1 to be different to the UID for the same process in User namespace 2
  • UID/GID mapping provides a mechanism for mapping IDs between two separate User namespaces

The following diagram attempts to visualise the above.

Pictured are two User namespaces, 1 and 2, with their corresponding UID and GID tables. Note that process C, running as non-root-user is able to spawn Process D, which is running as root.

The key implementation detail, and the thing that prevents the universe from imploding is the mapping between the two User namespaces (represented here by the dashed lines).

Process D only has root privileges within the context of User namespace 2. From the perspective of processes in User namespace 1, process D is running as non-root-user, and as such, doesn’t have those all-important root privileges.

This mapping is exactly what’s missing from ns-process at the moment, and it’s about time we sorted that out.

👉 Let’s Go

ID mappings can be applied by setting the UidMappings and GidMappings fields on cmd.SysProcAttr. Both fields are of type SysProcIDMap found in Go’s syscall package.

type SysProcIDMap struct {
ContainerID int // Container ID.
HostID int // Host ID.
Size int // Size.
}

The ContainerID and HostID fields should be fairly self-explanatory. Size is slightly less so. Size basically determines the range of IDs to map, which allows us to map more than one ID at a time. Let’s update our program to include some mappings.

# Git repo: https://github.com/teddyking/ns-process
# Git tag: 2.0
# Filename: ns_process.go
# ...
cmd.SysProcAttr = &syscall.SysProcAttr{
Cloneflags: syscall.CLONE_NEWNS |
syscall.CLONE_NEWUTS |
syscall.CLONE_NEWIPC |
syscall.CLONE_NEWPID |
syscall.CLONE_NEWNET |
syscall.CLONE_NEWUSER,
UidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: os.Getuid(),
Size: 1,
},
},
GidMappings: []syscall.SysProcIDMap{
{
ContainerID: 0,
HostID: os.Getgid(),
Size: 1,
},
},
}
# ...

Here we are adding a single UID and GID mapping. We set ContainerID to 0, HostID to the current user’s UID/GID and Size equal to 1. In other words, we are mapping ID = 0 (aka root) in our new User namespace to the ID of the user who invokes the ns-process command.

With all this in place, we should be able to build and run ns-process and see that we now become the root user inside the namespaced shell.

💁 The following has been tested on Ubuntu 16.04 Xenial with Go 1.7.1

$ go build
$ ./ns-process
-[ns-process]- # whoami
root
-[ns-process]- # id
uid=0(root) gid=0(root) groups=0(root)

And there we have it! With the addition of a simple UidMapping/GidMapping we have been able to restore our root identity inside the namespaced shell, while retaining the ability to run ns-process as a non-root user.

📺 On the next…

In the next article we’ll take a look at reexec. What is reexec and why is it relevant to Namespaces in Go? The answer to this and plenty more coming up, stay tuned…

Update: Part 4, “Namespaces in Go - Reexec” has been published and is available here.

--

--

Ed King

A Software Engineer currently working on and with Kubernetes.