AWS SDK の ClientSideMonitoring について調べてみた

2022.07.12

背景

https://github.com/iann0036/iamlive は AWS の API を読み取って動的に IAM を生成してくれる大変素晴らしいツールです。

iamlive ではモードは 2 種類あって、proxy として読み取るパターンと CSM を使って読み取るパターンがあります。

このツールを参考に CSM を取得して既存のスクリプトの挙動を理解するのに役立つツールを作れればと思い、コードを読んでみました。

CSM とは

python の AWS SDK 実装である boto では 実行した内容と実行結果をエージェントに送信する機構が追加されています。

https://github.com/boto/botocore/commit/14e0eab5c1e4aec437c3e558e6899de00fd5e98e

環境変数 AWS_CSM_ENABLED=true を設定すること, もしくは csm_enabled = true を AWS profile に記載することで有効になります。

全ての SDK が対応している訳ではなく、たとえば Go の AWS SDK v2 はユースケースの見直しから行われているようです。

https://github.com/aws/aws-sdk-go-v2/issues/1142

やってみた

ここのコードが CSM agent の全貌となっているようでした。

https://github.com/iann0036/iamlive/blob/main/iamlivecore/csm.go#L151-L204

  • 31000 ポートがデフォルトであること
  • UDP でやりとりしていること
  • 受け取るデータが JSON フォーマットであること

上記が agent 側で受け取るための要件のようです。

そのため、下記のようなスクリプトで実際のデータを確認してみました。

package main

import (
    "bytes"
    "flag"
    "fmt"
    "io"
    "log"
    "net"
    "os"
)

func main() {
    var (
        host string
        port int
    )

    flag.StringVar(&host, "host", "localhost", "")
    flag.IntVar(&port, "port", 31000, "")
    flag.Parse()

    if err := listenForEvents(host, port, os.Stdout); err != nil {
        log.Fatal(err)
    }
}

func listenForEvents(addr string, port int, w io.Writer) error {
    conn, err := net.ListenUDP("udp", &net.UDPAddr{
        IP:   net.ParseIP(addr),
        Port: port,
    })
    if err != nil {
        return fmt.Errorf("failed to start server: %w", err)
    }

    defer conn.Close()

    if err := conn.SetReadBuffer(1024 * 1024); err != nil {
        return fmt.Errorf("failed to set buffer: %w", err)
    }

    var buf [1024 * 1024]byte
    for {
        rlen, _, err := conn.ReadFromUDP(buf[:])
        if err != nil {
            return fmt.Errorf("failed to read data: %w", err)
        }

        if _, err := io.Copy(w, bytes.NewReader(buf[:rlen])); err != nil {
            return fmt.Errorf("failed to write data: %w", err)
        }

        if _, err := w.Write([]byte("\n")); err != nil {
            return fmt.Errorf("failed to write data: %w", err)
        }
    }
}

では 実際に aws s3 ls を実行して確認してみます。

$ export AWS_CSM_ENABLED=true
$ aws s3 ls

An error occurred (ExpiredToken) when calling the ListBuckets operation: The provided token has expired.
$ go run . | jq .
{
  "Version": 1,
  "ClientId": "",
  "Type": "ApiCallAttempt",
  "Service": "S3",
  "Api": "ListBuckets",
  "Timestamp": 1657606793456,
  "AttemptLatency": 878,
  "Fqdn": "s3.us-east-2.amazonaws.com",
  "UserAgent": "aws-cli/2.7.14 Python/3.10.5 Darwin/21.5.0 source/x86_64 prompt/off command/s3.ls",
  "AccessKey": "ASIAXXXXXXXXXXXXXXXX",
  "Region": "us-east-2",
  "SessionToken": "xxx",
  "HttpStatusCode": 400,
  "XAmzRequestId": "1X9N58VBR4VQHD7V",
  "XAmzId2": "VCsV9PkopYZPkU3oU3iDjSlFcjZf1KpygItV3UofMMi6j8MrNy6cW6HPoWdyACR4hmqRgjMbrEY=",
  "AwsException": "ExpiredToken",
  "AwsExceptionMessage": "The provided token has expired."
}
{
  "Version": 1,
  "ClientId": "",
  "Type": "ApiCall",
  "Service": "S3",
  "Api": "ListBuckets",
  "Timestamp": 1657606793455,
  "AttemptCount": 1,
  "Region": "us-east-2",
  "UserAgent": "aws-cli/2.7.14 Python/3.10.5 Darwin/21.5.0 source/x86_64 prompt/off command/s3.ls",
  "FinalHttpStatusCode": 400,
  "FinalAwsException": "ExpiredToken",
  "FinalAwsExceptionMessage": "The provided token has expired.",
  "Latency": 880,
  "MaxRetriesExceeded": 0
}

有効期限が切れたまま実行してしまいましたが、ログとしては残るようです。 ここらへんのログは CloudTrail にはないログという認識です。

$ eval "$(mfa gen aws | assume-profile --profile classmethod-dev)"
$ aws s3 ls

2020-03-30 10:21:11 xxx-xxxxxx-xxxxx-xxxxxxx-000000000000-xx-xxxxxxxxx-0
2021-09-10 14:33:16 xxx-xxx-xxx-xxxxxxx-xxxxxxx-xxxxxxxxxxxxxxxxxx-00xx0x00xxxx0
2021-11-18 10:23:17 xxx-xxx-xxx-xxxxxxx-xxxxxxx-xxxxxxxxxxxxxxxxxx-0x0xxxxxxxxx
2022-01-24 13:47:48 xxx-xxx000xxx-xxxxxx-000000000000-xx-xxxx-0
2020-06-18 23:42:04 xxxxxxxxxx-xxxxxxxxxxxxx-00xx0xxxxxxx0
2022-01-24 13:48:04 xxxxxxxxxx-xxxxxxxxxxxxx-xxxxx0x0xx00
2020-08-12 22:49:29 xxxxxxxxxx-xxxxxxxxxxxxx-xxxx0x00xxxx
2022-04-11 15:46:01 xx-xxxxxxxxx-xxx0x0xxxxxx-xx-xxxxxxxxx-0
2020-01-24 13:47:39 xx-xxxxxxx-000000000000
2021-04-27 13:09:16 xxxx-xxxxxxxxx-000000000000-xx-xxxxxxxxx-0
2021-04-27 11:48:30 xxxx-xxxxxxxxx-000000000000-xx-xxxx-0

実際の実行ログを取得できました。

{
  "Version": 1,
  "ClientId": "",
  "Type": "ApiCallAttempt",
  "Service": "S3",
  "Api": "ListBuckets",
  "Timestamp": 1657606929271,
  "AttemptLatency": 980,
  "Fqdn": "s3.us-east-2.amazonaws.com",
  "UserAgent": "aws-cli/2.7.14 Python/3.10.5 Darwin/21.5.0 source/x86_64 prompt/off command/s3.ls",
  "AccessKey": "ASIAXXXXXXXXXXXXXXXX",
  "Region": "us-east-2",
  "SessionToken": "xxx",
  "HttpStatusCode": 200,
  "XAmzRequestId": "VDKP9G5VJF9NYBM7",
  "XAmzId2": "4Ke5/rdd3U/ZX7Y3tzRSD4TN2GVMzTVadiUNN/1z8WcujWKRKQ/VT/wtz1p3aweN8pvpJGpkKnQ="
}
{
  "Version": 1,
  "ClientId": "",
  "Type": "ApiCall",
  "Service": "S3",
  "Api": "ListBuckets",
  "Timestamp": 1657606929270,
  "AttemptCount": 1,
  "Region": "us-east-2",
  "UserAgent": "aws-cli/2.7.14 Python/3.10.5 Darwin/21.5.0 source/x86_64 prompt/off command/s3.ls",
  "FinalHttpStatusCode": 200,
  "Latency": 982,
  "MaxRetriesExceeded": 0
}

まとめ

環境変数を設定するだけでCSMの送信が簡単にできました。またagent側も特に難しいコードは不要で実装できました。 一方で取得できる値もあくまでエラーやパフォーマンスで使用できることを目的としているように見受けられ、 今回の背景にあった既存スクリプトの概要をつかむ、といった内容には向いていませんでした。

参考サイトにもあったとおり、CloudTrail のほうがパラメータが含まれている点などでログは充実しており、またSDKの実装に依存しないため、 ユースケースによっては参照するログを使い分ける必要がありそうです。

参考