12k
All articles

Linux マシンにおける Docker のセキュリティ対策

Dockerグループへのアクセスはroot権限を与えます。rootlessモード、sudoルール、Podman、コンテナの強化、監査チェックリストでLinux上のDockerを保護します。

OpenReplay Team
OpenReplay Team
Linux マシンにおける Docker のセキュリティ対策

ユーザーを docker グループに追加すると、そのユーザーはマシン上で root 権限を得ます。「ほぼ root」ではなく、完全な root です。Docker ソケットと通信できる人は誰でも、ホストのファイルシステムをコンテナにマウントして編集できるからです。

sudo の入力を省くために usermod -aG docker $USER を実行した方も多いでしょう。Docker 公式のインストール後ガイドでも、そうするよう案内されています。ただし、同じ Docker Engine の Linux インストール後の手順ページには、このグループへの所属は root と同等であるという警告も記載されています。多くの人が読み飛ばしてしまう囲み欄の中です。Docker の基本をまだ把握しきれていない場合は、Docker イメージとコンテナの入門ガイドを参照してください。本記事では、まずこのリスクをコマンド 1 つで実証します。続いて対策を効果の高い順に解説し、最後に引き継いだマシンを監査するためのチェックリストを紹介します。

重要ポイント

  • docker グループのメンバー、およびそのメンバーとして動作するすべてのプロセスは、docker run --rm -it -v /:/host alpine chroot /host sh でホスト上の root シェルを取得できます。
  • Rootless Docker は dockerd とコンテナの両方をユーザー名前空間内で実行します。そのため、デーモンの侵害やコンテナエスケープが起きても、攻撃者が到達するのは root ではなく非特権のホスト UID です。
  • /usr/bin/docker に限定した sudoers ルールも root 相当である点は変わりません。ただし、Docker の実行前にパスワード入力を求め、すべての使用をログに記録できます。
  • Podman にはデーモンがなく、一般ユーザーとして使う場合はデフォルトでルートレスで動作します。CLI も Docker とほぼ互換性があるため、alias docker=podman で日常的なワークフローの大部分をカバーできます。
  • 引き継いだホストでは、docker グループの所属メンバー、dockerd を所有するユーザー、ソケットのパーミッション、デーモンが TCP ポート 2375 でリッスンしていないかを確認してください。

なぜ Docker グループで root 権限が得られるのか?

docker グループが root 相当である理由は、Docker デーモンが root として動作し、このグループがデーモンの Unix ソケットへの書き込み権限を制御しているからです。ソケットに書き込めるということは、root プロセスにどんな命令でも出せるということです。仕組みを詳しく知りたい場合は、Linux ファイルパーミッションガイドを参照してください。ファイルのグループパーミッションによって、誰がそのファイルを開けるかがどう決まるかを解説しています。Docker について重要なのは、ソケットが docker グループに対して書き込み可能になっているという一点だけです。

以下がその証明です:

docker run --rm -it -v /:/host alpine chroot /host sh

このコマンドは、ホストの / をコンテナにバインドマウントし、そこへ chroot します。これで、実際のホストファイルシステム上で root シェルが手に入ります。あとは /etc/shadow や /etc/sudoers を編集することも、/root に SSH 鍵を追加することも自由です。パスワードもエクスプロイトも必要ありません。あなたのユーザー権限で動くスクリプト、依存パッケージ、侵害されたエディタ拡張機能も、同じことができてしまいます。この点は、Docker のドキュメントの Docker デーモンの攻撃対象領域(Docker daemon attack surface)でも解説されています。

Rootless Docker をセットアップする

Rootless Docker は、dockerd とすべてのコンテナを、あなたのアカウントが所有するユーザー名前空間内で実行するモードです。docker グループ問題を直接解決する手段といえます。

  • ソケットの場所:/var/run/docker.sock ではなく $XDG_RUNTIME_DIR/docker.sock に配置されます。
  • 名前空間の作成:RootlessKit が担当します。
  • UID のマッピング:setuid ヘルパーの newuidmap と newgidmap が、名前空間内の UID 0 をあなた自身の UID にマッピングします。その他のコンテナ UID には、/etc/subuid と /etc/subgid で定義された範囲をマッピングします。
  • ネットワーク:slirp4netns、gvisor-tap-vsock、pasta などのユーザーモードネットワークスタックを経由します。

まず前提パッケージをインストールし、従属 ID(subordinate ID)の範囲を確認します。ルートレスモードのドキュメントでは、/etc/subuid と /etc/subgid の両方で、ユーザーに 65,536 個以上の従属 ID を割り当てるよう求めています。

sudo apt-get install -y uidmap docker-ce-rootless-extras
grep ^$(whoami): /etc/subuid /etc/subgid

次に root デーモンを無効化し、一般ユーザーとしてセットアップツールを実行します:

sudo systemctl disable --now docker.service docker.socket
dockerd-rootless-setuptool.sh install
systemctl --user enable --now docker
sudo loginctl enable-linger $USER
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

enable-linger の行は、ログアウト後もデーモンを動作させ続けるための設定です。Rootless Docker の動作を確認したら、sudo gpasswd -d $USER docker で自分を旧グループから削除してください。その他のディストリビューションについては、公式のルートレスモードのドキュメントを参照してください。セットアップ後にクライアントがデーモンに接続できない場合は、「Docker daemon not running」エラーのトラブルシューティングで、ソケットと systemd の確認手順を解説しています。

ルートレスモードには、非特権ユーザー名前空間が必要です。Ubuntu 24.04 以降では、これがデフォルトで AppArmor によって制限されています。ただし、Ubuntu の apparmor パッケージには rootlesskit 用のプロファイルが同梱されています。そのため、apt で docker-ce-rootless-extras をインストールした場合、追加の手順は不要です。一方、get.docker.com/rootless スクリプトでインストールする場合は、このプロファイルを自分で追加する必要があります。kernel.apparmor_restrict_unprivileged_userns=0 でシステム全体の制限を無効化するのは避けてください。その他のエラーについては、ルートレスモードのトラブルシューティングページを参照してください。

ルートレスモードのデメリットは?

ルートフルデーモンと比べた場合の主なトレードオフは、以下のとおりです:

制限事項回避策
デフォルトでは 1024 未満のポートをバインドできないsysctl net.ipv4.ip_unprivileged_port_start=80 を設定するか、リバースプロキシを使用する
--privileged を指定しても実際のホスト権限は付与されないユーザー名前空間内のケーパビリティのみが適用される
ユーザーモードネットワークはブリッジドライバより低速実験的な lxc-user-nic ドライバを使用するか、本番投入前にベンチマークを実施する
5.11 より前のカーネルでは fuse-overlayfs が必要カーネル 5.11 以降はネイティブの overlay2 をサポート

トラブルシューティングページには、ほかにも AppArmor 非対応、チェックポイント非対応、オーバーレイネットワーク非対応といった制限が記載されています。

限定したルールで sudo を使用する

ルートレスモードでワークロードが動かない場合の折衷案が、docker グループへの所属をやめ、Docker の実行に sudo を必須とする方法です。ただし、root 相当であることに変わりはありません。sudo docker でも、先ほどの chroot コマンドを実行できるからです。

この方法で得られるのは、呼び出しごとのパスワード入力と監査ログの記録です。また、あなたのユーザー権限で密かに動くコードからは、ソケットを利用できなくなります。

sudo gpasswd -d alice docker
sudo visudo -f /etc/sudoers.d/docker
alice ALL=(root) /usr/bin/docker

このルールで許可されるのは Docker バイナリのみで、任意のコマンドは実行できません。使用状況は journalctl _COMM=sudo で確認できます。インストール後の手順ドキュメントでは、newgrp を使う方式も紹介されています。この方式では、CLI を自分のユーザーとして実行したまま、Docker の前にグループパスワードを設定できます。

Podman に切り替える

Podman にはデーモンがなく、一般ユーザーとして実行する場合はデフォルトでルートレスです。コンテナはあなたのユーザーの子プロセスとして、ユーザー名前空間内で動作します。UID のマッピングには、Rootless Docker と同じ /etc/subuid を使用します。CLI も Docker とほぼ同じ体系のため、ほとんどのコマンドはそのまま使えます。

sudo apt-get install -y podman
alias docker=podman
podman run --rm -p 8080:80 docker.io/library/nginx

違いが出るのは周辺部分です。具体的には、完全修飾イメージ名の扱い、Compose のサポート、Docker ソケットを前提とするツールなどが挙げられます。チーム全体で切り替える前に、スクリプトをテストしてください。

コンテナ自体を堅牢化する

どのデーモンを使う場合でも、コンテナ内のプロセスができることは制限しておきましょう。リスクの大部分は、次の 4 つの設定でカバーできます:

  • 非 root の USER で実行する
  • アプリに必要なもの以外のケーパビリティを付与しない
  • ルートファイルシステムを読み取り専用にする
  • setuid バイナリによる権限昇格を禁止する
FROM node:22-alpine
WORKDIR /app
COPY --chown=node:node . .
USER node
CMD ["node", "server.js"]
docker run -d \
  --cap-drop=ALL \
  --read-only --tmpfs /tmp \
  --security-opt no-new-privileges \
  -p 8080:8080 myapp

各設定の役割は次のとおりです:

  • Dockerfile の USER 命令は、実行時の UID を設定します。
  • --cap-drop=ALL は、すべてのケーパビリティを削除します。アプリがどうしても必要とするものは、--cap-add で個別に追加します。この例のアプリはポート 8080 でリッスンするため、ケーパビリティは一切不要です。
  • --read-only と --tmpfs を組み合わせると、書き込み可能なのは /tmp だけになります。
  • no-new-privileges は、setuid バイナリによる権限昇格を防ぎます。

これらのオプションはすべて、docker run リファレンスに記載されています。

引き継いだマシンを監査する

他の人がセットアップした VPS では、次の 4 点を確認します:

  1. docker グループに誰が所属しているか
  2. どのユーザーが dockerd を実行しているか
  3. 誰がソケットに書き込めるか
  4. デーモンがネットワーク上でリッスンしていないか
getent group docker
ps -eo user,cmd | grep [d]ockerd
ls -l /var/run/docker.sock
sudo ss -ltnp | grep -E ':2375|:2376'
systemctl cat docker | grep -- '-H'
grep -s hosts /etc/docker/daemon.json

結果の読み方は次のとおりです:

  • グループメンバー:getent が出力するユーザーは、全員が root アクセス権を持っています。
  • デーモンの実行ユーザー:ps に root と表示される場合、デーモンはルートフルです。
  • ソケットのパーミッション:正しくは srw-rw---- root docker です。全ユーザーに書き込み権限がある場合は、ただちに修正してください。
  • ポート 2375:ここにリスナーがある場合、Docker API が TLS なしで公開されています。現行の Docker Engine はこの構成では起動しませんが、引き継いだホストの古いエンジンでは、まだ動作している可能性があります。このポートに到達できる人は誰でも root を取得できます。
  • ポート 2376:TLS リスナーはより安全ですが、クライアント鍵を持つ人は依然として root を取得できます。

デーモンソケットの保護ガイドに従って、SSH または相互 TLS に切り替えてください。

また、ソケットをマウントしているコンテナがないかも確認しましょう。そうしたコンテナは、それぞれが root へのハンドルになります:

docker ps -q | xargs -r docker inspect \
  --format '{{.Name}} {{range .Mounts}}{{.Source}} {{end}}' | grep docker.sock

Docker ソケットへのアクセスは、root アクセスそのものです。例外はありません。まずは、使用している各マシンで getent group docker を実行してください。そのうえで、該当するユーザーを Rootless Docker または Podman に移行するか、ソケットの利用をログ記録付きの sudo ルール経由に限定しましょう。

よくある質問

Rootless Docker と userns-remap の違いは何ですか?

userns-remap では、dockerd は引き続き root として動作し、コンテナだけが再マッピングされた従属 UID で実行されます。たとえば、/etc/docker/daemon.json で userns-remap を 'default' に設定すると、dockremap ユーザーが作成され、この UID が使われます。ただし、これでは docker グループの問題は解決しません。グループメンバーは --userns=host を指定するだけで、特定のコンテナの再マッピングを無効化し、chroot によるエスケープを実行できるからです。一方、ルートレスモードでは、デーモン自体があなたのユーザーとして動作します。

ルートレスとルートフルの Docker を同じマシンで併用できますか?

はい、できます。2 つのデーモンは別々のソケットを使用します。ルートフルは /var/run/docker.sock、ルートレスは $XDG_RUNTIME_DIR/docker.sock です。dockerd-rootless-setuptool.sh install を実行すると、'rootless' という名前の CLI コンテキストが作成され、自動的に切り替わります。root デーモンを操作するには docker context use default を、ルートレスに戻すには docker context use rootless を使用します。なお、ルートフルデーモンが動作している間は、そのソケットに書き込める人は依然として root 権限を持っています。

既存のイメージやコンテナは Rootless Docker に引き継がれますか?

いいえ、引き継がれません。ルートレスデーモンは独自のデータルート(デフォルトは ~/.local/share/docker)を使用し、root デーモンの /var/lib/docker とは別に管理されます。そのため、ルートフル Docker で作成したイメージ、コンテナ、ボリュームは、ルートレスデーモンからは見えません。イメージを再度 pull するか、ルートフルデーモンで docker save を使ってエクスポートし、ルートレスデーモンで docker load を使ってインポートしてください。/var/lib/docker のみを対象としたバックアップジョブがあれば、あわせて更新が必要です。

ルートレスコンテナが書き込んだファイルの所有者が、ホスト上で想定外になるのはなぜですか?

Rootless Docker は、コンテナの UID をホストの UID にマッピングするためです。コンテナ内の root はあなた自身の UID にマッピングされるので、root がバインドマウントに書き込んだファイルは、あなたの所有として表示されます。それ以外のコンテナ UID は、すべて /etc/subuid の範囲にマッピングされます。たとえば範囲が 100000 から始まる場合、コンテナ UID 1000 はホスト UID 100999 になり、あなたのユーザーでは直接編集できません。この場合は、コンテナ内で所有者を 0:0 に chown すると、ホスト上の所有権があなたのユーザーに戻ります。

DevTools for the frontend

Gain Debugging Superpowers

Unleash the power of session replay to reproduce bugs, track slowdowns and uncover frustrations in your app. Get complete visibility into your frontend with OpenReplay — the most advanced open-source session replay tool for developers.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.