2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kubernetesの監査ログ(Audit Log)入門

2
Last updated at Posted at 2026-09-30

はじめに

以前、Kubernetesのeventログの一覧という記事で、kubectl describe pod の下の方に出てくるEventのReasonをカテゴリ別に整理しました。Eventは「Podやノードに何が起きたか」をKubernetes自身が教えてくれる情報でしたが、これとは別に「誰が・いつ・何を・どうしようとしたか」というAPIリクエストそのものを記録する仕組みがKubernetesにはあります。これが監査ログ(Audit Log)です。

「セキュリティ的に監査ログを有効にしておいた方がいい」という話はよく聞きますが、実際に有効化しようとすると、Policyの書き方、kube-apiserverのフラグの多さ、ログに何が出てくるのかのイメージのつかみにくさで手が止まることがあります。

なお、Kubernetesの文脈で「Audit Log」と言った場合、Linuxのaudit(auditd)やFalcoが記録するsyscallの情報、GKE/EKS/AKSなどのマネージドサービスが提供するAudit Logを指すこともあります。本記事のスコープは、kube-apiserverが出力する監査ログ(audit.k8s.ioのEvent)です。それ以外のAudit Logとの違いは第4章にまとめています。

そこでこの記事では、監査ログとは何か、それをどう設定するか、そして実際にどんな内容が記録されるのかを、実際にkindクラスタで生成させたログと合わせて整理します。

対象コンポーネントとバージョン

コンポーネント バージョン リポジトリ 本記事での役割
kube-apiserver v1.37.1(タグ) kubernetes/kubernetes 監査ログの生成・Policy評価・バックエンド出力の実装をソースコードで確認
kind v0.33.0 kubernetes-sigs/kind 検証環境。ノードイメージはkindest/node:v1.37.0

TL;DR

  • 監査ログはEventとは別物です。 EventはPod/ノードの状態遷移を表すKubernetesの通常のAPIオブジェクトですが、監査ログはkube-apiserverが処理したAPIリクエストそのものを記録する専用の仕組みで、audit.k8s.ioという別のAPI群で表現されます。
  • 1つのリクエストはRequestReceived→ResponseStarted(long-runningのみ)→ResponseComplete(またはPanic)という複数のStageのEventを生成しうる。 同じauditIDを持つ複数行が1つのリクエストのライフサイクルに対応します。
  • 記録される粒度(level)はPolicyファイルで細かく制御でき、最初にマッチしたルールが使われます。 何も指定しなければNone(記録しない)がデフォルトです。
  • 監査ログはPolicyを介してlog backend(ファイル出力)とwebhook backend(外部API送信)の両方、または同時に出力できます。 どちらもバッチ処理・切り詰め(truncate)の設定を個別に持ちます。
  • 監査ログのannotationsには、認可の可否理由・PodSecurityの違反内容・非推奨API利用の警告・レイテンシ内訳など、level: Metadataだけでも読み取れる情報が多く詰まっています。 どのコンポーネントがどんなキーで何を書き込むかを知っていると、ログの読み方が変わります。

第1章: 監査ログとは何か

1.1 Eventとの違い

Kubernetesの公式ドキュメントは、監査(auditing)を次のように説明しています。

Kubernetes auditing provides a security-relevant, chronological set of records documenting the sequence of actions in a cluster. The cluster audits the activities generated by users, by applications that use the Kubernetes API, and by the control plane itself.

(参考: Auditing)

同じページには次のような注記もあります。

The configuration of an Audit Event configuration is different from the Event API object.

(参考: Auditing)

前回の記事で扱ったEvent(events.k8s.io)は「PodがスケジューリングされたのでSchedulerがScheduledというEventを書いた」というように、各コンポーネントが自発的に「何が起きたか」を通知するためのものでした。一方、監査ログのEvent(audit.k8s.io)は、kube-apiserver自身が「どんなHTTPリクエストを受けて、誰が、何に対して、何をしようとして、どう応答したか」を機械的に記録するものです。両者は名前が同じ"Event"でも別のAPIグループの別の型で、保持期間・生成契機・記録内容がまったく異なります。

1.2 Audit Eventの構造

監査ログの1行は、audit.k8s.io/v1のEventという型でシリアライズされます。主なフィールドは次の通りです。

フィールド 意味
level 記録の粒度。None/Metadata/Request/RequestResponse
auditID リクエスト単位で採番される一意なID。同じリクエストの複数Stageのイベントは同じauditIDを持つ
stage このイベントが生成された処理段階。RequestReceived/ResponseStarted/ResponseComplete/Panic
verb Kubernetesの動詞(get/list/create/update/delete/watch等)
user / impersonatedUser リクエストを認証したユーザー情報、および偽装(impersonation)後のユーザー情報
sourceIPs X-Forwarded-For等から復元したクライアントの送信元IP
objectRef 操作対象のリソース(resource/namespace/name/subresource等)
requestObject / responseObject リクエスト/レスポンスのボディ。levelがRequest/RequestResponseのときのみ記録
responseStatus 応答のHTTPステータスコードとStatusオブジェクト
annotations 認証・認可・admissionの各プラグインが自由に書き込めるキーバリュー

(参考: Event | Audit configuration (v1) reference)

ソースコード上の定義は次の通りです。

// Event captures all the information that can be included in an API audit log.
type Event struct {
	metav1.TypeMeta `json:""`

	// AuditLevel at which event was generated
	Level Level `json:"level" protobuf:"bytes,1,opt,name=level,casttype=Level"`

	// Unique audit ID, generated for each request.
	AuditID types.UID `json:"auditID" protobuf:"bytes,2,opt,name=auditID,casttype=k8s.io/apimachinery/pkg/types.UID"`
	// Stage of the request handling when this event instance was generated.
	Stage Stage `json:"stage" protobuf:"bytes,3,opt,name=stage,casttype=Stage"`
	...

(参考: types.go#L71-L146)

1.3 Levelは4段階

levelは「何を記録するか」を決める4段階です。

Level 記録内容
None 記録しない
Metadata user/verb/objectRef等のメタデータのみ。リクエスト・レスポンスのボディは含まない
Request Metadataに加えてリクエストボディを記録(非リソースリクエストには適用されない)
RequestResponse Requestに加えてレスポンスボディも記録(非リソースリクエストには適用されない)

(参考: types.go#L34-L49)

1.4 Stageは1リクエストにつき複数回生成されうる

stageは「そのイベントがリクエスト処理のどの段階で作られたか」を表します。

  • RequestReceived: 監査ハンドラがリクエストを受け取った直後、ハンドラチェーンに委譲する前
  • ResponseStarted: レスポンスヘッダーが送信された後、ボディが送信される前。long-runningなリクエスト(watch等)でのみ生成される
  • ResponseComplete: レスポンスボディの送信が完了した時点
  • Panic: panicが発生した場合

(参考: types.go#L51-L67)

この4段階は仕様上の区分ですが、実際に1つのリクエストが何回イベントを生成するかはソースコードを読むとより具体的にわかります。監査ハンドラの実装は次のようになっています。

func WithAudit(handler http.Handler, sink audit.Sink, policy audit.PolicyRuleEvaluator, longRunningCheck request.LongRunningRequestCheck) http.Handler {
	...
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		ac, err := evaluatePolicyAndCreateAuditEvent(req, policy, sink)
		...
		if processed := ac.ProcessEventStage(ctx, auditinternal.StageRequestReceived); !processed {
			...
		}

		// intercept the status code
		isLongRunning := false
		if longRunningCheck != nil {
			ri, _ := request.RequestInfoFrom(ctx)
			if longRunningCheck(req, ri) {
				isLongRunning = true
			}
		}
		respWriter := decorateResponseWriter(ctx, w, isLongRunning)

		// send audit event when we leave this func, either via a panic or cleanly. In the case of long
		// running requests, this will be the second audit event.
		defer func() {
			if r := recover(); r != nil {
				...
				ac.ProcessEventStage(ctx, auditinternal.StagePanic)
				return
			}
			...
			ac.ProcessEventStage(ctx, auditinternal.StageResponseComplete)
		}()
		handler.ServeHTTP(respWriter, req)
	})
}

(参考: audit.go#L42-L110)

つまり、RequestReceivedは(PolicyのomitStagesで除外していない限り)必ず生成され、ResponseComplete(またはpanic時のPanic)がリクエストの終端で必ず生成されます。ResponseStartedは、そのリクエストがLongRunningRequestCheckによって長時間実行系と判定された場合(watch、pods/log --follow、exec等)にだけ、レスポンスヘッダー送信のタイミングで追加生成されます。

実際にkindクラスタ(v1.37.0)でkubectl logsを実行して監査ログを見ると、pods/logサブリソースへのアクセスがResponseStarted→ResponseCompleteの2行になっていることが確認できます(同じauditIDであることに注目してください)。pods/logは--followを付けていなくても、kube-apiserver側でlong-runningなリクエストとして扱われるためです。

{
  "kind": "Event",
  "apiVersion": "audit.k8s.io/v1",
  "level": "Metadata",
  "auditID": "e24618d8-4c77-485c-a02c-3890311147f0",
  "stage": "ResponseStarted",
  "requestURI": "/api/v1/namespaces/audit-demo-ns/pods/demo-nginx2/log?container=demo-nginx2",
  "verb": "get",
  "user": { "username": "kubernetes-admin", "groups": ["kubeadm:cluster-admins", "system:authenticated"] },
  "objectRef": { "resource": "pods", "namespace": "audit-demo-ns", "name": "demo-nginx2", "apiVersion": "v1", "subresource": "log" },
  "responseStatus": { "metadata": {}, "code": 200 },
  "annotations": {
    "authorization.k8s.io/decision": "allow",
    "authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding \"kubeadm:cluster-admins\" of ClusterRole \"cluster-admin\" to Group \"kubeadm:cluster-admins\""
  }
}
{
  "kind": "Event",
  "apiVersion": "audit.k8s.io/v1",
  "level": "Metadata",
  "auditID": "e24618d8-4c77-485c-a02c-3890311147f0",
  "stage": "ResponseComplete",
  "requestURI": "/api/v1/namespaces/audit-demo-ns/pods/demo-nginx2/log?container=demo-nginx2",
  "verb": "get",
  "objectRef": { "resource": "pods", "namespace": "audit-demo-ns", "name": "demo-nginx2", "apiVersion": "v1", "subresource": "log" },
  "responseStatus": { "metadata": {}, "code": 200 },
  "annotations": {
    "authorization.k8s.io/decision": "allow",
    "authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding \"kubeadm:cluster-admins\" of ClusterRole \"cluster-admin\" to Group \"kubeadm:cluster-admins\""
  }
}

第2章: 監査ログを取得するための設定方法

2.1 Policyファイルの書き方

監査ログはデフォルトでは何も記録しません。kube-apiserverの--audit-policy-fileフラグでPolicyファイルを渡すことで初めて有効になります。

You can pass a file with the policy to kube-apiserver using the --audit-policy-file flag. If the flag is omitted, no events are logged. Note that the rules field must be provided in the audit policy file. A policy with no (0) rules is treated as illegal.

(参考: Auditing)

Policyはrulesの配列で、上から順に評価され、最初にマッチしたルールのLevelが採用されます。マッチするルールが無ければデフォルトのNoneになります。

func (p *policyRuleEvaluator) EvaluatePolicyRule(attrs authorizer.Attributes) auditinternal.RequestAuditConfig {
	for _, rule := range p.Rules {
		if ruleMatches(&rule, attrs) {
			return auditinternal.RequestAuditConfig{
				Level:             rule.Level,
				OmitStages:        rule.OmitStages,
				OmitManagedFields: isOmitManagedFields(&rule, p.OmitManagedFields),
			}
		}
	}

	return auditinternal.RequestAuditConfig{
		Level:             DefaultAuditLevel, // = audit.LevelNone
		OmitStages:        p.OmitStages,
		OmitManagedFields: p.OmitManagedFields,
	}
}

(参考: checker.go#L64-L80)

公式ドキュメントに掲載されている実際のPolicy例は次の通りです(実際にkindクラスタでこのPolicyを使って動作確認しました)。

apiVersion: audit.k8s.io/v1 # This is required.
kind: Policy
# Don't generate audit events for all requests in RequestReceived stage.
omitStages:
  - "RequestReceived"
rules:
  # Log pod changes at RequestResponse level
  - level: RequestResponse
    resources:
    - group: ""
      # Resource "pods" doesn't match requests to any subresource of pods,
      # which is consistent with the RBAC policy.
      resources: ["pods"]
  # Log "pods/log", "pods/status" at Metadata level
  - level: Metadata
    resources:
    - group: ""
      resources: ["pods/log", "pods/status"]

  # Don't log requests to a configmap called "controller-leader"
  - level: None
    resources:
    - group: ""
      resources: ["configmaps"]
      resourceNames: ["controller-leader"]

  # Don't log watch requests by the "system:kube-proxy" on endpoints or services
  - level: None
    users: ["system:kube-proxy"]
    verbs: ["watch"]
    resources:
    - group: "" # core API group
      resources: ["endpoints", "services"]

  # Don't log authenticated requests to certain non-resource URL paths.
  - level: None
    userGroups: ["system:authenticated"]
    nonResourceURLs:
    - "/api*" # Wildcard matching.
    - "/version"

  # A catch-all rule to log all other requests at the Metadata level.
  - level: Metadata
    # Long-running requests like watches that fall under this rule will not
    # generate an audit event in RequestReceived.
    omitStages:
      - "RequestReceived"

(参考: audit-policy.yaml)

resourcesは「グループ+リソース名」で指定し、podsとpods/logは別物として扱われる点に注意が必要です。上の例のコメントにもある通り、resources: ["pods"]はPod本体の作成・更新・削除にはマッチしますが、pods/logやpods/statusのようなサブリソースにはマッチしません。RBACのリソース指定と同じ考え方です。

このPolicyを実際にkindクラスタ(kubeadmのClusterConfiguration.apiServer.extraArgs経由で--audit-policy-fileと--audit-log-pathを設定)に適用し、Podを1つ作成すると、podsへのcreateはRequestResponseレベルで、それに続くkube-schedulerによるpods/<name>/bindingへのcreateは(pods/logやpods/statusではないため)最後の catch-all ルールにマッチしてMetadataレベルで、それぞれ別のイベントとして記録されました。

{
  "kind": "Event",
  "apiVersion": "audit.k8s.io/v1",
  "level": "RequestResponse",
  "auditID": "100e29a8-98bc-43a6-ac37-0842f98f8af5",
  "stage": "ResponseComplete",
  "requestURI": "/api/v1/namespaces/audit-demo-ns/pods?fieldManager=kubectl-run",
  "verb": "create",
  "user": { "username": "kubernetes-admin", "groups": ["kubeadm:cluster-admins", "system:authenticated"] },
  "objectRef": { "resource": "pods", "namespace": "audit-demo-ns", "name": "demo-nginx", "apiVersion": "v1" },
  "responseStatus": { "metadata": {}, "code": 201 },
  "requestObject": { "kind": "Pod", "apiVersion": "v1", "metadata": { "name": "demo-nginx", "labels": { "run": "demo-nginx" } }, "spec": { "containers": [{ "name": "demo-nginx", "image": "nginx:1.27" }] } },
  "responseObject": { "kind": "Pod", "apiVersion": "v1", "metadata": { "name": "demo-nginx", "namespace": "audit-demo-ns", "uid": "4d31fbce-4e0c-4e7d-b0b9-a87735493205" } }
}
{
  "kind": "Event",
  "apiVersion": "audit.k8s.io/v1",
  "level": "Metadata",
  "auditID": "bcb29c35-5c82-41fa-bf79-96720dba4d2f",
  "stage": "ResponseComplete",
  "requestURI": "/api/v1/namespaces/audit-demo-ns/pods/demo-nginx/binding",
  "verb": "create",
  "user": { "username": "system:kube-scheduler", "groups": ["system:authenticated"] },
  "userAgent": "kube-scheduler/v1.37.0 (linux/arm64) kubernetes/f54c212/scheduler",
  "objectRef": { "resource": "pods", "namespace": "audit-demo-ns", "name": "demo-nginx", "apiVersion": "v1", "subresource": "binding" },
  "responseStatus": { "metadata": {}, "status": "Success", "code": 201 },
  "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\""
  }
}

(requestObject/responseObjectは元のイベントから主要なフィールドのみ抜粋しています)

RequestResponseレベルではrequestObject/responseObjectにリソースの中身が丸ごと記録されるため、SecretやConfigMapのような機密情報を含みうるリソースに安易にRequestResponseを割り当てると、監査ログ自体が機密情報の置き場になってしまいます。公式ドキュメントも、リクエストボディがJSON Patchのような形式になりうる点を含めて注意を促しています。

In case of patches, request body is a JSON array with patch operations, not a JSON object with an appropriate Kubernetes API object.

(参考: Auditing)

2.2 バックエンド

kube-apiserverは、Policyによって記録対象になった監査イベントを、次の2種類のバックエンドに出力できます。

バックエンド 出力先 有効にするフラグ
log backend ローカルファイル(または標準出力) --audit-log-path
webhook backend 外部のHTTP API --audit-webhook-config-file

どちらのバックエンドにも、PolicyでNone以外に決まったイベントが渡されます。バックエンドごとに記録内容を変えることはできません。両方を同時に有効にでき、フラグを指定したバックエンドだけが有効になります。バッチ処理・切り詰め(truncate)の設定は、バックエンドごとに個別に持ちます(2.2.3節)。

2.2.1 log backend

log backendは監査イベントをファイルにJSON Lines形式(1行1JSON)で書き込みます。主なフラグは次の通りです。

フラグ 既定値 意味
--audit-log-path (なし) 出力先ファイルパス。未指定ならlog backendは無効。-で標準出力
--audit-log-format json jsonまたはlegacy(1行テキスト形式)
--audit-log-maxage 366 ローテートしたログをファイル名のタイムスタンプ基準で保持する日数。0で無期限
--audit-log-maxbackup 100 保持するローテート済みログファイル数の上限
--audit-log-maxsize 100 ローテートされるまでのログファイルサイズ上限(MB)
--audit-log-compress false ローテートしたファイルをgzip圧縮するか

(参考: kube-apiserver)

legacy形式を選んだ場合、1行は次のような文字列になります。

return fmt.Sprintf("%s AUDIT: id=%q stage=%q ip=%q method=%q user=%q groups=%q as=%q asgroups=%q user-agent=%q namespace=%q uri=%q response=\"%s\"",
	ev.RequestReceivedTimestamp.Format(time.RFC3339Nano), ev.AuditID, ev.Stage, ip, ev.Verb, username, groups, asuser, asgroups, ev.UserAgent, namespace, ev.RequestURI, response)

(参考: format.go#L30-L65)

legacy形式は1行に収まる分、requestObject/responseObjectのような構造化データを保持できません。ログ集約基盤にそのまま投入してjq等で加工したい場合はjson形式(既定値)を選ぶのが素直だと思います。

kube-apiserverをPodとして動かしている場合、Policyファイルとログファイルの両方をホストにマウントしておく必要があります。kubeadmクラスタではkube-apiserverはstatic podとして動くため、/etc/kubernetes/manifests/kube-apiserver.yamlに直接これらのフラグとvolumeMounts/volumesを追記する形になります。フラグ・マウント・ホスト側のパスの対応関係をまとめると、次のようになります。

apiVersion: v1
kind: Pod
metadata:
  name: kube-apiserver
  namespace: kube-system
spec:
  containers:
  - name: kube-apiserver
    image: registry.k8s.io/kube-apiserver:v1.37.1
    command:
    - kube-apiserver
    - --audit-policy-file=/etc/kubernetes/audit-policy.yaml
    - --audit-log-path=/var/log/kubernetes/audit/audit.log
    - --audit-log-format=json
    - --audit-log-maxbackup=10
    - --audit-log-maxsize=100
    volumeMounts:
    - name: audit-policy
      mountPath: /etc/kubernetes/audit-policy.yaml
      readOnly: true
    - name: audit-log
      mountPath: /var/log/kubernetes/audit/
      readOnly: false
  volumes:
  - name: audit-policy
    hostPath:
      path: /etc/kubernetes/audit-policy.yaml
      type: File
  - name: audit-log
    hostPath:
      path: /var/log/kubernetes/audit/
      type: DirectoryOrCreate

volumeMounts側のmountPath(コンテナ内のパス)とvolumes側のhostPath.path(ノード上の実パス)がそれぞれnameで対応付いている点に注目してください。この記事の検証で実際に使ったkindクラスタも、kubeadmConfigPatches経由でこの形のapiServer.extraArgs/extraVolumesをClusterConfigurationに反映しています。

(参考: Auditing)

2.2.2 webhook backend

webhook backendは監査イベントを外部のHTTP APIへ送信します。設定ファイルはkubeconfigと同じ形式です。

フラグ 意味
--audit-webhook-config-file webhookの接続先・認証情報を書いたkubeconfig形式のファイル
--audit-webhook-initial-backoff 初回失敗後、リトライまで待つ時間(既定10s)。以降は指数バックオフ

(参考: Auditing)

2.2.3 バッチ処理・切り詰め(truncate)

log backend・webhook backendのどちらも、送信をバッチ化するかどうかを--audit-<backend>-modeで選べます。

モード 挙動
batch イベントをバッファし、非同期にバッチ書き込みする。webhookの既定はこちら
blocking 各イベントの処理完了までAPIサーバーの応答をブロックする。logの既定はこちら
blocking-strict blockingと同様だが、RequestReceived段階での書き込み失敗時にリクエスト自体を失敗させる

(参考: Auditing)

ソースコード上も、この3モードの差はシンプルです。

func (o *AuditBatchOptions) wrapBackend(delegate audit.Backend) audit.Backend {
	if o.Mode == ModeBlockingStrict {
		return delegate
	}
	if o.Mode == ModeBlocking {
		return &ignoreErrorsBackend{Backend: delegate}
	}
	return pluginbuffered.NewBackend(delegate, o.BatchConfig)
}

(参考: audit.go#L397-L405)

logのバックエンドでbatchモードを使うのは非推奨とされています(ドキュメント曰く「Batching is not recommended for the log backend」)。ローカルファイルへの書き込みをわざわざ非同期化するメリットが薄く、プロセスクラッシュ時に未書き込みのイベントを失うリスクの方が大きいという判断だと考えられます。

また、1件のイベントやバッチが肥大化しすぎないよう、truncate機能もあります。

フラグ 既定値 意味
--audit-log-truncate-enabled false truncateを有効にするか
--audit-log-truncate-max-event-size 102400(100KiB) 1イベントの最大サイズ。超えるとまずrequestObject/responseObjectを削り、それでも収まらなければイベント自体を破棄
--audit-log-truncate-max-batch-size 10485760(10MiB) 1バッチの最大サイズ

webhook backendにも同名の--audit-webhook-truncate-enabled/--audit-webhook-truncate-max-event-size/--audit-webhook-truncate-max-batch-sizeがあり、既定のサイズは同じです。

(参考: Auditing、kube-apiserver)

truncateされたイベントにはaudit.k8s.io/truncated: "true"というannotationが付与されます(この後の一覧でも触れます)。

第3章: 監査ログに記録される内容の一覧

Eventの記事ではreasonをカテゴリ別に整理しましたが、監査ログにはreasonに相当する単一のフィールドはありません。その代わり、認証・認可・admission各プラグインがannotationsに自由なキーで情報を書き込む形になっています。ここでは、Kubernetes本体が標準で設定する主要なannotationsキーを、それがどこから来て何がわかるのかとあわせて整理します。

3.1 認可(authorization)系

Annotationキー 値の例 わかること ソース
authorization.k8s.io/decision allow / forbid リクエストが認可されたかどうか authorization.go#L39-L40
authorization.k8s.io/reason RBAC: allowed by ClusterRoleBinding "..." of ClusterRole "..." to User "..." 認可の判断根拠(どのRBACオブジェクトが効いたか等) 同上

実際に、権限を持たないServiceAccountでPodの削除を試みると、403 Forbiddenのレスポンスとともに次のようなイベントが記録されました。

{
  "kind": "Event",
  "apiVersion": "audit.k8s.io/v1",
  "level": "RequestResponse",
  "stage": "ResponseComplete",
  "requestURI": "/api/v1/namespaces/audit-demo-ns/pods/demo-nginx",
  "verb": "delete",
  "user": {
    "username": "system:serviceaccount:audit-demo-ns:limited-sa",
    "groups": ["system:serviceaccounts", "system:serviceaccounts:audit-demo-ns", "system:authenticated"]
  },
  "objectRef": { "resource": "pods", "namespace": "audit-demo-ns", "name": "demo-nginx", "apiVersion": "v1" },
  "responseStatus": {
    "status": "Failure",
    "message": "pods \"demo-nginx\" is forbidden: User \"system:serviceaccount:audit-demo-ns:limited-sa\" cannot delete resource \"pods\" in API group \"\" in the namespace \"audit-demo-ns\"",
    "reason": "Forbidden",
    "code": 403
  },
  "annotations": {
    "authorization.k8s.io/decision": "forbid",
    "authorization.k8s.io/reason": ""
  }
}

responseStatus.messageを見ればこの1件が何によって拒否されたかはわかりますが、authorization.k8s.io/decisionがあることで、jq等で「forbidのイベントだけを抽出する」という機械的な絞り込みが容易になります。逆にreasonが空文字列になるのは、RBACが「マッチするルールが無かった」という消極的な理由で拒否した場合で、「どのルールに違反したか」を積極的に説明できるケースとは異なる、という点も実際にログを見て気づいた挙動です。

3.2 PodSecurity admission系

Pod Security Admissionは、PodがどのレベルのPod Security Standardで評価・免除されたかを、次のannotationで記録します。

Annotationキー 値の例 わかること ソース
pod-security.kubernetes.io/exempt namespace user/namespace/runtimeClassのどの軸でPodSecurityの適用が免除されたか constants.go#L37-L49
pod-security.kubernetes.io/enforce-policy restricted:latest どのレベル・バージョンのPodSecurity Standardで許可/拒否したか 同上
pod-security.kubernetes.io/audit-violations would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false ... どのフィールドがどの基準に違反しているか 同上

(参考: Audit Annotations)

audit-violationsはPodSecurityがenforceではなくaudit/warnモードで動いているときに特に有用です。「今すぐPodを拒否はしないが、どのフィールドが将来のenforce化で引っかかるか」を監査ログだけから洗い出せます。

公式ドキュメントに掲載されている値をEventのフィールドに当てはめると、restrictedポリシーをaudit/warnモードで運用している名前空間でPodを作成したときのイベントは次のようになります(実クラスタからの採取ではなく、ドキュメントの値をもとに再構成した例です)。

kind: Event
apiVersion: audit.k8s.io/v1
level: Metadata
stage: ResponseComplete
requestURI: /api/v1/namespaces/demo/pods
verb: create
objectRef:
  resource: pods
  namespace: demo
  name: example
  apiVersion: v1
responseStatus:
  code: 201
annotations:
  pod-security.kubernetes.io/enforce-policy: "restricted:latest"
  pod-security.kubernetes.io/audit-violations: >-
    would violate PodSecurity "restricted:latest": allowPrivilegeEscalation != false
    (container "example" must set securityContext.allowPrivilegeEscalation=false)

一方、免除(exempt)が効いた場合は、enforce-policy/audit-violationsの代わりに次のようにexemptだけが記録されます。

annotations:
  pod-security.kubernetes.io/exempt: namespace

3.3 ValidatingAdmissionPolicy系

CELベースのValidatingAdmissionPolicyがリクエストを拒否、またはエラーとともに評価した場合は、次のannotationにJSON形式で詳細が記録されます。

Annotationキー 値の例 わかること ソース
validation.policy.admission.k8s.io/validation_failure [{"message": "...", "policy": "...", "binding": "...", "expressionIndex": "1", "validationActions": ["Audit"]}] どのValidatingAdmissionPolicy/ValidatingAdmissionPolicyBindingのどのCEL式が失敗したか、その結果どんなvalidationActionsが取られたか dispatcher.go#L336

validationActionsにAuditが含まれる場合、そのポリシーはリクエストをブロックせずに監査ログへ記録するだけの運用(いわゆるdry-run的な検証)になっているため、この値を見れば「実際に効いているポリシーか、まだ様子見中のポリシーか」を判別できます。

「ownerラベルの必須化」をAuditアクション付きで検証しているポリシーがあり、それに違反するPodを作成した場合のイベントを、Eventのフィールドに当てはめて示すと次のようになります(実クラスタからの採取ではなく、公式ドキュメントのvalidation_failureの形式をもとに再構成した例です)。JSON文字列がそのまま値になるため、YAMLのブロックスカラー(|-)で書くとエスケープなしで読みやすくなります。

kind: Event
apiVersion: audit.k8s.io/v1
level: Metadata
stage: ResponseComplete
requestURI: /api/v1/namespaces/demo/pods
verb: create
objectRef:
  resource: pods
  namespace: demo
  name: no-owner-label
  apiVersion: v1
responseStatus:
  code: 201
annotations:
  validation.policy.admission.k8s.io/validation_failure: |-
    [{"message": "must have label 'owner'", "policy": "require-owner-label.example.com", "binding": "require-owner-label-binding.example.com", "expressionIndex": "0", "validationActions": ["Audit"]}]

3.4 Mutating Webhook系

Mutating WebhookがPodやDeployment等をどう書き換えたかも、監査ログのannotationとして記録されます。

Annotationキー 記録されるlevel わかること
mutation.webhook.admission.k8s.io/round_{round}_index_{order} Metadata以上 どの順番のWebhookが呼ばれ、実際にオブジェクトを書き換えたか(mutated: true/false)
patch.webhook.admission.k8s.io/round_{round}_index_{order} Request以上 適用されたJSON Patchの中身

(参考: Mutating webhook auditing annotations、ソース: dispatcher.go#L396-L407)

公式ドキュメントに掲載されている例をEventのannotationとして書くと、次のようになります(JSON文字列がそのまま値になるため、YAMLのブロックスカラー(|-)で書くとエスケープなしで読みやすくなります)。

kind: Event
apiVersion: audit.k8s.io/v1
annotations:
  mutation.webhook.admission.k8s.io/round_0_index_0: |-
    {"configuration":"my-mutating-webhook-configuration.example.com","webhook":"my-webhook-always-mutate.example.com","mutated": true}

(参考: Mutating webhook auditing annotations)

roundはWebhookが再呼び出し(reinvocation)された回数、indexはそのWebhook Chain内での順序を表します。複数のMutating Webhookが連鎖している環境で「最終的にどのWebhookがどこを書き換えたか」を後から追跡したいときに、これらのannotationが手がかりになります。

3.5 非推奨API利用の検知

クライアントが非推奨のAPIバージョンを叩いた場合、次のannotationが付きます。

Annotationキー 値の例 わかること ソース
k8s.io/deprecated "true" このリクエストが非推奨のAPIバージョンに対するものであること metrics.go#L366-L372
k8s.io/removed-release "1.22" そのAPIバージョンが実際に削除される予定のリリース 同上

(参考: Audit Annotations)

クラスタを新しいマイナーバージョンへ上げる前に、「まだ非推奨APIを叩いているクライアントが残っていないか」を、この2つのannotationで監査ログから横断的に洗い出せます。個々のクライアントのマニフェストを1つずつ確認して回るより効率的です。

例として、extensions/v1beta1のIngress(v1.22で削除済み)がまだ現役だった当時にこのAPIへアクセスした場合のイベントをEventのフィールドに当てはめると、次のようになります(現在の1.37系クラスタではこのAPIバージョン自体が既に削除されているため実際には発生しません。あくまでk8s.io/deprecated/k8s.io/removed-releaseがどう記録されるかの再現例です)。

kind: Event
apiVersion: audit.k8s.io/v1
level: Metadata
stage: ResponseComplete
requestURI: /apis/extensions/v1beta1/namespaces/demo/ingresses
verb: list
objectRef:
  resource: ingresses
  namespace: demo
  apiVersion: extensions/v1beta1
responseStatus:
  code: 200
annotations:
  k8s.io/deprecated: "true"
  k8s.io/removed-release: "1.22"

3.6 レイテンシ計測系

APIサーバー内部のどこで時間がかかったかを示すannotationもあります。

Annotationキー 値の例 わかること ソース
apiserver.latency.k8s.io/etcd "4.730661757s" etcdへの書き込み・読み出しにかかった時間 webhook_duration.go#L308
apiserver.latency.k8s.io/decode-response-object "450.6649ns" etcdからのレスポンスのデコードにかかった時間 webhook_duration.go#L313
apiserver.latency.k8s.io/apf-queue-wait "100ns" API Priority and Fairnessのキューで待たされた時間 webhook_duration.go#L314
apiserver.latency.k8s.io/total "4.912395s" このリクエストの合計レイテンシ(他の内訳との比較用) audit.go#L173

(参考: Audit Annotations)

これらは全リクエストに付くわけではなく、レイテンシが一定の閾値(500ms)を超えたリクエストにだけ付与されます。

func writeLatencyToAnnotation(ctx context.Context) {
	ac := audit.AuditContextFrom(ctx)
	// we will track latency in annotation only when the total latency
	// of the given request exceeds 500ms, this is in keeping with the
	// traces in rest/handlers for create, delete, update,
	// get, list, and deletecollection.
	const threshold = 500 * time.Millisecond
	latency := time.Since(ac.GetEventRequestReceivedTimestamp().Time)
	if latency <= threshold {
		return
	}
	...
}

(参考: audit.go#L150-L175)

「APIサーバーが遅い」という漠然とした相談を受けたとき、メトリクスだけでなく監査ログのこれらのannotationを見ると、etcdが遅いのかAPFのキューで待たされているのかを個別のリクエスト単位で切り分けられます。

これらのannotationがすべて記録されたイベントをEventのフィールドに当てはめると、次のようになります(実クラスタからの採取ではなく、公式ドキュメントの値をもとに再構成した例です)。

kind: Event
apiVersion: audit.k8s.io/v1
level: Metadata
stage: ResponseComplete
requestURI: /api/v1/namespaces/demo/configmaps/big-config
verb: update
objectRef:
  resource: configmaps
  namespace: demo
  name: big-config
  apiVersion: v1
responseStatus:
  code: 200
annotations:
  apiserver.latency.k8s.io/etcd: "4.730661757s"
  apiserver.latency.k8s.io/decode-response-object: "450.6649ns"
  apiserver.latency.k8s.io/apf-queue-wait: "100ns"
  apiserver.latency.k8s.io/total: "4.912395s"

3.7 証明書の脆弱性検知系

集約API(aggregated API)やWebhookの証明書が、Goランタイムがすでに非推奨化・削除した形式(SAN無しのCommonNameのみの証明書、SHA-1署名の証明書)を使っている場合に付与されます。

Annotationキー わかること ソース
missing-san.invalid-cert.kubernetes.io/$hostname 接続先の証明書にsubjectAltNamesが無く、非推奨のCommonNameフィールドに依存していること server_cert_deprecations.go#L67,L137
insecure-sha1.invalid-cert.kubernetes.io/$hostname 接続先の証明書がSHA-1署名を使っていること 同上、L185

(参考: Audit Annotations)

これらはGo自体がこれらの証明書形式のサポートを段階的に打ち切ってきた経緯(CommonNameの非推奨化、SHA-1証明書の拒否)と対になっており、Kubernetesのバージョンを上げるとある日突然Webhookへの接続が失敗する、という事態を事前に検知するためのものです。

たとえば、集約APImetrics.k8s.ioを提供するmetrics-serverの証明書が上記の形式だった場合、そこへのリクエストのイベントをEventのフィールドに当てはめると次のようになります(実クラスタからの採取ではなく、公式ドキュメントの値をもとに再構成した例です)。

kind: Event
apiVersion: audit.k8s.io/v1
level: Metadata
stage: ResponseComplete
requestURI: /apis/metrics.k8s.io/v1beta1/nodes
verb: list
objectRef:
  resource: nodes
  apiVersion: metrics.k8s.io/v1beta1
responseStatus:
  code: 200
annotations:
  missing-san.invalid-cert.kubernetes.io/metrics-server.kube-system.svc: relies on a legacy Common Name field instead of the SAN extension for subject validation
  insecure-sha1.invalid-cert.kubernetes.io/metrics-server.kube-system.svc: uses an insecure SHA-1 signature

3.8 ログ自体の切り詰め

Annotationキー 値 わかること ソース
audit.k8s.io/truncated "true" このイベントが、設定した最大サイズを超えたために切り詰められたこと truncate.go#L33-L34

(参考: Audit Annotations)

このannotationが付いたイベントはrequestObject/responseObjectが欠落している(または最初から破棄されている)可能性があるため、「監査ログに全リクエストの内容が残っているはず」という前提でログを調査するときは、まずこのannotationの有無を確認しておくと手戻りが少なくなります。

--audit-log-truncate-max-event-sizeを超える大きなConfigMapを更新した場合のイベントをEventのフィールドに当てはめると、次のようになります(実クラスタからの採取ではなく、truncate backendの挙動をもとに再構成した例です)。requestObject/responseObjectが(削られた結果)存在しない点に注目してください。

kind: Event
apiVersion: audit.k8s.io/v1
level: Request
stage: ResponseComplete
requestURI: /api/v1/namespaces/demo/configmaps/huge-config
verb: update
objectRef:
  resource: configmaps
  namespace: demo
  name: huge-config
  apiVersion: v1
responseStatus:
  code: 200
annotations:
  audit.k8s.io/truncated: "true"

第4章: Kubernetesの文脈でAudit Logと呼ばれるものの一覧

Kubernetesの運用では、「Audit Log」という同じ言葉が、記録する主体も対象も異なる複数のものを指します。本記事で扱ったのは1行目のkube-apiserverの監査ログです。

名称 記録する主体 記録する対象 取得方法の概要
Kubernetes監査ログ(本記事) kube-apiserver Kubernetes APIへのリクエスト(誰が・何に対して・何をしたか)。audit.k8s.ioのEvent Policyで粒度を制御し、log backend(ファイル)またはwebhook backendに出力
Linux audit(auditd) Linuxカーネルの監査サブシステム ノード上のsyscallやファイルアクセスなど、OSレベルのイベント ユーザー空間のデーモンauditdがディスクに書き出す。ルールはauditctlなどで設定
Falco Falco syscallの挙動、およびKubernetes監査ログなどを入力にした検知。出力はルールに合致したアラート ルールに合致したアラートを出力する。Falco自体は監査ログの保管先ではない
マネージドサービスのAudit Log GKE/EKS/AKSなどのコントロールプレーン 基本的にはkube-apiserverの監査ログ クラウドのログ基盤に出力される。有効化の方法・粒度・出力先はサービスごとに異なる

4.1 Linux audit(auditd)

Linuxカーネルには、Kubernetesとは独立した監査の仕組み(Linux Auditing System)があります。ユーザー空間のコンポーネントであるauditdが監査レコードをディスクに書き出し、auditctlやausearchなどのユーティリティで設定・検索します。

Kubernetesの監査ログが「APIリクエスト」を記録するのに対し、Linux auditは「そのノード上のプロセスが何をしたか」を記録します。たとえばkubectl execでコンテナに入った場合、Kubernetesの監査ログにはexecリクエストが残りますが、コンテナ内で実行されたコマンドの内容は、Linux auditやFalcoのようなsyscallレベルの仕組みでないと追えません。

(参考: auditd(8))

4.2 Falco

Falcoは、ホスト、コンテナ、Kubernetesなどのランタイムセキュリティを監視するツールで、主にLinuxカーネルのsyscallを入力にします。加えてプラグインにより、Kubernetes audit eventsなどの別のイベントソースも入力にできます。

そのためFalcoにとって、本記事で扱った監査ログは入力の1つです。「Falcoが出力するaudit」と言った場合は、監査ログそのものではなく、syscallや監査ログを元に検知ルールが生成したアラートを指していることが多いです。

(参考: Falco)

4.3 マネージドサービスのAudit Log

GKE/EKS/AKSなどのマネージドKubernetesでは、コントロールプレーンをサービス側が運用しているため、kube-apiserverの監査ログも各サービスの機能として提供されます。どのリクエストをどの粒度で記録するかはサービスごとに異なり、本記事の第2章で説明したPolicyをそのまま適用できるとは限りません。

サービス 機能 出力先 有効化
GKE Cloud Audit Logs(Admin Activity、Data Access) Cloud Logging Admin Activityは常に有効で無効化できない。Data Accessは既定で無効
EKS control plane logging(auditログタイプ) CloudWatch Logs 既定ではどのログタイプも無効。クラスタごとに有効化する
AKS diagnostic settings(kube-audit、kube-audit-admin) Log Analyticsなど diagnostic settingsで有効化する。kube-audit-adminはget/listを除いた更新系のリクエストのみ

(参考: GKE Audit Logging、Amazon EKS control plane logs、Monitoring data reference for AKS)

おわりに

一度、監査ログでどんな情報が取れるのかを確認したいと思い、この記事にまとめました。「レイテンシ計測」や「証明書の脆弱性検知」の情報が監査ログから取れることは知らなかったので、実際に確認してみて面白かったです。

また、Kubernetesの文脈で「Audit Log」の話をしているときに、お互いが想定しているスコープが違うことがあります。対象を明確にしてから議論すると、認識のずれを防げると思います。

何も付けずに話している場合は、Kubernetesの公式ドキュメントと同じくaudit.k8s.ioの話であることが多いと思います。ただ、人によっては Linux の syscall を想定していることもあるので、議論の前に前提をそろえておくのが良いと思います。

参考

参考リンク

本文で登場する順に並べています。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?