Docker 入門

Docker コンテナセキュリティ:ユーザーとグループの管理

Dockerコンテナのようなアイソレーション(隔離)された環境であっても、高い特権(Elevated Privileges)でコンテナプロセスを実行することは、セキュリティ上の重大なリスクとなります。通常、ホストマシン上でプロダクション環境のアプリケーションを直接 root ユーザーとして実行しないのと同様に、コンテナ内部でも決してそのようなことを行うべきではありません。

コンテナ内で適切なユーザーとグループの管理を実装することで、アプリケーションが最小権限の原則に従うことを保証し、脆弱性がエクスプロイト(悪用)された際の潜在的な「ブラストラジアス(影響範囲)」を最小限に抑えることができます。このアプローチは、セキュアでレジリエンスの高いコンテナ化アプリケーションを構築するための基盤となります。非rootユーザーとグループを慎重に定義して使用することで、潜在的な攻撃に対する極めて重要な防御層を追加し、Dockerのデプロイメントをより強固なものにします。本章では、この中核となる知識について深く掘り下げていきます。

1. コンテナにおける最小権限の原則

最小権限の原則(PoLP, Principle of Least Privilege)は、いかなるユーザー、プログラム、またはプロセスも、その機能を実行するために必要な最小限の特権のみを持つべきであると規定しています。Dockerコンテナのコンテキストにおいては、これは絶対に必要でない限り、コンテナ内で実行されるプロセスを root ユーザーとして実行すべきではないことを意味します。

1.1 なぜRoot権限での実行を避けるべきなのか?

Dockerは強力なアイソレーションメカニズムを提供していますが、コンテナ内で root としてプロセスを実行することは、依然として以下のセキュリティリスクをもたらします。

  • コンテナエスケープの脆弱性 (Container Escape Vulnerabilities): 稀ではあるものの、Dockerデーモン、コンテナランタイム、またはLinuxカーネル自体の脆弱性により、コンテナ内で root として実行されているプロセスがコンテナの境界を突破し、ホストマシンシステム上の root 権限を取得する可能性があります。これは通常、最も深刻なタイプのコンテナ脆弱性です。
  • 不要な特権 (Unnecessary Privileges): 大半のアプリケーションは、実行に root 権限を全く必要としません。root として実行すると、アプリケーションにシステムレベルの操作、センシティブなファイル、およびネットワークコンフィギュレーションへのアクセス権が付与されますが、これらは本来必要のないものです。攻撃者が root 権限を持つプロセスをコントロールした場合、実行可能な破壊行為ははるかに広範囲に及びます。
  • ファイルシステムパーミッション (File System Permissions): コンテナが root として実行されている場合、コンテナ内のアプリケーションによって作成または変更されたファイルは、通常 root が所有します。後でこれらのファイルをコンテナ外にコピーしたり、データボリューム(Volumes)としてホストマシンにマウントしたりした場合、それらは root の所有権を維持する可能性があり、ホストマシン上でパーミッションの問題やセキュリティ上の懸念を引き起こします。
  • サプライチェーン攻撃 (Supply Chain Attacks): 悪意のあるパッケージやデペンデンシー(依存関係)がコンテナに導入され、アプリケーションが root として実行されている場合、その悪意のあるコードはそれらの特権を利用して、バックドアのインストールやコンテナからのセンシティブなデータの窃取など、甚大な被害をもたらす可能性があります。

1.2 UIDとGIDの理解

ユーザーID (UID) とグループID (GID) は、Linuxシステム(Dockerコンテナを含む)の基礎です。各ユーザーアカウントには一意のUIDがあり、各グループには一意のGIDがあります。カーネルはこれらのIDを使用して、ファイル、ディレクトリ、プロセスのアクセス権限を決定します。

  • コンテナ内部: Dockerコンテナ内でユーザーまたはグループを作成すると、そのコンテナのファイルシステム内でUIDとGIDが割り当てられます。例えば、root のUIDは通常 0 ですが、非rootユーザーのUIDは通常 1000 または 1001 から始まります(ベースイメージによって異なります)。
  • ホストマシン上: ホストマシンシステムにも独自のUIDとGIDのセットがあります。デフォルトでは、ホストマシン上でコンテナのプロセスをチェックした場合(例:ps aux の使用)、コンテナ内でUID 1000で実行されているプロセスは、ホストマシン上でもUID 1000として表示されます。コンテナのUID 1000がたまたまホストマシン上の特権ユーザーに対応している場合、これがセキュリティリスクになる可能性があります。
  • 高度な概念: ユーザーネームスペースのリマップ(User Namespace Remapping)は本章の範囲外ですが、DockerはコンテナのUID/GIDをホストマシン上の異なるUID/GIDにリマップするように構成できることを知っておく必要があります。これは、コンテナ内のUID 0 (root) をホストマシン上の番号の大きい非特権UIDにマッピングし、セキュリティをさらに強化できることを意味します。本章のコンテナユーザー管理では、コンテナ内部でUID/GIDをどのように管理するかに焦点を当てます。

2. Dockerfileでのユーザーとグループの作成と管理

ユーザーとグループの管理を実装する最も効果的な方法は、それらをDockerfile内で直接定義することです。これにより、イメージからビルドされたすべてのコンテナが、デフォルトで指定された非rootユーザーとして実行されることが保証されます。

2.1 RUNインストラクションを使用したユーザーとグループの作成

Dockerfileの RUN インストラクション内で標準のLinuxコマンド(groupadduseradd など)を使用して、新しいユーザーとグループを作成できます。具体的なコマンドは、使用するベースイメージ(Debian/Ubuntu vs. Alpineなど)によって若干異なる場合があります。

Debian/Ubuntuベースイメージの例 (例: ubuntu, debian, python:3.9-slim-buster):

# ログイン権限のない 'appgroup' という名前のシステムグループを作成する
RUN groupadd --system appgroup

# 'appuser' という名前のシステムユーザーを作成する
# -r, --system: システムアカウントを作成する(ホームディレクトリなし、シェルなしなど)
# -s, --shell: ユーザーのログインシェルを設定する(例: /sbin/nologin はシェルなしを示す)
# -g, --gid: ユーザーのプライマリグループを設定する
RUN useradd --system --uid 1001 --gid appgroup appuser
  • groupadd --system appgroup: appgroup という名前のシステムグループを作成します。システムグループのGIDは通常1000未満です。コンテナのユーザー/グループに --system を使用することは、完全なインタラクティブログイン機能を通常必要としないため、優れたプラクティスです。
  • useradd --system --uid 1001 --gid appgroup appuser: appuser という名前のシステムユーザーを作成します。
    • --uid 1001: ユーザーIDを明示的に1001に設定します。異なるイメージ間で一貫したUIDを維持することは、特にホストマシンシステムでバインドマウント(bind mounts)を使用する予定がある場合に非常に有益です。
    • --gid appgroup: appgroupappuser のプライマリグループとして割り当てます。

Alpineベースイメージの例 (例: alpine, node:alpine):

Alpine Linuxでは addgroupadduser を使用し、構文がわずかに異なります。

# 'appgroup' という名前のグループを作成する
RUN addgroup -S appgroup

# 'appuser' という名前のユーザーを作成し、'appgroup' に割り当てる
# -S: システムユーザーを作成する
# -G: ユーザーが所属するセカンダリグループを指定する
# -u: ユーザーIDを明示的に設定する
RUN adduser -S -u 1001 -G appgroup appuser

2.2 USERインストラクション

非rootユーザーを作成した後、Dockerfileの USER インストラクションを使用して、後続のコマンド(RUN, CMD, ENTRYPOINT など)をどのユーザーが実行するか、およびコンテナのメインプロセスをデフォルトでどのユーザーが実行するかを指定する必要があります。

# ... (ユーザー作成などの先行するインストラクション) ...

# 非rootユーザーに切り替える
USER appuser

# 以降のすべての RUN、CMD、または ENTRYPOINT コマンドは 'appuser' として実行される

USER を指定しない場合、Dockerはデフォルトで root ユーザーを使用します。USER インストラクションは、ユーザー名、UID、ユーザー名:グループ名 の組み合わせ、または UID:GID の組み合わせを受け入れることができます。

2.3 ファイルおよびディレクトリパーミッションの設定

非rootユーザーを作成した後、アプリケーションファイルとディレクトリがそのユーザーに所有されていることを確認することが極めて重要です。これにより、非rootユーザーが自身のアプリケーションデータを読み書きするために root 権限を必要とする状況を防ぎます。

通常、ファイルとディレクトリの所有権を変更するには chown(所有者の変更)を使用します。このコマンドは、Dockerfile内で USER を切り替える前に実行する必要があります。なぜなら、この操作には root 権限が必要だからです。

# ... (ユーザーとグループの作成) ...

# アプリケーションファイルをコピーする(WORKDIR が root によって所有されている場合、書き込みに root 権限が必要になる)
WORKDIR /app
COPY app.py requirements.txt ./

# アプリケーションディレクトリの所有権を非rootユーザーとグループに変更する
# これにより、'appuser' が '/app' に対する完全な制御権を持つことが保証される
RUN chown -R appuser:appgroup /app

# ここで非rootユーザーに切り替える
USER appuser

# ... (Dockerfileの残りの部分) ...

WORKDIRに関する重要な注意事項:

       WORKDIRUSER インストラクションの前に定義されている場合、それは高い確率で root に所有されています。その後非rootユーザーに切り替えた際、chown を使用して明示的に所有権を変更しない限り、そのユーザーは WORKDIR に対する書き込み権限を持たない可能性があります。ベストプラクティスは、アプリケーションの WORKDIR およびアプリケーションがデータを書き込む必要があるすべてのディレクトリに対して、常に chown 操作を実行することです。

3. 実践デモンストレーションとケーススタディ

これらの概念を説明するために、シンプルなPython Flaskアプリケーションを使用してみましょう。

3.1 シナリオ1:Root権限でのPython Flaskアプリの実行(アンチパターン)

まず、行うべきではない方法を見てみましょう。

app.py:

from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    # 現在のユーザー権限をデモンストレーションするため、パブリックなロケーションへのファイル書き込みを試行する
    try:
        with open("/tmp/container_user.txt", "w") as f:
            f.write(f"Hello from user: {os.getuid()} (root)" )
        file_status = "ファイルが /tmp/container_user.txt に書き込まれました"
    except Exception as e:
        file_status = f"/tmp に書き込めません: {e}"

    return f"Hello from Flask! 実行中の UID: {os.getuid()}。{file_status}"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

requirements.txt:

Flask

Dockerfile.root:

# Pythonのベースイメージから開始
FROM python:3.9-slim-buster

# コンテナ内の作業ディレクトリを設定
WORKDIR /app

# requirementsをコピーしてインストールする
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# アプリケーションファイルをコピーする
COPY app.py .

# アプリケーションが実行されるポートをエクスポーズ(公開)する
EXPOSE 5000

# アプリケーションを実行する(デフォルトではrootとして実行される)
CMD ["python", "app.py"]

ビルドして(rootとして)実行します:

docker build -t flask-root-app -f Dockerfile.root .
docker run -d -p 5000:5000 --name my-root-app flask-root-app

ブラウザで http://localhost:5000 にアクセスします。「実行中の UID: 0」と表示され、アプリケーションがroot権限で実行されていることがわかります。プロセスをチェックすることもできます:

docker exec my-root-app ps aux
# 'python app.py' を探し、'USER' 列を観察します。'root' となっているはずです。

3.2 シナリオ2:専用の非Rootユーザーを使用したアプリの実行(推奨プラクティス)

次に、アプリケーションを非rootユーザーとして実行することでセキュリティを確保しましょう。

Dockerfile.nonroot:

# Pythonのベースイメージから開始
FROM python:3.9-slim-buster

# コンテナ内の作業ディレクトリを設定。初期状態ではrootによって所有される。
WORKDIR /app

# requirementsをコピーしてインストール(これらのビルドステップは引き続きrootとして実行される)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# アプリケーションファイルをコピーする
COPY app.py .

# --- 開始:ユーザーとグループの管理 ---
# 1. 'appgroup' という名前のシステムグループを作成する
RUN groupadd --system appgroup

# 2. 'appuser' という名前のシステムユーザーを作成し、UIDを1001、プライマリグループを 'appgroup' に指定する
#    特定のUIDを使用することは、後でホストの権限(ボリュームなど)との一貫性を保つのに役立つ
RUN useradd --system --uid 1001 --gid appgroup appuser

# 3. アプリケーションディレクトリの所有権を新しい非rootユーザーとグループに変更する
#    'appuser' が自身のアプリケーションファイルを読み書きできるようにするため、これは極めて重要。
#    'chown' はroot権限を必要とするため、このステップはUSERを切り替える *前に* 完了しなければならない。
RUN chown -R appuser:appgroup /app

# 4. 非rootユーザーに切り替える。後続のすべてのコマンドは 'appuser' として実行される。
USER appuser
# --- 終了:ユーザーとグループの管理 ---

# アプリケーションが実行されるポートをエクスポーズ(公開)する
EXPOSE 5000

# 'appuser' としてアプリケーションを実行する
CMD ["python", "app.py"]

ビルドして(非rootとして)実行します:

docker build -t flask-nonroot-app -f Dockerfile.nonroot .
docker run -d -p 5001:5000 --name my-nonroot-app flask-nonroot-app

http://localhost:5001 にアクセスします。今度は「実行中の UID: 1001」と表示されるはずです。これにより、アプリケーションが専用のappuserで実行されていることが確認できます。プロセスをチェックします:

docker exec my-nonroot-app ps aux
# 'python app.py' を探します。'USER' 列は 'appuser' になっているはずです。

これは、Dockerfile内で専用のユーザーを効果的に作成し、そのユーザーに切り替えることで、アプリケーションのセキュリティ保護を向上させる方法をデモンストレーションしています。

3.3 シナリオ3:ランタイムでの docker run -u を使用したユーザーのオーバーライド

場合によっては、Dockerfileで明示的に設定されていない特定のユーザーとしてコンテナを実行したり、USER インストラクションを一時的にオーバーライドしたりする必要があるかもしれません。docker run -u(または --user)フラグを使用すると、これが可能になります。

以下を指定できます:

  • --user <ユーザー名>: ユーザーがコンテナイメージ内に存在する必要があります。
  • --user <uid>: コンテナは指定されたUIDで実行を試みます。そのUIDに対応する名前付きユーザーがコンテナ内に存在しない場合、そのUIDを持つ名前なしユーザーとして実行されます。
  • --user <ユーザー名>:<グループ名>
  • --user <uid>:<gid>

シナリオ1で使用した、デフォルトでrootとして実行される flask-root-app イメージを使用してみましょう。ランタイムでカスタムのUID/GIDを指定して実行を試みます。

# rootベースのFlaskアプリを実行するが、ランタイムでUIDとGIDを指定する
docker run -d -p 5002:5000 --name my-runtime-user-app --user 1002:1002 flask-root-app

次に、http://localhost:5002 にアクセスします。「実行中の UID: 1002」と表示されるはずです。

docker run -u の制限事項:

  • ユーザーの存在: コンテナイメージ内に存在しないユーザー名を指定した場合(例: docker run --user nonexistantuser)、システムが必要なユーザー設定を見つけられないため、コンテナの起動が失敗するか、権限エラーが発生する可能性が高くなります。
  • パーミッションの問題: 指定されたUID/GIDは、コンテナ内のアプリケーションファイルおよびディレクトリに対して適切なパーミッションを持っている必要があります。イメージがroot権限を持つ前提でビルドされていた場合(flask-root-app のように)、ランタイムで -u を通じてユーザーを変更するだけでは、新しく指定されたユーザーがそれらのファイルを所有していないため、重要なファイルで「アクセス拒否 (permission denied)」エラーが発生する可能性があります。たとえば、flask-root-app が(root所有の)/var/log に書き込もうとした場合、UID 1002は拒否されます。
  • 複雑さ: テストや特定のシナリオには役立ちますが、ユーザー管理をDockerfileに直接組み込む場合と比較して、常に docker run -u に依存することは、デプロイメントの予測可能性を低下させ、管理を困難にします。

ベストプラクティスのまとめ: Dockerfile内で RUN groupaddRUN useraddRUN chown、および USER を使用して、目的のユーザーとグループを定義してください。docker run -u の使用は最小限に抑え、主にデバッグや特定のユーザーを一時的にオーバーライドする必要がある高度なシナリオでのみ利用することを推奨します。