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]。
実はデスクトップ版のUbuntuであれば、Ubuntu 18.04 LTSから自動的にインストールされた状態になっています。つまり追加で何かインストールしなくても、最初からBubblewrapが使えるのです。このBubblewrapはたとえばGNOMEファイル(Nautilus)が「/usr/share/thumbnailers/」以下のスクリプトを使って、サムネイルを生成する際に使っていたりします。
同じようにユーザー権限でコンテナーを構築するツールとしてunshareコマンドが存在します。こちらはutil-linuxパッケージに所属しているため、ほぼすべてのUbuntuにインストールされていることでしょう。こちらはbubblewrapに比べるとよりシンプルなツールで、どちらかと言うとunshare(2)
システムコールのラッパーという位置づけです。ただし基本的にやっていることは同じなので、unshareコマンドを用いて同等の機能を実現することは原理的には可能となります。
Bubblewrapの基本的な使い方
デスクトップ版のUbuntuならBubblewrapは最初から入っているはずです。サーバー版のようにまだインストールされていなかったら、次のようにインストールしてください。
ソフトウェア名は「Bubblewrap」ですが、ややこしいことにコマンド名は「bwrap」です。
このように、setuidもなされていないことから、ユーザー権限で動くことがわかります。
ちなみにUbuntuのbwrapは以前からsetuidが外されていましたが、Debianは先日リリースされたDebian 11 bullseyeより前はsetuidがついていました。これはサポートしていたカーネルが古く、非特権ユーザーによる名前空間の利用に制約があったためです。また、次のsysctlが1になっている必要もあります。
実際にbashを別のユーザー名前空間の別の存在しないUIDで動かしてみましょう。
「--ro-bind / /
」でホストのルートファイルシステムをコンテナー内部のルートファイルシステムに読み込み専用でバインドしています。読み書きできるようにしたい場合は「--bind
」を使います。
「--dev /dev
」で新しいdevtmpfsをマウントしています。新規のdevtmpfsをマウントしているためコンテナーから見ると、ホストの「/dev/
」と異なり、基本的なデバイスしか見えません。
もしホストの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をコンテナーの中とホストからそれぞれ見てみましょう。
前者は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名前空間を作成する。
必要に応じて隔離度を変えると良いでしょう。
たとえば先ほど例だとユーザー名前空間だけ隔離していたため、コンテナーの中からもホストのネットワークインターフェースが見える状態でした。
これが「--unshare-net
」を指定して起動すると、ループバックインターフェースしか見えなくなります。
ちなみに「--bind-ro
」や「--bind
」はディレクトリ単位で指定できます。よって「/usr/lib」以下だけホストと同じで、他は別のシステム・ディレクトリーみたいな指定や、「/run
」以下だけ書き込みできるといった使い方も可能です。隔離用途だけでなく、ちょっとしたデバッグにも使えるでしょう。
ルートファイルシステムごとコンテナー化する
最後にbwrapコマンドを使って、ユーザー権限だけで特定のUbuntuの隔離環境を作ってみましょう。これと前述の「--bind
」を組み合わせれば、管理者権限がない環境でもユーザー権限だけで動作確認用のルートファイルシステムを作り、その中でコマンドを動かしてみるといった手順が可能になります[2]。
まずはベースとなるルートファイルシステムをダウンロードしておきます。wgetにしろcurlにしろ方法はなんでもかまいません。LTS向けならUbuntu Baseを使うとサイズを最小にできます。ただし実際のUbuntu環境に合わせるためにはそれなりの追加パッケージが必要です。たとえばvimやnanoはおろか、edもインストールされていないため、パッケージのインストールをせずにファイルを編集するにはsedとawkを駆使する必要があります。
その点、Ubuntu Cloud Imagesにあるクラウドイメージなら、各種クラウドサービスの最大公約数的なパッケージ構成になっているため、ほぼサーバー版のUbuntuと同じです。いろいろなイメージが用意されていますが「(リリース名)-server-cloudimg-(アーキテクチャー)-root.tar.xz
」を選んでください。しかしながらUbuntu Baseが27MiB程度なのに対して、Cloud Imagesは327MiBぐらいとサイズは10倍以上異なります。
ここではUbuntu Baseをダウンロードしてbwrapで切り替えてみましょう。
最後のコマンドだけ説明が必要です。最近のAptはパッケージのインターネットからのダウンロードや署名の検証などを、管理者権限ではなく「_apt
」ユーザーで行うようになっています。つまりaptコマンドの中で、このユーザーに変わらなくてはなりません。しかしながら、今回はできるだけシンプルにbwrapを使うようにする都合上、単に「コンテナーの中のrootを、ホストの非特権ユーザー」にのみマッピングしています。よってここでは「_apt
」ユーザーへのマッピングを用意したり、ダウンロードしたルートファイルシステムのパーミッションを正しく設定することは諦めて、Apt側の設定で「常に管理者権限で実行する」ように設定しました。
これでルートファイルシステムの作成は完了です。最後にそのルートファイルシステムにbwrapで「ログイン」してしまいましょう。
今回、ネットワークについてはホストのそれをそのまま相乗りするため、「--unshare-net
」は指定していません。ただしDNSによる名前解決は必要になるため「--ro-bind /etc/resolv.conf /etc/resolv.conf
」でホストのresolv設定をそのままコンテナーからも見えるようにしました。
これで管理者権限を取得することなく、コンテナーの中でパッケージのインストール等が使えるようになります。どの操作も基本的にホストから見ると、非特権ユーザーとして動いています。もちろん任意のデバイスファイルのアクセスやカーネルモジュールのロードなど、ホストの特権が必要になる処理は実行できません。もしろんsystemdも動いていないため、サービスの起動も一苦労です。このようにできることは限られてはしまいますが、できるだけ余計なパッケージをインストールせずに環境を整えるという意味では役に立つことでしょう。
ちなみに今回のコンテナーは隔離度が低く「セキュア」ではありません。一般的に非特権コンテナーと言うときはAppArmorなどの矯正アクセス制御やseccomp等を駆使して、コンテナーからできることを限定しています。まともにコンテナーとして運用したいなら、既存のシステムを使うようにしましょう。