隔離 docker 容器中的使用者

sparkdev發表於2018-09-13

筆者在前文《理解 docker 容器中的 uid 和 gid》介紹了 docker 容器中的使用者與宿主機上使用者的關係,得出的結論是:docker 預設沒有隔離宿主機使用者和容器中的使用者。如果你已經瞭解了 Linux 的 user namespace 技術(參考《Linux Namespace : User》),那麼自然會問:docker 為什麼不利用 Linux user namespace 實現使用者的隔離呢?事實上,docker 已經實現了相關的功能,只是預設沒有啟用而已。筆者將在本文中介紹如何配置 docker 來隔離容器中的使用者。
說明:本文的演示環境為 ubuntu 16.04。

瞭解 Linux user namespace

Linux user namespace 為正在執行的程式提供安全相關的隔離(其中包括 uid 和 gid),限制它們對系統資源的訪問,而這些程式卻感覺不到這些限制的存在。關於 Linux User Namespace 的介紹請參考筆者的《Linux Namespace : User》一文。

對於容器而言,阻止許可權提升攻擊(privilege-escalation attacks)的最好方法就是使用普通使用者許可權執行容器的應用程式。
然而有些應用必須在容器中以 root 使用者來執行,這就是我們使用 user namespace 的最佳場景。我們透過 user namespace 技術,把宿主機中的一個普通使用者(只有普通許可權的使用者)對映到容器中的 root 使用者。在容器中,該使用者在自己的 user namespace 中認為自己就是 root,也具有 root 的各種許可權,但是對於宿主機上的資源,它只有很有限的訪問許可權(普通使用者)。

User namespace 的使用者對映

在配置 docker daemon 啟用 user namespace 前,我需要先來了解一些關於從屬(subordinate)使用者/組和對映(remapping)的概念。從屬使用者和組的對映由兩個配置檔案來控制,分別是 /etc/subuid 和 /etc/subgid。看下它們的預設內容:在配置 docker daemon 啟用 user namespace 前,我需要先來了解一些關於從屬(subordinate)使用者/組和對映(remapping)的概念:

對於 subuid,這一行記錄的含義為:
使用者 nick,在當前的 user namespace 中具有 65536 個從屬使用者,使用者 ID 為 100000-165535,在一個子 user namespace 中,這些從屬使用者被對映成 ID 為 0-65535 的使用者。subgid 的含義和 subuid 相同。

比如說使用者 nick 在宿主機上只是一個具有普通許可權的使用者。我們可以把他的一個從屬 ID(比如 100000 )分配給容器所屬的 user namespace,並把 ID 100000 對映到該 user namespace 中的 uid 0。此時即便容器中的程式具有 root 許可權,但也僅僅是在容器所在的 user namespace 中,一旦到了宿主機中,你頂多也就有 nick 使用者的許可權而已。

當開啟 docker 對 user namespace 的支援時(docker 的 userns-remap 功能),我們可以指定不同的使用者對映到容器中。比如我們專門建立一個使用者 dockeruser,然後手動設定其 subuid 和 subgid:

nick:100000:65536
dockeruser:165536:65536

並把它指定給 docker daemon:

{
  "userns-remap": "dockeruser"
}

請注意 subuid 的設定資訊,我們為 dockeruser 設定的從屬 ID 和 nick 使用者是不重疊的,實際上任何使用者的從屬 ID 設定都是不能重疊的。

或者一切從簡,讓 docker 為我們包辦這些繁瑣的事情,直接把 docker daemon 的 userns-rempa 引數指定為 "default":

{
  "userns-remap": "default"
}

這時,docker 會自動完成其它的配置。

配置 docker daemon 啟用使用者隔離

這裡筆者採取簡單的方式,讓 docker 建立預設的使用者用於 user namespace。我們需要先建立 /etc/docker/daemon.json 檔案:

$ sudo touch /etc/docker/daemon.json

然後編輯其內容如下(如果該檔案已經存在,僅新增下面的配置項即可),並重啟 docker 服務:

{
  "userns-remap": "default"
}
$ sudo systemctl restart docker.service

下面我們來驗證幾個關於使用者隔離的幾個點。

首先驗證 docker 建立了一個名為 dockremap 的使用者:

然後檢視 /etc/subuid 和 /etc/subgid 檔案中是否新增了新使用者 dockremap 相關的項:

接下來我們發現在 /var/lib/docker 目錄下新建了一個目錄: 165536.165536,檢視該目錄的許可權:

165536 是由使用者 dockremap 對映出來的一個 uid。檢視 165536.165536 目錄的內容:

與  /var/lib/docker 目錄下的內容基本一致,說明啟用使用者隔離後檔案相關的內容都會放在新建的 165536.165536 目錄下。

透過上面的檢查,我們可以確認 docker daemon 已經啟用了使用者隔離的功能。

宿主機中的 uid 與容器中 uid

在 docker daemon 啟用了使用者隔離的功能後,讓我們看看宿主機中的 uid 與容器中 uid 的變化。

$ docker run -d --name sleepme ubuntu sleep infinity

uid 165536 是使用者 dockremap 的一個從屬 ID,在宿主機中並沒有什麼特殊許可權。然而容器中的使用者卻是 root,這樣的結果看上去很完美:

新建立的容器會建立 user namespace

在 docker daemon 啟用使用者隔離的功能前,新建立的容器程式和宿主機上的程式在相同的 user namespace 中。也就是說 docker 並沒有為容器建立新的 user namespace:

上圖中的容器程式 sleep 和宿主機上的程式在相同的 user namespace 中(沒有開啟使用者隔離功能的場景)。

在 docker daemon 啟用使用者隔離的功能後,讓我們檢視容器中程式的 user namespace:

上圖中的 4404 就是我們剛啟動的容器中 sleep 程式的 PID。可以看出,docker 為容器建立了新的 user namespace。在這個 user namespace 中,容器中的使用者 root 就是天神,擁有至高無上的權力!

訪問資料卷中的檔案

我們可以透過訪問資料卷中的檔案來證明容器中 root 使用者究竟具有什麼樣的許可權?建立四個檔案,分別屬於使用者 root 、165536 和 nick。rootfile 只有 root 使用者可以讀寫,使用者 nick 具有 nickfile 的讀寫許可權,uid 165536 具有檔案 165536file 的讀寫許可權,任何使用者都可以讀寫 testfile 檔案:

下面把這幾個檔案以資料卷的方式掛載到容器中,並檢查從容器中訪問它們的許可權:

$ docker run -it --name test -w=/testv -v $(pwd)/testv:/testv ubuntu

容器中的 root 使用者只能訪問 165536file 和 testfile,說明這個使用者在宿主機中只有非常有限的許可權。

在容器中禁用 user namespace

一旦為 docker daemon 設定了 "userns-remap" 引數,所有的容器預設都會啟用使用者隔離的功能(預設建立一個新的 user namespace)。有些情況下我們可能需要回到沒有開啟使用者隔離的場景,這時可以透過 --userns=host 引數為單個的容器禁用使用者隔離功能。--userns=host 引數主要給下面三個命令使用:

docker container create
docker container run
docker container exec

比如執行下的命令:

$ docker run -d --userns=host --name sleepme ubuntu sleep infinity

檢視程式資訊:

程式的有效使用者又成 root 了,並且也沒有為程式建立新的 user namespace:

已知問題

User namespace 屬於比較高階的功能,目前 docker 對它的支援還算不上完美,下面是已知的幾個和現有功能不相容的問題:

  • 共享主機的 PID 或 NET namespace(--pid=host or --network=host)
  • 外部的儲存、資料卷驅動可能不相容、不支援 user namespace
  • 使用 --privileged 而不指定 --userns=host

總結

Docker 是支援 user namespace 的,並且配置的方式也非常簡便。在開啟 user namespace 之後我們享受到了安全性的提升,但同時也會因為種種限制讓其它的個別功能出現問題。這時我們需要作出選擇,告別一刀切的決策,讓合適的功能出現的合適的場景中。

參考:
Understanding how uid and gid work in Docker containers
Introduction to User Namespaces in Docker Engine
Isolate containers with a user namespace

相關文章