LXCで学ぶコンテナ入門 -軽量仮想化環境を実現する技術

第50回Linuxの非特権コンテナで利用するID mappedマウント(1)

2014年に始めたこの連載もついに50回になりました。この連載は当初12回の予定で、LXCの機能を中心に紹介するつもりでした。しかし、書いているうちに予定の回数には収まらないほど書きたいことが出てきて、結局予定していた内容を書き終えたのは第26回でした。

内容についてはLXCの機能について書いたあとは、Linuxカーネルに実装されるコンテナ関連機能の紹介が中心になりました。ここ数年はLinuxカーネルに実装されるコンテナ関連の大きな機能の追加が少なくなったことと、それ以上に筆者が新しい機能を勉強する時間が少なくなり、記事として書ける内容を取得するスピードが減速したので、新しい記事を書く頻度は減りました。

このようなマイペースな連載に長くお付き合いいただきありがとうございます。スピードが減速したとはいえ、まだ書きたいネタがなくなったわけではないので引き続きお付き合いいただければと思います。

今回は、ここ数年いろいろな勉強会で登壇するたびに毎回簡単に紹介をしていた機能について、少し詳細に紹介したいと思います。

そして、毎年この連載で参加していたLinux Advent Calendarに今年もこの記事で参加します。この記事はLinux Advent Calendar 2022の23日目の記事です。


コンテナを安全に使う機能として、第16回でユーザ名前空間を紹介しました。ユーザ名前空間を使うことで、一般ユーザ権限でコンテナが起動しても、コンテナ内ではroot権限を持つことができ、コンテナが安全に起動できます。

この機能を使った一般ユーザ権限で起動するコンテナを使うために、これまでにいろいろな機能が追加されてきました。たとえば第44回で紹介したファイルケーパビリティの機能拡張であったり、第47回で紹介したseccomp notifyといった機能です。このような機能追加で、ユーザ名前空間を使ったコンテナでも実際のホスト環境に近い機能が使えるようになってきました。

今回は、そのようなコンテナ内の操作に関連する機能ではなく、ユーザ名前空間を使って一般ユーザ権限でコンテナを起動する際に非常に重要な、ID mappedマウントという機能を紹介したいと思います。

なお、今回の実行例は5.15カーネルを使用しているUbuntu 22.04上で実行しています。また、今回の実行例では、UID/GIDが1000であるgihyoユーザ、1001であるubuntuユーザ、1002であるu1002ユーザを用いており、それぞれシェルのプロンプトにユーザを表示し、誰が実行しているのかがわかるようにしています。

一般ユーザ権限で起動するコンテナで使用するコンテナファイルシステム

一般的にコンテナイメージとして配布されているコンテナのファイルシステムは、普通にOSをインストールした際と同様のファイルの所有権が設定されています。つまりほとんどのファイルがUID/GIDが0(root所有)で設定されているはずです。それ以外の所有者・グループが設定されているファイルについても、rootのUID/GIDである0をベースとしているはずです。

一般ユーザ権限でコンテナを起動するためには、ホストから見たコンテナのファイルシステムでは、その一般ユーザ権限に対する権限が必要になります。ファイルの所有権がUID/GIDが0ベースのファイルシステムをそのまま使うと、コンテナから利用する際に権限がなく、コンテナから正常に利用できないことがほとんどでしょう。

たとえば、次のようにmmdebstrapコマンドを使ってコンテナイメージを作成したとします。

gihyo@ubuntu2204:~$ sudo mmdebstrap --variant=essential jammy /data/jammy http://jp.archive.ubuntu.com/ubuntu

このように作ったコンテナのルートファイルシステムは、次のようにほとんどのディレクトリやファイルはroot所有です。

gihyo@ubuntu2204:~$ ls -l /data/jammy/
total 76
drwxr-xr-x  2 root root 4096 Nov 23 14:21 bin
drwxr-xr-x  2 root root 4096 Apr 18  2022 boot
drwxr-xr-x  4 root root 4096 Apr 18  2022 dev
drwxr-xr-x 27 root root 4096 Nov 23 14:21 etc
drwxr-xr-x  2 root root 4096 Apr 18  2022 home
  :(略)

ここでunshareコマンドを使い、ユーザ名前空間を使ってコンテナを作ります。

gihyo@ubuntu2204:~$ id
uid=1000(gihyo) gid=1000(gihyo) groups=1000(gihyo),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),110(lxd)
(現在のユーザはUID/GIDが1000/1000のユーザ)
gihyo@ubuntu2204:~$ unshare --user --map-root-user ls -l /data/jammy 
(現在のユーザを作成するユーザ名前空間内のrootにマッピングし、所有者を確認する)
total 76
drwxr-xr-x  2 nobody nogroup 4096 Nov 23 14:21 bin
drwxr-xr-x  2 nobody nogroup 4096 Apr 18  2022 boot
drwxr-xr-x  4 nobody nogroup 4096 Apr 18  2022 dev
drwxr-xr-x 27 nobody nogroup 4096 Nov 23 14:21 etc
drwxr-xr-x  2 nobody nogroup 4096 Apr 18  2022 home
  :(略)

上の例では、現在のユーザをコンテナ内のrootにマッピングしています--map-root-user⁠。元の名前空間のUIDが1000のユーザとコンテナ内のUIDが0のユーザだけをマッピングしましたので、その他のマッピングがないため、すべてのディレクトリがnobody:nogroup所有のように見えています[1]

この状態では、コンテナ内でのファイル操作ができません。

この問題を解決するには、ファイルやディレクトリの所有権をコンテナ内のrootにマッピングされるユーザが所有するように変更しなければいけません。この方法としてすぐに思い浮かぶのはchownコマンドやchown(2)システムコールでしょう。

次のようにコンテナのルートファイルシステム以下の所有権をchownコマンドを使って変更します。

gihyo@ubuntu2204:~$ sudo chown -R gihyo:gihyo /data/jammy/

すると、次のようにコンテナ内でもroot所有のディレクトリに見えますので、コンテナ内での操作ができるようになったようにみえます。

gihyo@ubuntu2204:~$ unshare --user --map-root-user ls -l /data/jammy 
total 76
drwxr-xr-x  2 root root 4096 Nov 23 14:21 bin
drwxr-xr-x  2 root root 4096 Apr 18  2022 boot
drwxr-xr-x  4 root root 4096 Apr 18  2022 dev
drwxr-xr-x 27 root root 4096 Nov 23 14:21 etc
drwxr-xr-x  2 root root 4096 Apr 18  2022 home
  :(略)

ところが待ってください。コンテナのルートファイルシステム内のファイルやディレクトリがすべてroot所有でしょうか? そんなことはありませんよね。

gihyo@ubuntu2204:~$ sudo find /data/jammy/ ! -user root -exec ls -ld {} \;
drwx------ 2 _apt root 4096 Nov 23 14:49 /data/jammy/var/lib/apt/lists/partial
drwx------ 2 _apt root 4096 Nov 23 14:52 /data/jammy/var/cache/apt/archives/partial

chown前のファイルシステムを見てみると、mmdebstrapで作った最小限のファイルシステムでもこのようにroot以外が所有するディレクトリが見つかります。実際に使用する場合は、もっとさまざまな所有権を持ったファイルやディレクトリが存在するはずです。

chownを使う場合、コンテナを正常に運用するには、ファイルシステム内のすべてのファイルやディレクトリの所有権をひとつずつ調べながら、マッピングに従って変換する操作が必要になります。これは非常にコストがかかる操作であることはご理解いただけると思います。

shiftfsの提案とカーネルのマウントAPIの変更

このような問題を解決するために、2017年ごろにshiftfsというファイルシステムが提案されました。shiftfsについては第47回でも少し紹介しています。このshiftfsは、今回紹介するID mappedマウントと同じことを実現する機能でした。

ただ、このshiftfsはマウント操作を2度行うという、通常のファイルシステムをマウントする操作とは異なる操作を行っていました。このような複雑なマウントを行っていたからか、shiftfs自体がカーネルにマージされることはありませんでした。

また、カーネルが持つマウント関連のAPIも、さまざまな複雑なマウントが必要とされる最近のユースケースをすべてmount(2)システムコールで対応していたためいろいろと問題がありました。そのため、マウント関連のAPIが5.2カーネルで新しくなり、さまざまなユースケースに対応できるようになりました[2]

このようにカーネル側で準備が整ったこともあり、その後長い議論の後に、5.12カーネルで今回紹介するID mappedマウントがマージされました。

ID mappedマウントの仕組み

ID mappedマウントは、先に紹介したshiftfsと同様の動きをします。ここまでで述べたように、一般ユーザ権限でコンテナを起動するには、コンテナを起動するユーザの権限でファイルシステムを操作できる必要があります。

そこで、図1のようにマウント時に一般ユーザ権限でコンテナを起動する際に使用するユーザ名前空間のマッピングを使用し、そのマッピングを使ってID情報を変換してマウントします。

図1 ID mappedマウントの動き
ID mappedマウントの動き

コンテナイメージの場合、リポジトリなどから取得して、すでにホストのファイルシステム上にコンテナイメージが存在する状態でマッピングを行いたいケースがほとんどでしょう。このような場合にID mappedマウントを使い、マッピングを使って所有権を変更して、バインドマウントのように別の場所にマウントし、そのマウントをコンテナのファイルシステムとして使用します。

ID mappedマウントの利用

ID mappedマウントは、カーネルでは実装されました。しかし、まだutil-linuxに含まれるmountコマンドでは利用できるようにはなっていません。

現時点では、アプリケーション内でID mappedマウントを使って実装された機能を通して使うことになります。たとえば、デフォルトでユーザ名前空間を使ったコンテナが起動するLXDでは、カーネルでサポートされている場合はID mappedマウントを使用します。

util-linuxでもすでにプルリクエストが出ており、マージ予定になっていますので、近いうちにmountコマンドを通して使えるようになるでしょう。

現時点では、このID mappedマウントを試すには、この機能を開発したChristian Brauner氏によるmount-idmappedコマンドを使うのが簡単です。今回はこのコマンドを使って、ID mappedマウントを紹介していきます。

mount-idmappedコマンドのビルドと利用

mount-idmappedコマンドをビルドするのは非常に簡単です。コンパイラをインストールした環境で次のように実行します。

gihyo@ubuntu2204:~$ git clone https://github.com/brauner/mount-idmapped.git
gihyo@ubuntu2204:~$ cd mount-idmapped
gihyo@ubuntu2204:~$ gcc -o mount-idmapped mount-idmapped.c (コンパイル)
gihyo@ubuntu2204:~$ ls 
mount-idmapped  mount-idmapped.c  README.md
gihyo@ubuntu2204:~$ sudo cp mount-idmapped /usr/local/bin/ (パスが通った場所にコマンドをコピー)

できあがったmount-idmappedコマンドは、上の例のようにパスが通った場所にコピーしましょう。

mount-idmappedコマンドは、オプションとしてIDのマッピング情報を与えます。そして通常、mountコマンドでバインドマウントを行う際のようにマウント元とマウント先のディレクトリ名を渡します。

マッピングは--map-mountオプションで指定します。このオプションで指定する値は<type>:<id-from>:<id-to>:<id-range>です。このオプションは複数回指定できます。

  • <type>: "b", "u", "g" のいずれか。"b" はUIDとGIDを両方とも一度にマッピングするように指定します。"u"はUIDのマッピングだけを指定します。"g"はGIDのマッピングだけを指定します。
  • <id-from>: マッピング元(元のユーザ名前空間)でのIDの開始番号
  • <id-to>: マッピング先(マウント先のユーザ名前空間)でのIDの開始番号
  • <id-range>: マッピングの範囲

たとえば次のようにマッピングとマウント元、マウント先のディレクトリを指定します。

gihyo@ubuntu2204:~$ sudo mount-idmapped --map-mount=b:1000:1001:1 /path/to/src /path/to/dest

上記の例では次のようになります。

  • <type>として"b"を指定しているのでUID/GIDの両方を同時にマッピング指定する
  • <id-from>として"1000"を指定しており、<id-to>として"1001"を指定しているので、UID/GID=1000を新たに作成するユーザ名前空間でUID/GID=1001にマッピングする
  • <id-range>として"1"を指定しているので、UID/GID=1000から1つだけIDをマッピングする、つまりUID/GID=1000→UID/GID=1001だけをマッピングする
  • /path/to/srcディレクトリを/path/to/destディレクトリにマウントする。このマウントはバインドマウントしたときと同じようにマウントされます

つまりマウント元のUID/GIDが1000であるオブジェクトは、新たにマウントした先ではUID/GIDが1001であるオブジェクトに見えるということです。ファイルにひもづくUIDとGIDの情報だけが変換されたバインドマウントと言ったところでしょう。

なお、これ以下のmount-idmappedコマンドを使った実行例では、unshareコマンドなどでユーザ名前空間を作成していないように見えるかもしれません。しかし、ID mappedマウント時にマッピングを行わなければいけないため、mount-idmappedコマンド内でマッピングを行うためのユーザ名前空間を作成しています。

ID mappedマウントを簡単に使ってみる

まずはシンプルにID mappedマウントを使ってみましょう。次のように、システム上にはUID/GIDが1001であるユーザubuntuと、1002であるユーザu1002が存在しています。次の実行例では、コマンドはUID/GIDが1002であるu1002ユーザで実行しています。

u1002@ubuntu2204:~$ id
uid=1002(u1002) gid=1002(u1002) groups=1002(u1002),4(adm),27(sudo)
u1002@ubuntu2204:~$ id ubuntu
uid=1001(ubuntu) gid=1001(ubuntu) groups=1001(ubuntu),4(adm),27(sudo)

UIDが1001であるubuntuユーザのホームディレクトリを、UIDを1002にIDマッピングして、/mntにマウントしてみます。ubuntuユーザのホームディレクトリである/home/ubuntuにはubuntuユーザの権限でubuntu-fileというファイルをひとつ作ってあります。

u1002@ubuntu2204:~$ sudo ls -l /home/ubuntu
total 0
-rw-rw-r-- 1 ubuntu ubuntu 0 Oct 13 13:40 ubuntu-file

ここでmount-idmappedコマンドでID mappedマウントを実行してみましょう。

u1002@ubuntu2204:~$ sudo mount-idmapped --map-mount b:1001:1002:1 /home/ubuntu /mnt

これで/home/ubuntuがIDマッピングされて/mntにマウントされているはずです。/mntディレクトリの所有権を確認し、/mnt配下にファイルをu1002ユーザ(UID/GID=1002)権限で作成してみましょう。

u1002@ubuntu2204:~$ ls -ld /mnt
drwxr-x--- 5 u1002 u1002 4096 Oct 13 13:35 /mnt (/mntはu1002権限)
u1002@ubuntu2204:~$ touch /mnt/u1002-file       (u1002権限でファイル作成がエラーなくできる) 

マウントした/mntu1002ユーザ権限になっており、ファイルも特に問題なく作成できました。

u1002@ubuntu2204:~$ ls -l /mnt
total 4
-rw-rwxr--  1 u1002 u1002 0 Oct 13 13:40 u1002-file
-rw-rw-r--  1 u1002 u1002 0 Oct 13 13:40 ubuntu-file

先の例で、マッピング元の/home/ubuntuではUID/GIDが1001であるubuntuユーザ所有になっていたubuntu-fileという名前のファイルが、マウント先ではきちんとマッピングされて所有権がu1002ユーザになっています。マウント後に作成したファイルも同様にu1002ユーザ所有となっています。

念のため、新たにID mappedマウントされたディレクトリである/mntで作成したu1002-fileを、マウント元の/home/ubuntuで確認しておきましょう。

u1002@ubuntu2204:~$ sudo ls -l /home/ubuntu
total 4
-rw-rwxr--+ 1 ubuntu ubuntu 0 Oct 13 13:40 u1002-file
-rw-rw-r--  1 ubuntu ubuntu 0 Oct 13 13:40 ubuntu-file

/mnt上でu1002ユーザの権限で作成したu1002-fileは、元の/home/ubuntuで確認すると、きちんとUID:1001のubuntuユーザ所有になっています。

ID mappedマウントとACL

ここまではLinuxが持つ基本的なアクセス制御だけを見てきました。ここでもう少しID mappedマウントのいろいろな動きを見てみましょう。

Linuxではchmodコマンドを使い、基本的なユーザ、グループ、その他のユーザに対する読み取り、書き込み、実行の権限を設定できます。しかし、もう少し細かな制御を行うためにACL(アクセス制御リスト)という機能があります。ID mappedマウントを行った場合、ACLがどのように設定されるのかも確認しておきましょう。

Ubuntuではaclパッケージに含まれるsetfaclコマンドで、ID mappedマウントされた側のファイルに対してACLを設定します。

u1002@ubuntu2204:~$ setfacl -m u:1002:rwx /mnt/u1002-file (ACLを設定する)
u1002@ubuntu2204:~$ ls -l /mnt/u1002-file 
-rw-rwxr--+ 1 u1002 u1002 0 Oct 13 13:40 /mnt/u1002-file (lsコマンドで確認)
u1002@ubuntu2204:~$ getfacl /mnt/u1002-file (getfaclで確認)
getfacl: Removing leading '/' from absolute path names
# file: mnt/u1002-file
# owner: u1002
# group: u1002
user::rw-
user:u1002:rwx (← setfaclで設定した内容が確認できる)
group::rw-
mask::rwx
other::r--

上のように特に問題なく設定され、ls -lで確認すると、ACLが設定された印である+が表示されています。ACLを確認するコマンドであるgetfaclコマンドで確認すると、setfaclコマンドで指定したu1002に対するACLが設定されています。ここまでは普通にACLを利用する場合の動きとしては変わらないと思います。

それではここでID mappedマウントの元になっているディレクトリ/home/ubuntuの方を確認してみましょう。こちらはUID/GIDが1001のubuntuユーザ所有でした。

u1002@ubuntu2204:~$ sudo getfacl /home/ubuntu/u1002-file (ID mappedマウントの元のACLを見る)
getfacl: Removing leading '/' from absolute path names
# file: home/ubuntu/u1002-file
# owner: ubuntu
# group: ubuntu
user::rw-
user:ubuntu:rwx
group::rw-
mask::rwx
other::r--

先ほど、ID mappedマウントされたディレクトリで見た場合はu1002ユーザに対するACLが設定されていました。しかしマウント元のディレクトリを見ると、元の所有権どおりubuntuユーザに対するACLが設定されています。

つまり、一般的なLinuxにおけるファイルのアクセス権で設定されるユーザ、グループだけでなく、ACLについてもきちんとマッピングが適用されてデータが保存されるということです。

ID mappedマウントとケーパビリティ

ACLの次はケーパビリティを見ておきましょう。Linuxカーネルのケーパビリティ機能については、第42回第44回で紹介しました。

ID mappedマウントと関係してくるのは、ファイルに設定するファイルケーパビリティです。このうち、第44回で紹介したユーザ名前空間内でのファイルケーパビリティ機能を試してみましょう。

このユーザ名前空間内で設定するファイルケーパビリティは、簡単に言うとユーザ名前空間内でファイルケーパビリティを設定すると、ユーザ名前空間内のrootとマッピングされている親のユーザ名前空間のUIDが記録される機能です。そのUIDがrootにマッピングされたユーザ名前空間内でのみ、有効なファイルケーパビリティとして働きます。詳細は第44回を参照してください。

このユーザ名前空間内でのみ有効なファイルケーパビリティを設定するにはsetcapコマンドに-nオプションを使用して設定します。今回の例では、-nオプションにu1002ユーザのUID1002を指定します。

u1002@ubuntu2204:~$ sudo setcap -n 1002 cap_net_raw+ep /mnt/u1002-file
(マッピングされるIDを1002としてファイルケーパビリティを設定)

ここでgetcapコマンドを使って、ID mappedマウントしたディレクトリと、元のディレクトリでケーパビリティがどのように設定されているように見えるかを確認してみましょう。

u1002@ubuntu2204:~$ getcap -n /mnt/u1002-file
(ID mappedマウントしたディレクトリで確認)
/mnt/u1002-file cap_net_raw=ep [rootid=1002]
u1002@ubuntu2204:~$ sudo getcap -n /home/ubuntu/u1002-file
(ID mappedマウントの元のディレクトリで確認)
/home/ubuntu/u1002-file cap_net_raw=ep [rootid=1001]

上のように、ID mappedマウントしたディレクトリ/mntではrootid=1002と表示されており、マウント元の/home/ubuntuではrootid=1001となっており、ファイルケーパビリティについても、きちんと変換されていることがわかります。

まとめ

今回は、ID mappedマウントが必要とされる理由と、機能の基本的な動きを紹介しました。だいぶ長くなってしまいましたので、続きは次回にしたいと思います。次回は、コンテナのファイルシステムをマウントするところで使用する際の動きや、それ以外のユースケースについて紹介したいと思います。

おすすめ記事

記事・ニュース一覧