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とカーネルのバージョンにより結果が異なるため、各段階で実状態を確認してください。

コンテナと仮想マシンの違い
| 項目 | コンテナ | 仮想マシン |
|---|---|---|
| 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を使えますが、ログインなしでプロセスが動き続ける運用・セキュリティ上の影響を承認してから適用します。
診断の順序
- podman ps –allとサービス状態で終了コードと再起動ループを確認する。
- logsとeventsでアプリケーションとengineの時刻を合わせる。
- image ID、digest、command、environment、mount、security optionをinspectする。
- port binding、rootless network、DNS、ホストのファイアウォール境界を確認する。
- SELinux AVC、volume label、UID mapping、ファイル権限を確認する。
- cgroupのmemory・PID・CPU制限とOOM・pressureを確認する。
- 同じ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を検証した。
公式資料と関連記事
- Podman rootless公式資料
- podman runのセキュリティ・リソースオプション
- Podman Quadlet
- Linux cgroup v2
- Linux namespaces
- Linuxカーネルの構造と診断
- NetBox Dockerの安全な構築とバックアップ
まとめ
安全な運用とは、イメージを起動する1コマンドではなく、共有カーネルのリスクを理解し、namespace、cgroups v2、user mapping、SELinux、OCIの供給網を併せて管理することです。rootlessを基本にdigest固定、最小限のcapability・mount・port、read-only FS、リソース制限を適用します。最後にQuadlet、systemdログ、health、backup、rollbackを検証することで、再現可能なサービスになります。