1、概述

Kubernetes 审计(Auditing) 功能提供了与安全相关的、按时间顺序排列的记录集, 记录每个用户、使用 Kubernetes API 的应用以及控制面自身引发的活动(所有访问kube-apiserver服务的客户端)。

审计功能使得集群管理员能够回答以下问题:

  • 发生了什么?
  • 什么时候发生的?
  • 谁触发的?
  • 活动发生在哪个(些)对象上?
  • 在哪观察到的?
  • 它从哪触发的?
  • 活动的后续处理行为是什么?

这对平台管理者来说十分重要,能够回答一些在故障时候出现的问题。哪个用户,从哪个ip上,发起了什么请求。如删除了一个命名空间,导致这个命名空间下的所有pod被回收,对故障进行复盘。

Kubernetes 审计功能由 kube-apiserver 服务提供。每个请求在不同执行阶段都会生成审计事件;这些审计事件会根据特定策略被预处理并写入后端。策略确定要记录的内容,当前的后端支持日志文件和 webhook。

每个请求都可被记录其相关的阶段(stage)。已定义的阶段有:

  • RequestReceived - 此阶段对应审计处理器接收到请求后,并且在委托给处理器处理之前生成的事件。
  • ResponseStarted - 在响应消息的头部发送后,响应消息体发送前生成的事件。 只有长时间运行的请求(例如 watch)才会生成这个阶段。
  • ResponseComplete - 当响应消息体完成并且没有更多数据需要传输的时候。
  • Panic - 当 panic 发生时生成。

审计日志记录功能会增加 API server 的内存消耗,因为需要为每个请求存储审计所需的某些上下文。 此外,内存消耗取决于审计日志记录的配置。

2、原理

API Server的处理包括很多个步骤,认证、限流、鉴权、跨域处理、webhook、审计处理、真正的处理单元、回复响应等等。审计功能是API Server请求处理流水线上的一环,是对API Sever能力的扩展。

Kubernetes 审计(Auditing)功能详解_sed

备注:RequestReceived、ResponseStarted、ResponseComplete、Panic这四个阶段都在kube-apiserver WithAudit这一个过滤器中(kube-apiserver中和审计相关的过滤器有2个WithFailedAuthenticationAudit和WithAudit,其中WithFailedAuthenticationAudit只拦截身份验证失败的请求,逻辑比较简单本文不再介绍)。

下面通过 kube-apiserver WithAudit过滤器源码进行审计原理讲解。

kube-apiserver默认过滤器链定义在kubernetes/staging/src/k8s.io/apiserver/pkg/server/config.go文件的DefaultBuildHandlerChain方法里面,这里我们只关注和审计相关过滤器。

// 包位置: kubernetes/staging/src/k8s.io/apiserver/pkg/server
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
  ......

  handler = filterlatency.TrackCompleted(handler)
  // 审计过滤器
  handler = genericapifilters.WithAudit(handler, c.AuditBackend, c.AuditPolicyChecker, c.LongRunningFunc)
  handler = filterlatency.TrackStarted(handler, "audit")

  failedHandler := genericapifilters.Unauthorized(c.Serializer)
  // WithFailedAuthenticationAudit过滤器只拦截身份验证失败的请求
  failedHandler = genericapifilters.WithFailedAuthenticationAudit(failedHandler, c.AuditBackend, c.AuditPolicyChecker)

  failedHandler = filterlatency.TrackCompleted(failedHandler)
  handler = filterlatency.TrackCompleted(handler)
  handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)
  handler = filterlatency.TrackStarted(handler, "authentication")

  handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true")

  // WithTimeoutForNonLongRunningRequests will call the rest of the request handling in a go-routine with the
  // context with deadline. The go-routine can keep running, while the timeout logic will return a timeout to the client.
  handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.LongRunningFunc)

  handler = genericapifilters.WithRequestDeadline(handler, c.AuditBackend, c.AuditPolicyChecker,
    c.LongRunningFunc, c.Serializer, c.RequestTimeout)
  handler = genericfilters.WithWaitGroup(handler, c.LongRunningFunc, c.HandlerChainWaitGroup)
  handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver)
  ......

  return handler
}

WithAudit过滤器源码如下:

// kubernetes/staging/src/k8s.io/apiserver/pkg/endpoints/filters/audit.go
func WithAudit(handler http.Handler, sink audit.Sink, policy policy.Checker, longRunningCheck request.LongRunningRequestCheck) http.Handler {
	// 可以看到开启k8s原生审计功能必须配置审计策略和审计后端否则不会执行审计过滤器逻辑
	if sink == nil || policy == nil {
		return handler
	}
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		// 从审计策略文件中获取当前请求的审计策略级别、创建审计事件对象并将审计事件对象放到context中
		req, ev, omitStages, err := createAuditEventAndAttachToContext(req, policy)
		if err != nil {
			utilruntime.HandleError(fmt.Errorf("failed to create audit event: %v", err))
			responsewriters.InternalError(w, req, errors.New("failed to create audit event"))
			return
		}
		ctx := req.Context()

		// 如果当前请求的审计策略为None,则不需要审计
		if ev == nil || ctx == nil {
			handler.ServeHTTP(w, req)
			return
		}

		// RequestReceived阶段:此阶段对应审计处理器接收到请求后,并且在委托给处理器处理之前生成的事件。
		ev.Stage = auditinternal.StageRequestReceived
		// 生成审计事件并输出到对应后端(注意:审计对象和审计事件的关系,审计事件会在不同阶段维护当前请求产生的唯一审计对象的字段属性)
		if processed := processAuditEvent(ctx, sink, ev, omitStages); !processed {
			audit.ApiserverAuditDroppedCounter.WithContext(ctx).Inc()
			responsewriters.InternalError(w, req, errors.New("failed to store audit event"))
			return
		}

		// intercept the status code
		var longRunningSink audit.Sink
		if longRunningCheck != nil {
			ri, _ := request.RequestInfoFrom(ctx)
			if longRunningCheck(req, ri) {
				longRunningSink = sink
			}
		}
		respWriter := decorateResponseWriter(ctx, w, ev, longRunningSink, omitStages)

		// 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.
		// 在处理完客户端请求后离开当前过滤器前,处理ResponseStarted、ResponseComplete、Panic这三个阶段的审计事件到对应后端
		defer func() {
			// Panic阶段:当当前请求发生panic错误时触发此阶段
			if r := recover(); r != nil {
				defer panic(r)
				ev.Stage = auditinternal.StagePanic
				ev.ResponseStatus = &metav1.Status{
					Code:    http.StatusInternalServerError,
					Status:  metav1.StatusFailure,
					Reason:  metav1.StatusReasonInternalError,
					Message: fmt.Sprintf("APIServer panic'd: %v", r),
				}
				// 生成审计事件并输出到对应后端
				processAuditEvent(ctx, sink, ev, omitStages)
				return
			}

			// if no StageResponseStarted event was sent b/c neither a status code nor a body was sent, fake it here
			// But Audit-Id http header will only be sent when http.ResponseWriter.WriteHeader is called.
			fakedSuccessStatus := &metav1.Status{
				Code:    http.StatusOK,
				Status:  metav1.StatusSuccess,
				Message: "Connection closed early",
			}
			// ResponseStarted阶段: 在响应消息的头部发送后,响应消息体发送前生成的事件。 只有长时间运行的请求(例如 watch)才会生成这个阶段。
			if ev.ResponseStatus == nil && longRunningSink != nil {
				ev.ResponseStatus = fakedSuccessStatus
				ev.Stage = auditinternal.StageResponseStarted
				// 生成审计事件并输出到对应后端
				processAuditEvent(ctx, longRunningSink, ev, omitStages)
			}

			// ResponseComplete阶段: 当响应消息体完成并且没有更多数据需要传输的时候。
			ev.Stage = auditinternal.StageResponseComplete
			if ev.ResponseStatus == nil {
				ev.ResponseStatus = fakedSuccessStatus
			}
			// 生成审计事件并输出到对应后端
			processAuditEvent(ctx, sink, ev, omitStages)
		}()
		handler.ServeHTTP(respWriter, req)
	})
}

审计过滤器中调用的核心方法如下:

// createAuditEventAndAttachToContext is responsible for creating the audit event
// and attaching it to the appropriate request context. It returns:
// - context with audit event attached to it
// - created audit event
// - error if anything bad happened
// 从审计策略文件中获取当前请求的审计策略级别、创建审计事件对象并将审计事件对象放到context中
func createAuditEventAndAttachToContext(req *http.Request, policy policy.Checker) (*http.Request, *auditinternal.Event, []auditinternal.Stage, error) {
  ctx := req.Context()

  // 基于requestInfo组织AttributesRecord结构体对象存放当前请求信息
  attribs, err := GetAuthorizerAttributes(ctx)
  if err != nil {
    return req, nil, nil, fmt.Errorf("failed to GetAuthorizerAttributes: %v", err)
  }

  // 从审计策略文件中获取当前请求的审计策略级别及配置的忽略的审计执行阶段(不配置omitStages的话默认RequestReceived、ResponseStarted、ResponseComplete这三阶段都会产生审计事件)
  level, omitStages := policy.LevelAndStages(attribs)
  audit.ObservePolicyLevel(ctx, level)
  // 如果当前请求的审计策略为None,则不需要审计
  if level == auditinternal.LevelNone {
    // Don't audit.
    return req, nil, nil, nil
  }

  requestReceivedTimestamp, ok := request.ReceivedTimestampFrom(ctx)
  if !ok {
    requestReceivedTimestamp = time.Now()
  }
  // 创建审计事件对象
  ev, err := audit.NewEventFromRequest(req, requestReceivedTimestamp, level, attribs)
  if err != nil {
    return req, nil, nil, fmt.Errorf("failed to complete audit event from request: %v", err)
  }

  req = req.WithContext(request.WithAuditEvent(ctx, ev))

  return req, ev, omitStages, nil
}

// 基于RequestInfo组织AttributesRecord结构体对象
func GetAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error) {
  attribs := authorizer.AttributesRecord{}

  user, ok := request.UserFrom(ctx)
  if ok {
    attribs.User = user
  }

  requestInfo, found := request.RequestInfoFrom(ctx)
  if !found {
    return nil, errors.New("no RequestInfo found in the context")
  }

  // Start with common attributes that apply to resource and non-resource requests
  attribs.ResourceRequest = requestInfo.IsResourceRequest
  attribs.Path = requestInfo.Path
  attribs.Verb = requestInfo.Verb

  attribs.APIGroup = requestInfo.APIGroup
  attribs.APIVersion = requestInfo.APIVersion
  attribs.Resource = requestInfo.Resource
  attribs.Subresource = requestInfo.Subresource
  attribs.Namespace = requestInfo.Namespace
  attribs.Name = requestInfo.Name

  return &attribs, nil
}


// 返回当前请求的审计策略级别及配置的忽略的审计执行阶段
func (p *policyChecker) LevelAndStages(attrs authorizer.Attributes) (audit.Level, []audit.Stage) {
  for _, rule := range p.Rules {
    if ruleMatches(&rule, attrs) {
      return rule.Level, rule.OmitStages
    }
  }
  // 如果当前请求在审计策略配置文件中没匹配到对应的规则则返回默认的审计策略(None)
  return DefaultAuditLevel, p.OmitStages
}

// Check whether the rule matches the request attrs.
func ruleMatches(r *audit.PolicyRule, attrs authorizer.Attributes) bool {
  user := attrs.GetUser()
  if len(r.Users) > 0 {
    if user == nil || !hasString(r.Users, user.GetName()) {
      return false
    }
  }
  if len(r.UserGroups) > 0 {
    if user == nil {
      return false
    }
    matched := false
    for _, group := range user.GetGroups() {
      if hasString(r.UserGroups, group) {
        matched = true
        break
      }
    }
    if !matched {
      return false
    }
  }
  if len(r.Verbs) > 0 {
    if !hasString(r.Verbs, attrs.GetVerb()) {
      return false
    }
  }

  if len(r.Namespaces) > 0 || len(r.Resources) > 0 {
    return ruleMatchesResource(r, attrs)
  }

  if len(r.NonResourceURLs) > 0 {
    return ruleMatchesNonResource(r, attrs)
  }

  return true
}


// 创建审计事件对象
func NewEventFromRequest(req *http.Request, requestReceivedTimestamp time.Time, level auditinternal.Level, attribs authorizer.Attributes) (*auditinternal.Event, error) {
  ev := &auditinternal.Event{
    RequestReceivedTimestamp: metav1.NewMicroTime(requestReceivedTimestamp),
    Verb:                     attribs.GetVerb(),
    RequestURI:               req.URL.RequestURI(),
    UserAgent:                maybeTruncateUserAgent(req),
    Level:                    level,
  }

  // prefer the id from the headers. If not available, create a new one.
  // TODO(audit): do we want to forbid the header for non-front-proxy users?
  ids := req.Header.Get(auditinternal.HeaderAuditID)
  if ids != "" {
    ev.AuditID = types.UID(ids)
  } else {
    ev.AuditID = types.UID(uuid.New().String())
  }

  ips := utilnet.SourceIPs(req)
  ev.SourceIPs = make([]string, len(ips))
  for i := range ips {
    ev.SourceIPs[i] = ips[i].String()
  }

  if user := attribs.GetUser(); user != nil {
    ev.User.Username = user.GetName()
    ev.User.Extra = map[string]authnv1.ExtraValue{}
    for k, v := range user.GetExtra() {
      ev.User.Extra[k] = authnv1.ExtraValue(v)
    }
    ev.User.Groups = user.GetGroups()
    ev.User.UID = user.GetUID()
  }

  if attribs.IsResourceRequest() {
    ev.ObjectRef = &auditinternal.ObjectReference{
      Namespace:   attribs.GetNamespace(),
      Name:        attribs.GetName(),
      Resource:    attribs.GetResource(),
      Subresource: attribs.GetSubresource(),
      APIGroup:    attribs.GetAPIGroup(),
      APIVersion:  attribs.GetAPIVersion(),
    }
  }

  for _, kv := range auditAnnotationsFrom(req.Context()) {
    LogAnnotation(ev, kv.key, kv.value)
  }

  return ev, nil
}


// 生成审计事件并输出到对应后端
func processAuditEvent(ctx context.Context, sink audit.Sink, ev *auditinternal.Event, omitStages []auditinternal.Stage) bool {
  // 如果当前阶段属于审计策略配置文件中配置的忽略阶段,则不需要将此阶段审计事件输出到对应后端
  for _, stage := range omitStages {
    if ev.Stage == stage {
      return true
    }
  }

  if ev.Stage == auditinternal.StageRequestReceived {
    ev.StageTimestamp = metav1.NewMicroTime(ev.RequestReceivedTimestamp.Time)
  } else {
    ev.StageTimestamp = metav1.NewMicroTime(time.Now())
  }
  audit.ObserveEvent(ctx)
  // 输出审计事件到对应后端
  return sink.ProcessEvents(ev)
}

通过源码内容我们可以总结出k8s原生审计原理:

1)  、开启k8s原生审计功能必须配置审计策略和审计后端否则不会执行审计过滤器逻辑。

2)、审计过滤器会过滤客户端的每一个请求,通过当前请求信息组织AttributesRecord结构体对象,然后基于AttributesRecord结构体对象和审计策略配置文件中的规则做比对,返回当前请求的审计策略级别。如果审计策略配置文件中没有当前请求对应的规则的话,或者当前请求的策略级别为None话,则当前请求不需要审计。

3)、基于请求信息、审计策略、AttributesRecord结构体对象等信息生成审计事件对象,并将审计事件对象放到context中。

4)、基于审计策略配置文件中的omitStages配置(不配置omitStages的话默认RequestReceived、ResponseStarted、ResponseComplete这三个阶段都会产生审计事件),在保留的审计阶段生成审计事件并输出到对应后端。

3、审计策略 

审计策略定义了关于应记录哪些事件以及应包含哪些数据的规则。 审计策略对象结构定义在 audit.k8s.io API 组。 处理事件时,将按顺序与规则列表进行比较。第一个匹配规则设置事件的审计级别(Audit Level)。已定义的审计级别有:

  • None - 符合这条规则的日志将不会记录。
  • Metadata - 记录请求的元数据(请求的用户、时间戳、资源、动词等等), 但是不记录请求或者响应的消息体。
  • Request - 记录事件的元数据和请求的消息体,但是不记录响应的消息体。 这不适用于非资源类型的请求。
  • RequestResponse - 记录事件的元数据,请求和响应的消息体。这不适用于非资源类型的请求。

你可以使用 --audit-policy-file 标志将包含策略的文件传递给 kube-apiserver。 如果不设置该标志,则不记录事件。 注意 rules 字段必须在审计策略文件中提供。没有rules规则的策略将被视为非法配置。

以下是一个审计策略文件的示例:

apiVersion: audit.k8s.io/v1 # 这是必填项。
kind: Policy
# 不要在 RequestReceived 阶段为任何请求生成审计事件。
omitStages:
  - "RequestReceived"
rules:
  # 在日志中用 RequestResponse 级别记录 Pod 变化。
  - level: RequestResponse
    resources:
    - group: ""
      # 资源 "pods" 不匹配对任何 Pod 子资源的请求,
      # 这与 RBAC 策略一致。
      resources: ["pods"]
  # 在日志中按 Metadata 级别记录 "pods/log"、"pods/status" 请求
  - level: Metadata
    resources:
    - group: ""
      resources: ["pods/log", "pods/status"]

  # 不要在日志中记录对名为 "controller-leader" 的 configmap 的请求。
  - level: None
    resources:
    - group: ""
      resources: ["configmaps"]
      resourceNames: ["controller-leader"]

  # 不要在日志中记录由 "system:kube-proxy" 发出的对端点或服务的监测请求。
  - level: None
    users: ["system:kube-proxy"]
    verbs: ["watch"]
    resources:
    - group: "" # core API 组
      resources: ["endpoints", "services"]

  # 不要在日志中记录对某些非资源 URL 路径的已认证请求。
  - level: None
    userGroups: ["system:authenticated"]
    nonResourceURLs:
    - "/api*" # 通配符匹配。
    - "/version"

  # 在日志中记录 kube-system 中 configmap 变更的请求消息体。
  - level: Request
    resources:
    - group: "" # core API 组
      resources: ["configmaps"]
    # 这个规则仅适用于 "kube-system" 名字空间中的资源。
    # 空字符串 "" 可用于选择非名字空间作用域的资源。
    namespaces: ["kube-system"]

  # 在日志中用 Metadata 级别记录所有其他名字空间中的 configmap 和 secret 变更。
  - level: Metadata
    resources:
    - group: "" # core API 组
      resources: ["secrets", "configmaps"]

  # 在日志中以 Request 级别记录所有其他 core 和 extensions 组中的资源操作。
  - level: Request
    resources:
    - group: "" # core API 组
    - group: "extensions" # 不应包括在内的组版本。

  # 一个抓取所有的规则,将在日志中以 Metadata 级别记录所有其他请求。
  - level: Metadata
    # 符合此规则的 watch 等长时间运行的请求将不会
    # 在 RequestReceived 阶段生成审计事件。
    omitStages:
      - "RequestReceived"

也可以使用最低限度的审计策略文件在 Metadata 级别记录所有请求:

# 在 Metadata 级别为所有请求生成日志
apiVersion: audit.k8s.io/v1beta1
kind: Policy
rules:
- level: Metadata

更多Policy配置参考请参见官方文档:kube-apiserver Audit 配置 (v1)

4、审计后端

审计后端实现将审计事件导出到外部存储。Kube-apiserver 默认提供两个后端:

  • Log 后端,将事件写入到文件系统
  • Webhook 后端,将事件发送到外部 HTTP API

4.1  Log 后端

Log 后端将审计事件写入 JSONlines 格式的文件。 你可以使用以下 kube-apiserver 标志配置 Log 审计后端:

  • --audit-log-path 指定用来写入审计事件的日志文件路径。不指定此标志会禁用日志后端。
  • --audit-log-maxage 定义保留旧审计日志文件的最大天数
  • --audit-log-maxbackup 定义要保留的审计日志文件的最大数量
  • --audit-log-maxsize 定义审计日志文件的最大大小(兆字节)

如果你的集群控制面以 Pod 的形式运行 kube-apiserver,记得要通过 hostPath 卷来访问策略文件和日志文件所在的目录,这样审计记录才会持久保存下来。

如下,通过修改/etc/kubernetes/manifests/kube-apiserver.yaml 配置文件开启审计Log后端配置:

--audit-policy-file=/etc/kubernetes/audit-policy.yaml   #审计策略文件
  --audit-log-path=/var/log/kubernetes/audit/audit.log    #审计事件的日志文件路径

接下来挂载数据卷:

volumeMounts:
  - mountPath: /etc/kubernetes/audit-policy.yaml
    name: audit
    readOnly: true
  - mountPath: /var/log/kubernetes/audit/
    name: audit-log
    readOnly: false

最后配置 hostPath: 

...
volumes:
- name: audit
  hostPath:
    path: /etc/kubernetes/audit-policy.yaml
    type: File

- name: audit-log
  hostPath:
    path: /var/log/kubernetes/audit/
    type: DirectoryOrCreate

4.2  Webhook 后端

Webhook 后端将审计事件发送到远程 Web API,该远程 API 应该暴露与 kube-apiserver 形式相同的 API,包括其身份认证机制。你可以使用如下 kube-apiserver 标志来配置 Webhook 审计后端:

  • --audit-webhook-config-file 设置 Webhook 配置文件的路径。Webhook 配置文件实际上是一个 kubeconfig 文件。
  • --audit-webhook-initial-backoff 指定在第一次失败后重发请求等待的时间。随后的请求将以指数退避重试。

Webhook 配置文件使用 kubeconfig 格式指定服务的远程地址和用于连接它的凭据。 

如下,通过修改/etc/kubernetes/manifests/kube-apiserver.yaml 配置文件开启审计webhook后端配置:

--audit-policy-file=/etc/kubernetes/audit-policy.yaml   #审计策略文件
  --audit-webhook-config-file=/etc/kubernetes/audit/audit-webhook.yaml    #审计配置文件

audit-webhook.yaml 文件定义了 Kubernetes 审计日志将要发送至的 Webhook。以下是 Kube-Auditing Webhook 的示例配置。

apiVersion: v1
kind: Config
clusters:
- name: kube-auditing
  cluster:
    server: https://{ip}:443/audit/webhook/event      #指定webhook服务端地址
    insecure-skip-tls-verify: true
contexts:
- context:
    cluster: kube-auditing
    user: ""
  name: default-context
current-context: default-context
preferences: {}
users: []

5、事件批处理

日志和 Webhook 后端都支持批处理。以 Webhook 为例,以下是可用参数列表。要获取日志后端的同样参数,请在参数名称中将 webhook 替换为 log。 默认情况下,在 webhook 中批处理是被启用的,在 log 中批处理是被禁用的。 同样,默认情况下,在 webhook 中启用带宽限制,在 log 中禁用带宽限制。

webhook参数:

批处理相关:
--audit-webhook-batch-buffer-size int     Default: 10000
指定存储批处理和写入事件的缓冲区字节数(默认值:10000)。只在批处理模式下使用。
--audit-webhook-batch-max-size int     Default: 400
指定一个批处理的最大长度(默认值:400)。只在批处理模式下使用。
--audit-webhook-batch-max-wait duration     Default: 30s
指定尚未达到最大值的批处理的强制写入等待时间(默认值:30s)。只在批处理模式下使用。
--audit-webhook-batch-throttle-burst int     Default: 15
指定在未使用 ThrottleQPS 时同时发送请求的最大数量(默认值:15)。只在批处理模式下使用。
--audit-webhook-batch-throttle-enable     Default: true
指定是否启用 batching throttling(默认值:true)。只在批处理模式下使用。
--audit-webhook-batch-throttle-qps float32     Default: 10
设定每秒内可执行的批处理的最大平均数(默认值:10)。只在批处理模式下使用。

webhook相关:
--audit-webhook-config-file string
指定 kubeconfig 格式的配置文件的路径。该文件设定了审计 webhook 配置。
--audit-webhook-initial-backoff duration     Default: 10s
指定重试第一个失败请求之前等待的时间(默认值:10s)。

处理模式:
--audit-webhook-mode string     Default: "batch"
指定发送审计事件的策略。blocking 表示发送事件时阻塞服务器响应;batch 表示在后端异步缓冲和写入事件。目前仅支持 batch 和 blocking(默认值:batch)。

是否截断:
--audit-webhook-truncate-enabled
指定是否允许截断事件和批处理。
--audit-webhook-truncate-max-batch-size int     Default: 10485760
指定发送到底层后端的批处理的最大字节数(默认值:10485760)。实际序列化时的大小会比设定值大几百字节。如果一个批处理超出该大小,它会被分为几个小的批处理。
--audit-webhook-truncate-max-event-size int     Default: 102400
指定发送到底层后端的审计事件的最大字节数(默认值:102400)。如果一个事件超出该大小,则第一个请求和回复会被删除,如果还没有减少到合适的大小,该事件将被丢弃。

--audit-webhook-version string     Default: "audit.k8s.io/v1beta1"
指定序列化审计日志的 API 组名和版本号(默认值:audit.k8s.io/v1beta1)。

log参数:

批处理相关:
--audit-log-batch-buffer-size int     Default: 10000
指定存储批处理和写入事件的缓冲区字节数(默认值:10000)。只在批处理模式下使用。
--audit-log-batch-max-size int     Default: 1
指定一个批处理的最大长度(默认值:1)。只在批处理模式下使用。
--audit-log-batch-max-wait duration
指定尚未达到最大值的批处理的强制写入等待时间。只在批处理模式下使用。
--audit-log-batch-throttle-burst int
指定在未使用 ThrottleQPS 时同时发送请求的最大数量。只在批处理模式下使用。
--audit-log-batch-throttle-enable
指定是否启用 batching throttling。只在批处理模式下使用。
--audit-log-batch-throttle-qps float32
设定每秒内可执行的批处理的最大平均数。只在批处理模式下使用。

日志格式:
--audit-log-format string     Default: "json"
指定存储审计日志的格式。legacy 表示每个事件记录 1 行文本;json 表示以结构化 json 格式记录。目前仅支持 legacy 和 json(默认值:json)。

日志大小等:
--audit-log-maxage int
指定历史审计日志的最大保存天数,以日志文件名中的时间戳为准。
--audit-log-maxbackup int
指定历史审计日志的最大保存数量。
--audit-log-maxsize int
指定审计日志流转前的最大大小(单位:MB)。

处理模式:
--audit-log-mode string     Default: "blocking"
指定发送审计事件的策略。blocking 表示发送事件时阻塞服务器响应;batch 表示在后端异步缓冲和写入事件。目前仅支持 batch 和 blocking(默认值:blocking)。
--audit-log-path string
如果指定该参数,则所有 API 服务器接受的请求都会记录到此文件。"-" 表示记录到标准输出。

是否截断:
--audit-log-truncate-enabled
指定是否允许截断事件和批处理。
--audit-log-truncate-max-batch-size int     Default: 10485760
指定发送到底层后端的批处理的最大字节数(默认值:10485760)。实际序列化时的大小会比设定值大几百字节。如果一个批处理超出该大小,它会被分为几个小的批处理。
--audit-log-truncate-max-event-size int     Default: 102400
指定发送到底层后端的审计事件的最大字节数(默认值:102400)。如果一个事件超出该大小,则第一个请求和回复会被删除,如果还没有减少到合适的大小,该事件将被丢弃。
--audit-log-version string     Default: "audit.k8s.io/v1beta1"

6、参数调整

需要设置参数以适应 API 服务器上的负载。

Kubernetes 审计(Auditing)功能详解_API_02

例如,如果 kube-apiserver 每秒收到 100 个请求,并且每个请求仅在 ResponseStarted 和 ResponseComplete 阶段进行审计,则应该考虑每秒生成约 200 个审计事件。假设批处理中最多有 100 个事件,则应将限制级别设置为至少 2 个 QPS。假设后端最多需要 5 秒钟来写入事件,您应该设置缓冲区大小以容纳最多 5 秒的事件,即 10 个 batch,即 1000 个事件。

但是,在大多数情况下,默认参数应该足够了,你不必手动设置它们。 你可以查看 kube-apiserver 公开的以下 Prometheus 指标,并在日志中监控审计子系统的状态。

  • apiserver_audit_event_total 包含所有暴露的审计事件数量的指标。
  • apiserver_audit_error_total 在暴露时由于发生错误而被丢弃的事件的数量。

7、文本Json格式

本文仅展示RequestResponse审计级别下的结构体:

{
    "kind": "Event",
    "apiVersion": "audit.k8s.io/v1",
    "level": "RequestResponse",
    "auditID": "b27cf01d-74ba-4a3d-930e-5986fb29b09f",
    "stage": "ResponseComplete",
    "requestURI": "/apis/apps/v1/namespaces/lc-test/deployments/test",
    "verb": "delete",
    "user": {
        "username": "system:serviceaccount:cloudbases-system:cloudbases",
        "uid": "37a93b9b-420a-49dd-aeeb-693b1314ca3e",
        "groups": [
            "system:serviceaccounts",
            "system:serviceaccounts:cloudbases-system",
            "system:authenticated"
        ],
        "extra": {
            "authentication.kubernetes.io/pod-name": [
                "cb-apiserver-766bf57b5b-r65sx"
            ],
            "authentication.kubernetes.io/pod-uid": [
                "f0e3bf25-e008-449a-838c-25c6fab23e8c"
            ]
        }
    },
    "sourceIPs": [
        "10.233.64.85",
        "10.233.64.47"
    ],
    "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36",
    "objectRef": {
        "resource": "deployments",
        "namespace": "lc-test",
        "name": "test",
        "apiGroup": "apps",
        "apiVersion": "v1"
    },
    "responseStatus": {
        "metadata": {
            
        },
        "status": "Success",
        "code": 200
    },
    "requestObject": {
        "kind": "DeleteOptions",
        "apiVersion": "apps/v1",
        "propagationPolicy": "Background"
    },
    "responseObject": {
        "kind": "Status",
        "apiVersion": "v1",
        "metadata": {
            
        },
        "status": "Success",
        "details": {
            "name": "test",
            "group": "apps",
            "kind": "deployments",
            "uid": "8c7ba9f6-474d-4eeb-bb9d-58686dff9972"
        }
    },
    "requestReceivedTimestamp": "2022-07-28T01:01:21.690683Z",
    "stageTimestamp": "2022-07-28T01:01:21.699671Z",
    "annotations": {
        "authorization.k8s.io/decision": "allow",
        "authorization.k8s.io/reason": "RBAC: allowed by ClusterRoleBinding \"cloudbases\" of ClusterRole \"cluster-admin\" to ServiceAccount \"cloudbases/cloudbases-system\""
    }
}

8、总结

1)、开启k8s原生审计功能必须配置审计策略和审计后端否则不会执行审计过滤器逻辑。
2)、审计过滤器会过滤客户端的每一个请求,通过当前请求信息组织AttributesRecord结构体对象,然后基于AttributesRecord结构体对象和审计策略配置文件中的规则做比对,返回当前请求的审计策略级别。如果审计策略配置文件中没有当前请求对应的规则的话,或者当前请求的策略级别为None话,则当前请求不需要审计。
3)、基于请求信息、审计策略、AttributesRecord结构体对象等信息生成审计事件对象,并将审计事件对象放到context中。
4)、基于审计策略配置文件中的omitStages配置(不配置omitStages的话默认RequestReceived、ResponseStarted、ResponseComplete这三个阶段都会产生审计事件),在保留的审计阶段生成审计事件并输出到对应后端。 

参考:https://kubernetes.io/zh-cn/docs/tasks/debug/debug-cluster/audit/

参考:https://kubernetes.io/zh-cn/docs/reference/config-api/apiserver-audit.v1/#audit-k8s-io-v1-Event

参考:kube-apiserver审计功能介绍及使用

参考:kubernetes审计