FargateでPID namespace共有を試す

ECS on Fargateで今まで出来なかったPID(プロセスID) namespace共有ができるようなったとアナウンスが来たので早速試した。→ [Announcing additional Linux controls for Amazon ECS tasks on AWS Fargate](https://aws.amazon.com/jp/blogs/containers/announcing-additional-linux-controls-for-amazon-ecs-tasks-on-aws-fargate/)
今まではFargateでは困難だったランタイ保護がこれで進むと思うが、逆に今まで隔離されていたものが貫通するので、そのリスクも確認した。

# まとめ
FargateでPID namespace共有(pidMode=task)を設定すると、
* 他コンテナプロセスの監視/干渉ができる
  * いままで出来なかった監視コンテナ追加でのランタイム保護が可能になった
* **逆にセキュリティ上のリスクも上る**
  * Fargateコンテナのrootユーザで出来る侵害が大幅に広がる
    * 可能な限り一般ユーザ(non-root)でコンテナ実行 
  * rootで動かす場合も、監視コンテナ以外にはSYS_PTRACEは絶対に付与してはいけない
* 他コンテナへのアクセス可否

|  | root+SYS_PTRACE | root | 一般ユーザ | 例 |
|:-:|:-:|:-:|:-:|:-:|
| プロセス確認 | ◯ | ◯ | ◯ | psコマンド |
| プロセスKILL | ◯ | ◯ | ☓ | kill {pid} |
| ファイルにアクセス | ◯ | ◯ | ☓ | /proc/{pid}/root でアクセス |
| システムコール監視 | ◯ | ☓ | ☓ | プロセス起動、ファイルI/O ソケット等の監視 |
| 環境変数(クレデンシャル)に<br>アクセス | △※ | ☓(?) | ☓ | ※proc/{pid}/environ からは見えないが、システムコール経由で露出の可能性あり |
| 通信の監視 | ◯※ | ☓(?) | ☓ | ※tcpdumpは出来ないが、システムコールの引数として見える |


# PID namespace共有とは
LinuxでのプロセスIDは普通1からプロセスが起動するたびに、被り無く付番されていく。これが**PID(プロセスID) namespace**。
一方、コンテナでは各個でPID番号が別になっており、各コンテナ内から見るとPID=1でプロセスが起動している。その中で子プロセスができると2, 3, ...とホスト側とは異なる空間で番号が付与される。これが**PID namespaceの分離**。
コンテナ間の隔離という意味では良いのだが、逆に別コンテナへの干渉がしたい場合は邪魔になる。
ECSのnamespace共有はこれを敢えて崩し、タスク内でPID名前空間を同じにする設定。
(通常のdockerでは元々可能、Fargateでは出来なかった)

# PID namespace共有で何が嬉しいか
プロダクション環境では稼働プロセスが侵害され不審な行動を取っていないか(不要なファイルにアクセス、子プロセスを起動等)を監視/遮断する**ランタイム保護**を行いたい。
通常のLinux環境であればAppArmor, SELinux, eBPF等の手段が使えるが、Fargateではこれらはすべて利用できない。
唯一ptraceによるシステムコール監視は可能なものの、これはPID namespaceが同じプロセス間でしかできないため、監視プロセスを本来のプロセス(nginx等)の親にするような無理な適用方法しかなかった。

今回使えるようになったPID namespace共有により他コンテナのプロセスが見えるようになったので、「監視用コンテナをタスクに追加してランタイム保護」のような方法が行えるようになった。



# 試験環境
[github fargate-pid-ns-share-sample](https://github.com/anunperson/fargate-pid-ns-share-sample)にサンプルを置いた。
よくあるWeb(nginx), App(php-fpm)＋監視コンテナ(alpine) の3コンテナを1タスクに収め、タスク定義に`"pidMode": "task"`を設定したもの。
また、監視コンテナ(alpine)にはSYS_PTRACEケーパビリティを追加している。


# 試験結果
以下全てalpineコンテナ rootユーザから操作している。
システムコールの監視はタスク定義でSYS_PTRACEの追加、それ以外はrootであれば可能。

## PID namespace共有状態を確認
実際に共有できているのを確認できた。
通常は各メインプロセスがPID=1になるが、7, 19, 54と1以外の値になっている。

```
$ ps axuw
PID   USER     TIME  COMMAND
    1 root      0:00 /pause
    7 root      0:00 php-fpm: master process (/usr/local/etc/php-fpm.conf)
   19 101       0:00 nginx: master process nginx -g daemon off;
   25 82        0:00 php-fpm: pool www
   26 82        0:00 php-fpm: pool www
   52 101       0:00 nginx: worker process
   53 101       0:00 nginx: worker process
   54 root      0:00 sleep infinity
  123 root      0:00 sh  ←これが今操作してるプロセス(@alpineコンテナ)
  124 root      0:00 ps axuw
※SSMの行は省略
```


### 他コンテナのシステムコールを監視
straceでalpineコンテナからphp-fpmコンテナプロセスにアタッチし、システムコールが追えることを確認した。


#### 例 他プロセス起動を監視
PHP内で`shell_exec('cat /etc/passwd')`を実行した。その際のclone()→execve()がしっかり見えている。
```
#pid 25 = php-fpmワーカープロセスにアタッチ
$ strace -f -p 25 -e execve,clone
strace: Process 25 attached
...
clone(child_stack=0x7fff85c11268, flags=CLONE_VM|CLONE_VFORK|SIGCHLDstrace: Process 222 attached
 <unfinished ...>
[pid   222] execve("/bin/sh", ["sh", "-c", "cat '/etc/passwd'"], 0x7f41081f45d0 /* 24 vars */ <unfinished ...>
[pid    25] <... clone resumed>)        = 222
[pid   222] <... execve resumed>)       = 0
[pid   222] execve("/bin/cat", ["cat", "/etc/passwd"], 0x7f940d2031d0 /* 24 vars */) = 0
```


#### 例 ファイルアクセスの監視
hogehoge.phpという存在しないファイルにアクセスした例。

```
$ strace -p 25 -e trace=%file
strace: Process 25 attached
...
lstat("/var/www/html/hogehoge.php", 0x7fff85c11520) = -1 ENOENT (No such file or directory)
stat("/var/www/html", {st_mode=S_IFDIR|S_ISVTX|0777, st_size=4096, ...}) = 0
stat("/var/www", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
stat("/var", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0
stat("", 0x7fff85c13740)                = -1 ENOENT (No such file or directory)
```

## 他コンテナプロセスのKILL
alpine側からnginx, phpの子プロセスをKILL出来るのを確認。
当然だが必須コンテナのマスタープロセスをKILLするとタスクごと死ぬ。

```
$ ps axuw
PID   USER     TIME  COMMAND
...
    7 root      0:00 php-fpm: master process (/usr/local/etc/php-fpm.conf)
   19 101       0:00 nginx: master process nginx -g daemon off;
   25 82        0:00 php-fpm: pool www
   26 82        0:00 php-fpm: pool www  ←これをKill
   52 101       0:00 nginx: worker process
   53 101       0:00 nginx: worker process  ←これをKill
...

$ kill 26 #php-fpm子プロセスをKILL
$ kill 53 #nginx子プロセスをKILL

$ ps auxw
PID   USER     TIME  COMMAND
    7 root      0:00 php-fpm: master process (/usr/local/etc/php-fpm.conf)
   19 101       0:00 nginx: master process nginx -g daemon off;
   25 82        0:00 php-fpm: pool www
   52 101       0:00 nginx: worker process
  125 82        0:00 php-fpm: pool www  ←ワーカーが再生成された
  126 101       0:00 nginx: worker process  ←ワーカーが再生成された
```

# 他コンテナのファイルにアクセス
`/proc/{pid}/root`経由で普通に可能。書き込みもできる。


```
#pid 7 = php-fpmのファイルシステム
$ ls /proc/7/root/var/www/html/
test.php

$ echo '<?php echo "test2";' > /proc/7/root/var/www/html/test2.php
$ cat /proc/7/root/var/www/html/test2.php
<?php echo "test2";
```



# 他コンテナの環境変数へアクセス
普通なら`/proc/{pid}/environ`で起動時の環境変数が見えるが、バグか仕様か、他コンテナのプロセス相手だと上手く動かなかった。
gdbも上手く行かず良い方法が思いつかなかったが、結局クレデンシャルを使うときは外部と通信するので、その際のシステムコールから読み取りが可能。
他コンテナのクレデンシャルまでダダ漏れは相当危険なので、監視コンテナ以外は一般ユーザ実行にする、最低でもSYS_PTRACEは付けない、という運用が必要になる。

environで↓謎の内容が表示される
```
$ cat /proc/7/environ
 master process (/usr/local/etc/php-fpm.conf)
```

# 他コンテナプロセスのtcpdump
一応試してみたものの、tcpdumpはダメそう。network namespaceは別れてるから当然。
とはいえ、システムコールが見えている以上は通信内容も見えるので、リスクあり。