Docker を使うなら当然 userns-remap してるよね! (KLabTechBook Vol.11)
この記事は2023年5月20日から開催された技術書典14にて頒布した「KLabTechBook Vol.11」に掲載したものです。
現在開催中の技術書典16オンラインマーケットにて新刊「KLabTechBook Vol.13」を頒布(電子版無料、紙+電子 500円)しています。 また、既刊も在庫があるものは物理本をオンラインマーケットで頒布しているほか、 KLabのブログからもすべての既刊のPDFを無料DLできます。 合わせてごらんください。
Docker1は開発作業において今や無くてはならないツールとなっています。
ところで、コンテナを利用してファイルを生成したとき、
所有者がホストマシン上でrootになってしまい困ったことはありませんか?
この章ではuserns-remap2という機能を使って、ファイルの所有権問題を解決する方法を紹介します。
Dockerとは
Dockerは、アプリケーションを「コンテナ」と呼ばれる隔離環境で実行するプラットフォームです。 コンテナの中にアプリケーションの実行に必要なファイルや設定を詰め込むことで、ホストマシンの環境を変更すること無くアプリケーションを実行できます。 またこれらのファイルや設定は「イメージ」という形で保存でき、イメージを共有すれば異なるマシン上でも同一の環境を再現できます。
Dockerの利用場面は常駐するサービスの実行環境だけでなく、単発のアプリケーションを実行するものとしても便利です。 特にファイル生成ツールのようなアプリケーションは、バージョンによって出力が微妙に異なることも少なくありません。 このようなとき、生成ツールをまるごとイメージとして共有しておくと、複数人での作業でもトラブルを避けられます。
ちなみに、このKLabTechBookもpdfの生成にDockerイメージのvvakame/review3を利用しています。
ファイルの所有権問題
Dockerでファイル生成する時、ボリュームマウントでホストのディレクトリをコンテナにマウントするのがよくある手法です。
リスト1では、カレントディレクトリを/workにマウントし、/work/hello.txtを生成しています。
▼リスト1 単純なファイルの生成
$ docker run --rm -v $(pwd):/work busybox sh -c 'echo Hello! > /work/hello.txt'
$ cat hello.txt
Hello!
$ ls -l
total 4
-rw-r--r-- 1 root root 7 Apr 14 18:40 hello.txt
ファイルを生成したあとlsコマンドで調べてみると、所有者がrootになっていました。
これではホストの一般ユーザーでは生成したファイルの編集ができず不便です。
これがファイルの所有権問題です。
DockerはLinuxカーネルの機能を組み合わせることでコンテナを実現しているため、 WindowsやmacOSでDockerを使うには、仮想マシンでLinuxを動かし、その上でDockerを動かすことになります。 これによりパフォーマンス上の問題がある一方で、ファイルの所有権問題は仮想マシンのファイル共有機能によって解決されている場合があります。 このためWebサーバーなどのLinuxマシンで直接Dockerを使おうとしてはじめて所有権問題に悩まされることになった人も多いのではないでしょうか。
Linuxでは、ユーザーはUID(User ID)という番号で識別されます。
ファイルやディレクトリにもUIDが割り当てられ、所有者を表します4。
一般的にrootのUIDには0が割り当てられていて、特権ユーザーを表します。
Dockerのコンテナ内では、基本的にはrootユーザーがコマンドを実行するため、
リスト1の例では、作成されたhello.txtの所有者としてUID=0が割り当てられました。
UID=0はホスト上でもrootなので、hello.txtの所有者はrootになってしまいました。
この問題への対処方法はいくつかやり方がありますが、ここではuserns-remapを使ってスマートに解決してみます。
userns-remapとは
デフォルトではDockerコンテナ内のUIDは、ホストのUIDとそのまま対応しています。
このためコンテナ内のroot(UID=0)が作ったファイルはホスト上でもroot(UID=0)の所有物でした。
また、コンテナ内のrootはホスト上でもrootと同じ権限を持ってしまうので、
万が一コンテナによる隔離が破られてしまった場合に重大なセキュリティリスクとなってしまいます。
userns-remapは、コンテナのUIDをホスト上の別の範囲にマッピングする機能です。
つまり、コンテナ内のrootはコンテナ内ではUID=0の特権ユーザーですが、ホスト上では一般ユーザーのUIDが割り振られていることになります。
これにより、コンテナによる隔離が破られてしまっても、コンテナ内のrootは一般ユーザーの権限しか持たないため、ホストマシン全体への悪影響を防げます。
また、ホストのrootやマッピング範囲外のユーザーの所有物はコンテナ内ではnobodyの所有物となり、
コンテナ内のrootであっても許可されていなければ書き換えられなくなります。
この機能を使ってファイルの所有権問題を解決するためには、コンテナ内のrootをホストの自分自身UIDにマッピングすればよいです。
つまり、コンテナ内のrootの所有物は、自動的にホスト上では自分自身の所有物になるという寸法です。
それでは、具体的な設定方法を見ていきましょう。
userns-remap の設定方法
例として、自分自身をログイン名makki、UID=1000、GID=1000として、userns-remapの設定方法を説明します。
ホストマシン上に、/etc/subuid(リスト2)、/etc/subgid(リスト3)、
/etc/docker/daemon.json(リスト4)の3つのテキストファイルを用意します5。
存在しない場合は作成してください。
▼リスト2 /etc/subuid
makki:1000:65536
▼リスト3 /etc/subgid
makki:1000:65536
▼リスト4 /etc/docker/daemon.json
{
"userns-remap": "makki"
}
/etc/subuidでは、ログインユーザーmakkiがUID1000番から65536個をマッピング先として利用できるようにする設定です。
すなわち、コンテナ内のUIDの0〜65535を、ホストの1000〜66535に対応させるようにできます。
/etc/subgidもGIDについての同様の設定です。
/etc/docker/daemon.jsonで、userns-remapに利用するユーザー名を指定しています。
もしすでに他の設定項目がある場合は、"userns-remap"の項目をそこに加えます。
これらのファイルを用意したら、Dockerデーモンを再起動してください。
これでuserns-remapが有効になりました。
戻すときはdaemon.jsonから"userns-remap"の項目を消して再起動すれば元通りです。
ひとつ注意点として、Dockerのイメージやボリュームなどの保存場所が、
デフォルトの/var/lib/dockerから/var/lib/docker/1000.1000に変更されます6。
このため、これまで使っていたイメージなどをそのまま使いたい場合は自身でエクスポート/インポートする必要があります。
それではさっそく、ファイル生成を試してみましょう。
▼リスト5 もう一度ファイルを生成
$ docker run --rm -v $(pwd):/work busybox sh -c 'echo Hello! > /work/hello2.txt'
$ ls -l
total 8
-rw-r--r-- 1 makki makki 7 Apr 14 20:36 hello2.txt
-rw-r--r-- 1 root root 7 Apr 14 18:40 hello.txt
コンテナ内で生成されたhello2.txtの所有者が自分になっているので、コンテナ外での編集も問題なくできます。
他の方法との比較
Rootlessモード
Rootlessモードは、Dockerのデーモン自体を一般ユーザーで動かすDockerの機能です。
このとき、コンテナ内のrootはデーモンを動かしている一般ユーザーにマッピングされます。
つまり、普段作業しているユーザーでRootlessモードのDockerデーモンを動かせば、ファイルの所有権問題は解決できます。
ただし、Rootlessモードでは一部の機能が制限されていて、 たとえばTCP/UDPの1024未満のポートをListenできないなど、普通の開発作業を行う上での利便性が損なわれてしまいます。
一方でuserns-remapでは、Dockerデーモン自体はrootユーザーで起動しているので、機能的な制限はほとんどありません。
コンテナ内にユーザーを作る
ホストの作業ユーザーと同じUID、GIDのユーザーをコンテナ内に作り、
そのユーザーでファイルを生成すれば、ホスト上でも作業ユーザーの所有物となるはずです。
このやり方はウェブ上で多くの記事を見かけますが、ファイルの所有権問題の解決法としては完全に悪手です。
まず、ホストの作業ユーザーのUIDは環境によって異なる可能性があります。
作業者のUIDが異なっていた場合、生成されたファイルの所有者は別のユーザーになってしまい、
問題が解決できていないばかりか、セキュリティリスクにもなりえます。
これを解決するために、作業ユーザーのUIDに合わせてコンテナのイメージを作り直すこともできますが、
これでは同じ環境を再現するというDockerの利点が失われてしまいます。
以前はコンテナ内に専用ユーザーを作ることをセキュリティの面で推奨する向きもありましたが、
これはuserns-remapやRootlessモードによってほとんど解消されています。
常駐型のサービスを動かすコンテナであれば専用ユーザーをつくるほうがよいケースもありますが、
ファイル生成のような単発のアプリケーションであれば、Dockerの標準的な方法のとおりrootで動作させるのがよいでしょう。
コンテナ内でファイルを生成するユーザーをrootに固定しておけば、
ホストごとに作業ユーザーのUIDが違っていてもuserns-remapでそれぞれのホストに適切なマッピングを設定すればよく、
問題なく同じイメージを共有することができます。
userns-remapの注意点
userns-remapでは、コンテナ内のUIDを単純な連番としてホストのUIDに割り当てます。
つまり、コンテナ内のrootをホストのUID=1000に割り当てた場合、
コンテナ内のUID=1はホストのUID=1001に割り当てられてしまいます。
ユーザーが自分のみであれば大きな問題にはなりませんが、ホストマシンを複数のユーザーで共用している場合、 他ユーザーのファイルにコンテナ内からアクセスできてしまいます。 このような環境では別の方法を考えないといけません。
また、userns-remapを有効にすると、一部のリソースへのアクセスが制限されます。 たとえば、GitHub Actionsをローカルで実行する「act7」というツールでは、ホストの名前空間のネットワークを必要としますが、 userns-remapが有効な場合、ネットワークの名前空間がホストとは別のものになってしまいアクセスできません。
これを回避するには、docker runコマンドなどに--userns=hostオプションを指定して実行することで、
一時的にuserns-remapを無効にできます。
特にactでは、~/.actrcファイルに--container-options --userns=hostと記載しておけば、
内部でのdocker呼び出し時にこのオプションが自動的に付加されるため便利です。
おわりに
LinuxのホストマシンでDockerを使ったときに直面しがちなファイルの所有権問題について、 userns-remapを使った解決方法を紹介しました。 これはセキュリティ面からも有効にしたほうがよいお勧めの機能です。
DockerDesktopが有料化して以降、 WindowsやmacOSではDockerを動かす方法が乱立していて、 迷ったり困ったりすることも多いのではないでしょうか。 ホストがLinuxであれば、公式のパッケージを無料で使えますし8、 パフォーマンス面でも圧倒的に有利です。 また、ホストをLinuxにしてまず直面するであろうファイルの所有権問題は、 ここまで解説したとおりuserns-remapによって解決できます。
ぜひみなさんも、Dockerを使う時はホストをLinuxにしましょう。
そしてDockerコンテナの中ではできるだけrootで動作させてください。
くれぐれもよろしくお願いします。
