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、capabilitieshypervisorと仮想ハードウェア
イメージOCI layerとmetadataディスク全体のイメージ
開始隔離したプロセスを生成Guest OSを起動
リソースcgroupsとruntimeの制限vCPU、RAM、仮想デバイスの割り当て
リスクの境界カーネル脆弱性の影響を共有Guestとhostの間に別カーネルの境界

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

構成要素

要素役割主な確認方法
OCI imageroot filesystem layerと実行metadatapodman image inspect・history
Container enginepull、build、network、storage、lifecyclepodman info
OCI runtimenamespace・cgroupを作り、プロセスを実行crun –versionまたはrunc –version
conmonプロセス監視、stdio、終了処理podman info –debug
namespacesID、mount、networkの見える範囲を分離lsns・/proc/PID/ns
cgroups v2CPU、メモリー、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を検証することで、再現可能なサービスになります。