<-- mermaid -->

Docker コンテナを非 root ユーザ nobody で動かしてみないか

こんにちは。Product Team の竹原です。

みなさん Dockerfile 書いてますか?
私たち Product Team では全アプリを Docker コンテナ内で動かしているので、すべてのアプリに対して Dockerfile を書いてます。

とはいえ、私は最近まで「コンテナ内でアプリが動くこと」以外に Dockerfile の書き方についてあまり意識を向けていませんでした。
強いて言えば「レイヤーが増えすぎないように RUN 命令はまとめる」といった、ちょっとかじった程度のベストプラクティスを思い起こす程度でした。

そんな中、最近 Dockerfile にちょっとした工夫をすることになったので、その内容についてご紹介します。

そのプロセス、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つです。

  1. USER nobody
  2. 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 リクエストを投げると、コマンド whoamils -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 ユーザ化を検討してみてください。

Page top