Ubuntu Weekly Recipe

第686回 Bubblewrap/bwrapを使って管理者権限なしで非特権コンテナーを作る

この記事を読むのに必要な時間:およそ 4.5 分

LXDにしろDockerにしろsystemd-nspawnにしろ,コンテナーを作るには直接的であれ間接的であれ管理者権限が必要です。しかしながら,これらのコンテナー技術の基になっているLinuxカーネルの名前空間は,必ずしも管理者権限で作らなければいけないわけではありません。今回は,ユーザー権限で非特権コンテナーを構築できるBubblewrap(bwrapコマンド)を紹介します。

Bubblewrapとunshare

LXDやDockerといったコンテナー管理ツールは,管理者権限でデーモンを立ち上げ,クライアントがそのデーモンと通信することでコンテナーインスタンスを構築しています。また第491回のいまから『あえて』systemdのコンテナ機能を使ってみるで紹介されているsystemd-nspawnもコンテナーインスタンス作成時にsudoコマンドを使っています。

しかしながらRed Hatが開発し,RHEL系を問わず広く使われているコンテナーエンジンであるpodman「デーモンレス」「ルートレス」としても使えることを謳っています。つまりコンテナーを使うだけなら,何か管理者権限で動くデーモンも必要ありませんし,ユーザー権限でできるのです。

Bubblewrapは,そんなユーザー権限でコンテナーインスタンス(サンドボックス)を作るツールのひとつです。Flatpakがxdg-appという名前だった頃に,アプリケーションをサンドボックス化するにあたって作られたツールであり,GNOMEで開発されていたlinux-user-chrootが元になっています。つまりどちらかと言うとデスクトップアプリをサンドボックスの中で動かすために作られたツールなのです※1⁠。

※1
CoreOSがRed Hatに買収されて,既存のProject Atomicの成果等との統合・整理が行われた結果として,現在のPodmanやFlatpak/Bubblewrapがある感じです。

実はデスクトップ版のUbuntuであれば,Ubuntu 18.04 LTSから自動的にインストールされた状態になっています。つまり追加で何かインストールしなくても,最初からBubblewrapが使えるのです。このBubblewrapはたとえばGNOMEファイル(Nautilus)「/usr/share/thumbnailers/」以下のスクリプトを使って,サムネイルを生成する際に使っていたりします。

同じようにユーザー権限でコンテナーを構築するツールとしてunshareコマンドが存在します。こちらはutil-linuxパッケージに所属しているため,ほぼすべてのUbuntuにインストールされていることでしょう。こちらはbubblewrapに比べるとよりシンプルなツールで,どちらかと言うとunshare(2)システムコールのラッパーという位置づけです。ただし基本的にやっていることは同じなので,unshareコマンドを用いて同等の機能を実現することは原理的には可能となります。

Bubblewrapの基本的な使い方

デスクトップ版のUbuntuならBubblewrapは最初から入っているはずです。サーバー版のようにまだインストールされていなかったら,次のようにインストールしてください。

$ sudo apt install bubblewrap

ソフトウェア名は「Bubblewrap」ですが,ややこしいことにコマンド名はbwrapです。

$ ls -l $(which bwrap)
-rwxr-xr-x 1 root root 68032  1月  3  2021 /usr/bin/bwrap
$ getcap $(which bwrap)

このように,setuidもなされていないことから,ユーザー権限で動くことがわかります。

ちなみにUbuntuのbwrapは以前からsetuidが外されていましたが,Debianは先日リリースされたDebian 11 bullseyeより前はsetuidがついていました。これはサポートしていたカーネルが古く,非特権ユーザーによる名前空間の利用に制約があったためです。また,次のsysctlが1になっている必要もあります。

$ sysctl kernel.unprivileged_userns_clone
kernel.unprivileged_userns_clone = 1

実際にbashを別のユーザー名前空間の別の存在しないUIDで動かしてみましょう。

$ id 2000
id: `2000': no such user

$ bwrap --ro-bind / / --dev /dev --unshare-user --uid 2000 --gid 2000 bash
私は名前がありません!@nuc:~$ id
uid=2000 gid=2000 groups=2000,65534(nogroup)

--ro-bind / /でホストのルートファイルシステムをコンテナー内部のルートファイルシステムに読み込み専用でバインドしています。読み書きできるようにしたい場合は--bindを使います。

--dev /devで新しいdevtmpfsをマウントしています。新規のdevtmpfsをマウントしているためコンテナーから見ると,ホストの/dev/と異なり,基本的なデバイスしか見えません。

私は名前がありません!@nuc:~$ ls /dev/
console  core  fd  full  null  ptmx  pts  random  shm  stderr  stdin  stdout  tty  urandom  zero

もしホストのdevtmpfsをコンテナーから見えるようにして,各種デバイスを使えるようにしたい場合は--dev-bind /dev /devのように指定します。ただしデバイスファイルにアクセスできるようにするためには,ホスト・コンテナーともにUID/GIDについて慎重に検討する必要があります。たとえば--unshare-userで名前空間をホストから隔離します。非特権ユーザーで実行した場合は,コンテナー内部のUID/GIDはホストから見るとそのUID/GIDとして見えます。

--uid 2000 --gid 2000でコンテナーの中のUID/GIDをそれぞれ2000にしています。今回はルートファイルシステムがホストと同じ状態で,なおかつそのホストにはUIDが2000となるユーザーが存在しないため,bashのプロンプトには「私は名前がありません!」と表示されてしまっています。

最後に実行するコマンドとして「bash」を指定しています。試しに起動したbashのPIDをコンテナーの中とホストからそれぞれ見てみましょう。

私は名前がありません!@nuc:~$ ps --pid $$ -f n
     UID     PID    PPID  C STIME TTY      STAT   TIME CMD
    2000 3287247 3287246  0 19:37 ?        S      0:00 bash

$ ps --pid 3287247 -f n
     UID     PID    PPID  C STIME TTY      STAT   TIME CMD
    1000 3287247 3287246  0 19:37 pts/12   S+     0:00 bash

前者はUID=2000のプロセスになっていますが,ホストから見るとbwrapを実行したUID=1000として見えていることがわかります。

今回は読み込み専用でバインドしているため,ルートファイルシステムには書き込みできませんが,たとえば--bind / /オプションにしていた場合は,⁠bwrapを実行したユーザーがアクセス可能な領域ならコンテナー内部からも書き込める」ということになります。

bwrapではユーザー名前空間以外にも次のようなオプションで各名前空間を隔離可能です。

--unshare-user
ユーザー名前空間を作成し,UID/GIDをホストから隔離する。
--unshare-user-try
可能ならユーザー名前空間を作成するものの,できなかったら無視する。
--unshare-ipc
IPC(プロセス間通信)名前空間を作成し,共有メモリーやセマフォなどをホストから隔離する。
--unshare-pid
PID名前空間を作成し,プロセス情報をホストから隔離する。
--unshare-net
ネットワーク名前空間を作成し,ネットワークインターフェース等をホストから隔離する。
--unshare-uts
UTS(Unixt Time Sharing)名前空間を作成し,ホスト名やドメイン名をホストから隔離する。
--unshare-cgroup
cgroup名前空間を作成し,cgroup機能をホストから隔離する。
--unshare-cgroup-try
可能ならcgroup名前空間を作成するものの,できなかったら無視する。
--unshare-all
ユーザー・IPC・PID・ネットワーク・UTS・cgroup名前空間を作成する。

必要に応じて隔離度を変えると良いでしょう。

たとえば先ほど例だとユーザー名前空間だけ隔離していたため,コンテナーの中からもホストのネットワークインターフェースが見える状態でした。

私は名前がありません!@nuc:~$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
2: enp5s0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc mq state DOWN mode DEFAULT group default qlen 1000
    link/ether d4:5d:df:1d:f8:93 brd ff:ff:ff:ff:ff:ff
3: eno1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    link/ether d4:5d:df:1d:f8:92 brd ff:ff:ff:ff:ff:ff
    altname enp0s31f6
(以下略)

これが--unshare-netを指定して起動すると,ループバックインターフェースしか見えなくなります。

私は名前がありません!@nuc:~$ exit
exit
shibata@nuc:~$ bwrap --ro-bind / / --dev /dev --unshare-user --unshare-net --uid 2000 --gid 2000 bash

私は名前がありません!@nuc:~$ ip link
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN mode DEFAULT group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

ちなみに--bind-ro--bindはディレクトリ単位で指定できます。よって「/usr/lib」以下だけホストと同じで,他は別のシステム・ディレクトリーみたいな指定や,/run以下だけ書き込みできるといった使い方も可能です。隔離用途だけでなく,ちょっとしたデバッグにも使えるでしょう。

著者プロフィール

柴田充也(しばたみつや)

Ubuntu Japanese Team Member株式会社 創夢所属。数年前にLaunchpad上でStellariumの翻訳をしたことがきっかけで,Ubuntuの翻訳にも関わるようになりました。