LinuxでRootless Dockerを導入する:rootなしでコンテナを実行してサーバーを守る

Security tutorial - IT technology blog
Security tutorial - IT technology blog

コンテナが脱出したときに起きること

ある朝サーバーを確認すると一見正常に動いているように見えたが、内部では誰かがリソースを使って仮想通貨をマイニングしていた。詳しく調査すると、攻撃者がDockerコンテナの内側から権限昇格を行い、Docker daemonがrootで動いていたというだけの理由でサーバー全体を乗っ取っていたことが判明した。

これは架空の話ではない。デフォルト設定でDockerを動かしている多くのサーバーで実際に起きているのを見てきた — 厄介なのは、このリスクが潜んでいることをほとんどのsysadminが気づいていない点だ。

Dockerのデフォルト設定がなぜそれほど危険なのか

公式ガイドに従ってDockerをインストールすると、dockerd(Docker daemon)はrootで起動し、/var/run/docker.sockソケットを監視する。このソケットに接続できるプロセスは何でもできる:システムディレクトリをコンテナにマウントする、privilegedコンテナを実行する、さらにはホストシステム上の任意のファイルを読み書きすることさえ可能だ。

典型的な攻撃の流れはこうなる:

  1. 攻撃者がコンテナ内で動いているアプリケーションの脆弱性を悪用する
  2. コンテナの内側から、bind-mountされていればDockerソケットにアクセスする
  3. Docker APIを使って新しいコンテナを作成し、ホストの/をマウントする
  4. ホストファイルシステムにchrootする — サーバー全体が掌握される

「socketをコンテナにbind-mountしなければ安全だ」と思っている人は多い。リスクは下がる、確かに。しかし他のベクターも存在する:コンテナエスケープ、runcの脆弱性(CVE-2019-5736は特殊なバイナリを実行するだけでコンテナから脱出できた)、namespaceのブレイクアウト。コンテナが脱出すると、ホスト上のrootのnamespaceに落ちてしまう — ゲームオーバーだ。

よく見かける3つの対処法 — そしてなぜ不十分なのか

方法1:dockerグループにユーザーを追加する

sudo usermod -aG docker youruser

一見問題なさそうだが、dockerグループのメンバーはDocker daemonができることを何でもできる — つまりホスト上のrootと同等だ。Docker公式ドキュメントはこれを「docker group grants privileges equivalent to the root user」と明言している。作業のしやすさのためにdeveloperをdockerグループに追加することは、実質的にroot権限を与えることに等しい。

方法2:コマンドのwhitelistでsudoを使う

# /etc/sudoers.d/docker-restricted
developer ALL=(root) NOPASSWD: /usr/bin/docker run *

維持が複雑で、argument injectionでバイパスされやすく、根本的な解決にならない — daemonは依然としてrootで動いているからだ。

方法3:AppArmor/SELinuxでコンテナを制限する

有効だが、あくまで追加レイヤーに過ぎない — daemonがrootで動くという事実は変わらない。daemonが侵害されると、プロファイルはコンテナを守るがdaemon自体は守れない。

Rootless Docker:特権を根本から断ち切る

Docker 20.10以降、Rootlessモードはstable機能となった — daemonもコンテナも通常のuser namespaceで実行でき、どの段階でもroot権限は不要だ。たとえ攻撃者がコンテナから脱出しても、ホスト上でそのユーザーの権限しか持てない — システムのrootには昇格できない。

インストール前の要件

  • Linux kernel 5.11+(または4.18+で追加設定が必要)
  • uidmapパッケージがインストール済み(newuidmap、newgidmap)
  • ユーザーが/etc/subuidと/etc/subgidにエントリを持っていること
  • sudoなしで実行できる — これが核心だ

Ubuntu/Debianへのインストール

# uidmapをインストール
sudo apt-get install -y uidmap

# subuid/subgidのエントリを確認
grep $(whoami) /etc/subuid /etc/subgid

# エントリがなければ追加する:
echo "$(whoami):100000:65536" | sudo tee -a /etc/subuid
echo "$(whoami):100000:65536" | sudo tee -a /etc/subgid

# Rootless Dockerのセットアップスクリプトを実行
dockerd-rootless-setuptool.sh install
# またはconvenience scriptを使う:
curl -fsSL https://get.docker.com/rootless | sh

環境変数の設定

# ~/.bashrcまたは~/.zshrcに追加
export PATH=/home/$USER/bin:$PATH
export DOCKER_HOST=unix:///run/user/$(id -u)/docker.sock

source ~/.bashrc

systemdユーザーでdaemonを起動する

# ユーザーレベルでサービスを有効化して起動 — sudoは不要
systemctl --user enable docker
systemctl --user start docker

# ユーザーログアウト後もdaemonが動くようlingerを有効化
sudo loginctl enable-linger $(whoami)

# 状態を確認
systemctl --user status docker

本当にrootlessかどうか確認する

# コンテナを実行して内部のUIDを確認
docker run --rm alpine id
# 出力: uid=0(root) gid=0(root) — rootのように見えるがこれはUID namespace

# ホスト上のプロセスの実際のUIDを確認
docker run -d --name test nginx
ps aux | grep nginx | head -3
# nginxのPIDはホスト上であなたのUIDで動く、0ではない

ここが面白いところだ:コンテナの内側では、プロセスは自分がroot(uid=0)だと認識するが、ホスト側では自分のUIDで動いている — user namespaceが両者の間でUIDを変換するブリッジとして機能する。コンテナはroot権限を持っているつもりだが、ホストシステムには何も触れない。

Rootless Dockerの日常的なワークフロー

# 通常のwebサーバーを起動
docker run -d -p 8080:80 --name webserver nginx

# Docker Composeも同じように動く
docker compose up -d

# imageをbuild
docker build -t myapp:latest .

# logsを確認
docker logs webserver

日常的なワークフローのほとんどは変わらない — build、run、compose、logsは以前と同じように使える。

あらかじめ知っておくべき制限

Rootless Dockerにもトレードオフがある — いくつかの機能は使えなかったり、workaroundが必要だったりする:

  • 1024未満のポートのbind:通常のユーザーは1024未満のポートをbindできない。sysctlを使うか、高いポートにマップしてreverse proxyを使う必要がある。
  • Overlay filesystem:kernel 5.11+またはFUSE-overlayfsが必要。古いkernelではVFSドライバーを使うことになり、write-heavyなワークロードではoverlay2と比べて2〜3倍遅い。
  • Cgroups v1:リソース制限(memory/CPU)にはcgroups v2が必要。Ubuntu 22.04+はデフォルトでv2を使っているので心配不要。
  • ネットワークモードhost:--net=hostは実際のホストネットワークnamespaceを使わない — コンテナはあなたのuser network namespaceしか使えない。
# ポート80/443のworkaround:ユーザーが低いポートをbindできるようにする
echo "net.ipv4.ip_unprivileged_port_start=80" | sudo tee /etc/sysctl.d/99-rootless.conf
sudo sysctl -p /etc/sysctl.d/99-rootless.conf

# または高いポートを使いCaddy/nginxをreverse proxyとして使う
docker run -d -p 8080:80 nginx
# ホスト上のCaddy/nginxが :80 → localhost:8080 にプロキシ

全体的なセキュリティとの組み合わせ

Rootless Dockerは大きな穴をふさいでくれるが、サーバーセキュリティはそこで終わりではない。それと並行して、認証情報も見直す — パスワードが十分に強く、使い回しがないことを確認する。サーバー用のパスワードはtoolcraft.app/ja/tools/security/password-generatorのパスワードジェネレーターを使って生成している — 100%ブラウザ上で動作し、ネットワーク経由で漏洩する心配がない。SSH鍵認証と組み合わせれば、認証周りは十分に堅固だ。

新しいサーバーでは、Rootless Dockerをオプションではなくデフォルトの設定として扱っている — セットアップに10分ほど余分にかかるが、デフォルトインストールでは見落とされるリスクの層をひとつ塞いでくれる。共有サーバー上の開発チームにはさらに有効だ:それぞれが独自のdaemonを持ち、あるユーザーのコンテナが他のユーザーに影響を与えない。そして誰もホストrootへの近道を持たない — 意図的であれ偶発的であれ。

Share: