Photon OS 5.0 で Rootless コンテナを試す

[前回](https://qiita.com/iwaseyusuke/items/6c64995e35126a5000a5) の記事でも紹介した通り、この 5月頭に Photon OS 5.0 が GA しました。
この GA に関する情報を紹介した VMware Blog の記事の中に、Rootless コンテナという面白そうなキーワードを見つけました。

https://blogs.vmware.com/vsphere/2023/05/announcing-photon-os-5-0-general-availability.html

> **Towards Enhanced Security**
> ...(中略)...
> In addition, Rootless Containers allow unprivileged users to create and manage containers without having root privileges on the host machine, further preventing threats to the host.

通常、Docker コンテナは、Root 権限で実行されます。しかし、Root 権限でコンテナが実行されていると、コンテナ内の Root ユーザーは、コンテナホストから見ても Root ユーザーであり、非常に強い権限を持ちます。もし、この状況でコンテナ内に悪意のあるアプリが混入してしまった場合、システムの脆弱性を突かれ、コンテナホスト側を攻撃されてしまう可能性があります。そのため、コンテナ内でアプリを実行するユーザーとして、Root を避けることは非常に重要です。ただ、全ての開発者にこれを徹底させたり、また出来合いのコンテナアプリであった場合などには、なかなか難しいケースが存在します。

そこで、コンテナランタイムとして、そもそも Root 権限でコンテナを実行しない、つまり Rootless でコンテナを実行する技術があります。コンテナ自体が、そもそも Root 権限を持っていないので、仮にコンテナ内で Root が使われていても、コンテナホスト側からは権限を制御しやすい状態を保てます。

Rootless コンテナについては、もっと素晴らしい解説記事がネット上に沢山あるので、これ以上の説明は割愛しますが、コンテナ環境のセキュリティは、非常に注目を集めている領域であり、この Rootless コンテナは、なかなか面白い技術だと思っています。

冒頭で紹介した Photon OS 5.0 では、この Rootless コンテナの実装として、Rootless Docker (Docker の Rootless mode) が利用可能になっているので、今回はそれで遊んでみたいと思います。


# 前提

[Photon OS の公式ドキュメント](https://vmware.github.io/photon/docs-v5/administration-guide/containers/docker-rootless-support/) には、Photon OS 4.0 以降、かつ Docker 20.10.14-1 以降で利用可能と記載がありますが、せっかくなので、今回の記事では、Photon OS 5.0 を利用しています。

```text
# cat /etc/photon-release
VMware Photon OS 5.0
PHOTON_BUILD_NUMBER=dde71ec57
```


# 手順

基本は、Photon OS の公式ドキュメントに従って、実施していきます。

https://vmware.github.io/photon/docs-v5/administration-guide/containers/docker-rootless-support/


## 依存パッケージのインストール

Rootless Docker では、`newuidmap` と `newgidmap` を利用します。これらは `shadow` というパッケージを通じて提供されているので、その他の依存パッケージと一緒にインストールしておきます。

```sh
tdnf install -y shadow fuse slirp4netns libslirp
```


## `docker-rootless` パッケージのインストール

Docker の Rootless mode を利用するためのパッケージをインストールします。

```sh
tdnf install -y docker-rootless
```


## 実行ユーザーの作成

Rootless という名前が付いている通り、当たり前かも知れませんが、Root ユーザーでは実行できません。
しかし、Photon OS を OVA からデプロイしたままでは、Root ユーザーのみだと思いますので、Rootless Docker を試すためのユーザーを作成しておきます。

Photon OS でユーザーを作成する際、どのグループに所属させておくのが良いか、イマイチ正解が分かっていませんが、私は、↓こんな感じでユーザーを作成しています。

```sh
NAME=test_user  # 適宜変更
useradd -m -f -1 -G adm,sudo,docker $NAME
passwd $NAME
```

* `-m`, `--create-home`
    * HOME ディレクトリを作成。
* `-f`, `--inactive`
    * `-1` を指定することで、パスワード失効期間を無効化し、無期限に指定。
* `-G`, `--groups`
    * 所属させたいグループをカンマ区切りで指定。

また、ご参考までに、`sudo` コマンドがデフォルトではインストールされていないので、まだの場合には、インストールしておくと、ユーザーを切り替えた後、管理者権限での操作ができるので便利です。

```sh
tdnf install -y sudo
```


## 必要な設定の投入

公式ドキュメントでは、`docker-rootless` パッケージを入れた後、SSH で作成したユーザーとしてログインし直してから、必要な設定を投入しているのですが、権限周りで面倒なので、先に必要な設定を入れてしまいます。

まず、Rootless でコンテナを実行するユーザーが使って良いユーザー ID、グループ ID を設定しておきます。

```sh
NAME=test_user  # 適宜変更
echo "$NAME:100000:65536" >> /etc/subuid
echo "$NAME:100000:65536" >> /etc/subgid
chmod 644 /etc/subuid /etc/subgid
```

ファイルに書き込んだ内容の意味としては、どちらも「UID の 100000番から 65536個を $NAME のユーザーのサブ ID として割り当てる」という意味になります。
もし、複数ユーザーいる場合には、この範囲が被らないように設定する必要がありますので、注意してください。
例えば、もう一つユーザーを追加する場合には、「100000 + 65536番から、65536個」という設定に書き換える必要があります。

次に、非 Root ユーザーでも、コンテナを作るための Namespace を作成できるように、カーネルパラメータを投入します。

```sh
echo "kernel.unprivileged_userns_clone = 1" >> /etc/sysctl.d/50-rootless.conf
chmod 644 /etc/sysctl.d/50-rootless.conf
sysctl --system
```

コマンドの出力として、↓こんな感じの結果が返ってくれば、OK です。

```text: 出力例
* Applying /usr/lib/sysctl.d/50-coredump.conf ...
* Applying /usr/lib/sysctl.d/50-default.conf ...
* Applying /usr/lib/sysctl.d/50-pid-max.conf ...
* Applying /etc/sysctl.d/50-rootless.conf ...      # <-- チェック
* Applying /etc/sysctl.d/50-security-hardening.conf ...
...(略)...
/proc/sys/kernel/pid_max = 4194304
/proc/sys/kernel/unprivileged_userns_clone = 1     # <-- チェック
/proc/sys/kernel/randomize_va_space = 2
...(略)...
```

最後に、公式ドキュメントにおいては、「詳しくは、スクリプトによるチェック結果に従ってね」と書かれている部分の補足になりますが、`ip_tables` のカーネルモジュールをロードしておきます。これは、後述のスクリプトにおいて、スキップするためのオプションも用意されているとのことですが、念の為、ロードしました。

```sh
modprobe ip_tables
```


## スクリプトによるチェック

実際に、Rootless Docker を実行するユーザーに切り替えて、要件が満たされているか、確認してみましょう。

まず、SSH コマンドを使って、先ほど作成したユーザーとしてログインします。

```sh
NAME=test_user  # 適宜変更
ssh $NAME@localhost
```

その上で、setup スクリプトで、要件が満たされているか、確認します。

```sh
dockerd-rootless-setuptool.sh check
```

何かしらのエラーが出力されたり、追加で「この設定を入れてくれ」と指示があれば、それを実行します。

例えば、通常の Docker (区別のために Rootful Docker と表現されてますが) が動いている場合には、それを停止する必要があります。

```sh
$ dockerd-rootless-setuptool.sh check
[ERROR] Aborting because rootful Docker (/var/run/docker.sock) is running and accessible. Set --force to ignore.
```

要件が満たせていれば、↓こんな感じの出力で、チェックが通ったことが分かります。

```sh
$ dockerd-rootless-setuptool.sh check
[INFO] Requirements are satisfied
```


## スクリプトによるセットアップ

では、実際に setup スクリプトを実行していきます。

```sh
dockerd-rootless-setuptool.sh install
```

スクリプトが通った場合、最後に、↓こんな感じの指示が出るので、注意してください。

```text: 出力例
...(略)...
[INFO] Make sure the following environment variables are set (or add them to ~/.bashrc):

export PATH=/usr/bin:$PATH
Some applications may require the following environment variable too:
export DOCKER_HOST=unix:///run/user/1000/docker.sock
```

公式ドキュメントには、「`.bashrc` または `.bash_profile` のどっちかに書いてね」とありますが、実際に `.bash_profile` を開くと、↓こんな感じの記載があるので、`.bash_profile` に追記しています。

```bash: .bash_profile
...(略)...
# Personal environment variables and startup programs.

# ↓が追記部分
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

# Personal aliases and functions should go in ~/.bashrc.  System wide
# environment variables and startup programs are in /etc/profile.
# System wide aliases and functions are in /etc/bashrc.
...(略)...
```

`.bash_profile` を読み込み直すか、再度 SSH ログインして、変更は反映させて、セットアップは完了です。


## 動作確認

まず、`docker` コマンドが使えるか、確認してみます。

```text
$ docker login
Login with your Docker ID to push and pull images from Docker Hub. If you don't have a Docker ID, head over to https://hub.docker.com to create one.
Username: <USERNAME>
Password: <PASSWD>
WARNING! Your password will be stored unencrypted in /home/yusukei/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store

Login Succeeded

$ docker ps -a
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
```

このあたりは、Rootless とあまり関係無いので、動きそうです。

ただ、実際に、お試しでコンテナを起動しようとすると、私の場合は、下記のようなエラーに遭遇しました。

```sh
$ docker run --rm -it photon
Unable to find image 'photon:latest' locally
latest: Pulling from library/photon
fc76184eab19: Pull complete
Digest: sha256:00b26232bb7244a7b4cc9c7737e7a0b0a722d5d2eb8284bca41dd16dcbaf2b03
Status: Downloaded newer image for photon:latest
docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: unable to apply cgroup configuration: unable to start unit "docker-7cf2a1a5b40984dbcccc9bf1c58db1c9225b9504f0ef5696751e680673d054c7.scope" (properties [{Name:Description Value:"libcontainer container 7cf2a1a5b40984dbcccc9bf1c58db1c9225b9504f0ef5696751e680673d054c7"} {Name:Slice Value:"user.slice"} {Name:Delegate Value:true} {Name:PIDs Value:@au [1122]} {Name:MemoryAccounting Value:true} {Name:CPUAccounting Value:true} {Name:IOAccounting Value:true} {Name:TasksAccounting Value:true} {Name:DefaultDependencies Value:false}]): Permission denied: unknown.
```

調べてみると、`systemd` を `cgroup` のドライバとして利用している場合、`dbus` がユーザーセッションサービスとして起動している必要があるようです。
私が知識不足で、うまく説明できないのですが、情報源は↓こちらです。

https://github.com/containerd/nerdctl/blob/main/docs/faq.md#error-failed-to-create-shim-task-oci-runtime-create-failed-runc-create-failed-unable-to-start-container-process-unable-to-apply-cgroup-configuration-unable-to-start-unit--nameslice-valueuserslice-namedelegate-valuetrue--permission-denied-unknown

詰まるところ、何をすれば良いかと言うと、↓下記のコマンドを Rootless Docker を実行したいユーザーの権限 (= `sudo` は付けない) で実行しておけば良いようです。

```sh
systemctl --user start dbus
```

これでリトライして、コンテナが動けば OK です。

```sh
$ docker run --rm -it photon
root [ / ]# echo hoge
hoge
```


# 最後に

動作確認の部分で、ちょっと調べないといけないエラーに遭遇しましたが、Photon OS を使って、無事に Rootless コンテナを動作させることができました。
ちなみに... 実は Photon OS 4.0 の時点で、似たようなことができないか、試したことがあったのですが、記事にできていないということは... お察しの通りです...
が、Photon OS 5.0 では、これだけパッケージが用意されていたので、個人的には「案外あっさり動いたな」という印象です。

ただ、公式ドキュメントにも記載がある通り、コンテナに Root 権限がない分、Well-Known ポートをバインドしようとした時に、別途、権限の割り当てが必要だったり、いくらかの制限があります。そのため、既に出来合いのアプリを Rootless コンテナで動かそうとしたら、場合によっては、うまく動作させられない場合もありそうなので、注意が必要です。

とは言え、コンテナ環境においてセキュリティを向上させるためには、非常に面白い技術だと思うので、ご興味あれば、ぜひお試しいただければと思います。