Rootless Podmanは「安全」の前に、ポートとボリューム権限で一度止めてみる

Rootless Podmanは安全そうに見える前にポートとボリューム権限で止めて見るのアイキャッチ画像
目次

Rootless Podmanへ移す前に、止まる場所を先に見ておく

小さなWebサービスや管理ツールをコンテナで動かしていると、それらをずっとrootで動かし続けることが少し気になり始めます。Rootless Podmanはその不安に対する一つの答えに見えますが、「rootlessだから安全」という言葉だけで常用環境を置き換えると、たいていポート公開とファイル権限のところで手が止まります。

この記事では、安全性の美談としてrootlessを語る前に、Docker Composeに慣れた運用者が最初に踏む段差を、使い捨ての検証用VM上で実コマンドと実ログとして記録します。確認したいのは次の4点です。

  • rootless状態はどのコマンドで確認できるのか
  • 低番ポート(<1024)で失敗したとき、何が表示されるのか
  • ホスト側ボリューム権限の失敗はどう見えるのか
  • rootful Dockerから置き換える前に、何を小さく試しておくべきか

検証環境

検証は、使い捨てのエフェメラルVM上で行いました。常用環境ではなく、終わったら破棄する前提の隔離環境です。

  • ディストリビューション: Debian 12 (bookworm) 系
  • Podman: version 4.3.1
  • 関連パッケージ: uidmap / slirp4netns / fuse-overlayfs / curl
  • 実行ユーザー: root権限で検証用一般ユーザー labuser を作成し、以降のコンテナ操作はすべて labuser で実施

Podmanの導入とユーザー名前空間(subuid/subgid)の設定にはパッケージ導入とカーネル機能が必要なため、ネットワークやパッケージ導入が制限された環境ではなく、導入可能な使い捨てVMを使っています。

実行コマンドと実行ログ

1. 導入と検証用ユーザーの用意

まずPodmanと依存パッケージを導入し、検証用の一般ユーザー labuser にsubuid/subgidの範囲を割り当てます。

# export 設定項目=noninteractive
# apt-get update && apt-get install -y podman uidmap slirp4netns fuse-overlayfs curl
# useradd -m -s /bin/bash labuser
# usermod --add-subuids 100000-165535 --add-subgids 100000-165535 labuser
# loginctl enable-linger labuser
# grep labuser /etc/subuid /etc/subgid; id labuser

割り当て後の確認結果は次のとおりです。

/etc/subuid:labuser:231072:65536
/etc/subgid:labuser:231072:65536
uid=1002(labuser) gid=1002(labuser) groups=1002(labuser)

2. rootless状態の確認

まず「本当にrootlessで動いているか」を確認します。Podmanはここを明示的に答えてくれます。

# su - labuser -c "podman --version && podman info --format '{{.Host.Security.Rootless}}'"
podman version 4.3.1
true

podman infoHost.Security.Rootlesstrue であること、これがrootless運用の入口です。さらに名前空間のマッピングも見ておきます。

# su - labuser -c "id; podman unshare cat /proc/self/uid_map"
uid=1002(labuser) gid=1002(labuser) groups=1002(labuser)
         0       1002          1
         1     231072      65536

コンテナ内のUID 0(root)が、ホスト側ではUID 1002(labuser)に対応しています。コンテナ内のroot以外のUIDは231072以降にマッピングされます。後で出てくるボリューム権限の失敗は、ここのズレが原因になります。

3. 高番ポートでの正常起動(成功例)

先に成功する例を押さえます。1024以上のポート(ここでは8080)でnginxを起動し、HTTP到達を確認します。

# su - labuser -c "podman run -d --name web-high -p 8080:80 docker.io/library/nginx:alpine; sleep 3; curl -s -o /dev/null -w '設定項目=%{http_code}\n' http://127.0.0.1:8080"
db61b80d99fe29ac2f2ef2e9a9be1aa28f5a4b24a601ad48ca18ad6b5d3137fe
設定項目=200

起動し、設定項目=200 が返りました。ここまではDockerと同じ感覚で進みます。

4. 低番ポート公開の失敗(失敗1)

次に、同じ構成のままポートだけを80(<1024)に変えます。Dockerでは何気なく書く設定です。

# su - labuser -c "podman run -d --name web-low -p 80:80 docker.io/library/nginx:alpine"

ここで止まります。終了コードは126で、メッセージは次のとおりです。

Error: rootlessport cannot expose privileged port 80, you can add 'net.ipv4.ip_unprivileged_port_start=80' to /etc/sysctl.conf (currently 1024), or choose a larger port number (>= 1024): listen tcp 0.0.0.0:80: bind: permission denied

失敗の理由と回避策がそのまま書かれているのが、このログの良いところです。要点は2つです。

  • 一般ユーザーは既定で1024未満の特権ポートをバインドできない(bind: permission denied)
  • 回避するなら、net.ipv4.ip_unprivileged_port_start を下げてホスト側で特権ポートの開始位置を変えるか、1024以上のポートを使う

つまり「80番でそのまま公開」をしたい場合、リバースプロキシで80を受けて内部は高番ポートへ回す、もしくはホスト側のカーネル設定に手を入れる、という判断が先に必要になります。

5. ホスト側ボリューム権限の失敗(失敗2)

もう一つの段差は永続化データです。root所有・パーミッション700のディレクトリをホスト側に作り、それをコンテナへマウントして書き込みを試します。

# mkdir -p /tmp/issue165-rootdata && chown root:root /tmp/issue165-rootdata && chmod 700 /tmp/issue165-rootdata
# su - labuser -c "podman run --rm -v /tmp/issue165-rootdata:/data docker.io/library/alpine sh -c 'echo test > /data/probe.txt'"

結果は終了コード1で、書き込みに失敗します。

sh: can't create /data/probe.txt: Permission denied

コンテナ内のrootは、ホスト側ではlabuser(UID 1002)として振る舞います。root所有で700のディレクトリには、labuserは書き込めません。Dockerをrootで動かしていたときは「コンテナのrootがホストのrootとして書ける」感覚だったため、同じディレクトリ設計のまま移すと、ここで原因不明の保存失敗に見えます。

対処の方向は、マウント先のディレクトリ所有者・パーミッションをlabuser(または名前空間でマッピングされるUID)に合わせること、もしくは名前付きボリュームを使ってPodman側に管理を委ねることです。

6. rootful Dockerとの最小対比

参考として、同じ80番ポートをrootful Dockerで公開すると通る点だけを最小コマンドで確認しました。

# docker run -d --name dock-low -p 80:80 nginx:alpine && sleep 3 && curl -s -o /dev/null -w '設定項目=%{http_code}\n' http://127.0.0.1:80
# docker rm -f dock-low; docker rmi nginx:alpine
設定項目=200

rootful Dockerでは80番がそのまま通ります。これはDockerが安全という意味ではなく、「rootで動くから特権ポートをそのまま開けている」というだけです。rootless化で失われるのはこの暗黙の特権であり、それが今回の失敗1の正体です。

7. ログの読みやすさ

失敗の原因追跡には podman logs がそのまま使えます。高番ポートで起動したコンテナのログには、起動とアクセスがDockerと同じ形式で記録されていました。

2026/06/25 13:33:52 [notice] 1#1: nginx/1.31.2
...
<アクセス元> - - [25/Jun/2026:13:33:55 +0000] "GET / 設定項目/1.1" 200 896 "-" "curl/7.88.1" "-"

失敗側(低番ポート)はコンテナが起動に至っていないため、ログは空でした。失敗はコンテナのログではなく、podman run の標準エラー出力側に出る、という点も覚えておくと迷いません。

8. ロールバック

最後に、コンテナとイメージをまとめて削除し、環境を元に戻せることを確認しました。

# su - labuser -c "podman rm -f web-high web-low; podman rmi -a -f; podman system prune -a -f; podman ps -a; podman images"
Total reclaimed space: 0B
--- ps ---
設定項目 ID  設定項目       設定項目     設定項目     設定項目      設定項目       設定項目
--- images ---
設定項目  TAG         設定項目 ID    設定項目     設定項目

podman ps -apodman images がどちらも空になり、rootless環境はユーザー空間内で完結して片付きました。常用ではなく、まず使い捨て環境で試して破棄する、という流れがそのまま取れます。

結果のまとめ

比較の軸ごとに整理すると、判断材料は次のようになります。

評価軸 rootful Docker Rootless Podman 一般ユーザー単体systemd
起動権限 rootで動作 一般ユーザーで動作(Rootless=true) 一般ユーザーで動作
低番ポート公開 そのまま可(80でも200) 既定では不可、要カーネル設定か高番ポート 既定では不可(同じ制約)
永続化データ ホストrootと同一視で書けることが多い UID名前空間のマッピングに権限を合わせる必要あり プロセスのUIDに権限を合わせる
失敗時のログ docker logs / 標準エラー podman logsと標準エラー、メッセージに回避策が含まれる journalctl 等
ロールバック docker rm/rmi podman system pruneでユーザー空間内に完結 サービス停止・無効化

結論:置き換える前に、この2点を自分のサービスで試す

今回の検証から言えるのは、「Rootless Podman=安全だから全部置き換えられる」ではない、ということです。rootless化で得られるのは権限の縮小ですが、その代償として、これまでrootで動かしていたことに暗黙に依存していた2点が表に出てきます。

Rootless Podmanへ移してよい条件は、おおむね次のように言えます。

  • 公開ポートを1024以上に寄せられる、もしくはリバースプロキシやカーネル設定で80/443を扱う構成を許容できる
  • 永続化データのディレクトリ所有者・パーミッションを、コンテナ内UIDの名前空間マッピングに合わせて設計し直せる(または名前付きボリュームに寄せられる)
  • まず使い捨て環境で起動・書き込み・削除まで通してから、常用サービスを移す手順を取れる

逆に、「80番で直接公開し続けたい」「既存のroot所有ディレクトリ構成を一切変えたくない」という状態のままなら、移行はポートと権限のところで必ず一度止まります。その場合は、急いで全置換するより、対象を1サービスに絞って高番ポート+ボリューム権限の見直しから小さく試すのが、いちばん安全な進め方です。

安全性は、起動が成功したかどうかではなく、失敗が予測でき、戻せるかどうかで測れます。今回のように「どこで止まるか」を先に見ておくことが、その判断の材料になります。

参考

この検証を回している環境

この検証は、自宅の常設ラボ(使い捨てVM/LXCを回す母艦+GPU+VLAN分離ネットワーク)で動かしています。使っている機材と選定理由、全体構成は1本にまとめています。

ラボ構成のまとめを見る →
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

目次