在編輯“容器如何工作”愛好者雜誌的能力頁面時,我想試著解釋一下為什麼 strace
在 Docker 容器中無法運作。
這裡的問題是 —— 如果我在筆電上的 Docker 容器中執行 strace
,就會出現這種情況:
$ docker run -it ubuntu:18.04 /bin/bash$ # ... install strace ...[email protected]:/# strace lsstrace: ptrace(PTRACE_TRACEME, ...): Operation not permitted
strace
通過 ptrace
系統呼叫起作用,所以如果不允許使用 ptrace
,它肯定是不能工作的! 這個問題很容易解決 —— 在我的機器上,是這樣解決的:
docker run --cap-add=SYS_PTRACE -it ubuntu:18.04 /bin/bash
但我對如何修復它不感興趣,我想知道為什麼會出現這種情況。為什麼 strace
不能工作,為什麼--cap-add=SYS_PTRACE
可以解決這個問題?
CAP_SYS_PTRACE
能力。我一直以為原因是 Docker 容器進程預設不具備 CAP_SYS_PTRACE
能力。這和它可以被 --cap-add=SYS_PTRACE
修復是一回事,是吧?
但這實際上是不合理的,原因有兩個。
原因 1:在實驗中,作為一個普通使用者,我可以對我的使用者執行的任何進程進行 strace
。但如果我檢查我的當前進程是否有 CAP_SYS_PTRACE
能力,則沒有:
$ getpcaps $$Capabilities for `11589': =
原因 2:capabilities
的手冊頁對 CAP_SYS_PTRACE
的介紹是:
CAP_SYS_PTRACE * Trace arbitrary processes using ptrace(2);
所以,CAP_SYS_PTRACE
的作用是讓你像 root 一樣,可以對任何使用者擁有的任意進程進行 ptrace
。你不需要用它來對一個只是由你的使用者擁有的普通進程進行 ptrace
。
我用第三種方法測試了一下(LCTT 譯註:此處可能原文有誤) —— 我用 docker run --cap-add=SYS_PTRACE -it ubuntu:18.04 /bin/bash
執行了一個 Docker 容器,去掉了 CAP_SYS_PTRACE
能力,但我仍然可以跟蹤進程,雖然我已經沒有這個能力了。什麼?為什麼?!
我的下一個(沒有那麼充分的依據的)假設是“嗯,也許這個過程是在不同的使用者名稱空間裡,而 strace
不能工作,因為某種原因而行不通?”這個問題其實並不相關,但這是我觀察時想到的。
容器進程是否在不同的使用者名稱空間中?嗯,在容器中:
root@e27f594da870:/# ls /proc/$$/ns/user -l... /proc/1/ns/user -> 'user:[4026531837]'
在宿主機:
bork@kiwi:~$ ls /proc/$$/ns/user -l... /proc/12177/ns/user -> 'user:[4026531837]'
因為使用者名稱空間 ID(4026531837
)是相同的,所以容器中的 root 使用者和主機上的 root 使用者是完全相同的使用者。所以,絕對沒有理由不能夠對它建立的進程進行 strace
!
這個假設並沒有什麼意義,但我(之前)沒有意識到 Docker 容器中的 root 使用者和主機上的 root 使用者同一個,所以我覺得這很有意思。
我也知道 Docker 使用 seccomp-bpf 來阻止容器進程執行許多系統呼叫。而 ptrace
在被 Docker 預設的 seccomp 組態檔阻止的系統呼叫列表中!(實際上,允許的系統呼叫列表是一個白名單,所以只是ptrace
不在預設的白名單中。但得出的結果是一樣的。)
這很容易解釋為什麼 strace
在 Docker 容器中不能工作 —— 如果 ptrace
系統呼叫完全被遮蔽了,那麼你當然不能呼叫它,strace
就會失敗。
讓我們來驗證一下這個假設 —— 如果我們禁用了所有的 seccomp 規則,strace
能在 Docker 容器中工作嗎?
$ docker run --security-opt seccomp=unconfined -it ubuntu:18.04 /bin/bash$ strace lsexecve("/bin/ls", ["ls"], 0x7ffc69a65580 /* 8 vars */) = 0... it works fine ...
是的,很好用!很好。謎底解開了,除了…..
--cap-add=SYS_PTRACE
能解決問題?我們還沒有解釋的是:為什麼 --cap-add=SYS_PTRACE
可以解決這個問題?
docker run
的手冊頁是這樣解釋 --cap-add
引數的。
--cap-add=[] Add Linux capabilities
這跟 seccomp 規則沒有任何關係! 怎麼回事?
當文件沒有幫助的時候,唯一要做的就是去看原始碼。
Go 語言的好處是,因為依賴關係通常是在一個 Go 倉庫裡,你可以通過 grep
來找出做某件事的程式碼在哪裡。所以我克隆了 github.com/moby/moby
,然後對一些東西進行 grep
,比如 rg CAP_SYS_PTRACE
。
我認為是這樣的。在 containerd
的 seccomp 實現中,在 contrib/seccomp/seccomp/seccomp_default.go 中,有一堆程式碼來確保如果一個進程有一個能力,那麼它也會(通過 seccomp 規則)獲得存取許可權,以使用與該能力相關的系統呼叫。
case "CAP_SYS_PTRACE": s.Syscalls = append(s.Syscalls, specs.LinuxSyscall{ Names: []string{ "kcmp", "process_vm_readv", "process_vm_writev", "ptrace", }, Action: specs.ActAllow, Args: []specs.LinuxSeccompArg{}, })
在 moby 中的 profile/seccomp/seccomp.go 和 預設的 seccomp 組態檔中,也有一些其他的程式碼似乎做了一些非常類似的事情,所以有可能就是這個程式碼在做這個事情。
所以我想我們有答案了!
--cap-add
做的事情比它說的要多結果似乎是,--cap-add
並不像手冊頁裡說的那樣,它更像是 --cap-add-and-also-whiteelist-some-extra-system-calls-if-required
。這很有意義! 如果你具有一個像 --CAP_SYS_PTRACE
這樣的能力,可以讓你使用 process_vm_readv
系統呼叫,但是該系統呼叫被 seccomp 組態檔阻止了,那對你沒有什麼幫助!
所以當你給容器 CAP_SYS_PTRACE
能力時,允許使用 process_vm_readv
和 ptrace
系統呼叫似乎是一個合理的選擇。
這是個有趣的小事情,我認為這是一個很好的例子,說明了容器是由許多移動的部件組成的,它們以不完全顯而易見的方式一起工作。