特定の外部ネットワークへの通信の制限にはファイアウォールなどを利用することが多いですが、コンテナや実行されたコマンド名などをもとに、通信を制御したいという需要が自分の中でありました。
具体的には GitHub Self-hosted runner のような CI / CD 環境で、依存パッケージに悪意あるコードが入り込んでしまうようなサプライチェーン攻撃などを検知・防御し、意図せずにクレデンシャルなどの秘匿すべき情報が外部に漏洩するのを防ぎたいと思っていました。
このようなサプライチェーン攻撃への対策は様々ですが、実行時に悪意のある動作を検出するものとして、GitLab が Falco をベースとした Package Hunter などがあります。このツールは依存パッケージなどをインストールする際に実行されるシステムコールなどを監視するものです。
検知するだけであれば Package Hunter のように Falco を使えば良いのですが、怪しい動作をブロックするとなると Falco では実現できません。

そこで、KRSI(Kernel Runtime Security Instrumentation)で実現できないか試してみました。

KRSI

KRSI とは LSM + eBPF による実装で、LSM フックで BPF Program を実行することで、システムワイドな MAC / Audit policy を適用することができるようになります。

例えば、次のような BPF プログラムで socket_connect に attach して送信先アドレスが denylist に含まれていれば -EPERM を返して接続をブロックすることができます。

SEC("lsm/socket_connect")
int BPF_PROG(socket_connect, struct socket *sock, struct sockaddr *address, int addrlen) {
  struct sockaddr_in *inet_addr = (struct sockaddr_in*)address;

  // denylist は BPF map
  if (bpf_map_lookup_elem(&denylist, &inet_addr->sin_addr))
    return -EPERM;

  return 0;
}

LSM Hook + eBPF を使った実例

このような仕組みは Teleport に含まれていて、SSH セッションに対して外部ネットワークアクセスの制限を適用することができます。 また、systemd では RestrictFileSystemsRestrictNetworkInterfaces のオプションが追加されており、systemd サービスのプロセスがアクセスできるファイルシステムやネットワークインターフェイスを制限できるようです。

bouheki

ここからさらに、コンテナ環境であるかどうかや、コマンド名、UID/GID などをベースに制限ができるようにしたくて、 bouheki というツールを作ってみました。 IPv6 へ未対応だったり、荒削りなところが多々ありますが、とりあえず動きます。

GitHub - mrtc0/bouheki: bouheki is KRSI(eBPF+LSM) based Linux security auditing tool.
bouheki is KRSI(eBPF+LSM) based Linux security auditing tool. - GitHub - mrtc0/b...

bouheki の機能

bouheki には次のような機能を持たせました。

bouheki を動かすのに必要な環境

README に書いていますが、新しめのカーネルが必要になります。

主要なディストリビューションだと Ubuntu 20.10 以上で動作可能ですが、デフォルトでは CONFIG_LSMbpf が含まれていないので、/etc/default/grub などでブートパラメータに付与して再起動を行う必要があります。

(cgroup_skb/egress などに attach するようにすれば(cgroup v2 が使える)古いカーネルにも対応しつつ同じことが実現できますが、BTF / CO-RE などを考えるとやはり新しめのカーネル/ディストリビューションが必要になります。XDP はワカラナイ…)

動作例

bouheki では設定を YAML で記述します。例えば、以下の設定は次のようなポリシーを適用することになります。

network:
  # block か monitor を設定
  # block ... 接続をブロックする
  # monitor ... 接続をブロックせずにログに出力するだけ
  mode: block
  # container か host を設定
  # container ... コンテナの通信のみを対象
  # host ... ホスト全体の通信を対象
  target: container
  # 許可/制限するネットワークを指定
  cidr:
    allow:
      - 10.0.1.1/24
    deny:
      - 10.0.1.71/32

実際に試してみます。

$ sudo ./bouheki -config config/container.yaml
# コンテナから 10.0.1.1 へのアクセスはできる
$ docker run --rm curlimages/curl:latest -s -I http://10.0.1.1
HTTP/1.1 301 Moved Permanently
Location: https://10.0.1.1:443/
Date: Sun, 26 Sep 2021 13:21:36 GMT
Server: Server

# コンテナから 10.0.1.71 へのアクセスはできない
$ docker run --rm curlimages/curl:latest http://10.0.1.71
curl: (7) Couldn't connect to server

# ホストからは自由に通信可能
$ curl -I -s 10.0.1.71
HTTP/1.1 301 Moved Permanently
Date: Sun, 26 Sep 2021 13:23:10 GMT
Location: https://10.0.1.71/
Connection: close
Content-Type: text/html
Content-Length: 56

このときの bouheki がブロックしたログは次のようになります。

{
  "Action": "BLOCKED",
  "Addr": "10.0.1.71",
  "Comm": "curl",
  "Hostname": "cdb99d181368",
  "PID": 936350,
  "Port": 80,
  "level": "info",
  "msg": "Traffic is trapped in the filter.",
  "time": "2021-09-26T13:21:58Z"
}

その他の設定例

前述したように、コマンド名や UID/GID などをポリシーの条件として設定できます。

93.184.216.34 への通信に wget は使えるが curl は使えないポリシー

network:
  mode: block
  target: host
  cidr:
    allow:
      - 0.0.0.0/0
    deny:
      - 93.184.216.34/32
  command:
    allow:
      - 'wget'
    deny:
      - 'curl'
log:
  format: json
$ sudo ./bouheki -config testdata/command_deny.yml
...
$ curl 93.184.216.34
curl: (7) Couldn't connect to server

$ wget 93.184.216.34/index.html
--2021-09-26 13:31:38--  http://93.184.216.34/index.html
Connecting to 93.184.216.34:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 94 [text/html]
Saving to: ‘index.html’

index.html                        100%[==========================================================>]      94  --.-KB/s    in 0s

2021-09-26 13:31:39 (17.5 MB/s) - ‘index.html’ saved [94/94]

UID 0 は自由に通信できるが、UID 1000 はどこにも通信ができないポリシー

network:
  mode: block
  target: host
  cidr:
    allow:
      - 0.0.0.0/0
    deny: []
  uid:
    allow:
      - 0
    deny:
      - 1000
log:
  format: json
$ curl 93.184.216.34
curl: (7) Couldn't connect to server

$ sudo su
[sudo] password for mrtc0:
# curl http://93.184.216.34
<?xml version="1.0" encoding="iso-8859-1"?>
...

これから

KRSI はこれからの IDS / HIDS を大きく進化させる技術だと思うので、今後そういった製品が出てくると思う。ただし、新しめのカーネルが必要になるのがネックではある…。

eBPF