Falcoとaws-for-fluent-bitを使ってCloudWatchにEKSノード上の危険なシステムコール情報を収集する(Proxy環境)

この記事はBeeX Advent Calendar 2020の12/12の記事です。

EKSのデータプレーンをどう選ぶか考えたとき、個人的にはFargateを積極的に使いたいところですが、まだ制限も多くて要件にフィットしない場合もあります。

そんな時に選択肢として挙がるManaged Node Groupですが、ReInvent2020でSpotインスタンスの利用できる機能が発表されたり、また使いやすくなったと感じています。

ここではそんな、Managed Node Groupで展開したEC2ノードのランタイムセキュリティをチェックする方法としてFalcoを使う手順を書いていきます。

概要としては以下の図の通りです。

f:id:yomon8:20201211113918p:plain

Falcoは何か公式のドキュメントから引用します。

Falcoは実行時にカーネルからのLinuxシステムコールを解析し、強力なルールエンジンに対してストリームをアサートします。 ルールに違反した場合は、Falcoのアラートが発せられます。

falco.org

セキュリティ的に問題がありそうなシステムコールをルールベースで検知できます。ルールはデフォルトで用意されているものも利用できますし、自身で追加することも可能です。

では設定入れていきます。

EKS情報確認

以下のEKSクラスタで作業します。クラスタ名は MyCluster でプライベートクラスタとして構築しています。

$ aws eks describe-cluster --name MyCluster --query 'cluster | {"platformVersion": platformVersion, "k8sVersion": version}'
{
    "platformVersion": "eks.3", 
    "k8sVersion": "1.18"
}

Managed Node Groupで2台起動しています。

$ kubectl get node
NAME                                            STATUS   ROLES    AGE     VERSION
ip-10-0-1-254.ap-northeast-1.compute.internal   Ready    <none>   4m48s   v1.18.9-eks-d1db3c
ip-10-0-2-180.ap-northeast-1.compute.internal   Ready    <none>   5m44s   v1.18.9-eks-d1db3c

この記事の手順でProxy経由でイメージを取得できるようにしています。

yomon.hatenablog.com

Falcoのインストール

名前空間作成

今回利用する名前空間を作成します。この記事では falco-ns として作成します。

kubectl create namespace falco-ns

HelmでFalcoのインストール

完全プライベート環境なのでProxy通してHelmでインストールします。Proxyが無ければPorxy関係のオプションは外して実行してください。

オプションに指定できる値はこちらを参考にしてください。

PROXY_HOST=YOUR_PROXY_HOST
PORXY_PORT=8080

# Proxy設定をExport
export http_proxy=http://${PROXY_HOST}:${PORXY_PORT}
export https_proxy=http://${PROXY_HOST}:${PORXY_PORT}

helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
#-> Update Complete. ⎈Happy Helming!⎈

# 名前空間falco-nsにFalcoをインストール
# 要求リソースも調整しています
# それぞれデフォルト値は以下になります
# resources.limits.cpu         = 200m
# resources.limits.memory = 1024Mi  = 1Gi
helm install -n falco-ns falco falcosecurity/falco \
  --set proxy.httpProxy=${http_proxy} \
  --set proxy.httpsProxy=${https_proxy} \
  --set resources.limits.cpu=100m \
  --set resources.limits.memory=512Mi

DaemonSetなのでノード毎にFalcoのPodが1つ立ち上がります。

$ kubectl get po -n falco-ns -owide
NAME          READY   STATUS    RESTARTS   AGE   IP           NODE                                            NOMINATED NODE   READINESS GATES
falco-cpqhc   1/1     Running   0          59s   10.0.2.116   ip-10-0-2-180.ap-northeast-1.compute.internal   <none>           <none>
falco-t6r7z   1/1     Running   0          59s   10.0.1.228   ip-10-0-1-254.ap-northeast-1.compute.internal   <none>           <none>

Falcoの挙動確認

Falcoの挙動を動かしながら確認してみます。ここからはターミナルを2つ立ち上げて作業します。

説明上以下の番号付けます。

  • ① 今作業してるターミナル
  • ② 新しいターミナル

[ターミナル②] 被監視用コンソール

新しいターミナルを開き、Pod上のセッションに接続します。

kubectl run --generator=run-pod/v1 -it --rm console --image=debian:bullseye-slim
#↓接続できました
root@console:/#

[ターミナル①] イベントをキャプチャするFalco Podの特定

元のターミナルから先程起動したconsoleという名前のPodがどのノードで起動しているか確認します。

kubectl get po console -o=jsonpath='{.spec.nodeName}'
ip-10-0-1-254.ap-northeast-1.compute.internal

次にFalcoのDaemonsetがどこで起動しているか確認します。 今回は falco-t6r7zconsole と同じノードで起動しているので、ここで危険なシステムコールをキャプチャしたイベントがログ出力されるはずなのでこれを確認します。

$ kubectl get po -n falco-ns -owide
$ kubectl get po -n falco-ns -owide
NAME          READY   STATUS    RESTARTS   AGE   IP           NODE                                            NOMINATED NODE   READINESS GATES
falco-cpqhc   1/1     Running   0          36m   10.0.2.116   ip-10-0-2-180.ap-northeast-1.compute.internal   <none>           <none>
falco-t6r7z   1/1     Running   0          36m   10.0.1.228   ip-10-0-1-254.ap-northeast-1.compute.internal   <none>           <none>

tail -f をして次の待ち受けます。

$ kubectl logs -f -n falco-ns falco-t6r7z

f:id:yomon8:20201211110914p:plain

[ターミナル②] セキュリティイベントの発生

ここで以下のように /etc/shdow の読み込みと、 /bin/ 配下へのファイル作成を行ってみます。

$ root@console:/# cat /etc/shadow
$ root@console:/# touch /bin/dummy

f:id:yomon8:20201211111201p:plain

[ターミナル①] イベントのログ出力確認

Falcoの方でtail -fしていたと思いますが、そちらに戻るとイベントがキャプチャできているのがわかります。

f:id:yomon8:20201211111635p:plain

[ターミナル②] ログオフ

動きは確認できたので、console Podからはログオフしておきます。ログオフすると自動でPodも削除されます。

CloudWatchへの転送

Service Account作成

FluentbitのDaemonSetからEC2ノードのIAMロールを使ってCloudWatchに転送することも可能ですが、ここではサービスアカウントを作成しようと思います。

eksctl使ってない環境なので、こちらで使ったスクリプト使います。

yomon.hatenablog.com

以下の通り実行してService Accountと紐付いたIAMロールを作成します。

CLUSTER_NAME=MyCluster
SA_NAME=fluent-bit
SA_NAMESPACE=falco-ns
/bin/bash ./create_service_account.sh ${CLUSTER_NAME} ${SA_NAMESPACE} ${SA_NAME}
#-> serviceaccount/fluent-bit created

上記のスクリプトで作成されたIAMロールに設定する権限は以下を参考に設定します。

https://raw.githubusercontent.com/sysdiglabs/falco-aws-firelens-integration/master/eks/fluent-bit/aws/iam_role_policy.json

実際に設定した画面です。

ここから先の 内容は以下のリポジトリを元に調整していきます。

github.com

Kubernetesロールとバインディングの作成

先程作成したService AccountにK8sのロールを設定していきます。

Manifestを作成します。

SA_NAMESPACE=falco-ns
cat <<EOF > fluent-bit-sa-role-bindings.yml
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:
  name: pod-log-reader
  namespace: ${SA_NAMESPACE}
rules:
- apiGroups: [""]
  resources:
  - namespaces
  - pods
  verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:
  name: pod-log-crb
  namespace: ${SA_NAMESPACE}
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: pod-log-reader
subjects:
- kind: ServiceAccount
  name: fluent-bit
  namespace: ${SA_NAMESPACE}
EOF

適用します。

kubectl apply -f ./fluent-bit-sa-role-bindings.yml
#->clusterrole.rbac.authorization.k8s.io/pod-log-reader created
#->clusterrolebinding.rbac.authorization.k8s.io/pod-log-crb created

FluentBitの設定をConfigMapで作成

ConfigMapを作成しますが、ここの [Output] セクションを調整すると出力先のCloudWatch Logsのグループやログ名を指定できます。

設定については以下などが参考になります。

Amazon CloudWatch - Fluent Bit: Official Manual

今回はProxy配下のプライベート環境だったので、VPC Endpointを経由してログを渡したかったので、以下の設定を調整しています。

    [OUTPUT]
#省略~~~~~~~~
        endpoint logs.ap-northeast-1.amazonaws.com
        sts_endpoint sts.ap-northeast-1.amazonaws.com
#省略~~~~~~~~

ConfigMapのManifestを作成します。

SA_NAMESPACE=falco-ns
cat <<EOF > fluentbit-configmap.yml
apiVersion: v1
kind: ConfigMap
metadata:
  name: fluent-bit-config
  namespace: ${SA_NAMESPACE}
  labels:
    app.kubernetes.io/name: fluentbit
data:
  fluent-bit.conf: |
    [SERVICE]
        Parsers_File  parsers.conf
    [INPUT]
        Name              tail
        Tag               falco.*
        Path              /var/log/containers/falco*.log
        Parser            falco
        DB                /var/log/flb_falco.db
        Mem_Buf_Limit     5MB
        Skip_Long_Lines   On
        Refresh_Interval  10
    [OUTPUT]
        Name cloudwatch_logs
        Match falco.**
        region ap-northeast-1
        log_group_name falco
        log_stream_prefix fluentbit_alerts_
        auto_create_group true
        endpoint logs.ap-northeast-1.amazonaws.com
        sts_endpoint sts.ap-northeast-1.amazonaws.com
  parsers.conf: |
    [PARSER]
        Name        falco
        Format      json
        Time_Key    time
        Time_Format %Y-%m-%dT%H:%M:%S.%L
        Time_Keep   Off
        # Command      |  Decoder | Field | Optional Action
        # =============|==================|=================
        Decode_Field_As   json    log
EOF

適用します。

kubectl apply -f ./fluentbit-configmap.yml
#->configmap/fluent-bit-config created

FluentBitのDaemonSet作成

なおこの手順で利用するのはこちらのイメージとなります。

github.com

DockerHubからイメージを取得する場合は以下ですし、

https://hub.docker.com/r/amazon/aws-for-fluent-bit/tags

ECRからもイメージを取得できます。その場合は以下のssmコマンドでURL取得できます。

$ aws ssm get-parameters-by-path --path /aws/service/aws-for-fluent-bit/ --query 'Parameters[*].{Name:Name,URL:Value}' --output table
---------------------------------------------------------------------------------------------------------------------------
|                                                   GetParametersByPath                                                   |
+-----------------------------------------+-------------------------------------------------------------------------------+
|                  Name                   |                                      URL                                      |
+-----------------------------------------+-------------------------------------------------------------------------------+
|  /aws/service/aws-for-fluent-bit/2.0.0  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.0.0   |
|  /aws/service/aws-for-fluent-bit/2.1.0  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.1.0   |
|  /aws/service/aws-for-fluent-bit/2.10.0 |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.10.0  |
|  /aws/service/aws-for-fluent-bit/2.2.0  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.2.0   |
|  /aws/service/aws-for-fluent-bit/2.4.0  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.4.0   |
|  /aws/service/aws-for-fluent-bit/2.5.0  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.5.0   |
|  /aws/service/aws-for-fluent-bit/2.6.1  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.6.1   |
|  /aws/service/aws-for-fluent-bit/2.7.0  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.7.0   |
|  /aws/service/aws-for-fluent-bit/2.9.0  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.9.0   |
|  /aws/service/aws-for-fluent-bit/latest |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:latest  |
|  /aws/service/aws-for-fluent-bit/2.1.1  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.1.1   |
|  /aws/service/aws-for-fluent-bit/2.3.0  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.3.0   |
|  /aws/service/aws-for-fluent-bit/2.3.1  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.3.1   |
|  /aws/service/aws-for-fluent-bit/2.6.0  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.6.0   |
|  /aws/service/aws-for-fluent-bit/2.8.0  |  906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.8.0   |
+-----------------------------------------+-------------------------------------------------------------------------------+

今回は以下を使うことにします。

906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.10.0

DaemonSetのManifestを作成します。

SA_NAMESPACE=falco-ns
FLUENTBIT_IMAGE=906394416424.dkr.ecr.ap-northeast-1.amazonaws.com/aws-for-fluent-bit:2.10.0
cat <<EOF > fluentbit-daemonset.yml
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: fluentbit
  namespace: ${SA_NAMESPACE}
  labels:
    app.kubernetes.io/name: fluentbit
spec:
  selector:
    matchLabels:
      name: fluentbit
  template:
    metadata:
      labels:
        name: fluentbit
    spec:
      serviceAccountName: fluent-bit
      containers:
      - name: aws-for-fluent-bit
        image: ${FLUENTBIT_IMAGE}
        volumeMounts:
        - name: varlog
          mountPath: /var/log
        - name: varlibdockercontainers
          mountPath: /var/lib/docker/containers
          readOnly: true
        - name: fluent-bit-config
          mountPath: /fluent-bit/etc/
        - name: mnt
          mountPath: /mnt
          readOnly: true
        resources:
          limits:
            memory: 500Mi
          requests:
            cpu: 500m
            memory: 100Mi
      volumes:
      - name: varlog
        hostPath:
          path: /var/log
      - name: varlibdockercontainers
        hostPath:
          path: /var/lib/docker/containers
      - name: fluent-bit-config
        configMap:
          name: fluent-bit-config
      - name: mnt
        hostPath:
          path: /mnt
EOF
kubectl apply -f ./fluentbit-daemonset.yml
#-> daemonset.apps/fluentbit created

FalcoとFluentBitがノード数(今回は2台)分、それぞれのノード上で起動しました。

$ kubectl get po -n falco-ns -owide
NAME              READY   STATUS    RESTARTS   AGE    IP           NODE                                            NOMINATED NODE   READINESS GATES
falco-cpqhc       1/1     Running   0          126m   10.0.2.116   ip-10-0-2-180.ap-northeast-1.compute.internal   <none>           <none>
falco-t6r7z       1/1     Running   0          126m   10.0.1.228   ip-10-0-1-254.ap-northeast-1.compute.internal   <none>           <none>
fluentbit-4ftrj   1/1     Running   0          65s    10.0.2.53    ip-10-0-2-180.ap-northeast-1.compute.internal   <none>           <none>
fluentbit-n2hz6   1/1     Running   0          65s    10.0.1.212   ip-10-0-1-254.ap-northeast-1.compute.internal   <none>           <none>

動作確認

では、検知されるような作業をしてみます。

$ kubectl run --generator=run-pod/v1 -it --rm console --image=debian:bullseye-slim
$ root@console:/# cat /etc/shadow
$ root@console:/# touch /bin/dummy2
$ root@console:/# exit
#->pod "console" deleted

ログが出てます。上記の動作から、以下が検知されているのがわかります。

  • ターミナルアタッチされた
  • /etc/shadowが読まれた
  • /binディレクトリに書き込まれた
  • .bash_historyが書き換えられた

f:id:yomon8:20201211164909p:plain

Tips

どんなルールが設定されているか

以下のコマンドで設定ファイルそのものを見ることができます。

$ kubectl exec -n falco-ns falco-t6r7z cat /etc/falco/falco_rules.yaml | grep -e "rule:" -e "desc:"

バージョン合っていれば、こちらの内容とも同じです。 https://github.com/falcosecurity/charts/blob/master/falco/rules/falco_rules.yaml

掃除

kubectl delete -f ./fluentbit-daemonset.yml
kubectl delete -f ./fluentbit-configmap.yml
kubectl delete -f ./fluent-bit-sa-role-bindings.yml
kubectl delete sa -n falco-ns fluent-bit
helm uninstall -n falco-ns falco
kubectl delete ns falco-ns

後は、SAに紐付けたIAMロールを削除します。

参考URL

GitHub - sysdiglabs/falco-aws-firelens-integration

charts/README.md at master · falcosecurity/charts · GitHub

https://hub.docker.com/r/amazon/aws-for-fluent-bit

falco/falco_rules.yaml at master · falcosecurity/falco · GitHub