特定の外部ネットワークへの通信の制限にはファイアウォールなどを利用することが多いですが、コンテナや実行されたコマンド名などをもとに、通信を制御したいという需要が自分の中でありました。
具体的には 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 を適用することができるようになります。
- https://www.kernel.org/doc/html/latest/bpf/bpf_lsm.html
- https://archive.fosdem.org/2020/schedule/event/security_kernel_runtime_security_instrumentation/attachments/slides/3842/export/events/attachments/security_kernel_runtime_security_instrumentation/slides/3842/Kernel_Runtime_Security_Instrumentation.pdf
- https://goteleport.com/blog/preventing-data-exfiltration-with-ebpf/
- https://kinvolk.io/blog/2021/04/extending-systemd-security-features-with-ebpf/
例えば、次のような 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 では RestrictFileSystems
と RestrictNetworkInterfaces
のオプションが追加されており、systemd サービスのプロセスがアクセスできるファイルシステムやネットワークインターフェイスを制限できるようです。
bouheki
ここからさらに、コンテナ環境であるかどうかや、コマンド名、UID/GID などをベースに制限ができるようにしたくて、 bouheki というツールを作ってみました。 IPv6 へ未対応だったり、荒削りなところが多々ありますが、とりあえず動きます。
bouheki の機能
bouheki には次のような機能を持たせました。
- 指定した CIDR への通信のブロックとモニタリング
- CIDR に加えて「コマンド名」「UID/GID」「コンテナ環境のみ」をポリシーとして設定できる
bouheki を動かすのに必要な環境
README に書いていますが、新しめのカーネルが必要になります。
- Linux Kernel >= 5.7.0 であること
CONFIG_DEBUG_INFO_BTF
が有効であることCONFIG_LSM
にbpf
が含まれていること
主要なディストリビューションだと Ubuntu 20.10 以上で動作可能ですが、デフォルトでは CONFIG_LSM
に bpf
が含まれていないので、/etc/default/grub などでブートパラメータに付与して再起動を行う必要があります。
(cgroup_skb/egress
などに attach するようにすれば(cgroup v2 が使える)古いカーネルにも対応しつつ同じことが実現できますが、BTF / CO-RE などを考えるとやはり新しめのカーネル/ディストリビューションが必要になります。XDP はワカラナイ…)
動作例
bouheki では設定を YAML で記述します。例えば、以下の設定は次のようなポリシーを適用することになります。
- 10.0.1.1/24 へのアクセスのみ許可するが、10.0.1.71 へのアクセスはブロックする
- ただし、ブロックするのはコンテナからの通信のみで、ホスト側では自由に通信が可能
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 を大きく進化させる技術だと思うので、今後そういった製品が出てくると思う。ただし、新しめのカーネルが必要になるのがネックではある…。