Linux provides the following and we will see how we can demonstrate these with Go

       Namespace   Constant          Isolates
       Cgroup      CLONE_NEWCGROUP   Cgroup root directory
       IPC         CLONE_NEWIPC      System V IPC, POSIX message queues
       Network     CLONE_NEWNET      Network devices, stacks, ports, etc.
       Mount       CLONE_NEWNS       Mount points
       PID         CLONE_NEWPID      Process IDs
       User        CLONE_NEWUSER     User and group IDs
       UTS         CLONE_NEWUTS      Hostname and NIS domain name

This post is one which explains the how we can implement User namespace
Namespaces were introduced in Linux to isolate a process .This is the fundamental idea which evolved to Linux containers (Docker,LXC etc..)

Lets start with a program user_namesapce_demo.go to execute a binary – here we will start a shell

package main

import (
        "os"
        "os/exec"
)

func main() {
       cmd := exec.Command("/bin/sh")
       cmd.Stdin = os.Stdin
       cmd.Stdout = os.Stdout
       cmd.Stderr = os.Stderr
       cmd.Run()
}

Compile and execute it

[email protected]:~/.../Golang-Tutor> go build user_namesapce_demo.go
[email protected]:~/.../Golang-Tutor> ./user_namesapce_demo
sh-4.3$ id
uid=000(linxlabs) gid=00(users) groups=00(users)
sh-4.3$

We can see the user id is same as the parent shell as expected.
How can we change this behavior so that the newly spawned process will get a new user ID instead of its parent user ID
To accomplish that ,we will use USER Namespace
Lets rewrite the program to include user namespace

package main

import (
       "os"
       "os/exec"
       "syscall" // For SysProcAttr to pass clone flag CLONE_NEWUSER
)

func main() {
     cmd := exec.Command("/bin/sh")
     cmd.Stdin = os.Stdin
     cmd.Stdout = os.Stdout
     cmd.Stderr = os.Stderr
     cmd.SysProcAttr = &syscall.SysProcAttr{
         Cloneflags: syscall.CLONE_NEWUSER,
         }
     cmd.Run()
}

Compile and execute it to see the user id

[email protected]:~/.../Golang-Tutor> go build user_namesapce_demo.go
[email protected]:~/.../Golang-Tutor> ./user_namesapce_demo
sh-4.3$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)
sh-4.3$

Now you can see “nobody” instead of “linxlabs”

We didn’t define any mappings in our program ,so Go will assign overflow ID 65534
So lets see what is UID/GID mappings

 

We will get a user id from parent process which will be called as “HostID”
In our “containerized” process we need a custom user ID which will be called as “ContainerID”

You may read more detailed implementation in this LKML article.
Below copied snipp is from the same article.

Normally, one of the first steps after creating a new user namespace is to define the mappings used for the user and group IDs of the processes that will be created in that namespace.
This is done by writing mapping information to the /proc/PID/uid_map and /proc/PID/gid_map files corresponding to one of the processes in the user namespace. (Initially, these two files are empty.)
This information consists of one or more lines, each of which contains three values separated by white space:

ID-inside-ns ID-outside-ns length
Together, the ID-inside-ns and length values define a range of IDs inside the namespace that are to be mapped to an ID range of the same length outside the namespace.
The ID-outside-ns value specifies the starting point of the outside range.
How ID-outside-ns is interpreted depends on the whether the process opening the file /proc/PID/uid_map (or /proc/PID/gid_map) is in the same user namespace as the process PID:

As per the documentation of SysProcAttr, we can add UID/GID mappings with below two structure members

UidMappings []SysProcIDMap // User ID mappings for user namespaces.
GidMappings []SysProcIDMap // Group ID mappings for user namespaces.

SysProcIDMap structure have three members which matches with the kernel documentation (ID-inside-ns ,ID-outside-ns and length)

 

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

So lets add few lines of codes to map both UID and GID

package main

import (
      "os"
      "os/exec"
      "syscall"
)

func main() {
     cmd := exec.Command("/bin/sh")
     cmd.Stdin = os.Stdin
     cmd.Stdout = os.Stdout
     cmd.Stderr = os.Stderr
     cmd.SysProcAttr = &syscall.SysProcAttr{
         Cloneflags: syscall.CLONE_NEWUSER,
          UidMappings: []syscall.SysProcIDMap{
           {
              ContainerID: 0,
              HostID: os.Getuid(),
              Size: 1,
           },
        },
         GidMappings: []syscall.SysProcIDMap{
           {
              ContainerID: 0,
              HostID: os.Getgid(),
              Size: 1,
          },
       },
    }
cmd.Run()
}

Lets compile and execute the program

[email protected]:~/.../Golang-Tutor> go build user_namesapce_demo.go
[email protected]:~/.../Golang-Tutor> ./user_namesapce_demo
sh-4.3#
sh-4.3# id
uid=0(root) gid=0(root) groups=0(root)
sh-4.3#

Now the shell shows root instead of nobody as we did the UID/GID mappings to “0” which is root

But that’s not all . Even Though the shell shows root , we will not be able to execute any privileged commands
For example , changing hostname whill be denied with an error shown below

sh-4.3# hostname test
hostname: you must be root to change the host name
sh-4.3#

This is because we only used one namespace and there is more name spaces to initialize which will allow the program to execute privileged commands in their own isolated place
The hostname command modifies a kernel paramter which is part of UTS namespace and we will see that implementation and demo in next article . Stay tunen

If you like this article , please subscribe and follow us on social media



Source link

LEAVE A REPLY

Please enter your comment!
Please enter your name here