はじめに
コンテナはアプリケーションをパッケージ化して実行しますが、パッケージ化だけでは安全なプロセス ID は選ばれません。用意された DeskLite のワーカーは、小さなチケットレポートを生成し、現在は root として動作しています。
このラボでは、最低限の Docker ワークフローを学んだ後、root 以外の ID と、作業に必要なファイルアクセスだけを使って、ワーカーを再ビルドして実行します。Docker と固定されたベースイメージは用意されています。レジストリアカウントや Docker の事前経験は不要です。
用意されたコンテナプロジェクトを知る
このステップでは、レポート作成の処理を構成するファイルを確認します。
イメージは、ファイルシステムと起動設定をまとめたものです。コンテナは、独自のプロセスと書き込み可能なファイルシステム層を持つ、イメージの実行インスタンスです。Dockerfile は、イメージのビルド方法を記述します。ticket-report:practice のようなタグは、イメージに付ける便利な名前です。再ビルド後は、新しいイメージを指すように変更できます。
cd /home/labex/project/ticket-worker
ls
Dockerfile、worker.sh、input、output があるはずです。用意されたワーカーは CSV の行を読み取り、その件数をレポートに書き込んだ後、確認できるように実行し続けます。
cat input/tickets.csv
ヘッダーの後に、3 行のチケットがあります。期待するレポートは tickets=3 です。
cat Dockerfile
FROM はキャッシュ済みのベースイメージを、WORKDIR はイメージの作業ディレクトリを選択し、COPY はワーカーを追加します。USER root はプロセスの ID を選択します。CMD は、リストで表したデフォルトの起動コマンドです。変更するのは USER だけです。
ベースタグ desklite-base:1.37.0 は、固定された BusyBox イメージからすでにキャッシュされています。その後のビルドと実行は、すべてローカルファイルを使います。Docker コマンドは、このラボのデーモンにアクセスするために sudo を使います。この宿主側のアクセスは強い権限を持ち、コンテナ内部で権限を減らすアプリケーションの ID とは別のものです。
既存のワーカーをビルドして実行する
このステップでは、動作しているものの権限が過剰な初期状態を確認します。
docker build は、Dockerfile とビルドコンテキストを読み取ります。ビルドコンテキストは、COPY で利用できるファイルがあるディレクトリです。ここでの . は現在のディレクトリを意味します。-t はローカルイメージのタグを付けます。
sudo docker build -t ticket-report:practice .
ビルドが成功するはずです。イメージは存在しますが、ワーカーはまだ実行されていません。
docker run はコンテナを作成します。-d はバックグラウンドで実行し、--name は再利用できる名前を付けます。このレポートが必要とするのはファイルだけなので、--network none でネットワークを無効にします。
バインドマウントは、宿主のパスをコンテナ内のパスで利用できるようにします。それぞれの --mount に、種類、ソース(src)、宛先(dst)を記述します。この初期状態には readonly オプションがないため、両方のディレクトリが書き込み可能として公開されます。入力は後で制限します。
sudo docker run -d --name ticket-worker --network none --mount type=bind,src=/home/labex/project/ticket-worker/input,dst=/data/input --mount type=bind,src=/home/labex/project/ticket-worker/output,dst=/data/output ticket-report:practice
Docker はコンテナ ID を表示します。これはワーカーのユーザー ID ではありません。元のワーカーがレポートを生成するはずです。次に、それを確認します。
実際の実行時 ID を確認する
このステップでは、レポートの成功と、最小権限での実行状態を区別します。
docker ps は実行中のコンテナを一覧表示します。
sudo docker ps
ticket-worker を探してください。docker logs はアプリケーションの標準出力を読み取ります。
sudo docker logs ticket-worker
worker uid=0 と tickets=3 が表示されるはずです。UID 0 は root です。業務上の処理は動作していますが、必要以上の権限を使っています。
docker exec は、この実行中のコンテナ内で確認用コマンドを実行します。id -u は、実効ユーザー ID を数値で表示します。
sudo docker exec ticket-worker id -u
再び 0 が表示されるはずです。docker inspect は設定を読み取ります。--format は 1 つのフィールドを選択します。引用符で囲まれたテンプレートは、正確にコピーしてください。.Config.User は設定されたユーザーです。
sudo docker inspect --format '{{.Config.User}}' ticket-worker
root が表示されるはずです。イメージの設定と実際のプロセスの確認結果が一致しています。ファイルに「non-root」と書いてあるだけでは、この証拠にはなりません。
用意された非 root ユーザーで再ビルドする
このステップでは、イメージのデフォルトのアプリケーション ID を設定します。
キャッシュ済みのベースには、UID 65534 の nobody がすでに含まれています。ユーザーの作成やパスワードは不要です。Dockerfile を開きます。
nano Dockerfile
USER 命令だけを変更します。ファイル全体は次のようになるはずです。
FROM desklite-base:1.37.0
WORKDIR /app
COPY worker.sh /app/worker.sh
USER nobody
CMD ["sh", "/app/worker.sh"]
Ctrl+O、Enter で保存し、Ctrl+X で終了します。
sudo docker build -t ticket-report:practice .
タグは、再ビルドされたイメージを指すようになりました。既存のコンテナは、再ビルドされたイメージに自動的には切り替わりません。古い root コンテナは、まだ古いインスタンスです。次のステップでは、それを置き換え、新しい ID に必要な出力アクセスを与えます。
マウントを制限してコンテナを置き換える
このステップでは、非 root の ID と、動作に必要なファイル権限を組み合わせます。
docker rm はコンテナを削除します。-f は、実行中であれば先に停止します。この演習の名前付きコンテナだけを削除します。
sudo docker rm -f ticket-worker
レポートディレクトリは、初期状態では root が所有していました。ファイル権限のラボと同様に、非 root のプロセスが出力を書き込むには、所有権と権限が必要です。chown -R は、ディレクトリと既存のレポートの所有権を再帰的に UID と GID 65534 に変更します。モード 750 は、誰でも書き込める状態にせず、所有者にすべてのアクセス権限を与えます。
sudo chown -R 65534:65534 output
sudo chmod 750 output
次に、置き換え用のコンテナを作成します。入力マウントには readonly を追加し、出力マウントは書き込み可能なままにします。これにより、レポートを生成できる状態を保ちながら、入力の変更を防ぎます。
sudo docker run -d --name ticket-worker --network none --mount type=bind,src=/home/labex/project/ticket-worker/input,dst=/data/input,readonly --mount type=bind,src=/home/labex/project/ticket-worker/output,dst=/data/output ticket-report:practice
新しいコンテナ ID が表示されるはずです。今度はイメージが非 root ユーザーを指定するため、実行時に root に上書きする必要はありません。
ID、アクセス、業務出力を検証する
このステップでは、置き換えたコンテナで、結果を構成する 3 つの部分すべてを確認します。
sudo docker exec ticket-worker id -u
65534 が表示されるはずです。ワーカーの実際の出力を確認します。
sudo docker logs ticket-worker
worker uid=65534 と tickets=3 が表示されるはずです。コンテナを通して、実際の出力ファイルを読み取ります。
sudo docker exec ticket-worker cat /data/output/report.txt
tickets=3 が表示されるはずです。これにより、権限を減らしても処理が壊れなかったことを確認できます。
マウントを調べます。引用符で囲まれたテンプレートは .Mounts を選択し、JSON 形式に整形します。
sudo docker inspect --format '{{json .Mounts}}' ticket-worker
/data/input に "RW":false、/data/output に "RW":true があることを確認してください。RW は読み取り/書き込みを意味します。同じ入力のバイト列を読み取れますが、マウントは書き込みを拒否します。
無害なファイル作成を試して、この制限をテストします。touch は、許可されていれば空のファイルを作成します。
sudo docker exec ticket-worker touch /data/input/probe.txt
Read-only file system エラーとなり、非ゼロの終了コードになるはずです。この拒否は意図したものです。エラーをなくすために入力を書き込み可能にしないでください。レポートは、読み取り専用の入力ですでに成功しています。
最終的なワーカーが実行中であることを確認する
このステップでは、意図したワーカーを、確認できる状態で残します。
sudo docker ps
名前付きコンテナ ticket-worker が実行中のはずです。設定されたユーザーとレポートを、もう一度確認します。
sudo docker inspect --format '{{.Config.User}}' ticket-worker
sudo docker exec ticket-worker cat /data/output/report.txt
nobody と tickets=3 が表示されるはずです。コースのチェックでは、実行中のイメージと提供用タグも比較し、実際のワーカーの PID を確認して、元の入力と出力をテストします。コンテナを置き換えずに Dockerfile を再ビルドするだけでは不十分です。
この演習は、アプリケーションの ID の権限とファイルアクセスを減らします。Docker デーモンへのアクセスを非特権にしたり、本番コンテナの包括的な堅牢化方針を確立したりするものではありません。
まとめ
アプリケーションが非 root の ID を使う実際のイメージをビルドし、そのイメージを、制限された入力と正常に機能する出力アクセスで実行して、実際のプロセスとレポートを検証しました。イメージ設定、実行時の ID、マウントモード、機能動作のすべてが、最終状態を構成しています。



