使用Golang实现针对K8s资源的准入控制审计日志系统

6次阅读

admissionreview 的 request.Object 有时为 nil,因 k8s 在 delete 或部分 update 场景下仅发送元信息(如 uid、name),不传完整对象;需先检查 request.object.raw 非空再反序列化,delete 时应优先用 request.oldobject 或异步 get 获取资源,但审计日志中应避免同步阻塞式 get。

使用Golang实现针对K8s资源的准入控制审计日志系统

为什么 AdmissionReview 的 request.object 有时是 nil?

因为 K8s 在某些准入场景下(比如 DELETE 或部分 UPDATE)不会把完整对象发给 webhook,只传 request.uidrequest.name 等元信息。这时直接解码 request.object 会 panic 或得到空结构。

  • 必须先检查 request.Object.Raw 是否非空,再尝试反序列化
  • DELETE 请求,真正要审计的“被删资源”得靠 request.oldObject(如果存在)或后续从集群中 get —— 但注意:audit 日志应尽量避免同步阻塞式 get,否则拖慢准入链路
  • 更稳妥的做法是:只对 CREATE 和带 request.objectUPDATE 做结构化解析;其余情况记录为 “object unavailable”,并保留 request.kindrequest.Namespacerequest.name 和操作类型

如何用 controller-runtime 快速启动一个合规的 ValidatingWebhook

别自己手写 http server 处理 TLS、证书、AdmissionReview 解包 —— controller-runtimeadmission.Server封装好这些。重点在注册逻辑和错误返回格式。

  • admission.WithServer(&admission.Server{Port: 9443}) 启动,它自动处理证书挂载(需确保容器里有 /tmp/k8s-webhook-server/serving-certs/
  • Handler 实现必须返回 admission.PatchResponseFromRawadmission.Allowed/admission.Denied,不能直接 return json
  • 审计日志本身不参与准入决策,所以建议在 Handle 方法末尾异步写日志(用 go func() { ... }()),避免阻塞响应
  • 示例片段:
    func (h *AuditHandler) Handle(ctx context.Context, req admission.Request) admission.Response {     // ... 解析 request ...     go h.logAudit(req, result) // 异步审计     return admission.Allowed("") }

admissionv1.AdmissionRequest 中哪些字段必须提取进审计日志?

不是所有字段都有审计价值,K8s 审计策略(audit.k8s.io/v1)也只要求核心上下文。漏掉关键字段会导致事后无法还原操作主体或资源归属。

  • 必填:req.UIDreq.Kind.Groupreq.Kind.Kindreq.Resource.Resourcereq.Namespacereq.Namereq.Operation
  • 身份相关:req.UserInfo.Usernamereq.UserInfo.Groupsreq.UserInfo.Extra(如 "authentication.kubernetes.io/pod-name"
  • 避免记录 req.Object.Raw 全量内容 —— 体积大、敏感信息多、且可能超限(K8s 对 webhook 响应大小有限制);可选记录 len(req.Object.Raw) 或摘要(如 sha256.Sum256(req.Object.Raw).String()[:12]
  • 特别注意:req.DryRunbool,必须记录,它直接影响该操作是否真实变更了集群状态

审计日志写入时为什么总丢数据或卡住?

因为默认 stdout/stderr 写入在高并发准入请求下容易成为瓶颈,尤其当后端是文件、网络或低吞吐日志服务时。golanglog 包默认无缓冲、同步写,而 admission handler 要求 sub-second 响应。

立即学习go语言免费学习笔记(深入)”;

  • 绝不要在 Handle 里直接 log.printffmt.Fprintln(os.Stdout, ...)
  • 用带缓冲的 channel + 单独 goroutine 消费(例如 chan AuditEvent,buffer size 设为 1000)
  • 写入失败时,本地 fallback 到 ring buffer(如 golang.org/x/exp/slices 管理的固定长度 slice),避免丢弃全部;但注意内存占用
  • 如果对接 Loki 或 ES,优先用批量写(POST /loki/api/v1/push)而非单条,batch size 控制在 10–50 条之间,延迟 ≤ 1s

最常被忽略的是 DryRun 和 UserInfo.Extra 的完整性 —— 这俩字段一旦缺失,就分不清是用户真实操作还是 CI 工具触发,也定位不到具体 Pod 或 ServiceAccount。别省那几行字段提取代码。

text=ZqhQzanResources