Ce diaporama a bien été signalé.
Nous utilisons votre profil LinkedIn et vos données d’activité pour vous proposer des publicités personnalisées et pertinentes. Vous pouvez changer vos préférences de publicités à tout moment.

[Container Runtime Meetup] runc & User Namespaces

4 663 vues

Publié le

Container Runtime Meetup #1 runcコードリーディング資料
https://runtime.connpass.com/event/145088/

Publié dans : Logiciels
  • Soyez le premier à commenter

[Container Runtime Meetup] runc & User Namespaces

  1. 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. 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. 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. 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. 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. 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​ などで利用されている

×