namespaces之 User Namespace機制

USER Namespaces

主要是對用戶和用戶組進行隔離。它是從Linux 3.8內核才慢慢支持的,現在有些linux發行版還不支持user namespaces(主要是因爲還不完全成熟,處於對安全的擔心,在編譯內核時並未開啓USER Namespaces,Centos 7就不支持,後邊的測試基於Ubuntu 14.04). 
User namespaces允許每個命名空間中User ID和 group ID的映射。在容器上下文環境中,這就意味着User和User Group 在容器中的操作可以擁有特權,在容器外邊沒有。換句話說,一個進程在user namespace中操作的權限設置不同於在宿主系統中的權限設置。user namespace一個特定的目標就是允許一個進程在容器中的操作擁有root特權,與此同時,在託管容器的宿主機上是一個無特權的正常進程。 
爲了支持這種行爲,每個進程的UID實際上有兩種值:容器中的是一種,容器外是另一種。Group ID和進程UID相似。這種對偶性通過維護每個 User namespace用戶ID的映射完成的:每個用戶命令空間有一張表,這張表中記錄着宿主機中用戶ID對應namespace 的用戶ID。這種映射是通過讀寫/proc/PID/uid_map(/proc/PID/gid_map gid映射文件)僞文件來設置,其中PID是user namespace這個進程的進程ID。例如,宿主機中UID爲1000的用戶被映射到namespace中可能會爲0,用戶ID爲1000的進程在宿主機中是一個常態的用戶(普通用戶),但是它在namespace中將擁有root特權。如果在宿主機系統中沒有爲特定用戶ID提供映射,在user namespace中,這個user ID會映射到/proc/sys/kernel/overflowuid(gid文件爲overflowgid)文件提供的值(這個文件中默認的值是65534)。

測試使,只需要在clone函數中添加CLONE_NEWUSER標誌即可:

clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUSER | SIGCHLD, NULL);

編譯後,普通用戶(ty)就可以執行,先查看該用戶ID:

ty@ubuntu:~$ id
uid=1000(ty) gid=1000(ty) groups=1000(ty)

使用clone函數創建進程,激活user namespace:

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
 
#define STACK_SIZE (1024 * 1024)
 
static char child_stack[STACK_SIZE];
 
int child_main(void* arg) {
char path[256];
printf("Child inside Namespace\n");
printf("euid=%d, egid=%d, pid=%d\n",geteuid(),getegid(), getpid());
execl("/bin/bash", "bash", NULL);
printf("execv error\n");
return -1;
}
 
int main()
{
printf("Parent outside Namespace \n");
int child_pid = clone(child_main, child_stack + STACK_SIZE, CLONE_NEWUSER | SIGCHLD, NULL);
if(child_pid == -1)
perror("clone");
waitpid(child_pid, NULL, 0);
printf("child exit...\n");
 
exit(EXIT_SUCCESS);
}

再執行namespace_user文件 :

ty@ubuntu:~/program/git/Linux-LXC/namespace$ gcc namespace-user.c -o namespace-user
ty@ubuntu:~/program/git/Linux-LXC/namespace$ ./namespace-user
Parent outside Namespace
Child inside Namespace
nobody@ubuntu:~/program/git/Linux-LXC/namespace$ id
uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup)

可以發現ty用戶映射到user namespace中爲nobody,ID爲65534,使用/proc/sys/kernel/overflowuid中默認值;下邊測試是否擁有特權: 
Inside namespace:

nobody@ubuntu:~/program/git/Linux-LXC/namespace$ ls -l /etc/passwd
-rw-r--r-- 1 nobody nogroup 1979 4 24 22:00 /etc/passwd

outside namespace:

ty@ubuntu:~$ ls -l /etc/passwd
-rw-r--r-- 1 root root 1979 4 24 23:21 /etc/passwd

如上顯示,在namespace中/etc/passwd用戶擁有者和用戶組都已成當前登錄用戶nobody,但是仍不能對齊進行修改。 
因爲當前只是隔離了用戶和用戶組,系統的其他資源仍是和宿主機共用,所以不能修改。在User Namespace中映射的用戶和用戶組只能對被隔離的資源有特權,沒有被隔離的資源雖然發現該用戶已經成爲其擁有者和組,但操作仍是依據於宿主機中的用戶權限。

uid-gid映射

上邊使用的是系統默認的映射的UID和GID值,即就是/proc/sys/kernel/overflowuid(overflowgid)文件中的默認值。接下來,自己設置UID和GID的映射。 
通過上邊的學習,知道/proc/PID/uid_map和/proc/PID/gid_map文件中是關於映射設置的信息,所以通過修改這兩個文件達到映射工作,這兩個文件是通過三組數字實現映射設置: 
first-ns-id first-target-id count 
first-ns-id:是給定進程的namespace中第一個合法的ID,常被設置爲 0 ,即就是root的ID,這個ID在user namespace中是合法的; 
first-target-id:是父進程(宿主機)命令空間中的真實ID,first-ns-id會被映射到宿主機中的first-target-id; 
count:表示映射的範圍,爲 1 表示只映射一個,大於1表示按順序映射; 
docker容器中一般如下,表示把namespace內部從0 開始的 ID 映射到外部從0開始的 ID,最大範圍爲4294967295 
0 0 4294967295

測試CAP_SETUID權限

對於UID和GID的映射,需要特權操作,這個執行的進程必須要有CAP_SETUID權限(屬於linux內核能力的概念,實現訪問控制。這塊只是單純的使用了,後邊詳細學習下)可用。 
引用頭文件sys/capability.h(沒有該頭文件,需要安裝libcap-dev包),使用如下命令獲取並輸出權限查看子進程是否擁有cap_setuid和cap_setgid權限:

caps = cap_get_proc();
printf("capabilities:%s\n",cap_to_text(caps,NULL));

輸出如下即就是擁有該權限:

capabilities:= cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,...37+ep

添加UID、GID映射

上一步已經測試子進程擁有cap+和cap_setgid權限,直接在子進程中向文件中添加映射信息,代碼如下:

void set_uid_map(pid_t pid, int first_ns_id, int first_target_id, int count){ //設置UID映射
char path[256];
sprintf(path,"/proc/%d/uid_map", pid);
FILE *uid_map = fopen(path, "w");
fprintf(uid_map, "%d %d %d",first_ns_id, first_target_id, count);
fclose(uid_map);
}
 
void set_gid_map(pid_t pid, int first_ns_id, int first_target_id, int count){ //設置GID映射
char path[256];
int n;
 
sprintf(path,"/proc/%d/gid_map", pid);
FILE *gid_map = fopen(path, "w");
n= fprintf(gid_map, "%d %d %d",first_ns_id, first_target_id, count);
printf("fprintf return = %d\n",n);
fclose(gid_map);
}
 
int child_main(void* arg) {
char path[256];
cap_t caps;
int fd;
char buf[256];
 
caps = cap_get_proc();
printf("capabilities:%s\n",cap_to_text(caps,NULL));
 
set_uid_map(getpid(), 0, 1000, 1);
set_gid_map(getpid(), 0, 1000, 1);
 
printf("euid=%d, egid=%d, pid=%d\n",geteuid(),getegid(), getpid());
execl("/bin/bash", "bash", NULL);
printf("execv error\n");
return -1;
}

運行如下:

ty@ubuntu:~namespace$ gcc namespace-user.c -o namespace-user -lcap
ty@ubuntu:~namespace$ ./namespace-user
capabilities:= cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,...37+ep
Child first_ns Namespace
fprintf return = 8
euid=0, egid=65534, pid=13910
root@ubuntu:~namespace# id
uid=0(root) gid=65534(nogroup) groups=0(root),65534(nogroup)

根據輸出發現,user namespace中的用戶已經映射爲root(UID=0),但是gid還是默認值,沒有成功。 
問題:測試發現set_gid_map函數中fprintf語句確實寫入了8個字符(0 1000 1),但是打開gid_map仍未空,還不知道爲什麼,正在調試,有知道了原因的可以提點我下,謝謝


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章