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.gihyo
ユーザ、1001であるubuntu
ユーザ、1002であるu1002
ユーザを用いており、それぞれシェルのプロンプトにユーザを表示し、誰が実行しているのかがわかるようにしています。
一般ユーザ権限で起動するコンテナで使用するコンテナファイルシステム
一般的にコンテナイメージとして配布されているコンテナのファイルシステムは、普通にOSをインストールした際と同様のファイルの所有権が設定されています。つまりほとんどのファイルがUID/
一般ユーザ権限でコンテナを起動するためには、ホストから見たコンテナのファイルシステムでは、その一般ユーザ権限に対する権限が必要になります。ファイルの所有権がUID/
たとえば、次のように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.
このようにカーネル側で準備が整ったこともあり、その後長い議論の後に、5.
ID mappedマウントの仕組み
ID mappedマウントは、先に紹介したshiftfsと同様の動きをします。ここまでで述べたように、一般ユーザ権限でコンテナを起動するには、コンテナを起動するユーザの権限でファイルシステムを操作できる必要があります。
そこで、図1のようにマウント時に一般ユーザ権限でコンテナを起動する際に使用するユーザ名前空間のマッピングを使用し、そのマッピングを使ってID情報を変換してマウントします。
コンテナイメージの場合、リポジトリなどから取得して、すでにホストのファイルシステム上にコンテナイメージが存在する状態でマッピングを行いたいケースがほとんどでしょう。このような場合に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/
なお、これ以下のmount-idmapped
コマンドを使った実行例では、unshare
コマンドなどでユーザ名前空間を作成していないように見えるかもしれません。しかし、ID mappedマウント時にマッピングを行わなければいけないため、mount-idmapped
コマンド内でマッピングを行うためのユーザ名前空間を作成しています。
ID mappedマウントを簡単に使ってみる
まずはシンプルにID mappedマウントを使ってみましょう。次のように、システム上にはUID/ubuntu
と、1002であるユーザu1002
が存在しています。次の実行例では、コマンドはUID/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-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/
がIDマッピングされて/mnt
にマウントされているはずです。/mnt
ディレクトリの所有権を確認し、/mnt
配下にファイルをu1002
ユーザ
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権限でファイル作成がエラーなくできる)
マウントした/mnt
はu1002
ユーザ権限になっており、ファイルも特に問題なく作成できました。
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/
ではUID/ubuntu
ユーザ所有になっていたubuntu-file
という名前のファイルが、マウント先ではきちんとマッピングされて所有権がu1002
ユーザになっています。マウント後に作成したファイルも同様にu1002
ユーザ所有となっています。
念のため、新たにID mappedマウントされたディレクトリである/mnt
で作成したu1002-file
を、マウント元の/home/
で確認しておきましょう。
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/
で確認すると、きちんとUID:1001のubuntu
ユーザ所有になっています。
ID mappedマウントとACL
ここまではLinuxが持つ基本的なアクセス制御だけを見てきました。ここでもう少しID mappedマウントのいろいろな動きを見てみましょう。
Linuxではchmod
コマンドを使い、基本的なユーザ、グループ、その他のユーザに対する読み取り、書き込み、実行の権限を設定できます。しかし、もう少し細かな制御を行うために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
ユーザ所有でした。
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/
ではrootid=1001
となっており、ファイルケーパビリティについても、きちんと変換されていることがわかります。
まとめ
今回は、ID mappedマウントが必要とされる理由と、機能の基本的な動きを紹介しました。だいぶ長くなってしまいましたので、続きは次回にしたいと思います。次回は、コンテナのファイルシステムをマウントするところで使用する際の動きや、それ以外のユースケースについて紹介したいと思います。