LXC + AppArmor

LXC を AppArmor で守るメモ。

LXC のようなコンテナ技術は環境を隔離するので、コンテナの中からホストに悪さできないようになってて欲しいですが、回避できない様々な攻撃方法があります。そこで他の技術と組み合わせてセキュリティを確保します。[Linux Containers - LXC - Security](https://linuxcontainers.org/lxc/security/) によるとそもそも LXC は一般ユーザで動かすべきで AppArmor に頼ってはならぬとの事ですが、どうしても AppArmor が必要になった場合のメモです。

私がテストに使っている Ubuntu-18 では最初から LXC の AppArmor サポートが有効になっていました。もしも `/etc/apparmor.d/lxc` が無ければ `--enable-apparmor` 付きで LXC をビルドし直します。(例: Yocto だと `PACKAGECONFIG_append_pn-lxc = "apparmor"` を local.conf に追加)

それではこんな例題をやっていきましょう。

* ルート権限で LXC の busybox コンテナ bbox を作る。
* ホストの `/tmp/` をコンテナ bbox の `/host/` にマウントする。
* コンテナからは `/host/black.txt` を読む事はできない。

AppArmor は本来ホワイトリスト形式なので、デフォルト禁止で読めるファイルだけ記述するのが正しいやり方です。しかし LXC + AppArmor の組み合わせでは禁止するファイルを記述するブラックリスト形式しか使えません。理由は後述します。

## ルート権限で LXC の busybox コンテナ bbox を作る。

まず以下のコマンドでさくっと busybox コンテナを作り、設定ファイル `/var/lib/lxc/bbox/config` を編集します。

```shell
sudo lxc-create --template busybox --name bbox
sudo chmod a+rx /var/lib/lxc/bbox
sudo chmod a+rw /var/lib/lxc/bbox/config
vi /var/lib/lxc/bbox/config
```

追加する内容は以下です。

```
lxc.mount.entry = /tmp host none rw,bind,create=dir 0 0
```

これでホストの `/tmp/` をコンテナの `/host/` にマウントします。内容は fstab のフォーマットですが以下のような意味です。

* 1枡目: `/tmp` : コンテナに公開したいホストのディレクトリ位置。
* 2枡目: `host` : コンテナ側の位置です。相対パスを書くと、この場合 `/var/lib/lxc/bbox/rootfs` からの位置。
* 3枡目: `none` : bind mount を使うので none にします。
* 4枡目: `rw,bind,create=dir` : 追加設定。
    * `rw` : 読み書き可能。
    * `bind` : ディレクトリをそのままマウント。
    * `create=dir` : マウント位置が無かったら作る。
* 5桁目: `0` : dump が使うらしい。デフォルトのままにします。
* 6桁目: `0` : fsck が使うらしい。デフォルトのままにします。

さていよいよ実験します。`lxc-execute` を使うとコンテナを開始してコマンドを実行して即コンテナを終了します。別に `lxc-start` を使うとバックグラウンドで動かす事もできますが、テストには `lxc-execute` の方が便利です。

```shell
$ sudo lxc-execute -n bbox -- sh -c 'echo Hello white >  /host/white.txt'
$ sudo lxc-execute -n bbox -- sh -c 'echo Hello black >  /host/black.txt'
$ cat /tmp/*.txt
Hello black
Hello white
```

このようにコンテナの `/host/` に書き込んだものがホストの `/tmp/` に現れたら OK です。

## AppArmor で守る

それでは、いよいよ AppArmor でコンテナに制限をかけてみます。実は Ubuntu ではデフォルトで apparmor が有効になっていて、最初からある程度の制限がかかっています。例えば `/sys/kernel/debug/` にアクセスできません。

```shell
$ sudo lxc-execute -n bbox -- ls /sys/kernel/debug/
ls: can't open '/sys/kernel/debug/': Permission denied
```

このデフォルトの profile としては [lxc-container-default-cgns](https://github.com/lxc/lxc/blob/lxc-3.0.3/config/apparmor/profiles/lxc-default-cgns) が使われていて、特に [container-base](https://github.com/lxc/lxc/blob/lxc-3.0.3/config/apparmor/abstractions/container-base) に主要なルールが記述されています。これで余計なリソースにアクセスできないようになっています。

自分で新たにルールを作るにはこの既存の profile をコピーして作ります。

```shell
cd /etc/apparmor.d/lxc/
sudo cp lxc-default-cgns bbox
sudo vi bbox
```

例えばこんな感じ。ここで重要な注意ですが、ルールに使うパスはコンテナから見たパス (例: `/host`) にします。ホストから見たパス `/tmp` ではありません。

```
# profile 名を bbox に変更する
profile bbox flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/lxc/container-base>
  deny mount fstype=devpts,
  mount fstype=cgroup -> /sys/fs/cgroup/**,
  mount fstype=cgroup2 -> /sys/fs/cgroup/**,
  
  # 以下追加部分 /host/white.txt を許可して /host/black.txt を禁止します。
  audit deny /host/black.txt rw,
  audit allow /host/white.txt rw,
}
```

ややこしいことに更新をカーネルに通知するにはするにはこのファイルではなく `../lxc-containers` の更新を `apparmor_parser -r` コマンドで伝えます。この `/etc/apparmor.d/lxc-containers` の中で `lxc/` 内の各種 profile を include する仕組みになっています。

```
sudo apparmor_parser -r  ../lxc-containers
```

仕上げに `/var/lib/lxc/bbox/config` の `lxc.apparmor.profile` に作った profile 名を設定すると、指定した profile が有効になります。

```
lxc.apparmor.profile = bbox
```

試してみます。

```shell
$ sudo lxc-execute -n bbox -- sh -c 'echo Hello white >  /host/white.txt'
$ sudo lxc-execute -n bbox -- sh -c 'echo Hello black >  /host/black.txt'
sh: can't create /host/black.txt: Permission denied
```

うまくいきました！実は、上で

    audit allow /host/white.txt rw,

のように許可するファイルを書きましたが実はこの行には効果がありません。[container-base](https://github.com/lxc/lxc/blob/lxc-3.0.3/config/apparmor/abstractions/container-base) の中にすべてのファイルアクセスを許可する `file` というルールがあるために、すべてのファイルアクセスが予め許可されてしまっています。これが LXC + AppArmor の組み合わせではブラックリスト方式しか動かない理由です。一方で container-base の中を見ると涙ぐましい努力のグロブパターンでホワイトリストを実現しているので興味のある方は参考にしてください。

頑張れば container-base を使わず自分で profile ホワイトリスト方式で書いて更にセキュアな環境を作れると思いますが、私は大変過ぎて諦めました。

以上分かってしまえば簡単ですが、マニュアルを読むだけでは絶対に分からないノウハウでした。大枠 ChatGPT に教えてもらいました。

参考

* [Linux Containers - LXC - Security](https://linuxcontainers.org/lxc/security/)
    * LXC を使う際のセキュリティの考え方。
* [Linux Containers - LXC - Manpages - lxc.container.conf.5](https://linuxcontainers.org/lxc/manpages/man5/lxc.container.conf.5.html)
    * `lxc.apparmor.profile` が profile 名を表すという事が明記されておらず不明瞭なマニュアル。
* [LXC - Qiita](https://qiita.com/propella/items/18165c933bd485c2c0ae)
    * LXC の基本。前に書いた記事です。
* [LXC - Debian Wiki](https://wiki.debian.org/LXC#Configure_AppArmor)
    * なんとなく `lxc.apparmor.profile` が profile 名を表す事が分かる。