コンテナのセキュリティ対策!AWS ECS (fargate)でタスクをreadonlyRootFilesystemで運用する

https://aws.amazon.com/jp/fargate/

クラウドソリューション事業部の原口です。

最近は本番環境をコンテナで運用する事はあたりまえとなってきましたが弊社ではコンテナ環境としてAWS ECS + Fargateを利用しています。そんなコンテナのセキュリティを高める施策の中でECSにある readonlyRootFilesystem を有効にするというものがあります。
この設定を行なうと、文字通り立ち上がったコンテナ内のルートファイルシステムが読み取り専用となります。

設定は非常に簡単で、コンテナのタスク定義にて「読み取り専用ルートファイルシステム」にチェックを入れるだけで設定ができます。


もしくは、CIやcodedeployなどで管理されている場合は、taskdef.json にて以下のように設定するのみとなります

"readonlyRootFilesystem": true

read-onlyはセキュアである

これはセキュリティ的に非常に素晴らしい事だと思います。

アプリケーションを運用するとどれだけ気をつけても脆弱性は発生しうるものです。攻撃者が脆弱性をついてハッキングした場合にまず行う事はバックドアと呼ばれる不正ファイルを設置する事ですが、read-onlyなファイルシステムではまずこのファイルの設置などができません。また既存ファイルの改竄などもできなくなるため攻撃の成功率は著しく低くなるでしょう。

考慮すべき点

設定は簡単ですが、read-only ルートファイルシステム で運用するには、ディスクへの書き込みを行わないアプリケーション設計が必要です。
プログラムへのアクセス後に中間キャッシュファイルを作成するようなものですと、これらのファイルの生成はメモリ上に展開するようにするか、無理であれば設計を変更しなければいけません。

また、それぞれのコンテナはステートレスである必要があります。データベースは外に出す必要がありますし、ログは標準出力にしてFireLensなどに送る、Sessionやキャッシュはredisに、、、

めんどくさくなってきましたがこれらは冗長化する上での対応と同じと言えますのでまだなんとかできそうですね。

どうしても書き込まなければいけないもの

完全な読み取り専用の実現を目指すのであれば、この表現自体が不適切かもしれませんのでその点は注意してください。

実際のところ多くのアプリケーションでは /var/run へのpidファイルであったり /tmp でのsockファイルであったりといった一時ディレクトリへの書き込みが必要です。これらが書き込みできない場合は多くのプログラムはエラーとなって終了します。

この問題は、読み取り専用のファイルシステムの中でも、特定部分へVolumeをマウントし書き込み許可する事で回避する事ができます。
Volumeマウントの方法はECS fargateを使っている場合は2通りの方法があります。

Volumeを定義する (Dockerfile)

もしDockerfileを修正可能であればDockerfile内でVolume定義をしてしまえば最も楽に対応可能です。

FROM node:15.14.0-alpine

RUN apk update && apk upgrade

VOLUME /usr/local/share/
VOLUME /tmp/

RUN mkdir -p /app
COPY ./ /app
WORKDIR /app

RUN yarn install --pure-lockfile --prod && \
    yarn build

ENV HOST 0.0.0.0
EXPOSE 3000
USER node

ENTRYPOINT [ "yarn", "start" ]

これは実際にread-onlyで動かしているコンテナの設定例です。ここではnodeコンテナでyarn startしていますが、yarn startを実行すると起動時に /usr/local/share/ 及び /tmp/ への書き込みが発生します。

ここで、5-6行目の記述がホスト上(ここではfargate上)に一時的な場所を設定してコンテナにマウントしている設定になります。

VOLUME /usr/local/share/
VOLUME /tmp/

この設定によりこの領域は書き込みが可能となります。この一時的なデータはホスト側に公開されてしまうことになりますが、fargateですし、エフェメラルストレージ(デフォルトでは20GB)ですのでコンテナ停止後は永続しません。

Volumeを定義する(ECS タスク定義)

Dockerファイルへの修正が難しい場合はECSのタスク定義でも定義可能です。
タスク定義の一番したの方にあるボリュームの追加を行います。ボリュームタイプはBind Mountとなります。

次に、各コンテナ定義の中のストレージよりVolumeをマウント可能です。

こちらの設定で、 /tmp への書き込みができる状態となります。taskdef.jsonでの記述は見れていませんが、この設定状態のjsonを取得すれば大丈夫かと思います。

Bind MountについてはAWS公式ドキュメントが参考になります。

バインドマウント
https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/bind-mounts.html

その他

fargate以外の場合の方法としては tmpfs をマウントするという方法もあります。tmpfsは一時ファイルシステムをローカルメモリから作り出した領域で、普通のファイルシステムと同じように使えるのに実態はメモリ上、という領域ですね。

実は AWS ECSではこのtmpfsがサポートされており、今回のように /tmp などの一時的な書き込み領域としての利用としては非常に最適です。コンテナ内での作業となるのでホスト側への公開もありませんし、再起動したら消える揮発性のある領域を作り出せます。

Amazon ECS が shm-size と tmpfs パラメーターへのサポートを追加
https://aws.amazon.com/jp/about-aws/whats-new/2018/03/amazon-ecs-adds-support-for-shm-size-and-tmpfs-parameters/

はじめは、これだ!!!って思ったのですが、fargateではサポートされていないようなのです。

Fargate 起動タイプを使用するタスクを使用している場合、tmpfs パラメータはサポートされません。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/task_definition_parameters.html

残念。