こんにちは。Product Team の竹原です。
みなさん Dockerfile 書いてますか?
私たち Product Team では全アプリを Docker コンテナ内で動かしているので、すべてのアプリに対して Dockerfile を書いてます。
とはいえ、私は最近まで「コンテナ内でアプリが動くこと」以外に Dockerfile の書き方についてあまり意識を向けていませんでした。
強いて言えば「レイヤーが増えすぎないように RUN
命令はまとめる」といった、ちょっとかじった程度のベストプラクティスを思い起こす程度でした。
そんな中、最近 Dockerfile にちょっとした工夫をすることになったので、その内容についてご紹介します。
- そのプロセス、root ユーザで動いてない?
- 何がダメ?どうなってるのが理想?
- nobody ユーザって何?
- nobody ユーザを使うように Dockerfile を書き換える
- 本当に nobody ユーザになってる?権限絞れてる?
- まとめ
そのプロセス、root ユーザで動いてない?
今回話題に挙がったのは、Kotlin と Ktor で書かれたバックエンドアプリでした。
とりあえずサンプルとして単純化していますが、ディレクトリ構成はこんな感じで、
% tree -L 2 . ├── Dockerfile └── ktor-sample ├── pom.xml └── src
Dockerfile はだいたいこんな感じです。
FROM maven AS build WORKDIR /work COPY ./ktor-sample /work RUN --mount=type=cache,target=/root/.m2 mvn clean package FROM eclipse-temurin:11.0.17_8-jdk WORKDIR /usr/local/ktor-sample COPY --from=build \ /work/target/ktor-sample-0.0.1-jar-with-dependencies.jar \ . ENTRYPOINT ["java", "-jar", "./ktor-sample-0.0.1-jar-with-dependencies.jar"]
※ アプリを mvn package
でビルド → ビルドしたものを別ステージで実行してるだけ
とりあえずアプリは動かせる Dockerfile になっているんですが、
これだとjava
のプロセスが root ユーザで実行されることになります。
何がダメ?どうなってるのが理想?
root ユーザでプロセスが動いているということは、仮にアプリ (上記 ktor-sample-0.0.1-jar-with-dependencies.jar
) に脆弱性があった場合、攻撃されうる範囲がとても広くなってしまいます。
なんでもかんでも権限を持っているユーザですので当然ですね。
理想は、アプリの実行ユーザの権限を最小に絞れている状態です。
そこでひとまずやっておきたい対応として、nobody
ユーザを使う ことが挙げられます。
nobody ユーザって何?
ぶっちゃけ私はあまりnobodyユーザについて理解できていません
nobody ユーザは Docker でしか出てこない概念ではなく、UNIX のユーザに関する概念 (らしい) です。
[linux user nobody] で検索すると色々出てきます。
ちなみに私はそうとも知らず [dockerfile nobody] とか [docker user nobody] とかで検索しまくって時間を浪費してしまいました。かなしいですね。
nobody ユーザは root ユーザの対極にあり、必要最低限の権限しか持っていません。
root よりはよほど安全ですね。
nobody ユーザを使うように Dockerfile を書き換える
さて、上の方に示した Dockerfile をもう一度見てみましょう。
FROM maven AS build WORKDIR /work COPY ./ktor-sample /work RUN --mount=type=cache,target=/root/.m2 mvn clean package FROM eclipse-temurin:11.0.17_8-jdk WORKDIR /usr/local/ktor-sample COPY --from=build \ /work/target/ktor-sample-0.0.1-jar-with-dependencies.jar \ . ENTRYPOINT ["java", "-jar", "./ktor-sample-0.0.1-jar-with-dependencies.jar"]
この Dockerfile について、プロセスの実行ユーザを nobody にしてみます。
FROM maven AS build WORKDIR /work COPY ./ktor-sample /work RUN --mount=type=cache,target=/root/.m2 mvn clean package FROM eclipse-temurin:11.0.17_8-jdk USER nobody WORKDIR /usr/local/ktor-sample COPY --from=build \ --chown=nobody:nogroup \ /work/target/ktor-sample-0.0.1-jar-with-dependencies.jar \ . ENTRYPOINT ["java", "-jar", "./ktor-sample-0.0.1-jar-with-dependencies.jar"]
このシンプルな Dockerfile については、ポイントは2つです。
USER nobody
COPY --chown=nobody:nogroup
ユーザを変えて、ファイルコピー時に --chown
オプションでオーナーを変更してやれば OK です。
本当に nobody ユーザになってる?権限絞れてる?
さて、nobody ユーザをちゃんと使えてるか適当に確認してみましょう。
今回は Ktor 製アプリに以下のようなコードを仕込んでみました。
fun Application.configureSerialization() { install(ContentNegotiation) { json() } routing { get("/ls") { try { val files = exec("ls", "-alF", call.request.queryParameters["path"]!!) val whoami = exec("whoami")[0].trim() val responseJson = ResponseJson(whoami, files) call.respond(HttpStatusCode.OK, responseJson) } catch (t: Throwable) { t.printStackTrace() call.respond(HttpStatusCode.BadRequest) } } } } private fun exec(vararg commands: String): List<String> { val process = Runtime.getRuntime().exec(commands) process.waitFor() return process.inputStream.bufferedReader().use { it.readLines() } } @Serializable data class ResponseJson(val whoami: String, val files: List<String>)
いろんなコードを端折りに端折りましたが、つまり「http://localhost:8080/ls?path=/root
← この HTTP リクエストを投げると、コマンド whoami
と ls -alF /root
の実行結果を得られる」という感じです。
じゃあこのアプリを使って Dockerfile (変更前) と Dockerfile (変更後) をそれぞれ docker image build
して docker container run
してなんやかんやあって得たレスポンスを以下に示します。
root ユーザの方では /root
以下のファイルの情報が返ってきていますが、
nobody ユーザの方では空のリストが返ってきています。
正しくユーザが設定できていて、正しく権限が絞られているようです。
rootユーザで実行した結果.json
{ "whoami": "root", "files": [ "total 20", "drwx------ 1 root root 4096 Dec 9 01:44 ./", "drwxr-xr-x 1 root root 4096 Dec 27 07:12 ../", "-rw-r--r-- 1 root root 3106 Oct 15 2021 .bashrc", "-rw-r--r-- 1 root root 161 Jul 9 2019 .profile", "-rw-r--r-- 1 root root 165 Dec 9 01:44 .wget-hsts" ] }
nobodyユーザで実行した結果.json
{ "whoami": "nobody", "files": [] }
まとめ
とりあえず Dockerfile 書いたら nobody ユーザで動かすようにしとこう!
正直、機能がたくさん増えた後に非 root 化をやるのは大変だと思います。
全機能ちゃんと動くかな?テストしきれてないところはないかな?とかを後になって考えるより、先にやっておいたほうがよさそうです。
今までずっと root ユーザで動かしていたそこのあなた!ぜひ nobody ユーザ化を検討してみてください。