この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
背景
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の実装に依存しないため、 ユースケースによっては参照するログを使い分ける必要がありそうです。