k8s源碼分析8-Audit審計功能

audit審計的總結

  • Kubernetes 審計(Auditing) 功能提供了與安全相關的、按時間順序排列的記錄集,記錄每個用戶、使用 Kubernetes API 的應用以及控制面自身引發的活動
  • 審計功能使得集羣管理員能夠回答以下問題:
    • 發生了什麼?
    • 什麼時候發生的?
    • 誰觸發的?
    • 活動發生在哪個(些)對象上?
    • 在哪觀察到的?
    • 它從哪觸發的?
    • 活動的後續處理行爲是什麼?
  • 審計策略
    • None - 符合這條規則的日誌將不會記錄。
    • Metadata - 記錄請求的元數據(請求的用戶、時間戳、資源、動詞等等), 但是不記錄請求或者響應的消息體。
    • Request - 記錄事件的元數據和請求的消息體,但是不記錄響應的消息體。 這不適用於非資源類型的請求。
    • RequestResponse - 記錄事件的元數據,請求和響應的消息體。這不適用於非資源類型的請求。

審計功能介紹

audit源碼閱讀

  • 入口位置 D:\go_path\src\github.com\kubernetes\kubernetes\cmd\kube-apiserver\app\server.go
  • buildGenericConfig

	lastErr = s.Audit.ApplyTo(genericConfig)
	if lastErr != nil {
		return
	}

1. 從配置的 --audit-policy-file加載audit策略

  • 你可以使用 --audit-policy-file 標誌將包含策略的文件傳遞給 kube-apiserver
  • 如果不設置該標誌,則不記錄事件
  • rules 字段 必須 在審計策略文件中提供。沒有(0)規則的策略將被視爲非法配置。
	// 1. Build policy evaluator
	evaluator, err := o.newPolicyRuleEvaluator()
	if err != nil {
		return err
	}

2. 從配置的 --audit-log-path設置 logBackend

	// 2. Build log backend
	var logBackend audit.Backend
	w, err := o.LogOptions.getWriter()
	if err != nil {
		return err
	}
  • 如果後端 --audit-log-path="-" 代表記錄到標準輸出
func (o *AuditLogOptions) getWriter() (io.Writer, error) {
	if !o.enabled() {
		return nil, nil
	}

	if o.Path == "-" {
		return os.Stdout, nil
	}

	if err := o.ensureLogFile(); err != nil {
		return nil, fmt.Errorf("ensureLogFile: %w", err)
	}

	return &lumberjack.Logger{
		Filename:   o.Path,
		MaxAge:     o.MaxAge,
		MaxBackups: o.MaxBackups,
		MaxSize:    o.MaxSize,
		Compress:   o.Compress,
	}, nil
}

獲取到日誌writer對象後校驗下有沒有evaluator

  • 如果沒有evaluator,打印條提示日誌
  • 如果將backend設置爲w
	if w != nil {
		if evaluator == nil {
			klog.V(2).Info("No audit policy file provided, no events will be recorded for log backend")
		} else {
			logBackend = o.LogOptions.newBackend(w)
		}
	}

3 根據配置構建webhook的 後端

	// 3. Build webhook backend
	var webhookBackend audit.Backend
	if o.WebhookOptions.enabled() {
		if evaluator == nil {
			klog.V(2).Info("No audit policy file provided, no events will be recorded for webhook backend")
		} else {
			if c.EgressSelector != nil {
				var egressDialer utilnet.DialFunc
				egressDialer, err = c.EgressSelector.Lookup(egressselector.ControlPlane.AsNetworkContext())
				if err != nil {
					return err
				}
				webhookBackend, err = o.WebhookOptions.newUntruncatedBackend(egressDialer)
			} else {
				webhookBackend, err = o.WebhookOptions.newUntruncatedBackend(nil)
			}
			if err != nil {
				return err
			}
		}
	}

4 如果有webhook就把它封裝爲dynamicBackend

	// 4. Apply dynamic options.
	var dynamicBackend audit.Backend
	if webhookBackend != nil {
		// if only webhook is enabled wrap it in the truncate options
		dynamicBackend = o.WebhookOptions.TruncateOptions.wrapBackend(webhookBackend, groupVersion)
	}

5. 設置審計的策略計算對象 evaluator

	// 5. Set the policy rule evaluator
	c.AuditPolicyRuleEvaluator = evaluator

6 把logBackend和 dynamicBackend 做union


	// 6. Join the log backend with the webhooks
	c.AuditBackend = appendBackend(logBackend, dynamicBackend)
func appendBackend(existing, newBackend audit.Backend) audit.Backend {
	if existing == nil {
		return newBackend
	}
	if newBackend == nil {
		return existing
	}
	return audit.Union(existing, newBackend)
}

7.最終的運行方法

  • backend接口方法
  • D:\go_path\src\github.com\kubernetes\kubernetes\staging\src\k8s.io\apiserver\pkg\audit\types.go
type Sink interface {
	// ProcessEvents handles events. Per audit ID it might be that ProcessEvents is called up to three times.
	// Errors might be logged by the sink itself. If an error should be fatal, leading to an internal
	// error, ProcessEvents is supposed to panic. The event must not be mutated and is reused by the caller
	// after the call returns, i.e. the sink has to make a deepcopy to keep a copy around if necessary.
	// Returns true on success, may return false on error.
	ProcessEvents(events ...*auditinternal.Event) bool
}

type Backend interface {
	Sink

	// Run will initialize the backend. It must not block, but may run go routines in the background. If
	// stopCh is closed, it is supposed to stop them. Run will be called before the first call to ProcessEvents.
	Run(stopCh <-chan struct{}) error

	// Shutdown will synchronously shut down the backend while making sure that all pending
	// events are delivered. It can be assumed that this method is called after
	// the stopCh channel passed to the Run method has been closed.
	Shutdown()

	// Returns the backend PluginName.
	String() string
}

  • 最終調用audit的ProcessEvents方法,以log舉例,位置 D:\go_path\src\github.com\kubernetes\kubernetes\staging\src\k8s.io\apiserver\plugin\pkg\audit\log\backend.go
  • 流程分析,根據format類型做最後的line生成,然後調用 fmt.Fprint(b.out, line),向b.out中寫入line即可
func (b *backend) ProcessEvents(events ...*auditinternal.Event) bool {
	success := true
	for _, ev := range events {
		success = b.logEvent(ev) && success
	}
	return success
}

func (b *backend) logEvent(ev *auditinternal.Event) bool {
	line := ""
	switch b.format {
	case FormatLegacy:
		line = audit.EventString(ev) + "\n"
	case FormatJson:
		bs, err := runtime.Encode(b.encoder, ev)
		if err != nil {
			audit.HandlePluginError(PluginName, err, ev)
			return false
		}
		line = string(bs[:])
	default:
		audit.HandlePluginError(PluginName, fmt.Errorf("log format %q is not in list of known formats (%s)",
			b.format, strings.Join(AllowedFormats, ",")), ev)
		return false
	}
	if _, err := fmt.Fprint(b.out, line); err != nil {
		audit.HandlePluginError(PluginName, err, ev)
		return false
	}
	return true
}

8. http側調用的handler

  • D:\go_path\src\github.com\kubernetes\kubernetes\staging\src\k8s.io\apiserver\pkg\endpoints\filters\audit.go
// WithAudit decorates a http.Handler with audit logging information for all the
// requests coming to the server. Audit level is decided according to requests'
// attributes and audit policy. Logs are emitted to the audit sink to
// process events. If sink or audit policy is nil, no decoration takes place.
func WithAudit(handler http.Handler, sink audit.Sink, policy audit.PolicyRuleEvaluator, longRunningCheck request.LongRunningRequestCheck) http.Handler {
	if sink == nil || policy == nil {
		return handler
	}
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		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()
		if ev == nil || ctx == nil {
			handler.ServeHTTP(w, req)
			return
		}

		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.
		defer func() {
			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",
			}
			if ev.ResponseStatus == nil && longRunningSink != nil {
				ev.ResponseStatus = fakedSuccessStatus
				ev.Stage = auditinternal.StageResponseStarted
				processAuditEvent(ctx, longRunningSink, ev, omitStages)
			}

			ev.Stage = auditinternal.StageResponseComplete
			if ev.ResponseStatus == nil {
				ev.ResponseStatus = fakedSuccessStatus
			}
			processAuditEvent(ctx, sink, ev, omitStages)
		}()
		handler.ServeHTTP(respWriter, req)
	})
}

audit審計的總結

  • Kubernetes 審計(Auditing) 功能提供了與安全相關的、按時間順序排列的記錄集,記錄每個用戶、使用 Kubernetes API 的應用以及控制面自身引發的活動
  • 審計功能使得集羣管理員能夠回答以下問題:
    • 發生了什麼?
    • 什麼時候發生的?
    • 誰觸發的?
    • 活動發生在哪個(些)對象上?
    • 在哪觀察到的?
    • 它從哪觸發的?
    • 活動的後續處理行爲是什麼?
  • 審計策略
    • None - 符合這條規則的日誌將不會記錄。
    • Metadata - 記錄請求的元數據(請求的用戶、時間戳、資源、動詞等等), 但是不記錄請求或者響應的消息體。
    • Request - 記錄事件的元數據和請求的消息體,但是不記錄響應的消息體。 這不適用於非資源類型的請求。
    • RequestResponse - 記錄事件的元數據,請求和響應的消息體。這不適用於非資源類型的請求。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章