はじめに
Docker を本番で運用していると、どうしても頭をよぎるのが「もしコンテナが侵害されたら?」という問いです。脆弱性はゼロになりません。依存ライブラリ、アプリの実装ミス、設定ミス、サプライチェーン…どこかで穴は空きます。だからこそ本番では、**“侵入されない”だけでなく、“侵入されても被害を限定する”**発想が必要になります。
ここで効いてくるのが Docker ホスト側の保護です。特に次の2つは、比較的現実的に導入できる「被害縮小」策として定番です。
- rootless Docker:Docker デーモンもコンテナも root を使わない(非特権ユーザーで動く)
- userns-remap:コンテナ内の root を、ホスト上の非特権 UID/GID に写像(実体は root ではない)
どちらも「コンテナ内 root = ホスト root」という最悪の構図を崩し、ホスト侵害のハードルを上げる方向に働きます。
ただし万能ではなく、ネットワーク、ストレージ、運用負荷、互換性などのトレードオフがあります。
この記事では、rootless Docker / userns-remap の違いと選び方を整理し、ハンズオンで「まず動かして、制約を体感し、運用に落とし込む」までを扱います。
座学
1) なぜ “ホスト保護” が必要なのか
Docker は “隔離” の仕組みを多層で使っています(namespaces / cgroups / capabilities など)。それでも コンテナはVMほど強い境界ではありません。攻撃者目線だと、狙いはだいたいこうです。
- コンテナ内で任意コード実行(RCE)を得る
- コンテナからホストへ脱出(権限昇格や設定不備を突く)
- ホストを取って横展開(同一ホスト上の他コンテナ、ネットワーク、資格情報へ)
ここで最大の怖さは「ホスト root を取られる」ことです。
逆に言えば、ホスト root までの距離を伸ばすだけでも、防御力は大きく上がります。
2) rootless Docker と userns-remap の違い(思想が違う)
両者は似て見えますが、狙っている層が違います。
rootless Docker(“そもそも root を使わない”)
- Docker デーモン(dockerd 相当)も、コンテナも 非rootユーザーで起動
- つまり、Docker という仕組み自体がホスト上で強い権限を持ちにくい
- “Docker を触れる人 = ホスト root に近い” という構図を崩しやすい
一方で、rootless は 制約が出やすいです。
特に「低いポートを使いたい」「高性能なネットワークが欲しい」「既存の運用資産が root 前提」だと壁に当たることがあります。
userns-remap(“root に見えるけど、実体は非root”)
- Docker デーモンは従来どおり root で動く(ここがrootlessと決定的に違う)
- ただし、コンテナ内部の UID/GID をホスト上の別 UID/GID 範囲に写像して、ホスト上の実体は非特権にする
- “コンテナ内 root でファイルを書いても、ホスト側では別ユーザー扱い” になる
userns-remap は 既存 Docker に比較的合わせやすい反面、デーモン自体は root なので、守れる範囲は rootless より狭い、という見方もできます。
3) ざっくり選び方(実務目線)
- 新規構築 / セキュリティ最優先 / 単一ホスト運用
→ まず rootless を検討(ただし制約を許容できるか確認) - 既存の運用に乗せたい / 互換性を保ちたい / 段階導入したい
→ userns-remap が現実的(移行の衝撃が比較的小さい) - 結論としては:
“理想は rootless、現実は userns-remap から始める” のパターンが多いです。
ただ、どちらが正しいではなく「あなたの要件で落とし所がどこか」です。
4) rootless の代表的な制約(導入前に知っておく)
- 特権操作が難しい:
--privilegedやホストに強く触る系は相性が悪い - 低いポート(例: 80/443)の扱い:非rootは通常バインドできない
- 回避策:ホスト側で 8080 受けてリバプロ / 80/443 は別プロセスで受ける など
- ネットワークが user-space 寄りになりがち(slirp4netns 等)
- 高スループットが必要だと検討が必要
- ストレージドライバ:環境によっては fuse-overlayfs 前提になることがある
- systemd での常駐管理:ユーザー単位の systemd(
systemctl --user)が前提になりやすい
5) userns-remap の代表的な落とし穴
- ボリューム/ホストマウントの権限問題が起きやすい
- “コンテナ内 root” がホスト上では別 UID になるため、既存ディレクトリに書けない等
- 既存コンテナ/イメージ/ボリュームの扱いに注意
- 有効化で挙動が変わるため、切り替えは計画的に
- トラブル時の切り分けが少し難しくなる
- ホスト上のファイル所有者が見慣れない UID になる、など
ハンズオン
ここからは “体感” するパートです。
やることは2本立てです。
- rootless Docker をユーザー単位で動かす
- userns-remap を有効化して UID 写像を確認する
どちらも本番ホストでいきなりやらず、まず検証環境で試してください(切り替えの衝撃があるため)。
コマンドは Linux(Ubuntu/Debian系)を想定しつつ、概念は他ディストリでも同じです。
A. rootless Docker を動かす(ユーザー単位)
1) 事前確認:サブUID/GIDが割り当てられているか
rootless は subuid/subgid を使います。まず確認。
id -u
id -g
cat /etc/subuid | grep -E "^$(whoami):" || true
cat /etc/subgid | grep -E "^$(whoami):" || true
もし割り当てが無い場合は(例:youruser:100000:65536 を追加):
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$(whoami)"
ログアウト/ログイン(または再起動)して反映させます。
2) rootless セットアップ(代表的な流れ)
環境によりパッケージ名は差がありますが、概念は「rootless 用のセットアップツールで導入 → ユーザー systemd で常駐」です。
dockerd-rootless-setuptool.sh install
成功すると、ユーザーサービスが作られます。起動確認:
systemctl --user start docker
systemctl --user status docker --no-pager
3) rootless の docker CLI を使う(ソケットの向き先)
rootless はユーザーのランタイムディレクトリ配下にソケットができます。
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock
docker info | head -n 30
ここで rootless っぽい情報が出ればOKです。
4) 動作確認:コンテナを起動して“ホスト側権限”を意識する
docker run --rm -it alpine:3.20 sh -lc 'id && whoami && echo hello'
「コンテナ内は root に見える」ことが多いですが、重要なのは ホスト側の実体が非rootに寄る点です。
(ここが “root で見えるけど root じゃない” の世界観です)
5) ポートの制約を体感する
rootless で 80 を直接 bind しようとすると、通常はできません。
docker run --rm -p 80:80 nginx:alpine
うまくいかない/制約に当たる場合は、実務では次のように設計します。
- rootless コンテナは 8080/8443 で待ち受け
- ホスト側の root 管理プロセス(Nginx 等)で 80/443 を受けてリバプロ
- あるいはロードバランサで 80/443 を終端して高ポートへ転送
rootless の採用は「アプリのポート設計」まで影響することがある、というのがここでの学びです。
B. userns-remap を有効化して UID 写像を確認する
こちらは Docker デーモンの設定変更です。検証環境で実施してください。
有効化後、既存のコンテナ/ボリュームの扱いが変わる可能性があります。
1) remap 用ユーザーの確認
Docker には dockremap を使う代表例があります。存在確認:
getent passwd dockremap || true
無ければ作成(環境によりパッケージが用意する場合も):
sudo useradd -r -s /usr/sbin/nologin dockremap
サブUID/GID を割り当て:
echo "dockremap:100000:65536" | sudo tee -a /etc/subuid
echo "dockremap:100000:65536" | sudo tee -a /etc/subgid
2) Docker デーモン設定に userns-remap を追加
/etc/docker/daemon.json を編集します。
sudo mkdir -p /etc/docker
sudo sh -lc 'cat > /etc/docker/daemon.json <<EOF
{
"userns-remap": "dockremap"
}
EOF'
Docker 再起動:
sudo systemctl restart docker
sudo systemctl status docker --no-pager
3) 写像が効いているか確認
コンテナを起動し、ホスト上のファイル所有者を確認します。
docker run --rm -d --name ns-test alpine:3.20 sh -lc 'sleep 300'
docker exec -it ns-test sh -lc 'id; touch /tmp/hello; ls -ln /tmp/hello'
次にホスト側でコンテナの実体を探すのは少し面倒ですが、ポイントはここです:
- コンテナ内で
rootが作ったファイルでも - ホスト上では 100000 番台などの別 UID になっていることがある
(これが userns-remap の “被害縮小” の核です)
4) よくある権限問題を再現して理解する(ホストマウント)
例えばホスト側のディレクトリをマウントして書き込み:
mkdir -p /tmp/remap-vol
chmod 755 /tmp/remap-vol
docker run --rm -it -v /tmp/remap-vol:/data alpine:3.20 sh -lc 'touch /data/x && ls -ln /data'
ここで「書けない」「所有権が変」などを体感するはずです。
userns-remap を採用すると、ホストマウントの権限設計が必須になります。
(“今まで動いてたからOK” が通用しなくなる代表例)
まとめ
rootless Docker と userns-remap は、どちらも Dockerホスト侵害リスクを下げる有力な手段です。ただし、目的と副作用が違います。
- rootless Docker:Docker の実行基盤そのものを非root化し、より強く「ホスト root に近づきにくい構造」を作る
- その代わり、ネットワーク/ポート/互換性で制約が出やすい
- userns-remap:既存 Docker の互換性を保ちつつ、コンテナ内 root をホスト上では非特権にして被害を縮小する
- その代わり、ボリューム/ホストマウントの権限や移行の設計が重要になる
どちらを選ぶにせよ、共通して大事なのは「入れたら安全」ではなく、運用の形に落とすことです。
- 低ポートはどう受ける?
- ボリューム権限はどう設計する?
- 障害時に誰がどう切り分ける?
- 移行手順とロールバックは?
“守る” は設定だけでは完結しません。手順と責任分界と設計まで含めて初めて武器になります。
コメントを残す