はじめに
以前、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-apiserverusing the--audit-policy-fileflag. If the flag is omitted, no events are logged. Note that therulesfield 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 を想定していることもあるので、議論の前に前提をそろえておくのが良いと思います。
参考
参考リンク
本文で登場する順に並べています。
- Auditing
- Event | Audit configuration (v1) reference
- audit-policy.yaml
- JSON Lines
- kube-apiserver
- Pod Security Standards
- Audit Annotations
- Validating Admission Policy
- Mutating webhook auditing annotations
- API Priority and Fairness
- Go 1.15 Release Notes (CommonName)
- Go 1.18 Release Notes (SHA-1)
- auditd(8)
- Falco
- GKE Audit Logging
- Amazon EKS control plane logs
- Monitoring data reference for AKS