CentOS Stream 8でrootlessモードでpodmanを実行した時のメモ

# はじめに

:::note
このドキュメントはNFSで共有されているホームディレクトリを利用してpodmanを起動したい方を対象としています。Docker CEでも基本的には同様で、方法は最後に追記しました。
:::

これまでKubernetes環境を利用するために、Docker環境を利用してきましたが、数名がログインするクライアント端末上で、podmanを利用してみた時の顛末を残しておきます。

この作業は、LDAPで認証を行ないホームディレクトリをNFSで共有する利用者が、podmanを非root権限で利用できるか、確認した時のメモです。

一般的なCentOS/RHEL単体でpodmanを利用する方は、他のドキュメントが参考になると思います。

# 参考資料

* [RedHat - Running rootless Podman as a non-root user](https://www.redhat.com/sysadmin/rootless-podman-makes-sense)
* https://www.si1230.com/?p=40757
* [RedHat RHEL8 Manual - 3.2. LDAP を使用し、TLS 認証を必要とする SSSD の設定](https://access.redhat.com/documentation/ja-jp/red_hat_enterprise_linux/8/html/configuring_authentication_and_authorization_in_rhel/configuring-sssd-to-use-ldap-and-require-tls-authentication_configuring-sssd-to-use-ldap-and-require-tls-authentication)
* https://serverfault.com/questions/1018828/editing-authselect-files
* [RedHat Blog - New features for running containers on NFS with rootless Podman](https://www.redhat.com/sysadmin/nfs-rootless-podman)
* [RedHat Blog - Why can’t rootless Podman pull my image?](https://www.redhat.com/sysadmin/rootless-podman)

# 環境

* VMware Workstation Pro 16 (2-core, 4GB-memory)
* CentOS 8 Stream (4.18.0-338.el8.x86_64)
  * 認証: LDAP
  * ホームディレクトリ: autofsによるNFS領域 (with root_squash)

## 想定される課題

* NFS領域からのコンテナの起動に失敗する

# 準備作業

最小構成でCentOS8 Streamをセットアップしてから、必要なモジュールを導入しています。

```bash
$ sudo yum update
$ sudo yum install podman python3
```

## non-root対応していないpodmanを一般ユーザーで実行してみる

確認のため、以下のようなコマンドを実行してみます。

```bash:sudoを使わないpodmanの利用
$ podman pull docker.io/library/nginx:latest
$ podman run -it -d --name nginx nginx:latest
$ podman stop nginx
$ podman run -it -d -p 8080:80 --name nginx nginx:latest
$ podman stop nginx
```

問題なく、これらのコマンドが実行できた場合には、useraddコマンドなどでユーザーを作成した際に、/etc/subuid, /etc/subgid ファイルが設定済みと思われます。おそらくホームディレクトリもローカルディスク上にあると推測されます。

今回の動作確認のためには、これらのファイルを空にしておくか、該当ユーザーのエントリを削除しておいてください。

```bash:/etc/{subuid,subgid}を空にする手順
$ sudo cp -ip /etc/subuid /etc/subuid.$(date +%Y%m%d.%H%M%S)
$ sudo cp -ip /etc/subgid /etc/subgid.$(date +%Y%m%d.%H%M%S)
$ cat /dev/null | sudo tee /etc/subuid
$ cat /dev/null | sudo tee /etc/subgid

## /etc/subuid, /etc/subgidを編集した後は反映させるために再起動してください
```

ホームディレクトリについては、NFS経由でマウントしている事を確認してください。

```bash:ホームディレクトリがNFSマウントされているか確認する
$ cd
$ df .
```

成功すれば次のような出力結果が得られているはずです。

```text:成功例
## 成功例: NFSを利用している場合、NFSサーバーのIPアドレス/ホスト名から始まります
Filesystem                1K-blocks     Used  Available Use% Mounted on
192.168.1.10:/export/home 2883091456 17161216 2719407104   1% /nfs/home
```

次のように出力されていれば、ホームディレクトリはローカルデバイス上に存在していることになります。

```text:失敗例
## 失敗例: ローカルディスクを利用している場合は、/devで始まるデバイス名から始まります
Filesystem                  1K-blocks    Used Available Use% Mounted on
/dev/mapper/cs_s10tp08-home 398374860 2810800 395564060   1% /home
```

/etc/subuid,/etc/subgidファイルの該当エントリを空にした後には、エラーが表示されるようになります。

```bash:確認のためのコマンド
$ podman pull docker.io/library/nginx:1.17
```

```text:エラーの様子（意図した動作です）
Trying to pull docker.io/library/nginx:1.17...
Getting image source signatures
Copying blob 11fa52a0fdc0 done  
Copying blob afb6ec6fdc1c done  
Copying blob b90c53a0b692 done  
Error: writing blob: adding layer with blob "sha256:afb6ec6fdc1c3ba04f7a56db32c5ff5ff38962dc4cd0ffdef5beaa0ce2eb77e2": \
    Error processing tar file(exit status 1): potentially insufficient UIDs or GIDs available in user \
    namespace (requested 0:42 for /etc/gshadow): Check /etc/subuid and /etc/subgid: lchown /etc/gshadow: \
    invalid argument
```

ここでエラーが出力されることは意図した動作です。

反映は即時されるはずですが、既に稼動しているpodmanプロセスが存在していると正しく反映されないかもしれません。
podmanプロセスがある場合には停止してから、再度、動作を確認してください。

```bash:podmanを停止する
$ sudo killall /usr/bin/podman
$ podman pull docker.io/library/nginx:1.17
...
```

# podman以外の追加の操作について

## ansibleを利用して、LDAP認証の準備を行なう

まっさらなCentOS8 Streamを準備したので、LDAP認証などを行なっていきます。
podmanと一緒にpython3を導入しているので、他のサーバーからansible経由で操作をしていきます。

```makefile:Makefileによるansibleの利用
.PHONY: setup

setup:
        ansible all -m yum -a "name={{ pkglist }}" --extra-vars='{"pkglist":[autofs,podman,openldap-clients,ss
sd,sssd-ldap,sssd-tools,authselect,oddjob-mkhomedir]}'
        ansible all -m command -b -a "authselect select sssd --force"
        ansible all -m command -b -a "authselect select sssd with-mkhomedir"
        ansible all -m lineinfile -b -a "dest=/etc/openldap/ldap.conf regexp='^URI' line='URI ldaps://ldap.example.com'"
        ansible all -m lineinfile -b -a "dest=/etc/openldap/ldap.conf regexp='^BASE' line='BASE dc=example,dc=com'"
        ansible all -m copy -b -a "src=files/sssd.ldap.conf dest=/etc/sssd/conf.d/ldap.conf owner=root group=root mode=600"
```

Makefileの中で指定している files/sssd.ldap.conf ファイルの内容は、参考にしたRedHatのドキュメントどおりで、次のようになっています。

```text:files/sssd.ldap.conf
[domain/default]
id_provider = ldap
autofs_provider = ldap
auth_provider = ldap
chpass_provider = ldap
ldap_uri = ldaps://ldap.example.com/
ldap_search_base = dc=example,dc=com
ldap_id_use_start_tls = True
cache_credentials = True
ldap_tls_cacertdir = /etc/openldap/certs
ldap_tls_reqcert = allow

[sssd]
services = nss, pam, autofs
domains = default

[nss]
homedir_substring = /home
```

実際にはroleにして操作をまとめたり、yumモジュールにリストで必要なパッケージを指定するなどして、ansible-playbookを利用するのがお勧めです。


設定はmakeコマンドで反映させています。

```bash:設定の反映と再起動
$ make setup
$ ansible all -m command -b -a "shutdown -r now"
```

この再起動後に、LDAPに登録されたユーザーが認識されます。

## NFSによるホームディレクトリの共有

既にファイルサーバー側で、特定の領域をユーザーのホームディレクトリとしてexportしているので、これを認識できるようにクライアントの設定だけ行なっていきます。

```makefile:Makefileに追加したタスク

autofs:
        ansible all -m yum -a "name={{ pkglist }}" --extra-vars='{"pkglist":[nfs-utils]}'
        ansible all -m copy -b -a "src=files/etc.auto.master.d.nfs.autofs dest=/etc/auto.master.d/nfs.autofs owner=root group=root mode=644"
        ansible all -m copy -b -a "src=files/etc.auto.nfs dest=/etc/auto.nfs owner=root group=root mode=644"
        ansible all -m systemd -b -a "name=autofs state=restarted"
```

この中で指定している files/etc.auto.master.d.nfs.autofs, files/etc.auto.nfs の2つのファイルの内容は以下のとおりです。

```text:files/etc.auto.master.d.nfs.autofs

/nfs  /etc/auto.nfs

```

```text:files/etc.auto.nfs

home          -rw,soft,intr           192.168.1.5:/export/home

```

files/etc.auto.nfs(/etc/auto.nfs) の内容は適宜変更してください。

サーバー側の設定については、下記の記事に記述しています。

* https://qiita.com/YasuhiroABE/items/2e18bb546beea4660a99 

# LDAP認証したユーザーでのpodmanの利用について

LDAP認証したユーザーでログインし、先ほどと同じコマンドを実行します。

この段階ではLDAP上に存在するユーザー名に対応するエントリは、/etc/subuid,/etc/subgidファイルには存在していません。

```bash:LDAP認証したユーザーでのpodmanの実行
$ podman pull docker.io/library/nginx:latest
Trying to pull docker.io/library/nginx:latest...
Getting image source signatures
Copying blob a4723e260b6f done  
Copying blob fca7e12d1754 done  
Copying blob 1c84ebdff681 done  
Copying blob 745ab57616cb done  
Copying blob 858292fd2e56 done  
Copying blob b380bbd43752 done  
Error: writing blob: adding layer with blob "sha256:b380bbd43752f83945df8b5d1074fef8dd044820e7d3aef33b655a2483e030c7": \
    Error processing tar file(exit status 1): Error setting up pivot dir: \
    mkdir /home/nfs/user01/.local/share/containers/storage/overlay/e81bff2725dbc0bf2003db10272fef362e882eb96353055778a66cda430cf81b/diff/.pivot_root392550752: \
    permission denied
```

このようにNFS上のホームディレクトリ以下にコンテナイメージを保存しようとしてエラーになっています。

これを回避するために、``環境変数 XDG_DATA_HOME``、あるいは、``.config/containers/storage.confファイル`` を使用して、ローカルディスク上の書き込み可能なディレクトリを指定しています。

参考文献に挙げたRedHat Blogの中で、/var/tmpなどを利用する場合には、定期的に古いファイルを削除するdaemonプロセスが必要なファイルを稼動中に削除する可能性があることが指摘されています。

```text:.config/containers/storage.confファイルの内容
[storage]
driver = "overlay"
rootless_storage_path = "/var/tmp/$USER"
```

なお、podmanが認識するstorage.confの正確なファイルパスは、``podman info``コマンドで確認してください。

```bash:infoコマンドによる設定状況の確認
$ podman info
...
store:
  configFile: /nfs/home/user01/.config/containers/storage.conf
...
  graphRoot: /var/tmp/user01
...
```

環境変数を使用した場合は、どこにpullしたcontainerのイメージファイルを保存したか忘れる可能性があります。
このためstorage.confの利用をお勧めします。

## /etc/{subuid,subgid}ファイルの編集について

最後にrootless起動に必要なことは、/etc/subuid, /etc/subgidファイルを正しく編集することです。

このファイルについては多くのサイトで説明が行なわれています。
/etc/passwdのようにコロン(:)で区切られた形式になっています。

```text:フォーマット
<uid or username>:<起点となるUIDを示す数字>:<利用できるIDの数>
```

実システム上でpodmanはnewuidmap, newgidmapコマンドを利用して、コンテナプロセスのroot(uid:0)のユーザーのUIDは、実行ユーザーのIDと同一ですが、1番から始まるUIDは、``<起点となるUIDを示す数字> + (UID - 1)`` に割り当てられます。

このコンテナ上で1000番のUIDでプロセスを動作させる(uid:1000)場合は、システム上の``<起点となるUIDを示す数字> + (1000 - 1)`` 番のUIDが割り当てられます。

例えば、postgres:14 を稼動します。これは、Dockerfileの定義上で、uid:999 を割り当てるpostgresユーザーが実行するようになっています。

```dockerfile:docker.io/library/postgres:14のDockerfile抜粋
# explicitly set user/group IDs
RUN set -eux; \
...
	useradd -r -g postgres --uid=999 --home-dir=/var/lib/postgresql --shell=/bin/bash postgres; \
...
```

このコンテナを実行するために、以下のように、/etc/{subuid,subgid} を設定します。

```text:/etc/subuid、/etc/subgid上の定義
user01:100000:1000
```

この状態でコンテナを実行し、実システム上のUIDを調べてみます。

```bash:
$ podman run -it -d --rm --name postgres -e POSTGRES_PASSWORD=password postgres:14

$ ps aux|grep postgres

...
100998     33687  0.0  0.0 213280 10032 ?        Ss   10:49   0:00 postgres: walwriter 
```

999番を利用しますが、1番が100000に割り当てられるので、999番は、100998番に割り当てられます。

このように利用するコンテナの設計によって、/etc/{subuid,subgid} ファイルの設定を調整する必要があります。

# Alpineコンテナにおける/etc/passwdのデフォルト設定

様々なコンテナをベースとすると思いますが、ここではalpineコンテナの/etc/passwdの設定を確認しておきます。

```text:alpine-3.14イメージの/etc/passwdファイル
## $ podman run -it --rm alpine:3.14 cat /etc/passwd

root:x:0:0:root:/root:/bin/ash
bin:x:1:1:bin:/bin:/sbin/nologin
daemon:x:2:2:daemon:/sbin:/sbin/nologin
adm:x:3:4:adm:/var/adm:/sbin/nologin
lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin
sync:x:5:0:sync:/sbin:/bin/sync
shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown
halt:x:7:0:halt:/sbin:/sbin/halt
mail:x:8:12:mail:/var/mail:/sbin/nologin
news:x:9:13:news:/usr/lib/news:/sbin/nologin
uucp:x:10:14:uucp:/var/spool/uucppublic:/sbin/nologin
operator:x:11:0:operator:/root:/sbin/nologin
man:x:13:15:man:/usr/man:/sbin/nologin
postmaster:x:14:12:postmaster:/var/mail:/sbin/nologin
cron:x:16:16:cron:/var/spool/cron:/sbin/nologin
ftp:x:21:21::/var/lib/ftp:/sbin/nologin
sshd:x:22:22:sshd:/dev/null:/sbin/nologin
at:x:25:25:at:/var/spool/cron/atjobs:/sbin/nologin
squid:x:31:31:Squid:/var/cache/squid:/sbin/nologin
xfs:x:33:33:X Font Server:/etc/X11/fs:/sbin/nologin
games:x:35:35:games:/usr/games:/sbin/nologin
cyrus:x:85:12::/usr/cyrus:/sbin/nologin
vpopmail:x:89:89::/var/vpopmail:/sbin/nologin
ntp:x:123:123:NTP:/var/empty:/sbin/nologin
smmsp:x:209:209:smmsp:/var/spool/mqueue:/sbin/nologin
guest:x:405:100:guest:/dev/null:/sbin/nologin
nobody:x:65534:65534:nobody:/:/sbin/nologin
```

# 考察: コンテナイメージ利用上の考慮点

一般的なUID,GIDとして利用される 0 〜 65534 の範囲を自由に使えるように、/etc/{subuid,subgid} ファイル を設定できれば問題ありませんが、多数のユーザーにサービスを提供したい場合には、もう少しレンジを狭めたい運用を希望する場合も考えられます。

ただ、そのような設定は、あまりうまくいかないと思われます。

## Officialコンテナを利用する場合

利用したいコンテナイメージが特定のUID,GIDを利用するように設定されている場合に、問題が発生する可能性があります。

例に挙げたpostgresのように、999番を利用するものもあれば、ネットワークサービスを提供する場合、1024番以上のサービスが利用するポート番号と同じUID,GIDを割り当てるような作成方法も一般的に思われます。

どのような番号を利用するか想定することは難しいですし、nobody:nogroup を利用する場合には、UID,GIDとして65534番を確保しておく必要があります。

/etc/subuidファイルなどの説明では、3つ目の値がサイズを示すとされていますが、コンテナの稼動を前提とすると、実際には65535を指定することが必須のようになってしまいます。

## コンテナを自作し、利用する場合

自作する場合にはUID,GIDをコントロールすることが可能なので、それほど問題はないと思われます。

少ないUID,GIDレンジの利用を前提としてシステムを運用することが可能となります。

## 現実的な/etc/{subuid,subgid} ファイルの利用

ここではrootless環境でコンテナを稼動させることだけを目的として整理しておきます。

各ユーザーがコンテナで利用できる 0 〜 65534 番をUID,GIDとして準備するという前提では、利用できるUID, GIDの数は 100000 〜 4294900000 のレンジになります。

古典的なUNIXが利用する16ビットの範囲を実システムで利用することを前提として、そこから十分に大きなIDレンジを各ユーザーに確保することを考えます。

一般的には、4桁のUID,GIDレンジをユーザーに開放していることが多いと思います。あるいはシステムIDのUID,GIDはwell-knownポート番号と一致させているかもしれません。

### 4桁のUID,GIDを持つユーザーを対象にする場合

4桁のUID,GIDを持つユーザーが利用できる仮想UID,GIDは、uid:1000のユーザーであれば、1000をprefixとして、0 〜 65534 のIDを確保すると、100000000 〜 100065534、uid:1001であれば 100100000 〜 100165534、のように1億番以上の番号をコンテナで利用するUID,GIDとして各ユーザーに確保することができるようになります。

### 5桁のUID,GIDを持つユーザーが存在する場合

5桁のUID,GIDを持つユーザーを対象として、同様に考えると、全体のUID,GIDで利用できるのは32bitの範囲l(4294967295)だけなので、この方法で利用できるレンジは少し狭くなります。5桁でもuid:10000〜42949の範囲を越えてしまうと、この方法ではうまくアサインできなくなってしまいます。もちろん使われていない xxxxx65535〜xxxxx99999 の範囲をうまく扱えば、効率を高めることはできそうです。

### 現実的な対応

ただ、0〜65534までのUID,GIDを準備する必要は必ずしもありません。せいぜい4桁の9999番くらいまでの範囲で大抵のイメージをUIDを割り当てると思われます。

手元のDockerfileを調べた限りでは、Rocket.Chatが65535を指定していた以外は、Apache Solrの8983が一番大きな値でした。

大勢のユーザーに学習環境を提供するという意味では、0 〜 9999 番程度の範囲で、UID,GIDが自由に使えれば現実的な利用は可能になりそうです。

# Docker CEを利用している場合

基本的な考え方はPodmanと同様ですが、変更するファイルは``~/.config/docker/daemon.json``です。

```bash:dockerコマンドで/var/tmp以下に作業領域を移動する手順
$ mkdir -p ~/.config/docker
$ echo "{ \"data-root\":\"/var/tmp/$(id -un)/\" }" | tee ~/.config/docker/daemon.json
```

~/.config/docker/daemon.json が次のような内容になれば完了です。

```text:~/.config/docker/daemon.jsonファイル全体
{ "data-root":"/var/tmp/yasu.docker" }
```

Docker Desktopが主流になってからは環境全体が重くなっているので、コンテナの学習だけを行いたいのであればpodmanがお勧めです。

以上