コンテナをread-onlyで動かす

社労夢事件こわいなー戸締まりしとこ。

コンテナセキュリティの一環として、実行時にファイルシステムに書き込めないようにできる。
これにより、スクリプトファイルを書き換えたりwebshellを設置されたりという攻撃を防ぐことができる。

ただし、何らかの書き込みが必要なアプリケーションも多く、個別に設定が必要となる。
具体的な設定例があまり見つけられなかったので、いくつか設定を調べたものをまとめる。

# コンテナread-only動作まとめ
* read only指定でコンテナを起動すると、実行時にルートファイルシステム (`/`以下)の書き込みができなくなる
  * イメージビルド段階は書き込み可能(要はDockerfile内の処理は影響ない)
  * 実行時(entrypoint含む)になってからの書き込みが不可
* ルートファイルシステム以外は制限されない
  * tmpfsの`/dev/shm` など別ファイルシステムは対象外
  * ボリュームを設定した場所も同じ理由で対象外(匿名/名前付き/バインドマウントいずれも)
* アプリケーションで書き込みが必要な場合は多いので、各々対処が必要
* 当然ながらファイル書き換え以外の攻撃には無力。(SQLiとか任意コード実行とか)
* [CIS Docker Benchmark](https://www.cisecurity.org/benchmark/docker)等でも推奨されている。

# read-onlyフラグの指定
以下いずれかの方法で指定する。
1. docker-composeに`read_only: true`を設定
2. docker runに`--read-only`オプションを設定
3. ECSの場合 タスク定義のコンテナ定義に`"readonlyRootFilesystem": true`を設定する

# Dockerfileの設定
アプリケーションから書き込みが必要なディレクトリにはvolumeを設定し、書き込み可能とする。
当然その場所は防御は甘くなるので注意が必要。

※Amazon ECSだとタスク定義側にvolume設定を書くこともできるが、ローカルでの実行や他クラウド乗り換えも想定し、ベンダー依存のないDockerfileに書く形式のほうが良いと思う


## だいたい共通 /tmp
大体のアプリケーションでは何かしら書き込みが必要で、とりあえず`/tmp`に押し付けることになる。
勿論完全に不要ならこの指定は要らない。

```
VOLUME /tmp
```

`/var/tmp`も必要な場合は追加する。無しで動くなら不要。今のところ必要になった経験はない。


## nginxinc/nginx-unprivileged:alpine
`/tmp/`と`/etc/nginx/conf.d/`に書き込みが必要。
前者はキャッシュやpidを書き込む場所、後者はentrypointで設定ファイル書き換えが走る都合で必要。
rootユーザでないのでもしconf.dを変更されてもreloadはできないと思う(多分)。

```
VOLUME /tmp
VOLUME /etc/nginx/conf.d
```

※通常版に対してunprivileged版では設定ファイル以外の書き込みを全て/tmpに押し込めるよう変更されているので楽

## nginx:alpine
以下が必要。
自分でnginx運用する場合はunprivilegedを使えば良いものの、何か使おうとしたらベースイメージこっちだったりする。(swagger-uiとか)


```
# entrypointで書き込みがある
VOLUME /etc/nginx

# nginx.pidがある
VOLUME /var/run

# client_body_temp_path などが設定されている
VOLUME /var/cache/nginx

# アプリケーションによってはここも要るかも。readonlyにする意味が無くなる
# VOLUME /usr/share/nginx/html
```


## php:8.\*-fpm / php:8.\*-fpm-alpine
アップロードされたファイルの処理などで書き込みが必須だが、`/tmp`だけでOK。

```
VOLUME /tmp
```

## phpmyadmin:fpm-alpine
そもそもこれに外部からアクセスされた時点で負けというのは置いておいて、
以下のボリューム設定と

```
VOLUME /tmp

# session保存場所 (SESSION_SAVE_PATHで変更できるはず)
VOLUME /sessions

# phpmyadmin設定ファイルの場所。スタートアップスクリプトで書き込みがあり書き込みが必要
VOLUME /etc/phpmyadmin
```

config.user.inc.php に以下の追加が必要。
```
$cfg['TempDir'] = '/tmp/phpmyadmin';
```

## 共通 ECS Exec用設定 (AWS ECSで動かす場合のみ)
ECSで動かすならSSM Agentでログインしたい、その場合は以下のボリューム設定が必要。
[Using ECS Exec with read-only root file system containers](https://toris.io/2021/06/using-ecs-exec-with-readonlyrootfilesystem-enabled-containers/)より、動いてるコンテナを見てパーミッションを下げただけ。
なお当然rootユーザで動かしていたらパーミッションの意味はない。

```
RUN mkdir -p /var/lib/amazon
RUN chmod 750 /var/lib/amazon
VOLUME /var/lib/amazon

RUN mkdir -p /var/log/amazon
RUN chmod 700 /var/log/amazon
VOLUME /var/log/amazon
```

## おまけ CakePHP用設定
dockerイメージではないけど。
php:8.\*-fpm or php:8.\*-fpm-alpineでのボリューム設定に加え、`config/paths.php`の `TMP`, `LOGS` を プロジェクトdir内のtmpから/tmpに変更する。
(コンテナで動かす場合はlogs使わないと思うけど一応)

```
#例
define('TMP', DS . 'tmp' . DS . 'caketmp' . DS); // = /tmp/caketmp/
define('LOGS', DS . 'tmp' . DS . 'cakelogs' . DS); // = /tmp/cakelogs/
```

# 要書き込み箇所の調査方法
* 設定ファイルやDockerfileから書き込みそうな場所を探す
* read only外した状態で動かして `docker diff` で書き込まれた場所を探る
  * 簡単だが処理後にファイルを消されれてしまうと分からない
* それでも駄目ならstraceで調べる


# 補足 匿名ボリュームが散らかる件
Dockerfile内で`VOLUME /tmp`とだけ書くと、毎度名前なしボリュームが生成され、自動で消されないので溜まっていく。
fargate運用だとタスクと一緒に消えてくれるので良いものの、そうでない環境であれば、`docker-compose.yml`の方に同じ場所への"名前付きボリューム"指定を書いておくと防止可能。
(docker-compose.ymlとDockerfile両方に同じ場所へのボリューム指定があれば前者が優先される)


# 補足 書き込み可能領域の保護
残念ながら現時点でAWS Fargateではtmpfsが使えないので、とりあえず互換性の高いVOLUME設定で書いた。使える環境ならnoexec,nosuid,nodev設定できるtmpfsを使ったほうが良い。

何れにせよリードオンリー化はあくまで防御の一つであって、別レイヤでの保護も必要。
/tmpに置かれたファイルの実行を防ぐならAppArmorが手っ取り早そうだが、fargateはコレも未対応…


※2023/08/06 phpmyadminで/sessionsが必要になっていたので設定修正