gashirar's blog

ウイスキーがすき/美味しいものがすき/k8sがすき

KubernetesのAudit webhook backendを試す

はじめに

KubernetesのAudit周りをいろいろ調べていたところFalcoという製品を教えてもらった。(Kuromatsuさんありがとう。)

github.com

サンプルを疎通しようとしたのだが、自分の設定がよろしくないのかFalco PodにKubernetesのAudit Logが流れてこない。
問題の切り分けとして、勉強がてら自分でAudit Backendを立てて疎通してみようというのが本記事の主旨。

Kubernetes Audit Backendsとは?

公式ドキュメントより引用。

Audit backends persist audit events to an external storage. Kube-apiserver out of the box provides three backends:

  • Log backend, which writes events to a disk
  • Webhook backend, which sends events to an external API
  • Dynamic backend, which configures webhook backends through an AuditSink API object.

簡単に言うとAudit Eventを外部に永続化させるために仕組みで、今回は3つ目のDynamic Backendの方式で実装してみる。
(ちなみにDynamic Backendは1.18時点でalphaなことに注意)
1つ目については過去に記事があるのでそちらをどうぞ。

gashirar.hatenablog.com

環境

実装

Backend Serverの構築

Audit Logを受け取るBackend Serverはクラスタ内で起動させても良いし、外部で起動させても良い。FalcoのサンプルではDaemonSetとして各Nodeに起動するような構成になっていたが、今回はHost上に直接立てることにする。

// HTTPエコーサーバー HTTPリクエストボディデータを返す
let http = require('http');
let server = http.createServer();
let port = process.env.SERVER_PORT || 8080;

// クライアントからリクエストボディデータをレスポンスとして返す
server.on('request', function (req, res) {
    var data = '';
    req.on('data', function(chunk) {
        data += chunk;
    });

    req.on('end', function () {
        res.writeHead(200, {'Content-Type': 'application/json'});
        res.end(JSON.stringify({
            "body": data,
            }
        ));
    });
});

// HTTPの生データを出力する
server.on('connection', function (socket) {
    console.log('=== Raw Socket Data Start ===');
    socket.on('data', function (chunk) {
        console.log(chunk.toString());
    });
    socket.on('end', function () {
        console.log('=== Raw Socket Data End ===');
    });
});

server.listen(port, function () {
    console.log('listening on ' + port);
})

API Serverからのリクエストを標準出力に出す簡単なもの。
API Serverへのレスポンスの形式についてはどのようにすればいいかちょっとわからなかったので、とりあえず適当に返却。Falcoはどうやってるかなと思いソースを読んでみたが、下記のようなHTMLを返しているようにみえる。

bool k8s_audit_handler::handlePost(CivetServer *server, struct mg_connection *conn)
{
    // Ensure that the content-type is application/json
    const char *ct = server->getHeader(conn, string("Content-Type"));

    if(ct == NULL || string(ct) != "application/json")
    {
        mg_send_http_error(conn, 400, "Wrong Content Type");

        return true;
    }

    std::string post_data;
    get_post_data(conn, post_data);
    std::string errstr;

    if(!accept_uploaded_data(post_data, errstr))
    {
        errstr = "Bad Request: " + errstr;
        mg_send_http_error(conn, 400, "%s", errstr.c_str());

        return true;
    }

    std::string ok_body = "<html><body>Ok</body></html>";
    mg_send_http_ok(conn, "text/html", ok_body.size());
    mg_printf(conn, "%s", ok_body.c_str());

    return true;
}

https://github.com/falcosecurity/falco/blob/master/userspace/falco/webserver.cpp#L151

Minikubeの起動

minikube start --driver=virtualbox

MinikubeのAPI ServerにAudit Backendを設定

Falcoのドキュメントを読む限り、Audit Webhook Backendの設定はminikube startの引数では現状渡せないらしい。

github.com

Falcoのサンプルで提供されてるパッチあてスクリプトがほぼ流用できるので、それを利用してAPI Serverに設定する。

まずはFalcoのclone

git clone https://github.com/falcosecurity/falco.git

examples/k8s_audit_configに移動したら、

 bash enable-k8s-audit.sh minikube dynamic

で有効化する。

Falcoのドキュメントではaudit-sink.yamlにFalco ServiceのIPを設定しているが、今回は上記で作成したNode.jsサーバに向けたいので、IPを適宜変更する。疎通環境のaudit-sink.yamlは下記の通り。

apiVersion: auditregistration.k8s.io/v1alpha1
kind: AuditSink
metadata:
  name: falco-audit-sink
spec:
  policy:
    level: RequestResponse
    stages:
      - ResponseComplete
      - ResponseStarted
  webhook:
    throttle:
      qps: 10
      burst: 15
    clientConfig:
      url: "http://192.168.254.128:8080/"

最後にaudit-sink.yamlkubectl apply -fすればOK。

疎通確認

Node.jsで起動させたBackend Serverに下記のようなリクエストが送られていることが確認できた。 EventListをPostしてきている模様。EventListを加工するもよし、別情報を加えて出力するもよし、いろいろできると思う。

POST /?timeout=30s HTTP/1.1
Host: 192.168.254.128:8080
User-Agent: kube-apiserver-admission
Content-Length: 1755
Accept: application/json, */*
Content-Type: application/json
Accept-Encoding: gzip

{"kind":"EventList","apiVersion":"audit.k8s.io/v1","metadata":{},"items":[{"level":"RequestResponse","auditID":"f65a0d0e-01db-4c0f-980b-7631b4fad9ab","stage":"ResponseComplete","requestURI":"/api/v1/namespaces/kube-system/endpoints/kube-scheduler?timeout=10s","verb":"get","user":{"username":"system:kube-scheduler","groups":["system:authenticated"]},"sourceIPs":["192.168.99.100"],"userAgent":"kube-scheduler/v1.18.2 (linux/amd64) kubernetes/52c56ce/leader-election","objectRef":{"resource":"endpoints","namespace":"kube-system","name":"kube-scheduler","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":200},"responseObject":{"kind":"Endpoints","apiVersion":"v1","metadata":{"name":"kube-scheduler","namespace":"kube-system","selfLink":"/api/v1/namespaces/kube-system/endpoints/kube-scheduler","uid":"d17b474d-8d66-4fa6-9c23-8dddec463a39","resourceVersion":"3479","creationTimestamp":"2020-05-14T08:04:21Z","annotations":{"control-plane.alpha.kubernetes.io/leader":"{\"holderIdentity\":\"minikube_e8dce5a6-8c40-4313-8685-39a3cf67bf2d\",\"leaseDurationSeconds\":15,\"acquireTime\":\"2020-05-14T08:04:21Z\",\"renewTime\":\"2020-05-14T08:27:49Z\",\"leaderTransitions\":0}"},"managedFields":[{"manager":"kube-scheduler","operation":"Update","apiVersion":"v1","time":"2020-05-14T08:27:49Z","fieldsType":"FieldsV1","fieldsV1":{"f:metadata":{"f:annotations":{".":{},"f:control-plane.alpha.kubernetes.io/leader":{}}}}}]}},"requestReceivedTimestamp":"2020-05-14T08:27:51.878748Z","stageTimestamp":"2020-05-14T08:27:51.881105Z","annotations":{"authorization.k8s.io/decision":"allow","authorization.k8s.io/reason":"RBAC: allowed by ClusterRoleBinding \"system:kube-scheduler\" of ClusterRole \"system:kube-scheduler\" to User \"system:kube-scheduler\""}}]}

まとめ

  • Audit BackendでKubernetesのAudit Eventを外部に送ることができる
    • 今回はWebhookを設定して外部のサーバに送信

本番環境上で動かす場合はセキュアな通信にしたり、AuditSinkのパラメータチューニングも必要だと思う。
とりあえず動きはつかめたので、もともとの目的であるFalcoもやっていきたい。