コンテナからNASへの書き込み権限エラー(#8)

はじめに

ある日、「研究室サーバーで起動したコンテナ上から、マウントしたNASに書き込みできない」という報告がありました。具体的にはPermission deniedが出ていて、以前は全く報告されていなかった事例です。

本記事では、ステップ・バイ・ステップでエラー解決の糸口を紹介します。デバッグに自信のある方は1章を読んで原因を推測してみてください。

1. エラーの再現

研究室サーバーでは全ユーザーがprimary グループとしてユーザー名と同じグループ(例:azuma)とsecondary グループとしてdockerグループに所属しています。

$ id
uid=2401(azuma) gid=2401(azuma) groups=2401(azuma),999(docker)

検証用に以下のようなDockerfileを用意しました。セキュリティには目を瞑ってください。

FROM ubuntu:latest

ARG UID=2401
ARG USERNAME=azuma

RUN apt update && apt upgrade -y && apt clean

RUN echo root:root | chpasswd && \
    useradd -m -s /bin/bash --uid ${UID} --groups sudo ${USERNAME} && \
    echo ${USERNAME}:${USERNAME} | chpasswd

RUN mkdir -p /tmp/nas/azuma

コンテナをビルドして起動します。

-uオプションで実行ユーザーとして自身を指定しています。

$ docker build -t azuma-ubuntu .

$ NAS_MOUNT_PATH=/mnt/nas/azuma
$ mkdir -p $NAS_MOUNT_PATH

$ docker run -it --rm \
  --name nas-write-test \
  -u $(id -u):$(id -g) \
  -v $NAS_MOUNT_PATH:/tmp/nas/azuma \
  azuma-ubuntu:latest /bin/bash

マウントしたディレクトリに対してコンテナ上から書き込みを行い、Permission deniedと表示されたのでエラーの確認は完了です。

azuma@479d704b616b:/$ echo "Hello World!" > /tmp/nas/azuma/hello.txt
bash: /tmp/nas/azuma/hello.txt: Permission denied

一方でrootユーザーからは対象のディレクトリに書き込むことができます。

azuma@308b7e25d385:/$ su
Password:
root@308b7e25d385:/# echo "Hello World!" > /tmp/nas/azuma/hello.txt
root@308b7e25d385:/# ls -ld /tmp/nas/azuma/
drwxrwxr-x 2 root 999 0 Oct 15 09:00 /tmp/nas/azuma/

最後に、ホストマシン上での対象ディレクトリの権限をチェックします。

$ ls -ld /mnt/nas/azuma
drwxrwxr-x 2 root docker 0 107 18:20 /mnt/nas/azuma

この時点で大まかな原因の予想がついたので、次章からは敢えて実行したコマンドと関連知識を掘り下げていきます。

2. コンテナの実行ユーザー指定

Dockerコンテナのデフォルトの実行ユーザーはrootuid=0 gid=0)です。

docs.docker.com

ほとんどの人が、DockerfileのUSER命令を使用してイメージビルド中に実行ユーザーを切り替えた経験があると思います。一方、docker runコマンドの-uオプションを使用することで、以下に示すようにコンテナを起動(し、コマンドを実行)する際のUSER命令を上書きできます。

--user=[ user | user:group | uid | uid:gid | user:gid | uid:group ]

どのユーザーでコンテナが起動されるのか理解できたので、改めてエラーの再現に利用したDockerfileとdocker runコマンドを確認します。

DockerfileのRUN命令の一部として、以下のコマンドが実行されています。

    useradd -m -s /bin/bash --uid 2401 --groups sudo azuma

これはユーザーazumaをUID2401で作成し、管理者権限を持つsudoグループに追加し、デフォルトシェルを/bin/bashに設定して、ホームディレクトリを作成するという意味です。-gオプションを指定しない場合はデフォルトで新しいユーザーと同じ名前のグループが作成されてプライマリグループになります。

次に、docker runコマンドの-uオプションのみ抜粋します。

$ docker run -u 2401:2401 azuma-ubuntu:latest /bin/bash

これは、コンテナ内のUID2401GID2401のユーザーでコンテナを起動することを意味しています。

ちなみに、-u 2401のようにユーザーIDを指定した場合は0-2147483647の整数値でなければならず、-u azumaのようにユーザー名を指定した場合は予めコンテナ内にユーザーが存在しなければならないそうです。

裏を返せば、Dockerfileでuseraddコマンドをコメントアウトしてユーザーを作成しない場合でも、-uオプションでユーザーIDを指定すれば、以下のようにユーザーが作成されるということです。

$ docker run -it -u 2401:2401 azuma-ubuntu:latest /bin/bash
groups: cannot find name for group ID 2401
I have no name!@a715efc4b574:/$ id
uid=2401 gid=2401 groups=2401

Dockerfileでuseraddコマンドを実行していましたが、これは必須ではなく、ユーザー名やホームディレクトリ、デフォルトシェルを作成・指定して便利にするためであると分かりました。

3. ファイルのアクセス権

今回のエラーはPermission deniedなので、ファイルのアクセス権について復習します。

まずは、ホストマシンから1章でNAS_MOUNT_PATHとして指定していたパスの詳細を表示します。

$ ls -la /mnt/nas/azuma
合計 4
drwxrwxr-x 2 root docker  0 1016 12:16 .
drwxrwxr-x 2 root docker  0 1015 10:33 ..
-rwxrwxr-x 1 root docker 13 1015 18:39 hello.txt

表記は左から、モード(ユーザー、グループ、その他のユーザー)、リンクの数、所有者、グループ、サイズ (バイト単位)、最終変更時刻、および名前です。

例えば、カレントディレクト.についてはrootユーザーとdockerグループが読み取り(r)/書き込み(w)/実行(x)可能で、その他のユーザーは読み取り(r)と実行(x)のみ可能です。

1章で確認した通り、全研究室メンバーのユーザーがdockerグループに所属しているため、ホストマシンからは問題なくディレクトリ内のファイルに書き込めます。

コンテナ内からもマウントした対象ディレクトリの詳細を取得してみます。

azuma@2e65c0193e67:/$ ls -la /tmp/nas/azuma/
total 8
drwxrwxr-x 2 root  999    0 Oct 16 03:16 .
drwxr-xr-x 3 root root 4096 Oct 16 03:43 ..
-rwxrwxr-x 1 root  999   13 Oct 15 09:39 hello.txt

GID999はホストマシン上でのdockerグループに該当していました。ホストマシンと同様、ユーザーがdockerグループに所属していれば書き込み(w)は可能なはずです。

4. NASのマウント

最後に、NASのマウント方法を確認します。

実例として研究室サーバーでは、/etc/fstabに以下のように追記することで起動時に自動でNASをマウントしています。

//192.168.1.xxx/data /mnt/nas cifs username=YOUR_USERNAME,password=YOUR_PASSWORD,gid=999,dir_mode=0775,file_mode=0775 0 0

オプションの内容は以下の通りです。詳細はmount.cifsを参照してください。

今回、ホストマシン上のdockerグループのGID999である前提で、全てのマシンの/etc/fstabに全く同じ内容を追記していました。しかし、3台のマシンを調査したところ、dockerグループのGIDはそれぞれ997/998/999でした。999の場合でもコンテナ内からの書き込み時にエラーが起きるのは事実ですが、997998の場合はホストマシンからの書き込みすらできません。

解決策としては

gid=arg sets the gid that will own all files or directories on the mounted filesystem when the server does not provide ownership information. It may be specified as either a groupname or a numeric gid. When not specified, the default is gid 0. The mount.cifs helper must be at version 1.10 or higher to support specifying the gid in non-numeric form. See the section on FILE AND DIRECTORY OWNERSHIP AND PERMISSIONS below for more information. https://linux.die.net/man/8/mount.cifs

から分かる通り、グループIDではなくgid=dockerのようにグループ名を指定するのがベターだと考えました。

5. エラーの解決

調査を通して分かったことをまとめます。

【2章】コンテナ起動時に指定するオプションによって、コンテナ内に既に存在しているユーザーを実行ユーザーとして指定できる

【3章】コンテナにマウントしたディレクトリが、コンテナ内でもホストマシンと同じ所有者・所有グループとして表示される

【4章】NASマウント時に所有グループとしてグループIDだけでなくグループ名も指定できる

最もシンプルな解決方法として「全てのユーザーをprimaryグループとしてdockerグループに所属させ、dockerグループがNASをマウントしたディレクトリを所有する」という方法があります。この方法であれば今まで通りの方法でユーザーがNASを利用できるようになりますが、Dockerに対する権限とNASに対する権限の切り分けが不可能です。また、あるユーザーが作成したディレクトリはdockerグループに所属する一般ユーザーからデフォルトで書き込み可能であることからも好ましくありません。実は、Ansibleによる構成管理を導入する以前はこの状態でした。

今回の解決策を下図に示します。

解決策のイメージ図

結論から言うと今回は、全ユーザーをサブグループとしてdockerグループとnas_accessグループ(NASにアクセス可能なグループ)の2つに所属させ、docker runコマンドで--group-addオプション*1を指定してもらう解決策を取ることにしました。

以下は、nas_accessグループのGID996である場合のコマンド実行例です。

$ docker run -it --rm \
  --name nas-write-test \
  -u $(id -u):$(id -g) \
  --group-add $(getent group nas_access | awk -F: '{print $3}') \
  -v $NAS_MOUNT_PATH:/tmp/nas/azuma \
  azuma-ubuntu:latest /bin/bash

groups: cannot find name for group ID 996
azuma@1556ffc1cf80:/$ id
uid=2401(azuma) gid=2401(azuma) groups=2401(azuma),996
azuma@1556ffc1cf80:/$ echo "Hello World!" > /tmp/nas/azuma/hello.txt
azuma@1556ffc1cf80:/$ cat !$
cat /tmp/nas/azuma/hello.txt
Hello World!

コンテナ内にGID996のグループは実際には存在しないため、groups: cannot find name for group ID 996というエラーが一瞬表示されます。エラーが気になる場合は、以下のようにDockerfile内でnas_accessグループを作成することで、--group-addオプションを使わずNASへの書き込みが可能になります(ただし、イメージビルド時にグループIDを引数として渡すため、同じイメージをグループIDが異なるホストマシン間で使い回すことはできません)。

FROM ubuntu:latest

ARG UID=2401
ARG USERNAME=azuma
ARG NAS_ACCESS_GID=996  # 追加

RUN apt update && apt upgrade -y && apt clean

RUN echo root:root | chpasswd && \
    # 追加
    groupadd -g ${NAS_ACCESS_GID} nas_access && \
    useradd -m -s /bin/bash --uid ${UID} --groups sudo,nas_access ${USERNAME} && \
    echo ${USERNAME}:${USERNAME} | chpasswd

RUN mkdir -p /tmp/nas/azuma

いずれにせよ、コンテナ上からマウントしたNASに書き込めないエラーは解消しました。nas_accessグループを作成やsecondaryグループの追加については、AnsibleのおかげでPlaybookを数行書き換えるだけで全マシンに適用されました。

Playbook変更の差分

4章で登場した/etc/fstabに関する修正点は、NAS利用者が居ないタイミングを見計らって1台ずつマウントし直しました。

$ vim /etc/fstab  # 次回起動時のために変更
$ umount -l /mnt/nas
$ mount -t cifs //192.168.1.xxx/data /mnt/nas -o username=YOUR_USERNAME,password=YOUR_PASSWORD,gid=nas_access,dir_mode=0775,file_mode=0775
$ ls -ld /mnt/nas
drwxrwxr-x 2 root nas_access 0 1015 10:33 /mnt/nas
$ echo "Hello World!" > /mnt/nas/azuma/hello.txt  # 成功

$ NAS_MOUNT_PATH=/mnt/nas/azuma
$ docker run -it --rm --name nas-write-test -u $(id -u):$(id -g) --group-add $(getent group nas_access | awk -F: '{print $3}') -v $NAS_MOUNT_PATH:/tmp/nas/azuma  azuma-ubuntu:latest /bin/bash
groups: cannot find name for group ID 996
azuma@286205c86ac7:/$ id
uid=2401(azuma) gid=2401(azuma) groups=2401(azuma),996
azuma@286205c86ac7:/$ echo "Hello Azuma!" > /tmp/nas/azuma/hello.txt
azuma@286205c86ac7:/$ cat !$  # 成功
cat /tmp/nas/azuma/hello.txt
Hello Azuma!

まとめ

「コンテナにマウントしたNASにコンテナ内から書き込めない」という問題に対して、丁寧に調査しながら解決策を示すことができました。また、調査する過程でLinuxやDockerの曖昧な知識を再確認できたことは大きな収穫でした。

余談ですが、dockerグループの権限はrootユーザーに匹敵するので、ホストのファイルシステム全体をコンテナにマウントすることで、任意のファイルを読み書きできます。セキュリティの観点からRootlessモードPodmanを導入したい気持ちはありますが、あくまでも研究室サーバーなので衝動を抑えつつ、自分の環境でこれらの技術をキャッチアップする必要があると考えました。

*1:docker composeの場合も同様にgroup_add属性を指定できますhttps://docs.docker.com/reference/compose-file/services/#group_add