LCL Engineers' Blog

バス比較なび・格安移動・バスとりっぷを運営する LCLの開発者ブログ

hadlintとdockleをつかってモダンでセキュアなDockerfileを書こう

f:id:kasei_san:20201204105440p:plain

こちらは、2020年LCLアドベントカレンダーの1日目です!

qiita.com

こんにちは。最近は作業用BGMがもっぱらバーチャル猫さんの曲になっている id:kasei_san です

今回は、LCLで実際に使用している LinterとセキュリティチェッカーをつかったモダンなDockerfileの書き方と、それらを利用して作成したDockerfileを紹介したいと思います!

みなさんはベストプラクティスに基づいたDockerfile書けていますか?

Dockerのベストプラクティスは、公式でも記事になっていますが、全てを理解して書くのは中々難しいですよね...

docs.docker.com

そんな時のために、自動的にDockerfileがベストプラクティスに則っているかチェックしてくれるLinterがあります!

それが、hadolintです

github.com

hadolintはDockerfileがベストプラクティスに沿っているかチェックして、おおまかな修正方法まで教えてくれます。便利!

hadolintのつかいかた

macOSであれば、brewでインストールできます

brew install hadolint

そして、以下のコマンドでDockerfileをチェックしてくれます

hadolint ${Dockerfileのpath}

例えば、Dockerfileに以下のようなコマンドがあった場合...

COPY Gemfile Gemfile.lock package.json yarn.lock app

ベストプラクティス DL3021 に反していると警告してくれます

Dockerfile:52 DL3021 COPY with more than 2 arguments requires the last argument to end with /

なお、ベストプラクティスは、hadolintのgithubのwikiにまとまっていて、そちらに具体的な解決方法も記されています。親切!

Home · hadolint/hadolint Wiki · GitHub

Dockerfileの書き方はいろいろなベストプラクティスがあり、自力で覚えていくのは大変ですが、こういったLinterを使うことで、素早く/自動的に身体に叩き込むことができますね!

これで、ベストプラクティスに沿ったDockerfileを書くことができました! しかし、ビルドされたImage。本当にセキュアでしょうか...?

セキュアなDockerfileを書けていますか?

と、いうわけで次にdockleを紹介します

dockleはビルドしたDockerImageをスキャンして、セキュリティ上の問題が無いかチェックしてくれるツールです

github.com

dockleのつかいかた

READMEに書いてあるとおりですね

$ brew untap goodwithtech/dockle # who use 0.1.16 or older version
$ brew install goodwithtech/r/dockle
$ dockle [YOUR_IMAGE_NAME]

実行してみると、例えば root ユーザで実行しようとしていると以下のような警告が出ます

WARN    - CIS-DI-0001: Create a user for the container
        * Last user should not be root

CIS-DI-0001 などのidで分類されているチェックポイントの詳細は以下で知ることができます

dockle/CHECKPOINT.md at master · goodwithtech/dockle · GitHub

すべてを理解するのはなかなか難しいセキュリティをしっかり抑えて教えてくれるので、 hadlintと同じく何度もつかっていくうちに、勘所がだんだん覚えられてきてとても良いです...!

できあがったモダンでセキュアなDockerfileを紹介

最後にdockleとhadlintをつかって作成した、Railsを動作させる本番用のDockerfileを紹介します

意図が分かりづらい部分には、チェックポイントのリンクをコメントしていますので、たどってみると参考になると思います!

################################################################
# rails Image
################################################################
FROM ruby:2.7.2-slim

ENV RAILS_ENV production

ENV ROOT_PATH /app
ENV LANG C.UTF-8
ENV LC_ALL C.UTF-8

# パイプ(|) を使用した場合、パイプ前の処理が失敗しても
# 最後の処理が成功すれば、その行のexitは0になる
# pipefail オプションをつけることで、パイプの前の処理が失敗した場合も
# 戻り値を非1とすることが可能
#
# see: https://github.com/hadolint/hadolint/wiki/DL4006
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# 特殊なアクセス権、suid, sgid を排除
# 想定外のアクセス権でコマンドを実行されることを防ぐ
#
# see: https://github.com/goodwithtech/dockle/blob/master/CHECKPOINT.md#CIS-DI-0008
RUN chmod u-s /bin/umount && \
    chmod g-s /usr/bin/expiry && \
    chmod u-s /bin/su && \
    chmod g-s /usr/bin/wall && \
    chmod u-s /usr/bin/gpasswd && \
    chmod u-s /bin/mount && \
    chmod u-s /usr/bin/passwd && \
    chmod g-s /sbin/unix_chkpwd && \
    chmod g-s /usr/bin/chage && \
    chmod u-s /usr/bin/chsh && \
    chmod u-s /usr/bin/chfn && \
    chmod u-s /usr/bin/newgrp

RUN apt-get update -qq && \
    DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
      build-essential=12.6 \
      gnupg=2.2.12-1+deb10u1 \
      wget=1.20.1-1.1 && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
    truncate -s 0 /var/log/*log

# pg をインストールするためにpostgresqlが必要
ARG PG_MAGER_VERSION=11
ARG PG_VERSION=11.10-1.pgdg100+1
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add - && \
    echo "deb http://apt.postgresql.org/pub/repos/apt/ buster-pgdg main" $PG_MAGER_VERSION > /etc/apt/sources.list.d/pgdg.list && \
    apt-get update -qq && \
    DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
      "postgresql-server-dev-$PG_MAGER_VERSION=$PG_VERSION" && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* && \
    truncate -s 0 /var/log/*log

ENV GEM_HOME=/bundle \
    BUNDLE_JOBS=4 \
    BUNDLE_RETRY=3 \
    BUNDLE_PATH=${GEM_HOME} \
    BUNDLE_BIN=${GEM_HOME}/bin \
    BUNDLE_APP_CONFIG=${GEM_HOME}

ARG BUNDLER_VERSION=2.1.4
RUN gem install "bundler:$BUNDLER_VERSION"

ENV PATH=/app/bin:$BUNDLE_BIN:$PATH

# 作業ディレクトリ & 実行ユーザ作成
RUN useradd -m -u 1000 rails && \
    mkdir $ROOT_PATH && \
    chown rails $ROOT_PATH
USER rails
WORKDIR $ROOT_PATH

# 全部コピーするとRailsアプリの変更の度にここからビルドし直しになるので
# 必要なGemfileやyarnのファイルを先にコピーする
COPY --chown=rails Gemfile Gemfile.lock $ROOT_PATH/

# bundle installの実行
RUN bundle config set without 'development test' && \
    bundle install -j4

# アプリのコピー
COPY --chown=rails . $ROOT_PATH

# HEALTHCHECK が失敗した場合は処理が終了する
#
# see: https://github.com/goodwithtech/dockle/blob/master/CHECKPOINT.md#cis-di-0006
HEALTHCHECK --interval=5m --timeout=30s \
  CMD curl -f http://localhost:10080/robots.txt || exit 1

# 起動
CMD ["/bin/bash", "-c", "echo 'コマンドを指定してください'"]

Dockerfileの解説

アプリサーバとバッチで同様のImageを使用するため、 CMD では何も実行しないようになっています

また、以下のdockleのチェックポイントは、対応不可能のため無効化しています

  • CIS-DI-0005: DOCKER_CONTENT_TRUST は、ECRではサポートされていないため無効化しています
  • DKL-LI-0003: ImageにDockerfileが含まれているという警告が出ますが、gem等に入っているDockerfileについても警告が出てしまうため無効化しています

まとめと感想

hadlintとdockleを使うことで、モダンでセキュアなDockerfileを作れるようになりました!

先程も書きましたが、やはりベストプラクティスやセキュリティについて、ツールがサポートしてくれることで開発者が学習していけるのはとても良いですね

ただ、見ての通りDockerfileはかなり複雑になってしまいます...。おそらくDocker初心者が見たら何をしているのか把握するのはとても大変なのではないでしょうか...

また、aptでインストールするアプリケーションのバージョンも固定化されるため、これらのアプリケーションのバージョンアップにも目を光らせる必要があります (こちらは、コンテナの脆弱性スキャンツールのClairやTrivyで対応する予定です)

github.com

github.com

EC2上で直接アプリを動かすことと比べると、だいぶ楽になったとはおもいますが、それでもベストプラクティスに基づいてセキュアなアプリを動かし続けることは中々大変です

現在のLCLの規模では、そのあたりをしっかりサポートしていくのはなかなか難しいため、AWS lambda のようなよりサーバレスなアーキテクチャに移行していくのがベストかも知れませんね...!

以上です!

明日は、naotsu-logさんの Nuxt.jsで始める省エネフロントエンド開発 です! 楽しみですね!

🎄 それでは皆さん。良いクリスマスを! 🎅