Antrea CNIのTrafficControlでIDSを使ってみる

[Antrea CNI](https://antrea.io/)は[v1.7.0でTrafficControl機能をサポート](https://github.com/antrea-io/antrea/releases/tag/v1.7.0)しました。[TrafficControl](https://github.com/antrea-io/antrea/blob/main/docs/traffic-control.md)はPodが送受信するトラフィックを制御し、特定のPodのトラフィックをローカルネットワークデバイスやトンネルを利用して転送やミラーすることが可能です。この機能を使うことによりIDS/IPSや外部のネットワークサービスを透過的に利用することが可能になります。今回は[このドキュメント](https://github.com/antrea-io/antrea/blob/main/docs/cookbooks/ids/README.md)に従ってTrafficControlを利用して、OSSのIDSである[Suricata](https://suricata.io)によるIDS/IPSを利用してみます。

## TrafficControlの有効化

TrafficControlはAntreaのFeatureGateで有効化します。

```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: antrea-config
  namespace: kube-system
  labels:
    app: antrea
data:
  antrea-agent.conf: |
    # Enable mirroring or redirecting the traffic Pods send or receive.
      TrafficControl: true
```

以下のマニフェストを利用すれば、githubで公開されているマニフェストを利用を使ってTrafficControlを有効化してAntreaをインストールできます。

```bash
$ curl -s https://raw.githubusercontent.com/antrea-io/antrea/main/build/yamls/antrea.yml | \
  sed "s/.*TrafficControl:.*/      TrafficControl: true/" | \
  kubectl apply -f -
```

CNIが有効化され、TrafficControl CRDが使えるようになります。

```bash
$ kubectl get pod -n kube-system -l app=antrea -o wide
NAME                                 READY   STATUS    RESTARTS   AGE   IP                NODE     NOMINATED NODE   READINESS GATES
antrea-agent-bpwbg                   2/2     Running   0          12m   192.168.200.101   master   <none>           <none>
antrea-agent-fbs74                   2/2     Running   0          12m   192.168.200.103   node2    <none>           <none>
antrea-agent-zcwbg                   2/2     Running   0          12m   192.168.200.102   node1    <none>           <none>
antrea-controller-7b84f75d88-5zbrd   1/1     Running   0          12m   192.168.200.102   node1    <none>           <none>

# kubectl explain trafficcontrols.crd.antrea.io
KIND:     TrafficControl
VERSION:  crd.antrea.io/v1alpha2

DESCRIPTION:
     <empty>

FIELDS:
   apiVersion   <string>
     APIVersion defines the versioned schema of this representation of an
     object. Servers should convert recognized schemas to the latest internal
     value, and may reject unrecognized values. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources

   kind <string>
     Kind is a string value representing the REST resource this object
     represents. Servers may infer this from the endpoint the client submits
     requests to. Cannot be updated. In CamelCase. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds

   metadata     <Object>
     Standard object's metadata. More info:
     https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata

   spec <Object> -required-
```

## TrafficControlリソースに関して

例えば、以下のTrafficControlリソースを定義することにより、`app=web`ラベルのあるPodのトラフィックを10.0.10.2のレシーバにVXLANでカプセル化してミラーリングします。

```yaml
apiVersion: crd.antrea.io/v1alpha2
kind: TrafficControl
metadata:
  name: mirror-web-app
spec:
  appliedTo:
    podSelector:
      matchLabels:
        app: web
  direction: Both
  action: Mirror
  targetPort:
    vxlan:
      remoteIP: 10.0.10.2
      destinationPort: 4789
      vni: 1
```

`.spec.action`は`Mirror`か`Redirect`を指定することが可能です。`Mirror`を指定する場合、パケットをミラーする先の`targetPort`の指定が必要です。`Redirect`を指定する場合、`targetPort`に加えて`returnPort`の指定が必要です。`returnPort`はOVSにトラフィックを送り返し、元の宛先に転送可能なポートを指定します。リダイレクトされたパケットはドロップするか、変更せずにOVSに送り返す必要があります。

`.spec.targetPort`は以下の6種類のポートを利用することができます。

- ovsInternal : 各ノードのOVS内部ポート。Podのトラフィックは同じノードのOVSの内部ポートにリダイレクトまたはミラーリングされます。指定された名前でAntreaが自動的にポートを生成します。
- device : すべてのノードに存在すネットワークデバイスにリダイレクトされます。Antreaはノード上のネットワークデバイスをOVSブリッジに接続して、トラフィックをリダイレクトまたはミラーリングします。
- geneve : GENEVEトンネルを利用して宛先にリダイレクトまたはミラーリングします。`remoteIP`を指定する必要があり、オプションとして`destinationPort`と`vni`を指定することが可能です。
- vxlan : VXALANトンネルを利用して宛先にリダイレクトまたはミラーリングします。`remoteIP`を指定する必要があり、オプションとして`destinationPort`と`vni`を指定することが可能です。
- gre : GREトンネルを利用して宛先にリダイレクトまたはミラーリングします。`remoteIP`を指定する必要があり、オプションとして`key`を指定することが可能です。
- erspan : ERSPANトンネルを利用して宛先にリミラーリングします。`remoteIP`を指定する必要があり、オプションとして`sessionID`と`version`を指定することが可能です。Version 2ではミラートラフィックの方向として`dir`(0:ingress/1:egress)と`hardwareID`を指定することが可能です。

## TrafficControlの設定

今回は以下のマニフェストで`app: web`ラベルがついたPodの送受信トラフィックをノードのOVSの`tap0`という名前のポートにミラーします。`tap0`はOVSのinternalPortとして自動的に作成されます。

```yaml
apiVersion: crd.antrea.io/v1alpha2
kind: TrafficControl
metadata:
  name: mirror-web-app-to-tap0
spec:
  appliedTo:
    podSelector:
      matchLabels:
        app: web
  direction: Both
  action: Mirror
  targetPort:
    ovsInternal:
      name: tap0
```

上記マニフェストを適用すると、Nodeにovsのinternalポートとして`tap0`が生成されたことが確認できます。

```bash
# kubectl exec -n kube-system -it antrea-agent-zcwbg -- ovs-vsctl show
64f956f9-bf6a-45f0-8130-47d5ba0222b3
    Bridge br-int
        datapath_type: system
        Port tap0
            Interface tap0
                type: internal
        Port antrea-tun0
            Interface antrea-tun0
                type: geneve
                options: {csum="true", key=flow, remote_ip=flow}
        Port coredns--e1e96f
            Interface coredns--e1e96f
        Port antrea-gw0
            Interface antrea-gw0
                type: internal
        Port coredns--5410d1
            Interface coredns--5410d1
    ovs_version: "2.17.0"
```

## Suricataのデプロイ

以下のマニフェストでを[Suricata](https://suricata.io)をDaemonSetとしてノードにデプロイします。Suricataは`hostNetwork`を利用し、起動時のオプションでインターフェースとして`tap0`を指定しているので、tap0インターフェースのトラフィックをモニタリングします。また、ホストのlogディレクトリをマウントしてIDSログをホストのファイルシステムにログとして出力する設定になっています。

```yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: suricata
spec:
  selector:
    matchLabels:
      app: suricata
  template:
    metadata:
      labels:
        app: suricata
      name: suricata
    spec:
      hostNetwork: true
      containers:
        - name: suricata
          image: jasonish/suricata:latest
          imagePullPolicy: IfNotPresent
          command:
            - /usr/bin/suricata
            - -i
            - tap0
          securityContext:
            capabilities:
              add:
                - NET_ADMIN
                - NET_RAW
                - SYS_NICE
          volumeMounts:
            - name: host-var-log-suricata
              mountPath: /var/log/suricata
      volumes:
        - name: host-var-log-suricata
          hostPath:
            path: /var/log/suricata
            type: DirectoryOrCreate
```

マニフェストを適用してPodの起動を確認します。

```bash
$ kubectl get pod -o wide
NAME             READY   STATUS    RESTARTS   AGE   IP                NODE    NOMINATED NODE   READINESS GATES
suricata-h64mm   1/1     Running   0          5s    192.168.200.102   node1   <none>           <none>
suricata-wx8jz   1/1     Running   0          5s    192.168.200.103   node2   <none>           <none>
```

## テスト

Web Podを起動して送受信トラフィックがSuricateによって認識されるかどうかを確認します。

Podを起動してNodePortで公開します。

```bash
$ kubectl create deploy web --image nginx:alpine

$ kubectl get pod -o wide
NAME                   READY   STATUS    RESTARTS   AGE   IP                NODE    NOMINATED NODE   READINESS GATES
suricata-h64mm         1/1     Running   0          15m   192.168.200.102   node1   <none>           <none>
suricata-wx8jz         1/1     Running   0          15m   192.168.200.103   node2   <none>           <none>
web-54f54687c6-t2zc7   1/1     Running   0          16s   10.244.2.3        node2   <none>           <none>

$ kubectl expose deploy web --type=NodePort --port=80

$ kubectl get svc
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP        54m
web          NodePort    10.97.224.182   <none>        80:30574/TCP   3s
```

NodePort経由でWeb podにアクセス可能なことを確認します。

```bash
$ curl --head http://192.168.200.103:30574
HTTP/1.1 200 OK
Server: nginx/1.23.1
Date: Wed, 14 Sep 2022 02:14:20 GMT
Content-Type: text/html
Content-Length: 615
Last-Modified: Tue, 19 Jul 2022 15:23:19 GMT
Connection: keep-alive
ETag: "62d6cc67-267"
Accept-Ranges: bytes
```

Web podに対して擬似的に攻撃を行ってみます。トラフィックはミラーされているだけなのでWeb podから404が帰ってきます。

```bash
$ curl --head http://192.168.200.103:30574/dlink/hwiz.html
HTTP/1.1 404 Not Found
Server: nginx/1.23.1
Date: Wed, 14 Sep 2022 02:20:16 GMT
Content-Type: text/html
Content-Length: 153
Connection: keep-alive
```

Podが起動しているノード(192.168.200.103)上で`/var/log/suricate/fast.log`を確認すると、Dlink Soho Routerの設定ページに対するアクセスが試みられたことが記録されています。

```bash
$ ssh root@192.168.200.103 cat /var/log/suricata/fast.log /var/log/suricata/fast.log
09/14/2022-02:20:16.558811  [**] [1:2008942:8] ET POLICY Dlink Soho Router Config Page Access Attempt [**] [Classification: Attempted Administrator Privilege Gain] [Priority: 1] {TCP} 10.244.2.1:3612 -> 10.244.2.3:80
```

次にWeb podから外部ホストに対して攻撃とみなされる通信を行ってみます。こちらも、通信は可能です。

```bash
$ kubectl exec deploy/web -- curl -s http://testmynids.org/uid/index.html
uid=0(root) gid=0(root) groups=0(root)
```

同じようにログファイルを確認すると、Outboundトラフィックに関してもIDSで攻撃の可能性が検出されています。

```bash
$ ssh root@192.168.200.103 cat /var/log/suricata/fast.log /var/log/suricata/fast.log
09/14/2022-02:25:03.789861  [**] [1:2013028:7] ET POLICY curl User-Agent Outbound [**] [Classification: Attempted Information Leak] [Priority: 2] {TCP} 10.244.2.3:55468 -> 18.65.168.10:80
09/14/2022-02:25:03.792058  [**] [1:2100498:7] GPL ATTACK_RESPONSE id check returned root [**] [Classification: Potentially Bad Traffic] [Priority: 2] {TCP} 18.65.168.10:80 -> 10.244.2.3:55468
```

## まとめ

AntreaのTrafficControlを利用すると、Podのネットワークトラフィックを転送・ミラーリング可能です。VMware NSXにが持つNetwork Intrspectionと同等の機能を提供できるため、将来的には[NSX Intelligence](https://www.vmware.com/products/nsx-intelligence-analytics-engine.html)で可視化されたり、[NSX NDR](https://www.vmware.com/products/nsx-network-detection-response.html)と連携できたりするんじゃないかと思います。