SlideShare a Scribd company logo
3
Most read
4
Most read
5
Most read
runc & User Namespaces
Container Runtime Meetup #1​ runcコードリーディング (2019/9/24)
対象とするruncのリビジョン: ​7507c64ff675606c5ff96b0dd8889a60c589f14d​ (2019/9/24時点で最新)
自己紹介
● GitHub: ​@AkihiroSuda​ / Twitter: ​@_AkihiroSuda_
● Moby (Docker)、containerd、BuildKitなどのメンテナ
● Rootlessコンテナなどセキュリティ関係を中心に取り組んでいる
User Namespaces とは
● 非rootユーザをrootユーザに見せかける
○ もしUserNS内のプロセスに脆弱性があっても、ホストのrootを奪われずにすむ
● UserNS内の見かけ上のrootは、真のrootとはもちろん異なる
○ UID・GIDはUserNS内では0に見えるが、UserNS外からは本来のユーザのUID・GIDとして見える
■ パーミッション制約は本来のユーザのUID・GID (kuid・kgid) に基づく
○ NS内に閉じた、見かけ上のケーパビリティを得られる (​CAP_SYS_ADMIN​など)
■ UserNSとあわせてMountNSも作るとファイルシステムのマウントもできる
● Bind-mount、tmpfs、procfs、sysfs くらいしかマウントできない
● Kernel 4.18 (2018年)からはFUSEもマウントできる
● ブロックデバイスはマウントできない
● OverlayFSはマウントできない。ただしUbuntuではカーネルにパッチが当てられてい
るのでマウントできる。
■ カーネルモジュールを読み込んだり、ホストを再起動したりなど、NS外に影響が及ぶ操作は
できない
● Kernel 2.6.23 (2007年)にて導入された
● Kernel 3.8 (2013年)からは非rootユーザが自分でUserNSを作れるようになった
利用事例
● Dockerでの利用事例
○ dockerd --userns-remap
■ コンテナ内のプロセスをUserNS内で動かす
■ runc自体、containerd自体、Docker自体は普通にrootで動く
■ runcがUserNSを作成した後のフローについて、脆弱性を軽減できる
● CVE-2016-3697​、​runc#1962​、​CVE-2019-5736​...
■ 将来的にはデフォルトになるかもしれない
○ Rootless Docker
■ コンテナ内のプロセスだけではなく、runc自体、containerd自体、Docker自体もUserNS内で
動かす
■ runcの脆弱性のみならず、Dockerデーモンなどの脆弱性も軽減できる
● CVE-2014-9357​、​CVE-2018-15664​、​CVE-2019-14271​...
■ Cgroup, checkpoint, AppArmorが使えない
● Cgroup v2移行後はcgroupも使えるようになる見込み
● Kubernetesでは未だに使えない
○ k3sは実験的にrootlessモードに対応
● LXDではデフォルトで用いられる (​dockerd --userns-remap​に類似)
Sub-users & sub-groups
● NS内に複数のUID・GIDをマップすることができる
○ NS内の見かけ上のrootから、NS内の非rootにスイッチすることで更に権限を分離できる
○ 複数UID・GIDを前提としているプログラムとの互換性
■ nginx、mysqlなど多くのミドルウェアはまずrootで起動してから、自身専用のUID・GIDに遷
移する
● /proc/​PID​/uid_map​、​/proc/​PID​/gid_map​ に、UserNS内と外のUIDの対応表を書き込むことで設定できる
(書き込みはNS外から行う)
○ 対応表を書き込むまでは、NS内には”​nobody​” (65534)だけが存在
● 複数のUID・GIDをマップするには、NS外でのroot権限 (​CAP_SETUID​・​CAP_SETGID​)が必要
○ CAP_SETUID​ がないと、1エントリ (自分のUIDだけ) しか ​/proc/​PID​/uid_map​ に書き込めない
○ CAP_SETGID​ がないと、1エントリ (自分のUIDだけ) しか​ /proc/​PID​/gid_map​ に書き込めない
■ さらに、予め​ /proc/​PID​/setgroups​ に​ “deny”​ を書き込んでおく必要が生じる
(​setgroups(2)​を呼び出せなくなるので、supplementary groupsを設定できなくなる)
● なので、非rootユーザで複数のUID・GIDをマップしたいとき(Rootless Dockerなど)は、SETUID バイナリ
/usr/bin/newuidmap​ および​ ​/usr/bin/newgidmap​ を用いる必要がある
○ 予め​ /etc/subuid ​および​ /etc/subgid​ に、ユーザが利用して良いUID・GIDのリストを書いてお
く
■ LDAP環境では使いにくいという問題がある
○ SETUIDしているdistroが多いが、実際はfile capability (​CAP_SETUID​、​CAP_SETGID​) だけでも十分
● より詳しい情報は ​user_namespaces(7)​ 参照
● runcを使わなくても​ ​unshare -U​ コマンドで空のUserNSを作成できる
○ unshare(2)​ ​システムコールを呼び出している
runc と UserNSの関係
似て非なる組み合わせが色々ある
● runcにUserNSを作らせる場合
○ rootでruncにUserNSを作らせる場合 (​dockerd --userns-remap​ など)
○ 非rootでruncにUserNSを作らせる場合 (本来の “rootless runc”)
● 既存UserNS内でruncを実行する場合 (Rootless Dockerなど)
● runcを既存UserNSにjoinさせる場合
runcにUserNSを作らせる場合
config.json​ に次のような設定を渡すと、runcにUserNSを作成させることが出来る
"linux": {
"uidMappings": [
{
"containerID": 0,
"hostID": 1001,
"size": 1
},
{
"containerID": 1,
"hostID": 100000,
"size": 65536
}
],
"gidMappings": [
{
"containerID": 0,
"hostID": 1001,
"size": 1
},
{
"containerID": 1,
"hostID": 100000,
"size": 65536
}
],
"namespaces": [
{
"type": "user"
},
...
}
● config.json​ からlibcontainer configへの変換:
libcontainer/specconv/spec_linux.go:setupUserNamespace()
func setupUserNamespace(spec *specs.Spec, config *configs.Config) error {
create := func(m specs.LinuxIDMapping) configs.IDMap {
return configs.IDMap{
HostID: int(m.HostID),
ContainerID: int(m.ContainerID),
Size: int(m.Size),
}
}
if spec.Linux != nil {
for _, m := range spec.Linux.UIDMappings {
config.UidMappings = append(config.UidMappings, create(m))
}
for _, m := range spec.Linux.GIDMappings {
config.GidMappings = append(config.GidMappings, create(m))
}
}
rootUID, err := config.HostRootUID()
if err != nil {
return err
}
rootGID, err := config.HostRootGID()
if err != nil {
return err
}
for _, node := range config.Devices {
node.Uid = uint32(rootUID)
node.Gid = uint32(rootGID)
}
return nil
}
● git grep NEWUSER​ すると、libcontainer config変換以後の、UserNS関係のフローが見えてくる
rootでruncにUserNSを作らせる場合 (​dockerd --userns-remap​ など)
● 主要な部分は​ libcontainer/nsenter/nsexec.c​ ​に集中
● nsexecにてchildがやること:​ ​libcontainer/nsenter/nsexec.c:nsexec():JUMP_CHILD
○ unshare(CLONE_NEWUSER)​ を用いてUserNSを作成
○ parentとの通信用のFDに ​SYNC_USERMAP_PLS​ を書き込み、​uid_map​・​gid_map​の設定を要求
○ SYNC_USERMAP_ACK​を待ち、​setresuid(0)​してNS内でrootに昇格
case JUMP_CHILD:{
...
if (config.cloneflags & CLONE_NEWUSER) {
if (unshare(CLONE_NEWUSER) < 0)
bail("failed to unshare user namespace");
config.cloneflags &= ~CLONE_NEWUSER;
/*
* We don't have the privileges to do any mapping here (see the
* clone_parent rant). So signal our parent to hook us up.
*/
/* Switching is only necessary if we joined namespaces. */
if (config.namespaces) {
if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0)
bail("failed to set process as dumpable");
}
s = SYNC_USERMAP_PLS;
if (write(syncfd, &s, sizeof(s)) != sizeof(s))
bail("failed to sync with parent: write(SYNC_USERMAP_PLS)");
/* ... wait for mapping ... */
if (read(syncfd, &s, sizeof(s)) != sizeof(s))
bail("failed to sync with parent: read(SYNC_USERMAP_ACK)");
if (s != SYNC_USERMAP_ACK)
bail("failed to sync with parent: SYNC_USERMAP_ACK: got %u", s);
/* Switching is only necessary if we joined namespaces. */
if (config.namespaces) {
if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0)
bail("failed to set process as dumpable");
}
/* Become root in the namespace proper. */
if (setresuid(0, 0, 0) < 0)
bail("failed to become root in user namespace");
}
...
● nsexecにてparentがやること:
libcontainer/nsenter/nsexec.c:nsexec():JUMP_PARENT:SYNC_USERMAP_PLS
○ SYNC_USERMAP_PLS​ が来たら、​update_uidmap(); update_gidmap();​ して ​SYNC_USERMAP_ACK
を応答
○ update_uidmap(); update_gidmap();​ は単に​ /proc/​PID​/uid_map​、​/proc/​PID​/gid_map​ に書き
込むだけ
○ parentはrootで動作しているので、​setgroups​を無効化したり、​newuidmap​・​newgidmap​ SUIDバイ
ナリを呼び出したりしなくてよい
case SYNC_USERMAP_PLS:
/*
* Enable setgroups(2) if we've been asked to. But we also
* have to explicitly disable setgroups(2) if we're
* creating a rootless container for single-entry mapping.
* i.e. config.is_setgroup == false.
* (this is required since Linux 3.19).
*
* For rootless multi-entry mapping, config.is_setgroup shall be true and
* newuidmap/newgidmap shall be used.
*/
if (config.is_rootless_euid && !config.is_setgroup)
update_setgroups(child, SETGROUPS_DENY);
/* Set up mappings. */
update_uidmap(config.uidmappath, child, config.uidmap, config.uidmap_len);
update_gidmap(config.gidmappath, child, config.gidmap, config.gidmap_len);
s = SYNC_USERMAP_ACK;
if (write(syncfd, &s, sizeof(s)) != sizeof(s)) {
kill(child, SIGKILL);
bail("failed to sync with child: write(SYNC_USERMAP_ACK)");
}
break;
● UserNSに入ったchildはデバイスノードを​mknod​できないので、ホストからbind-mount する:
libcontainer/rootfs_linux.go:createDevices()
func createDevices(config *configs.Config) error {
useBindMount := system.RunningInUserNS() || config.Namespaces.Contains(configs.NEWUSER)
oldMask := unix.Umask(0000)
for _, node := range config.Devices {
// containers running in a user namespace are not allowed to mknod
// devices so we can just bind mount it from the host.
if err := createDeviceNode(config.Rootfs, node, useBindMount); err != nil {
unix.Umask(oldMask)
return err
}
}
unix.Umask(oldMask)
return nil
}
非rootでruncにUserNSを作らせる場合 (本来の “rootless runc”)
● git grep RootlessEUID​ 、 ​git grep -i rootless_euid​ 、​git grep ‘os.Geteuid() != 0’​ すると、
非rootでruncを動作させる場合のフローが見えてくる
● parentはrootを持っていないので、​/proc/​PID​/uid_map​、​/proc/​PID​/gid_map​ にそれぞれ1エントリしか書
き込めない
○ 複数エントリがconfigで指定されている場合は、SUIDビットまたはfile capabilityがついた​newuidmap
、​newgidmap​バイナリを呼び出して対応表を書き込む
○ 単一のエントリしか指定されていない場合は、​/proc/PID/setgroups​ に ​“deny”​ を書き込んでか
ら、​/proc/​PID​/uid_map​、​/proc/​PID​/gid_map​ に書き込む
■ これが本来の”rootless runc”であるが、利用事例は稀
● Cgroup Managerがrootlessモードになる ​rootless_linux.go:shouldUseRootlessCgroupManager()
func shouldUseRootlessCgroupManager(context *cli.Context) (bool, error) {
...
if os.Geteuid() != 0 {
return true, nil
}
if !system.RunningInUserNS() {
// euid == 0 , in the initial ns (i.e. the real root)
return false, nil
}
// euid = 0, in a userns.
// As we are unaware of cgroups path, we can't determine whether we have the full
// access to the cgroups path.
// Either way, we can safely decide to use the rootless cgroups manager.
return true, nil
}
● rootlessモードのCgroup managerは、パーミッション関連のエラーを無視する:
libcontainer/cgroups/fs/apply_raw.go:*Manager.Apply()
○ Cgroupを使いたいなら、予めrootで​chmod​・​chown​しておく必要がある
■ Cgroup v1では非推奨
func (m *Manager) Apply(pid int) (err error) {
...
for _, sys := range m.getSubsystems() {
...
if err := sys.Apply(d); err != nil {
// In the case of rootless (including euid=0 in userns), where an explicit cgroup path
hasn't
// been set, we don't bail on error in case of permission problems.
// Cases where limits have been set (and we couldn't create our own
// cgroup) are handled by Set.
if isIgnorableError(m.Rootless, err) && m.Cgroups.Path == "" {
delete(m.Paths, sys.Name())
continue
}
return err
}
}
return nil
}
● checkpointやAppArmorは使えない
● UserNSと一緒にNetNSもunshareしたいなら工夫が必要 (「​既存UserNS内でruncを実行する場合​」参照)
既存UserNS内でruncを実行する場合 (Rootless Dockerなど)
● runcの外側で予めUserNSを作っておく必要がある
○ Docker、k3s、BuildKitなどのRootlessモードでは​RootlessKit​が使われる
○ PodmanのRootlessモードではPodman自身がUserNSを作成する
○ UserNSと一緒にNetNSもunshareしたいなら工夫が必要
■ NetNSをunshareしないと、コンテナ内のプロセスからコンテナ外の抽象UNIXソケットにア
クセスできてしまう
● →containerdのbreakoutに繋がる
■ 方法1: SUIDバイナリでNetNSを設定 (lxc-user-nic)
■ 方法2: NetNS内にTAPデバイスを作り、ユーザモードでTCP/IPをエミュレート (slirp4netns、
VPNKit)
■ RootlessKit系はlxc-user-nic、slirp4netns、VPNKitに対応
■ Podmanはslirp4netnsに対応
● git grep RunningInUserNS​ すると、非rootでruncを動作させる場合のフローが見えてくる
○ cgroup、checkpoint、AppArmorが使えない以外は、普通にrootでruncを動作させる場合とあまり変
わらない
runcを既存UserNSにjoinさせる場合
● 既存のUserNSのpathを指定して、nsenterさせることができる
{
...
"namespaces": [
{
"type": "user",
"path": "/proc/42/ns/user"
},
...
}
● 利用事例は稀
○ podman run --userns container:foo​ などで利用されている

More Related Content

PDF
Dockerを支える技術
PDF
Docker Compose入門~今日から始めるComposeの初歩からswarm mode対応まで
PPTX
root権限無しでKubernetesを動かす
PDF
Dockerからcontainerdへの移行
PDF
Linux女子部 systemd徹底入門
PDF
containerdの概要と最近の機能
PDF
ML2/OVN アーキテクチャ概観
PPTX
Kubernetesでの性能解析 ~なんとなく遅いからの脱却~(Kubernetes Meetup Tokyo #33 発表資料)
Dockerを支える技術
Docker Compose入門~今日から始めるComposeの初歩からswarm mode対応まで
root権限無しでKubernetesを動かす
Dockerからcontainerdへの移行
Linux女子部 systemd徹底入門
containerdの概要と最近の機能
ML2/OVN アーキテクチャ概観
Kubernetesでの性能解析 ~なんとなく遅いからの脱却~(Kubernetes Meetup Tokyo #33 発表資料)

What's hot (20)

PDF
Ethernetの受信処理
PDF
NEDIA_SNIA_CXL_講演資料.pdf
PDF
Dockerクイックツアー
PDF
Dockerイメージ管理の内部構造
PDF
PG-REXで学ぶPacemaker運用の実例
PDF
Docker入門: コンテナ型仮想化技術の仕組みと使い方
PPTX
Kubernetes環境に対する性能試験(Kubernetes Novice Tokyo #2 発表資料)
PDF
Keystone fernet token
PDF
Mavenの真実とウソ
PDF
今話題のいろいろなコンテナランタイムを比較してみた
PDF
initramfsについて
PDF
KubeCon + CloudNativeCon Europe 2022 Recap - Batch/HPCの潮流とScheduler拡張事例 / Kub...
PDF
コンテナセキュリティにおける権限制御(OCHaCafe5 #3 Kubernetes のセキュリティ 発表資料)
PDF
OCIランタイムの筆頭「runc」を俯瞰する
PPTX
Enable DPDK and SR-IOV for containerized virtual network functions with zun
PPTX
Slurmのジョブスケジューリングと実装
PDF
Dockerfileを改善するためのBest Practice 2019年版
PPTX
YoctoをつかったDistroの作り方とハマり方
PPTX
Dockerからcontainerdへの移行
PDF
ゼロからはじめるKVM超入門
Ethernetの受信処理
NEDIA_SNIA_CXL_講演資料.pdf
Dockerクイックツアー
Dockerイメージ管理の内部構造
PG-REXで学ぶPacemaker運用の実例
Docker入門: コンテナ型仮想化技術の仕組みと使い方
Kubernetes環境に対する性能試験(Kubernetes Novice Tokyo #2 発表資料)
Keystone fernet token
Mavenの真実とウソ
今話題のいろいろなコンテナランタイムを比較してみた
initramfsについて
KubeCon + CloudNativeCon Europe 2022 Recap - Batch/HPCの潮流とScheduler拡張事例 / Kub...
コンテナセキュリティにおける権限制御(OCHaCafe5 #3 Kubernetes のセキュリティ 発表資料)
OCIランタイムの筆頭「runc」を俯瞰する
Enable DPDK and SR-IOV for containerized virtual network functions with zun
Slurmのジョブスケジューリングと実装
Dockerfileを改善するためのBest Practice 2019年版
YoctoをつかったDistroの作り方とハマり方
Dockerからcontainerdへの移行
ゼロからはじめるKVM超入門
Ad

Similar to [Container Runtime Meetup] runc & User Namespaces (20)

PDF
Mincs 日本語版
PPTX
PFIセミナーH271022 ~コマンドを叩いて遊ぶ コンテナ仮想、その裏側~
PDF
Linux Namespace
PDF
Linux Namespaces
PPTX
KubeCon EU報告(ランタイム関連,イメージ関連)
PDF
バックアップに一番いいファイルシステムを頼む
PDF
20170124 linux basic_1
PDF
NPCA夏合宿 2014 講義資料
PDF
IaaSクラウドを支える基礎技術 演習編_v1_0
PPTX
CAMPHOR- day 2020 - Docker 超入門
PDF
LXC入門 - Osc2011 nagoya
PPTX
Functions
PDF
KVM+cgroup
PDF
スタート低レイヤー #0
PDF
20031030 「読み込み専用マウントによる改ざん防止Linuxサーバの構築」
PPTX
ContainerとName Space Isolation
PDF
Personal Cloud Automation
PDF
コンテナを突き破れ!! ~コンテナセキュリティ入門基礎の基礎~(Kubernetes Novice Tokyo #11 発表資料)
PDF
すごく分かるwarden
PDF
MINCS – containers in the shell script
Mincs 日本語版
PFIセミナーH271022 ~コマンドを叩いて遊ぶ コンテナ仮想、その裏側~
Linux Namespace
Linux Namespaces
KubeCon EU報告(ランタイム関連,イメージ関連)
バックアップに一番いいファイルシステムを頼む
20170124 linux basic_1
NPCA夏合宿 2014 講義資料
IaaSクラウドを支える基礎技術 演習編_v1_0
CAMPHOR- day 2020 - Docker 超入門
LXC入門 - Osc2011 nagoya
Functions
KVM+cgroup
スタート低レイヤー #0
20031030 「読み込み専用マウントによる改ざん防止Linuxサーバの構築」
ContainerとName Space Isolation
Personal Cloud Automation
コンテナを突き破れ!! ~コンテナセキュリティ入門基礎の基礎~(Kubernetes Novice Tokyo #11 発表資料)
すごく分かるwarden
MINCS – containers in the shell script
Ad

More from Akihiro Suda (20)

PDF
20250617 [KubeCon JP 2025] containerd - Project Update and Deep Dive.pdf
PDF
20250616 [KubeCon JP 2025] VexLLM - Silence Negligible CVE Alerts Using LLM.pdf
PDF
20250403 [KubeCon EU] containerd - Project Update and Deep Dive.pdf
PDF
20250403 [KubeCon EU Pavilion] containerd.pdf
PDF
20250402 [KubeCon EU Pavilion] Lima.pdf_
PDF
20241115 [KubeCon NA Pavilion] Lima.pdf_
PDF
20241113 [KubeCon NA Pavilion] containerd.pdf
PDF
【情報科学若手の会 (2024/09/14】なぜオープンソースソフトウェアにコントリビュートすべきなのか
PDF
【Vuls祭り#10 (2024/08/20)】 VexLLM: LLMを用いたVEX自動生成ツール
PDF
20240415 [Container Plumbing Days] Usernetes Gen2 - Kubernetes in Rootless Do...
PDF
20240321 [KubeCon EU Pavilion] Lima.pdf_
PDF
20240320 [KubeCon EU Pavilion] containerd.pdf
PDF
20240201 [HPC Containers] Rootless Containers.pdf
PDF
[Podman Special Event] Kubernetes in Rootless Podman
PDF
[KubeConNA2023] Lima pavilion
PDF
[KubeConNA2023] containerd pavilion
PDF
[DockerConハイライト] OpenPubKeyによるイメージの署名と検証.pdf
PDF
[CNCF TAG-Runtime] Usernetes Gen2
PDF
[DockerCon 2023] Reproducible builds with BuildKit for software supply chain ...
PDF
The internals and the latest trends of container runtimes
20250617 [KubeCon JP 2025] containerd - Project Update and Deep Dive.pdf
20250616 [KubeCon JP 2025] VexLLM - Silence Negligible CVE Alerts Using LLM.pdf
20250403 [KubeCon EU] containerd - Project Update and Deep Dive.pdf
20250403 [KubeCon EU Pavilion] containerd.pdf
20250402 [KubeCon EU Pavilion] Lima.pdf_
20241115 [KubeCon NA Pavilion] Lima.pdf_
20241113 [KubeCon NA Pavilion] containerd.pdf
【情報科学若手の会 (2024/09/14】なぜオープンソースソフトウェアにコントリビュートすべきなのか
【Vuls祭り#10 (2024/08/20)】 VexLLM: LLMを用いたVEX自動生成ツール
20240415 [Container Plumbing Days] Usernetes Gen2 - Kubernetes in Rootless Do...
20240321 [KubeCon EU Pavilion] Lima.pdf_
20240320 [KubeCon EU Pavilion] containerd.pdf
20240201 [HPC Containers] Rootless Containers.pdf
[Podman Special Event] Kubernetes in Rootless Podman
[KubeConNA2023] Lima pavilion
[KubeConNA2023] containerd pavilion
[DockerConハイライト] OpenPubKeyによるイメージの署名と検証.pdf
[CNCF TAG-Runtime] Usernetes Gen2
[DockerCon 2023] Reproducible builds with BuildKit for software supply chain ...
The internals and the latest trends of container runtimes

[Container Runtime Meetup] runc & User Namespaces

  • 1. runc & User Namespaces Container Runtime Meetup #1​ runcコードリーディング (2019/9/24) 対象とするruncのリビジョン: ​7507c64ff675606c5ff96b0dd8889a60c589f14d​ (2019/9/24時点で最新) 自己紹介 ● GitHub: ​@AkihiroSuda​ / Twitter: ​@_AkihiroSuda_ ● Moby (Docker)、containerd、BuildKitなどのメンテナ ● Rootlessコンテナなどセキュリティ関係を中心に取り組んでいる User Namespaces とは ● 非rootユーザをrootユーザに見せかける ○ もしUserNS内のプロセスに脆弱性があっても、ホストのrootを奪われずにすむ ● UserNS内の見かけ上のrootは、真のrootとはもちろん異なる ○ UID・GIDはUserNS内では0に見えるが、UserNS外からは本来のユーザのUID・GIDとして見える ■ パーミッション制約は本来のユーザのUID・GID (kuid・kgid) に基づく ○ NS内に閉じた、見かけ上のケーパビリティを得られる (​CAP_SYS_ADMIN​など) ■ UserNSとあわせてMountNSも作るとファイルシステムのマウントもできる ● Bind-mount、tmpfs、procfs、sysfs くらいしかマウントできない ● Kernel 4.18 (2018年)からはFUSEもマウントできる ● ブロックデバイスはマウントできない ● OverlayFSはマウントできない。ただしUbuntuではカーネルにパッチが当てられてい るのでマウントできる。 ■ カーネルモジュールを読み込んだり、ホストを再起動したりなど、NS外に影響が及ぶ操作は できない ● Kernel 2.6.23 (2007年)にて導入された ● Kernel 3.8 (2013年)からは非rootユーザが自分でUserNSを作れるようになった 利用事例 ● Dockerでの利用事例 ○ dockerd --userns-remap ■ コンテナ内のプロセスをUserNS内で動かす ■ runc自体、containerd自体、Docker自体は普通にrootで動く ■ runcがUserNSを作成した後のフローについて、脆弱性を軽減できる ● CVE-2016-3697​、​runc#1962​、​CVE-2019-5736​... ■ 将来的にはデフォルトになるかもしれない ○ Rootless Docker ■ コンテナ内のプロセスだけではなく、runc自体、containerd自体、Docker自体もUserNS内で 動かす ■ runcの脆弱性のみならず、Dockerデーモンなどの脆弱性も軽減できる ● CVE-2014-9357​、​CVE-2018-15664​、​CVE-2019-14271​... ■ Cgroup, checkpoint, AppArmorが使えない ● Cgroup v2移行後はcgroupも使えるようになる見込み ● Kubernetesでは未だに使えない ○ k3sは実験的にrootlessモードに対応 ● LXDではデフォルトで用いられる (​dockerd --userns-remap​に類似)
  • 2. Sub-users & sub-groups ● NS内に複数のUID・GIDをマップすることができる ○ NS内の見かけ上のrootから、NS内の非rootにスイッチすることで更に権限を分離できる ○ 複数UID・GIDを前提としているプログラムとの互換性 ■ nginx、mysqlなど多くのミドルウェアはまずrootで起動してから、自身専用のUID・GIDに遷 移する ● /proc/​PID​/uid_map​、​/proc/​PID​/gid_map​ に、UserNS内と外のUIDの対応表を書き込むことで設定できる (書き込みはNS外から行う) ○ 対応表を書き込むまでは、NS内には”​nobody​” (65534)だけが存在 ● 複数のUID・GIDをマップするには、NS外でのroot権限 (​CAP_SETUID​・​CAP_SETGID​)が必要 ○ CAP_SETUID​ がないと、1エントリ (自分のUIDだけ) しか ​/proc/​PID​/uid_map​ に書き込めない ○ CAP_SETGID​ がないと、1エントリ (自分のUIDだけ) しか​ /proc/​PID​/gid_map​ に書き込めない ■ さらに、予め​ /proc/​PID​/setgroups​ に​ “deny”​ を書き込んでおく必要が生じる (​setgroups(2)​を呼び出せなくなるので、supplementary groupsを設定できなくなる) ● なので、非rootユーザで複数のUID・GIDをマップしたいとき(Rootless Dockerなど)は、SETUID バイナリ /usr/bin/newuidmap​ および​ ​/usr/bin/newgidmap​ を用いる必要がある ○ 予め​ /etc/subuid ​および​ /etc/subgid​ に、ユーザが利用して良いUID・GIDのリストを書いてお く ■ LDAP環境では使いにくいという問題がある ○ SETUIDしているdistroが多いが、実際はfile capability (​CAP_SETUID​、​CAP_SETGID​) だけでも十分 ● より詳しい情報は ​user_namespaces(7)​ 参照 ● runcを使わなくても​ ​unshare -U​ コマンドで空のUserNSを作成できる ○ unshare(2)​ ​システムコールを呼び出している runc と UserNSの関係 似て非なる組み合わせが色々ある ● runcにUserNSを作らせる場合 ○ rootでruncにUserNSを作らせる場合 (​dockerd --userns-remap​ など) ○ 非rootでruncにUserNSを作らせる場合 (本来の “rootless runc”) ● 既存UserNS内でruncを実行する場合 (Rootless Dockerなど) ● runcを既存UserNSにjoinさせる場合
  • 3. runcにUserNSを作らせる場合 config.json​ に次のような設定を渡すと、runcにUserNSを作成させることが出来る "linux": { "uidMappings": [ { "containerID": 0, "hostID": 1001, "size": 1 }, { "containerID": 1, "hostID": 100000, "size": 65536 } ], "gidMappings": [ { "containerID": 0, "hostID": 1001, "size": 1 }, { "containerID": 1, "hostID": 100000, "size": 65536 } ], "namespaces": [ { "type": "user" }, ... } ● config.json​ からlibcontainer configへの変換: libcontainer/specconv/spec_linux.go:setupUserNamespace() func setupUserNamespace(spec *specs.Spec, config *configs.Config) error { create := func(m specs.LinuxIDMapping) configs.IDMap { return configs.IDMap{ HostID: int(m.HostID), ContainerID: int(m.ContainerID), Size: int(m.Size), } } if spec.Linux != nil { for _, m := range spec.Linux.UIDMappings { config.UidMappings = append(config.UidMappings, create(m)) } for _, m := range spec.Linux.GIDMappings { config.GidMappings = append(config.GidMappings, create(m)) } } rootUID, err := config.HostRootUID() if err != nil { return err } rootGID, err := config.HostRootGID() if err != nil { return err } for _, node := range config.Devices { node.Uid = uint32(rootUID) node.Gid = uint32(rootGID) } return nil } ● git grep NEWUSER​ すると、libcontainer config変換以後の、UserNS関係のフローが見えてくる
  • 4. rootでruncにUserNSを作らせる場合 (​dockerd --userns-remap​ など) ● 主要な部分は​ libcontainer/nsenter/nsexec.c​ ​に集中 ● nsexecにてchildがやること:​ ​libcontainer/nsenter/nsexec.c:nsexec():JUMP_CHILD ○ unshare(CLONE_NEWUSER)​ を用いてUserNSを作成 ○ parentとの通信用のFDに ​SYNC_USERMAP_PLS​ を書き込み、​uid_map​・​gid_map​の設定を要求 ○ SYNC_USERMAP_ACK​を待ち、​setresuid(0)​してNS内でrootに昇格 case JUMP_CHILD:{ ... if (config.cloneflags & CLONE_NEWUSER) { if (unshare(CLONE_NEWUSER) < 0) bail("failed to unshare user namespace"); config.cloneflags &= ~CLONE_NEWUSER; /* * We don't have the privileges to do any mapping here (see the * clone_parent rant). So signal our parent to hook us up. */ /* Switching is only necessary if we joined namespaces. */ if (config.namespaces) { if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) < 0) bail("failed to set process as dumpable"); } s = SYNC_USERMAP_PLS; if (write(syncfd, &s, sizeof(s)) != sizeof(s)) bail("failed to sync with parent: write(SYNC_USERMAP_PLS)"); /* ... wait for mapping ... */ if (read(syncfd, &s, sizeof(s)) != sizeof(s)) bail("failed to sync with parent: read(SYNC_USERMAP_ACK)"); if (s != SYNC_USERMAP_ACK) bail("failed to sync with parent: SYNC_USERMAP_ACK: got %u", s); /* Switching is only necessary if we joined namespaces. */ if (config.namespaces) { if (prctl(PR_SET_DUMPABLE, 0, 0, 0, 0) < 0) bail("failed to set process as dumpable"); } /* Become root in the namespace proper. */ if (setresuid(0, 0, 0) < 0) bail("failed to become root in user namespace"); } ... ● nsexecにてparentがやること: libcontainer/nsenter/nsexec.c:nsexec():JUMP_PARENT:SYNC_USERMAP_PLS ○ SYNC_USERMAP_PLS​ が来たら、​update_uidmap(); update_gidmap();​ して ​SYNC_USERMAP_ACK を応答 ○ update_uidmap(); update_gidmap();​ は単に​ /proc/​PID​/uid_map​、​/proc/​PID​/gid_map​ に書き 込むだけ ○ parentはrootで動作しているので、​setgroups​を無効化したり、​newuidmap​・​newgidmap​ SUIDバイ ナリを呼び出したりしなくてよい case SYNC_USERMAP_PLS: /* * Enable setgroups(2) if we've been asked to. But we also * have to explicitly disable setgroups(2) if we're * creating a rootless container for single-entry mapping. * i.e. config.is_setgroup == false. * (this is required since Linux 3.19). * * For rootless multi-entry mapping, config.is_setgroup shall be true and * newuidmap/newgidmap shall be used. */ if (config.is_rootless_euid && !config.is_setgroup) update_setgroups(child, SETGROUPS_DENY);
  • 5. /* Set up mappings. */ update_uidmap(config.uidmappath, child, config.uidmap, config.uidmap_len); update_gidmap(config.gidmappath, child, config.gidmap, config.gidmap_len); s = SYNC_USERMAP_ACK; if (write(syncfd, &s, sizeof(s)) != sizeof(s)) { kill(child, SIGKILL); bail("failed to sync with child: write(SYNC_USERMAP_ACK)"); } break; ● UserNSに入ったchildはデバイスノードを​mknod​できないので、ホストからbind-mount する: libcontainer/rootfs_linux.go:createDevices() func createDevices(config *configs.Config) error { useBindMount := system.RunningInUserNS() || config.Namespaces.Contains(configs.NEWUSER) oldMask := unix.Umask(0000) for _, node := range config.Devices { // containers running in a user namespace are not allowed to mknod // devices so we can just bind mount it from the host. if err := createDeviceNode(config.Rootfs, node, useBindMount); err != nil { unix.Umask(oldMask) return err } } unix.Umask(oldMask) return nil } 非rootでruncにUserNSを作らせる場合 (本来の “rootless runc”) ● git grep RootlessEUID​ 、 ​git grep -i rootless_euid​ 、​git grep ‘os.Geteuid() != 0’​ すると、 非rootでruncを動作させる場合のフローが見えてくる ● parentはrootを持っていないので、​/proc/​PID​/uid_map​、​/proc/​PID​/gid_map​ にそれぞれ1エントリしか書 き込めない ○ 複数エントリがconfigで指定されている場合は、SUIDビットまたはfile capabilityがついた​newuidmap 、​newgidmap​バイナリを呼び出して対応表を書き込む ○ 単一のエントリしか指定されていない場合は、​/proc/PID/setgroups​ に ​“deny”​ を書き込んでか ら、​/proc/​PID​/uid_map​、​/proc/​PID​/gid_map​ に書き込む ■ これが本来の”rootless runc”であるが、利用事例は稀 ● Cgroup Managerがrootlessモードになる ​rootless_linux.go:shouldUseRootlessCgroupManager() func shouldUseRootlessCgroupManager(context *cli.Context) (bool, error) { ... if os.Geteuid() != 0 { return true, nil } if !system.RunningInUserNS() { // euid == 0 , in the initial ns (i.e. the real root) return false, nil } // euid = 0, in a userns. // As we are unaware of cgroups path, we can't determine whether we have the full // access to the cgroups path. // Either way, we can safely decide to use the rootless cgroups manager. return true, nil } ● rootlessモードのCgroup managerは、パーミッション関連のエラーを無視する: libcontainer/cgroups/fs/apply_raw.go:*Manager.Apply() ○ Cgroupを使いたいなら、予めrootで​chmod​・​chown​しておく必要がある ■ Cgroup v1では非推奨 func (m *Manager) Apply(pid int) (err error) { ... for _, sys := range m.getSubsystems() { ... if err := sys.Apply(d); err != nil { // In the case of rootless (including euid=0 in userns), where an explicit cgroup path
  • 6. hasn't // been set, we don't bail on error in case of permission problems. // Cases where limits have been set (and we couldn't create our own // cgroup) are handled by Set. if isIgnorableError(m.Rootless, err) && m.Cgroups.Path == "" { delete(m.Paths, sys.Name()) continue } return err } } return nil } ● checkpointやAppArmorは使えない ● UserNSと一緒にNetNSもunshareしたいなら工夫が必要 (「​既存UserNS内でruncを実行する場合​」参照) 既存UserNS内でruncを実行する場合 (Rootless Dockerなど) ● runcの外側で予めUserNSを作っておく必要がある ○ Docker、k3s、BuildKitなどのRootlessモードでは​RootlessKit​が使われる ○ PodmanのRootlessモードではPodman自身がUserNSを作成する ○ UserNSと一緒にNetNSもunshareしたいなら工夫が必要 ■ NetNSをunshareしないと、コンテナ内のプロセスからコンテナ外の抽象UNIXソケットにア クセスできてしまう ● →containerdのbreakoutに繋がる ■ 方法1: SUIDバイナリでNetNSを設定 (lxc-user-nic) ■ 方法2: NetNS内にTAPデバイスを作り、ユーザモードでTCP/IPをエミュレート (slirp4netns、 VPNKit) ■ RootlessKit系はlxc-user-nic、slirp4netns、VPNKitに対応 ■ Podmanはslirp4netnsに対応 ● git grep RunningInUserNS​ すると、非rootでruncを動作させる場合のフローが見えてくる ○ cgroup、checkpoint、AppArmorが使えない以外は、普通にrootでruncを動作させる場合とあまり変 わらない runcを既存UserNSにjoinさせる場合 ● 既存のUserNSのpathを指定して、nsenterさせることができる { ... "namespaces": [ { "type": "user", "path": "/proc/42/ns/user" }, ... } ● 利用事例は稀 ○ podman run --userns container:foo​ などで利用されている