Fullmoon System

Linuxコンテナの仕組み:namespaces・cgroups v2・rootless Podman

EdwardMoon

コンテナは小さな仮想マシンではありません。ホストのLinuxカーネルを共有し、process、mount、network、user namespaceで見える範囲を分け、cgroupsでCPU、メモリー、PIDなどを制限したプロセスです。この違いを理解することで、権限、イメージ、volume、ネットワーク、障害範囲を正しく設計できます。

演習はRocky Linux 9系のcgroup v2とrootless Podmanを基準にします。SELinuxとfirewalldを維持し、遠隔インストールスクリプトを直接パイプ実行せず、イメージのtagだけで判断せずdigestと由来を記録します。Podmanとカーネルのバージョンにより結果が異なるため、各段階で実状態を確認してください。

Linuxコンテナ:rootlessユーザー、OCIイメージ層、namespaces、共有カーネル、cgroups v2
OCIイメージから作られた隔離環境が1つのカーネルを共有し、cgroupsでリソースを制限します

コンテナと仮想マシンの違い

項目 コンテナ 仮想マシン
Kernel ホストカーネルを共有 Guest OSごとのカーネル
隔離 namespaces、LSM、seccomp、capabilities hypervisorと仮想ハードウェア
イメージ OCI layerとmetadata ディスク全体のイメージ
開始 隔離したプロセスを生成 Guest OSを起動
リソース cgroupsとruntimeの制限 vCPU、RAM、仮想デバイスの割り当て
リスクの境界 カーネル脆弱性の影響を共有 Guestとhostの間に別カーネルの境界

rootlessはコンテナrootをホストの非特権UID範囲へ対応付けて被害を減らしますが、完全な境界ではありません。ホストカーネル、engine、OCI runtime、イメージを更新し、不要なcapability、device、socket、host namespaceを与えないでください。

構成要素

要素 役割 主な確認方法
OCI image root filesystem layerと実行metadata podman image inspect・history
Container engine pull、build、network、storage、lifecycle podman info
OCI runtime namespace・cgroupを作り、プロセスを実行 crun –versionまたはrunc –version
conmon プロセス監視、stdio、終了処理 podman info –debug
namespaces ID、mount、networkの見える範囲を分離 lsns・/proc/PID/ns
cgroups v2 CPU、メモリー、I/O、PIDの使用量計測と制限 podman stats・/sys/fs/cgroup
SELinux/seccomp 許可するファイル・system callを制限 getenforce・podman inspect

演習環境の確認

Podmanとカーネル機能

uname -r
cat /etc/os-release
podman --version
podman info --debug

podman info --format '{{.Host.CgroupsVersion}}'
stat -fc %T /sys/fs/cgroup
getenforce
systemctl is-active firewalld

cgroup2fsとPodmanのcgroup v2表示が一致するか確認します。Quadletはcgroup v2を必要とします。SELinuxをEnforcingに保ってvolume labelを正しく設定し、動かないという理由で防御機能全体を無効にしないでください。

rootlessのUID・GID範囲

id
grep -E "^${USER}:" /etc/subuid /etc/subgid
command -v newuidmap newgidmap
command -v pasta

podman unshare cat /proc/self/uid_map
podman unshare cat /proc/self/gid_map

rootless Podmanは/etc/subuidと/etc/subgidの追加ID範囲で、コンテナUIDをホストの非特権UIDへ対応付けます。範囲がなければ管理者がusermod –add-subuidsと–add-subgidsで重複しない範囲を割り当てます。共有NFS homeはuser namespaceを理解しないため、graphrootをローカルFSに置く構成を検討してください。

namespacesの演習

namespaceはプロセスから見えるグローバルリソースを分けます。PIDはプロセス番号、mountはマウント表、networkはインターフェース・経路・ポート、userはUID・GIDとcapability範囲を分離します。

現在のnamespace一覧

lsns
readlink /proc/self/ns/user
readlink /proc/self/ns/pid
readlink /proc/self/ns/mnt
readlink /proc/self/ns/net

非特権のuser・PID namespaceを作る

unshare --user --map-root-user --pid --fork sh -c '
  id
  echo "namespace PID: $$"
  readlink /proc/self/ns/user
  readlink /proc/self/ns/pid
'

uid=0は新しいuser namespace内のrootであり、ホストrootではありません。UID mappingと許可capabilityが制限されるため、全ホストファイルの読み取りや機器の制御はできません。ただしカーネルは共有され、脆弱性や誤ったdevice・socket mountの危険は残ります。

OCIイメージとdigest

イメージは変更不可のlayerと、config、entrypoint、環境変数などのmetadataで構成されます。tagは別digestへ付け替えられるため、本番では検証したmanifest digestを記録し、署名、SBOM、脆弱性の方針を併用します。

完全なイメージ名でpullする

IMAGE='docker.io/library/busybox:1.36.1'
podman pull "$IMAGE"

podman image inspect "$IMAGE"   --format 'ID={{.Id}} Digest={{.Digest}} Created={{.Created}}'
podman history --no-trunc "$IMAGE"
podman images --digests

short nameはregistries.conf次第で別registryへ解決されるため、registryとnamespaceを含む完全名を使います。開発でtagを使った場合は、inspectのdigestを承認記録へ残し、Quadletや配布manifestではimage@sha256で固定します。

OCI manifestを確認する

skopeo inspect   docker://docker.io/library/busybox:1.36.1   | jq '{Name,Digest,Created,Architecture,Os}'

skopeo inspect --raw   docker://docker.io/library/busybox:1.36.1   | jq .

rootless Podmanで実行する

BusyBox httpdをloopbackだけに公開し、root FSをread-onlyにします。既定capabilityをすべて落とし、権限昇格と過大なPID、メモリー、CPU使用を制限します。必要な書き込み先だけをtmpfsか明示的volumeで提供します。

IMAGE='docker.io/library/busybox:1.36.1'
install -d -m 0750 "$HOME/container-data"
printf 'hello from rootless Podman\n' > "$HOME/container-data/index.html"

podman run --detach --rm --name web-demo \
  --read-only --cap-drop=all --security-opt=no-new-privileges \
  --pids-limit=128 --memory=256m --cpus=0.50 \
  --publish 127.0.0.1:8080:8080 \
  --volume "$HOME/container-data:/www:ro,Z" \
  --tmpfs /tmp:rw,noexec,nosuid,nodev,size=32m \
  "$IMAGE" httpd -f -p 8080 -h /www

実行状態と制限を確認する

podman ps
podman port web-demo
curl --fail --silent --show-error http://127.0.0.1:8080/

podman stats --no-stream web-demo
podman top web-demo user hpid pid args
podman inspect web-demo > web-demo.inspect.json
jq '.[0].HostConfig | {ReadonlyRootfs,Memory,NanoCpus,PidsLimit}'   web-demo.inspect.json

–publishでホストIPを省略すると全インターフェースへ公開される場合があります。ローカルreverse proxy配下のサービスは127.0.0.1へbindし、外部公開が必要ならfirewalldのzone・source、TLS、認証を別途確認します。

プロセスとnamespaceを追跡する

HOST_PID=$(podman inspect --format '{{.State.Pid}}' web-demo)
printf 'host PID=%s
' "$HOST_PID"

ps -o user,pid,ppid,cmd -p "$HOST_PID"
sudo lsns -p "$HOST_PID"
sudo readlink "/proc/${HOST_PID}/ns/user"
sudo readlink "/proc/${HOST_PID}/ns/net"
sudo cat "/proc/${HOST_PID}/cgroup"

内部のPID 1もホストでは通常のPIDです。nsenterは隔離境界内を調べられる強い権限なのでアクセスを制限し、先にpodman exec、logs、inspectなどengineのインターフェースを使います。

podman exec web-demo sh -c '
  echo "container PID=$$"
  id
  cat /proc/self/status | grep -E "^(Name|Pid|NSpid|CapEff|NoNewPrivs):"
'

podman logs web-demo
podman events --since 10m --filter container=web-demo

cgroups v2の演習

cgroups v2は、見える範囲の隔離ではなく、リソースの計測と制限を担います。memory制限はOOMの影響、pids制限はfork bombの拡大、CPU quotaは他プロセスへの負荷干渉を抑えます。rootlessではsystemd user delegationとホスト方針により、一部controllerの制限を使えない場合があります。

podman stats --no-stream web-demo
podman inspect web-demo --format '{{.State.CgroupPath}}'

systemd-cgls "/user.slice/user-${UID}.slice"
systemctl --user status

podman update   --memory=192m   --pids-limit=96   web-demo
podman stats --no-stream web-demo

制限が低すぎると正常負荷でもOOM killや要求失敗が起こります。メモリーモデル、JVM・worker数、health check、再起動方針を併せて調整し、ホスト全体の余裕とPSIも監視してください。

volumeとSELinux

専用ホストディレクトリとprivate label

install -d -m 0750 "$HOME/volume-label-demo"
printf 'hello from a labeled volume\n' > "$HOME/volume-label-demo/index.html"

podman run --rm --read-only --cap-drop=all \
  --security-opt=no-new-privileges \
  --volume "$HOME/volume-label-demo:/data:ro,Z" \
  docker.io/library/busybox:1.36.1 cat /data/index.html

ls -Zd "$HOME/volume-label-demo"

:Zは対象を1コンテナ専用のprivate SELinux labelへ変更します。複数コンテナで共有するなら:zを検討しますが、システムディレクトリ全体や重要なhomeを広くrelabelすると、ホストサービスが壊れる場合があります。専用ディレクトリだけをmountし、バックアップ、所有権、UID mappingを先に設計します。

セキュリティ上のアンチパターン

設定・操作 リスク 推奨する方法
全特権モード device、capability、LSMの多くの境界を解除 必要なcapabilityとdeviceだけ許可
host network/PID ホストのnamespaceと観測範囲を共有 専用network namespaceと明示ポート
Podman/Docker socket mount ホストで任意のコンテナ・mountを実行可能 制限したAPI proxyか専用自動化アカウント
latest tag 再配布の結果が変化 検証済みdigestと署名方針
防御機能全体の無効化 SELinuxとファイアウォールの層を喪失 必要範囲のlabel、port、policyだけ修正
curlの出力をshellへ接続 レビュー・完全性確認なしで遠隔コードを実行 公式パッケージ、署名、checksumの確認

権限とmountを監査する

podman inspect web-demo   --format '{{json .HostConfig.SecurityOpt}} {{json .HostConfig.CapDrop}}'

podman inspect web-demo   --format '{{range .Mounts}}{{.Type}} {{.Source}} -> {{.Destination}} rw={{.RW}}{{println}}{{end}}'

podman diff web-demo
podman top web-demo capeff label

Quadletによる運用

一時的なpodman runをシェルスクリプトで包むより、Quadletの.containerをrootless user unitの検索パスへ置くと、systemdでライフサイクルとログを管理できます。本番ではImageを検証済みsha256 digestへ変更してください。

演習用コンテナを終了する

podman stop web-demo
podman ps --all --filter name=web-demo

# 先ほど--rmで実行したため、正常終了後にコンテナは残らないはずです。
podman container exists web-demo; printf 'exit=%s
' "$?"
mkdir -p "$HOME/.config/containers/systemd"
${EDITOR:-vi} "$HOME/.config/containers/systemd/web-demo.container"

~/.config/containers/systemd/web-demo.container

[Unit]
Description=Rootless read-only web demo

[Container]
Image=docker.io/library/busybox:1.36.1
ContainerName=web-demo
Exec=httpd -f -p 8080 -h /www
Volume=%h/container-data:/www:ro,Z
PublishPort=127.0.0.1:8080:8080
ReadOnly=true
NoNewPrivileges=true
DropCapability=all
PidsLimit=128

[Service]
MemoryMax=256M
Restart=on-failure
TimeoutStartSec=120

[Install]
WantedBy=default.target

Quadletの生成結果とサービスを確認する

mkdir -p "$HOME/.config/containers/systemd"
chmod 0700 "$HOME/.config/containers/systemd"
chmod 0644 "$HOME/.config/containers/systemd/web-demo.container"

systemctl --user daemon-reload
systemctl --user start web-demo.service
systemctl --user status web-demo.service
journalctl --user -u web-demo.service --since '-10 min'

podman info --format '{{.Host.CgroupsVersion}}'
curl --fail --silent --show-error http://127.0.0.1:8080/

生成されたサービスを直接enableせず、.containerの[Install] WantedByをgeneratorに反映させます。ログアウト後も維持するには管理者がloginctl enable-lingerを使えますが、ログインなしでプロセスが動き続ける運用・セキュリティ上の影響を承認してから適用します。

診断の順序

  1. podman ps –allとサービス状態で終了コードと再起動ループを確認する。
  2. logsとeventsでアプリケーションとengineの時刻を合わせる。
  3. image ID、digest、command、environment、mount、security optionをinspectする。
  4. port binding、rootless network、DNS、ホストのファイアウォール境界を確認する。
  5. SELinux AVC、volume label、UID mapping、ファイル権限を確認する。
  6. cgroupのmemory・PID・CPU制限とOOM・pressureを確認する。
  7. 同じdigestと最小入力で、隔離したホスト上に再現する。
podman ps --all --size
podman inspect web-demo
podman logs --timestamps web-demo
podman events --since 30m
podman stats --no-stream web-demo

journalctl --user -u web-demo.service --since '-30 min'
sudo ausearch -m AVC,USER_AVC -ts recent
ss -lntp | grep ':8080'

運用チェックリスト

  • VMとの共有カーネルの違いと、namespace・cgroupの役割を区別した。
  • subuid・subgid、cgroup v2、runtime、ローカルstorage、networkツールを確認した。
  • 完全なregistry名、検証済みdigest、署名、SBOM、脆弱性結果を記録した。
  • read-only rootfs、no-new-privileges、capability drop、PID・memory・CPU制限を適用した。
  • 必要なホストIPだけへbindし、privileged、host namespace、engine socketを避けた。
  • 専用volumeとSELinux labelを使い、防御機能を無効にしていない。
  • Quadlet、ログ、health、backup・restore、更新時のrollbackを検証した。

公式資料と関連記事

まとめ

安全な運用とは、イメージを起動する1コマンドではなく、共有カーネルのリスクを理解し、namespace、cgroups v2、user mapping、SELinux、OCIの供給網を併せて管理することです。rootlessを基本にdigest固定、最小限のcapability・mount・port、read-only FS、リソース制限を適用します。最後にQuadlet、systemdログ、health、backup、rollbackを検証することで、再現可能なサービスになります。