学习Docker的User Namespace

本文假设你已经了解了Linux Container,CGroup和基本的Namespace概念。

User Namespace是Linux 3.8新增的一种namespace,用于隔离安全相关的标识和属性。使用了user namespace之后,进程在namespace内部和外部的uid/gid可以不一样,常用来实现这种效果:进程在namespace外面是一个普通用户,但是在namespace里是root(uid=0),也就是进程在这个namespace里拥有所有的权限,在namespace外面只有普通用户的权限了。

既然都是解决安全方面问题的,就不得不提另外两个Linux安全方面的功能,另外两个常用的是capabilities和LSM (Linux Security Module),其中capabilities和User Namespace关系密切。通过调用带CLONE_NEWUSER参数的clone方法创建的子进程,自动拥有新User Namespace里所有capabilities。另外,进程通过unshare创建一个新的User Namespace,或者通过setns加入一个已有的Namespace,都自动获取对应User Namespace里所有的capabilities。如果接下来进程通过execve启动了新程序,就要按照capabilities计算规则重新计算新进程的capabilities。

先看一个例子 (from http://coolshell.cn/articles/17029.html)

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/capability.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>

#define STACK_SIZE (1024 * 1024)

static char container_stack[STACK_SIZE];
char* const container_args[] = {
    "/bin/bash",
    NULL
};

int pipefd[2];

void set_map(char* file, int inside_id, int outside_id, int len) {
    FILE* mapfd = fopen(file, "w");
    if (NULL == mapfd) {
        perror("open file error");
        return;
    }
    fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
    fclose(mapfd);
}

void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/uid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/gid_map", pid);
    set_map(file, inside_id, outside_id, len);
}

int container_main(void* arg)
{

    printf("Container [%5d] - inside the container!\n", getpid());

    printf("Container: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());

    / 等待父进程通知后再往下执行(进程间的同步) /
    char ch;
    close(pipefd[1]);
    read(pipefd[0], &ch, 1);

    printf("Container [%5d] - setup hostname!\n", getpid());
    //set hostname
    sethostname("container",10);

    //remount "/proc" to make sure the "top" and "ps" show container's information
    mount("proc", "/proc", "proc", 0, NULL);

    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}

int main()
{
    const int gid=getgid(), uid=getuid();

    printf("Parent: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());

    pipe(pipefd);

    printf("Parent [%5d] - start a container!\n", getpid());

    int container_pid = clone(container_main, container_stack+STACK_SIZE,
            CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);

    printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid);

    //To map the uid/gid,
    //   we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent
    //The file format is
    //   ID-inside-ns   ID-outside-ns   length
    //if no mapping,
    //   the uid will be taken from /proc/sys/kernel/overflowuid
    //   the gid will be taken from /proc/sys/kernel/overflowgid
    (container_pid, 0, uid, 1);
    set_gid_map(container_pid, 0, gid, 1);

    printf("Parent [%5d] - user/group mapping done!\n", getpid());

    / 通知子进程 /
    close(pipefd[1]);

    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}

上面的例子创建了一个子进程,其中包含了UTS, PID, MOUNT和USER Namespace。编译并且运行上面的代码

gcc -Wall u.c
sudo /sbin/setcap all+eip a.out
./a.out

运行效果如下

可以看到,在container内部已经是root了。非常神奇的是,原来uid=1000的文件,在container内部自动显示成uid=0,原来uid=0的,自动变成uid=65534 (nobody),很完美。

代码中关键的部分在set_uid_map中,要设置新创建User Namespace和Parent User Namespace(这里例子中是系统默认的Namespace)中uid和gid的映射,只要写进程对应的两个文件即可:

  • /proc/PID/uid_map
  • /proc/PID/gid_map

这里的PID是运行在User Namespace中的进程id。写入的格式是

id-inside-ns id-outside-ns length

  • id-inside-ns: Namespace内部的uid/gid
  • id-outside-ns: Namespace外部的uid/gid
  • length 映射范围

有人可能主要到 上面的setcap操作,这一步也很关键,创建username也需要特定的capabilities才行,为了方便,这里直接设置为all

User Namespace In Docker

待续

时间: 2024-08-18 16:31:32

学习Docker的User Namespace的相关文章

学习Docker容器网络模型 - 搭建分布式Zookeeper集群

ZooKeeper是一个流行的分布式协调服务.它提供了两种不同的部署方式:单机模式和分布式模式.其中单机模式的部署非常简单,网上也有很多资料,我们今天会利用Docker搭建分布式Zookeeper集群,并来帮助大家熟悉Docker中容器网络模型的使用. ZooKeeper集群中所有的节点作为一个整体对分布式应用提供服务.节点中有两个的角色:Leader和Follower.在整个集群运行过程中,只有一个Leader,其他节点的都是Follower,如果ZK集群在运行过程中Leader出了问题,系统

深入解析Docker背后的namespace技术

相信你在很多地方都看到过"Docker基于mamespace.cgroups.chroot等技术来构建容器"的说法,但你有没有想过为何容器的构建需要这些技术? 为什么不是一个简单的系统调用就可以搞定?原因在于Linux内核中并不存在"linux container"这个概念,容器是一个用户态的概念. Docker软件工程师Michael Crosby将撰写一些列博客,深入到Docker运行的背后,探索在 docker run这段代码的背后发生了什么,这是系列博客的第

docker学习 - docker run

docker run流程 创建容器 通过用户指定的镜像名和tag,在TagStore中查找image_id,获取image对象(本地的image json文件里有镜像信息) 检查镜像layer数(不超过127),镜像层数太多会造成性能问题 将runconfig.Config和image.Config合并 创建{Container.ID}-init和Container.ID镜像的目录,在init layer创建.dockerinit./etc/hosts./etc/hostname等, Conta

docker学习 - docker启动和镜像

docker daemon启动 载体为daemon,调度管理engine,任务执行靠jobEngine是map[string]Handler,type Handler func(*Job) Status Daemon的启动流程: 注册serve job.pull job.create job.start job等 构建serveapi job,并执行ServeApi handler,创建支持多种协议请求(tcp/socker fd/默认unix socket)的http Server,支持TLS

《自己动手写docker》之namespace部门实验

动手写一遍,印象不一样! package main import ( "log" "os" "os/exec" "syscall" ) func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr.Cloneflags = syscall.CLONE_NEWNS | s

Docker学习路线图 (持续更新中)

Docker学习路线图 Docker基础 101 学习Docker基本概念 Docker容器 Docker镜像 Dockerfile初步概念 练习 安装Docker Toolbox或Docker Engine Docker Toolbox 阿里云镜像源 Docker Engine 阿里云镜像源 配置本地Docker环境 利用Docker Machine创建本地Docker Environment 配置阿里云Docker镜像加速器 管理容器的生命周期与配置 创建.删除.检查.启动.停止容器 ...

Docker应用容器基础技术:Linux Namespace 学习教程

我们开始.先从Linux Namespace开始.  简介 Linux Namespace是Linux提供的一种内核级别环境隔离的方法.不知道你是否还记得很早以前的Unix有一个叫chroot的系统调用通过修改根目录把用户jail到一个特定目录下chroot提供了一种简单的隔离模式chroot内部的文件系统无法访问外部的内容.Linux Namespace在此基础上提供了对UTS.IPC.mount.PID.network.User等的隔离机制. 举个例子我们都知道Linux下的超级父亲进程的P

Docker基础技术:Linux Namespace【上】

点点收获: //之前发现Coolshell上好久不更新了, 博主果然去搞大业去了,只恨这几篇文章看到太晚了啊~太厉害了. 1.  clone(), unshare(), setns()初识; 主要是š三个系统调用 šclone() - 实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离. šunshare() - 使某进程脱离某个namespace šsetns() - 把某进程加入到某个namespace 2.  学习了一个命令 -- ipcs -- report XSI

【技术干货】Docker精华学习资料集锦,老司机快上车

Docker是一个开源的应用容器引擎,提供了一种在安全.可重复的环境中自动部署软件的方式,允许开发者将他们的应用和依赖包打包到一个可移植的容器中,然后发布到任何流行的Linux机器上,也可以实现虚拟化.容器完全使用沙箱机制,相互之间不会有任何接口.几乎没有性能开销,可以很容易地在机器和数据中心中运行.重要的是,它不依赖于任何语言.框架或包装系统. 正如Docker的logo一样,Docker的思想就是源于集装箱,集装箱解决了各种型号.规格的货物在各种运输工具上进行运输的问题,而集装箱和集装箱之间