安全管控模块

2025-10-15T10:30:00+08:00 | 23分钟阅读 | 更新于 2025-10-15T10:30:00+08:00

@

学习目标

学完本模块,你应该能够:

  1. 说清镜像安全扫描如何在「镜像推送即拦截」模式下阻断高危漏洞上线,并能配置 Trivy/Harbor 的阻断阈值。
  2. 在租户 Namespace 内落地零信任网络:编写 NetworkPolicy、启用 mTLS,并验证东西向流量最小化。
  3. 使用 KMS 信封加密托管 Secret,确保密钥在 etcd、磁盘、配置分发链路中均不以明文落地。
  4. 通过 kube-bench / CIS Benchmark 对多套物理隔离集群做合规基线巡检,并对接联邦监控形成趋势看板。
  5. 把安全事件(漏洞、逃逸、异常进程、横向移动)转化为带 tenant/app/env/cluster 标签的指标与告警,进入审计留痕 90 天以上的闭环。

前置知识

  • 熟悉 Kubernetes 核心对象(Namespace、Pod、Secret、Service、NetworkPolicy)。
  • 了解镜像仓库(Harbor)与 CI/CD 基本流程。
  • 读过本白皮书「模块 02 多 K8s 集群纳管」「模块 05 统一观测平台」,理解统一 K8s API 网关与 Prometheus 联邦架构。
  • 基本了解对称/非对称加密、KMS、mTLS 的概念。

本章你会动手做的事

  1. 给一个测试租户 Namespace 写一条默认拒绝(default-deny)的 NetworkPolicy,并用 curl 验证跨 Pod 访问被拦。
  2. 用 Trivy 对本团队镜像扫描一次,把 HIGH/CRITICAL 漏洞数量拉到 0 后再推送到 Harbor。
  3. 在联邦 Grafana 里打开「安全事件大盘」,观察 security_event_total 指标带租户标签的实时曲线。

一、模块概述与企业商用价值

类比:安全管控就像写字楼的「物业安保体系」——摄像头与门禁(镜像扫描与准入网关)挡住带病人员进楼;楼层之间的防火门与访客登记(零信任网络与 mTLS)防止火情跨层蔓延;保险柜(KMS 信封加密)保证合同原件不外露;而消防年检(CIS 基线)则用统一标准证明这栋楼「合规」。

对企业自研商用 PaaS 而言,安全不是可选项,而是金融、政企客户验收的硬门槛。平台把安全能力下沉为「平台默认即安全」的控制面,让上层租户无需各自重复建设,又能满足等保、行业监管对漏洞管理、密钥保护、合规审计的要求。

适用角色:平台安全管理员、集群运维、租户应用负责人、合规审计人员。

核心商业价值:

  • 风险左移:漏洞在镜像入库与部署准入阶段即被拦截,避免高危组件跑在生产集群。
  • 合规可证:CIS 基线、密钥加密、审计留痕 90 天以上,直接支撑外部审计与认证。
  • 责任清晰:所有安全事件带租户/环境标签,问题定位到具体团队,避免「扯皮」。

下面这张图说明安全管控模块在整体平台中的定位:它同时保护多 K8s 集群层与统一观测层,并把安全事件反向注入联邦监控。

flowchart LR
    A[企业自研PaaS平台] --> B[多K8s集群层
物理隔离 加 API网关] A --> C[统一观测层
联邦Prometheus 加 Thanos] A --> D[安全管控模块] D --> D1[镜像安全扫描] D --> D2[零信任网络] D --> D3[KMS密钥加密] D --> D4[CIS合规基线] D --> D5[运行时入侵检测] D -.保护.-> B D -.安全事件进指标.-> C

项目结构与文件清单

本模块的实现代码按「平台 Go 控制面 + K8s IaC + Prometheus 规则」三层组织,目录树如下(后续章节逐一完整展示每个文件):

security-module/
├── go.mod                              # Go module 依赖(gin + client-go + prometheus)
├── cmd/security-server/
│   └── main.go                        # gin HTTP 服务:扫描/校验/加解密/基线 统一入口 + /metrics
├── internal/security/
│   ├── trivy/client.go                # ① Trivy 扫描 API 封装(rest 调 Trivy API Server)
│   ├── admission/gatekeeper.go        # ④ OPA/Gatekeeper 策略校验(rest 调准入 webhook)
│   ├── kms/envelope.go                # ② KMS 信封加密(调 KMS SDK 生成/解开 DEK)
│   ├── cis/kubebench.go               # ③ kube-bench 基线触发(client-go 在目标集群跑 Job)
│   └── policy/push.go                 # 策略下发(client-go 向租户 NS 写 NetworkPolicy)
└── deploy/
    ├── gatekeeper/
    │   ├── constraint-template.yaml   # Gatekeeper ConstraintTemplate(强约束 tenant/env 标签)
    │   └── constraint.yaml            # Gatekeeper Constraint 实例
    ├── kyverno/
    │   └── require-labels.yaml        # Kyverno ClusterPolicy(Pod 必带 tenant/app/env)
    ├── networkpolicy/
    │   ├── default-deny.yaml          # 默认拒绝 NetworkPolicy
    │   └── allow-dns.yaml             # 放行 DNS(default-deny 后先放通 53)
    ├── podsecurity/
    │   └── restrict-baseline.yaml     # Pod Security Standards: restricted
    └── prometheus/
        ├── security-alerts.yaml       # 安全事件指标与告警(扫描失败/策略违反/基线/明文)
        └── servicemonitor-scanners.yaml # 采集扫描器/控制面指标的 ServiceMonitor

端到端调用关系:CI/Harbor 触发 ① Trivy → 结果进 security_event_totalimage_scan_vulnerabilities;部署经 ④ Gatekeeper 校验标签与基线;Secret 写入 etcd 前经 ② KMS 信封加密③ kube-bench 周期巡检多集群;policy/push.go 经 API 网关把零信任策略下发到租户 Namespace。所有指标带 tenant/app/env/cluster,经联邦 Prometheus + Thanos 长期存储。


二、细分功能详解(商用生产级)

下面用表格区分【基础能力】与【高级企业增值能力】。标注「★禁止删减」的三项(扫描、加密、基线)是金融/政企交付底线,任何交付裁剪都必须保留。

分类功能关键组件说明删减约束
基础能力① 镜像安全扫描Trivy / Harbor推送即扫描,HIGH/CRITICAL 直接阻断入库与部署★禁止删减(扫描)
基础能力② 密钥加密KMS 信封加密(如 Vault Transit / 云 KMS)Secret 写入 etcd 前加密,明文不落盘★禁止删减(加密)
基础能力③ 合规基线检查CIS Benchmark / kube-bench集群与控制面按 CIS 标准打分,不达标项告警★禁止删减(基线)
高级企业增值能力④ 零信任网络NetworkPolicy / mTLS(Istio/Linkerd)默认拒绝东西向流量,服务间强制身份双向认证建议保留
高级企业增值能力⑤ 漏洞治理闭环Trivy + 工单/ITSM扫描结果派单、修复、复扫、关闭全流程建议保留
高级企业增值能力⑥ 运行时入侵检测Falco / 运行时探针检测容器逃逸、异常进程、敏感文件读取建议保留
高级企业增值能力⑦ 敏感操作二次认证SSO / MFA / 审批流高危操作(删集群、改基线)需二次确认建议保留
高级企业增值能力⑧ 安全审计视图联邦审计日志安全相关操作统一留痕 90 天以上,可检索建议保留

基础能力逐项拆解

① 镜像安全扫描(★禁止删减):在 Harbor 配置 Trivy 扫描器,推送钩子触发。阻断策略按严重度分级——CRITICALHIGH 默认阻断入库;MEDIUM 进入工单但允许放行(需审批)。扫描结果同时写入联邦指标 image_scan_vulnerabilities{tenant,app,env,cluster,severity}

下面是平台侧对 Trivy API Server 的 Go 封装(rest 调用 /v1/scan),负责扫描、按严重度聚合、并依据阻断阈值判断是否拦截:

// file: internal/security/trivy/client.go
package trivy

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

// ScanRequest 对应 Trivy API Server 的 /v1/scan 请求体(精简字段)。
type ScanRequest struct {
	Target       string   `json:"target"`
	ArtifactType string   `json:"artifact_type"`
	Scanners     []string `json:"scanners"`
	Options      struct {
		Severity []string `json:"severity"`
	} `json:"options"`
}

// Vulnerability 单条漏洞。
type Vulnerability struct {
	VulnerabilityID  string `json:"VulnerabilityID"`
	PkgName          string `json:"PkgName"`
	InstalledVersion string `json:"InstalledVersion"`
	FixedVersion     string `json:"FixedVersion"`
	Severity         string `json:"Severity"`
}

// ScanResult 单个扫描目标结果。
type ScanResult struct {
	Target          string          `json:"Target"`
	Vulnerabilities []Vulnerability `json:"Vulnerabilities"`
}

// ScanResponse 对应 Trivy API Server 响应。
type ScanResponse struct {
	Results []ScanResult `json:"Results"`
}

// Client 封装对 Trivy API Server 的调用。
type Client struct {
	baseURL    string
	httpClient *http.Client
}

// NewClient 创建 Trivy 客户端。
func NewClient(baseURL string) *Client {
	return &Client{
		baseURL:    baseURL,
		httpClient: &http.Client{Timeout: 5 * time.Minute},
	}
}

// Scan 对单个镜像执行漏洞扫描,返回完整扫描结果。
func (c *Client) Scan(ctx context.Context, image string, severities []string) (*ScanResponse, error) {
	if len(severities) == 0 {
		severities = []string{"UNKNOWN", "LOW", "MEDIUM", "HIGH", "CRITICAL"}
	}
	reqBody := ScanRequest{
		Target:       image,
		ArtifactType: "image",
		Scanners:     []string{"vuln"},
	}
	reqBody.Options.Severity = severities

	payload, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("marshal scan request: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost,
		c.baseURL+"/v1/scan", bytes.NewReader(payload))
	if err != nil {
		return nil, fmt.Errorf("build scan request: %w", err)
	}
	httpReq.Header.Set("Content-Type", "application/json")

	resp, err := c.httpClient.Do(httpReq)
	if err != nil {
		return nil, fmt.Errorf("call trivy api: %w", err)
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("trivy api returned %d", resp.StatusCode)
	}

	var out ScanResponse
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return nil, fmt.Errorf("decode scan response: %w", err)
	}
	return &out, nil
}

// CountBySeverity 统计各严重度漏洞数量。
func (r *ScanResponse) CountBySeverity() map[string]int {
	counts := map[string]int{}
	for _, res := range r.Results {
		for _, v := range res.Vulnerabilities {
			counts[v.Severity]++
		}
	}
	return counts
}

// Blocked 按阻断阈值(HIGH/CRITICAL 默认阻断)判断是否应拦截。
func (r *ScanResponse) Blocked(blockSeverities []string) bool {
	set := map[string]struct{}{}
	for _, s := range blockSeverities {
		set[s] = struct{}{}
	}
	for _, res := range r.Results {
		for _, v := range res.Vulnerabilities {
			if _, ok := set[v.Severity]; ok {
				return true
			}
		}
	}
	return false
}

⚠️ 新手坑:不要把扫描器只挂在 CI 阶段而放行「本地直推 Harbor」。生产环境必须通过准入网关(Gatekeeper/动态准入 webhook)在部署侧二次校验镜像扫描状态,防止绕过 CI 直接部署未扫描镜像。

② 密钥加密(★禁止删减):采用 KMS 信封加密——应用 Secret 由数据密钥(DEK)加密,DEK 再由 KMS 主密钥(KEK)加密后随密文存储。etcd 中只保存密文;API 网关向租户 Namespace 下发时在线解密,明文只在内存短暂存在,绝不落盘。

下面是 KMS 信封加密的 Go 实现:向 KMS 申请被 KEK 加密的 DEK,本地用 AES-GCM 加密明文,密文包(encDEK+nonce+ciphertext)写入 etcd。解密时先经 KMS 解开 DEK,再解开数据。

// file: internal/security/kms/envelope.go
package kms

import (
	"bytes"
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"time"
)

// KMSClient KMS 服务接口(信封加密:生成数据密钥 / 解密数据密钥)。
type KMSClient interface {
	// GenerateDataKey 向 KMS 申请一个被主密钥(KEK)加密的数据密钥(DEK)。
	GenerateDataKey(ctx context.Context, keyID string) (plaintextDEK, encryptedDEK []byte, err error)
	// DecryptDataKey 用主密钥解密出数据密钥明文。
	DecryptDataKey(ctx context.Context, keyID string, encryptedDEK []byte) (plaintextDEK []byte, err error)
}

// HTTPKMSClient 通过 KMS 网关 REST 接口调用 KMS(云 KMS 或自研 KMS 均可替换)。
type HTTPKMSClient struct {
	endpoint   string
	httpClient *http.Client
}

func NewHTTPKMSClient(endpoint string) *HTTPKMSClient {
	return &HTTPKMSClient{
		endpoint:   endpoint,
		httpClient: &http.Client{Timeout: 10 * time.Second},
	}
}

type genKeyReq struct{ KeyID string `json:"key_id"` }
type genKeyResp struct {
	PlaintextDEK string `json:"plaintext_dek"` // base64
	EncryptedDEK string `json:"encrypted_dek"` // base64(KEK 加密)
}
type decKeyReq struct {
	KeyID        string `json:"key_id"`
	EncryptedDEK string `json:"encrypted_dek"`
}

// GenerateDataKey 向 KMS 申请数据密钥(明文 DEK + KEK 加密的 DEK)。
func (c *HTTPKMSClient) GenerateDataKey(ctx context.Context, keyID string) ([]byte, []byte, error) {
	payload, _ := json.Marshal(genKeyReq{KeyID: keyID})
	var resp genKeyResp
	if err := c.post(ctx, "/v1/keys/generate-data-key", payload, &resp); err != nil {
		return nil, nil, err
	}
	p, err := base64.StdEncoding.DecodeString(resp.PlaintextDEK)
	if err != nil {
		return nil, nil, fmt.Errorf("decode plaintext dek: %w", err)
	}
	e, err := base64.StdEncoding.DecodeString(resp.EncryptedDEK)
	if err != nil {
		return nil, nil, fmt.Errorf("decode encrypted dek: %w", err)
	}
	return p, e, nil
}

// DecryptDataKey 用 KMS 主密钥解密出 DEK 明文。
func (c *HTTPKMSClient) DecryptDataKey(ctx context.Context, keyID string, encryptedDEK []byte) ([]byte, error) {
	payload, _ := json.Marshal(decKeyReq{KeyID: keyID, EncryptedDEK: base64.StdEncoding.EncodeToString(encryptedDEK)})
	var resp genKeyResp
	if err := c.post(ctx, "/v1/keys/decrypt-data-key", payload, &resp); err != nil {
		return nil, err
	}
	return base64.StdEncoding.DecodeString(resp.PlaintextDEK)
}

func (c *HTTPKMSClient) post(ctx context.Context, path string, body []byte, out interface{}) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.endpoint+path, bytes.NewReader(body))
	if err != nil {
		return fmt.Errorf("build request: %w", err)
	}
	req.Header.Set("Content-Type", "application/json")
	resp, err := c.httpClient.Do(req)
	if err != nil {
		return fmt.Errorf("call kms: %w", err)
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("kms returned %d", resp.StatusCode)
	}
	return json.NewDecoder(resp.Body).Decode(out)
}

// envelopePkg 可序列化的密文包:encryptedDEK + nonce + ciphertext 均 base64。
type envelopePkg struct {
	EncryptedDEK string `json:"k"` // base64(KEK 加密的 DEK)
	Nonce        string `json:"n"` // base64(AES-GCM nonce)
	Ciphertext   string `json:"c"` // base64(DEK 加密的数据)
}

// EnvelopeSeal 用 KMS 信封加密明文:本地用随机 DEK(AES-GCM) 加密数据,DEK 经 KMS 加密后随密文存储。
// 返回可直接写入 etcd 的密文包(JSON 字符串,明文不落盘)。
func EnvelopeSeal(ctx context.Context, kmsCli KMSClient, keyID string, plaintext []byte) (string, error) {
	dek, encDEK, err := kmsCli.GenerateDataKey(ctx, keyID)
	if err != nil {
		return "", fmt.Errorf("generate data key: %w", err)
	}
	block, err := aes.NewCipher(dek)
	if err != nil {
		return "", fmt.Errorf("new cipher: %w", err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return "", fmt.Errorf("new gcm: %w", err)
	}
	nonce := make([]byte, gcm.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return "", fmt.Errorf("rand nonce: %w", err)
	}
	ciphertext := gcm.Seal(nil, nonce, plaintext, nil)

	pkg := envelopePkg{
		EncryptedDEK: base64.StdEncoding.EncodeToString(encDEK),
		Nonce:        base64.StdEncoding.EncodeToString(nonce),
		Ciphertext:   base64.StdEncoding.EncodeToString(ciphertext),
	}
	b, err := json.Marshal(pkg)
	if err != nil {
		return "", fmt.Errorf("marshal envelope: %w", err)
	}
	return string(b), nil
}

// EnvelopeOpen 解密密文包:先经 KMS 解出 DEK 明文,再用 DEK 解开数据。明文仅在内存短暂存在。
func EnvelopeOpen(ctx context.Context, kmsCli KMSClient, keyID string, sealed string) ([]byte, error) {
	var pkg envelopePkg
	if err := json.Unmarshal([]byte(sealed), &pkg); err != nil {
		return nil, fmt.Errorf("unmarshal envelope: %w", err)
	}
	encDEK, err := base64.StdEncoding.DecodeString(pkg.EncryptedDEK)
	if err != nil {
		return nil, fmt.Errorf("decode dek: %w", err)
	}
	dek, err := kmsCli.DecryptDataKey(ctx, keyID, encDEK)
	if err != nil {
		return nil, fmt.Errorf("decrypt dek: %w", err)
	}
	nonce, err := base64.StdEncoding.DecodeString(pkg.Nonce)
	if err != nil {
		return nil, fmt.Errorf("decode nonce: %w", err)
	}
	ct, err := base64.StdEncoding.DecodeString(pkg.Ciphertext)
	if err != nil {
		return nil, fmt.Errorf("decode ciphertext: %w", err)
	}
	block, err := aes.NewCipher(dek)
	if err != nil {
		return nil, fmt.Errorf("new cipher: %w", err)
	}
	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("new gcm: %w", err)
	}
	plaintext, err := gcm.Open(nil, nonce, ct, nil)
	if err != nil {
		return nil, fmt.Errorf("gcm open: %w", err)
	}
	return plaintext, nil
}

③ 合规基线(★禁止删减):kube-bench 按 CIS Kubernetes Benchmark 对 API Server、etcd、kubelet、控制平面逐条检查。多集群通过统一 K8s API 网关并发巡检,结果进联邦指标 cis_check_failures{tenant,cluster,check_id}

下面是平台用 client-go 在目标集群(经 API 网关路由)以 Job 方式触发 kube-bench、等待完成并解析 FAIL/WARN 项的 Go 实现:

// file: internal/security/cis/kubebench.go
package cis

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"time"

	batchv1 "k8s.io/api/batch/v1"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
)

// ClusterClient 经统一 K8s API 网关取得某物理隔离集群的 clientset。
func ClusterClient(kubeconfigPath, cluster string) (*kubernetes.Clientset, error) {
	cfg, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath)
	if err != nil {
		return nil, fmt.Errorf("build rest config for %s: %w", cluster, err)
	}
	// 生产环境经 API 网关:把 apiserver 地址指向网关并带上集群路由。
	cfg.Host = "https://k8s-gateway.paas.local/" + cluster
	cfg.QPS = 50
	cfg.Burst = 100
	cs, err := kubernetes.NewForConfig(cfg)
	if err != nil {
		return nil, fmt.Errorf("new clientset for %s: %w", cluster, err)
	}
	return cs, nil
}

func int32Ptr(i int32) *int32 { return &i }

// kubeBenchOutput 对应 kube-bench --format json 的精简结构。
type kubeBenchOutput struct {
	Controls []struct {
		ID      string `json:"id"`
		Results []struct {
			TestNumber  string `json:"test_number"`
			TestDesc    string `json:"test_desc"`
			Status      string `json:"status"` // PASS / FAIL / WARN / INFO
			Remediation string `json:"remediation"`
		} `json:"results"`
	} `json:"Controls"`
}

// RunBenchmark 在目标集群触发 kube-bench 基线扫描,等待完成并返回不达标项。
func RunBenchmark(ctx context.Context, cs *kubernetes.Clientset, cluster, namespace string) ([]string, error) {
	jobName := fmt.Sprintf("kube-bench-%s-%d", cluster, time.Now().Unix())
	job := &batchv1.Job{
		ObjectMeta: metav1.ObjectMeta{Name: jobName, Namespace: namespace},
		Spec: batchv1.JobSpec{
			BackoffLimit: int32Ptr(1),
			Template: corev1.PodTemplateSpec{
				Spec: corev1.PodSpec{
					RestartPolicy: corev1.RestartPolicyNever,
					HostPID:       true,
					Containers: []corev1.Container{{
						Name:    "kube-bench",
						Image:   "aquasec/kube-bench:latest",
						Command: []string{"kube-bench", "run", "--targets", "master,node", "--format", "json"},
						VolumeMounts: []corev1.VolumeMount{
							{Name: "var-lib-etcd", MountPath: "/var/lib/etcd", ReadOnly: true},
							{Name: "var-lib-kubelet", MountPath: "/var/lib/kubelet", ReadOnly: true},
							{Name: "etc-systemd", MountPath: "/etc/systemd", ReadOnly: true},
							{Name: "etc-kubernetes", MountPath: "/etc/kubernetes", ReadOnly: true},
						},
					}},
					Volumes: []corev1.Volume{
						{Name: "var-lib-etcd", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/var/lib/etcd"}}},
						{Name: "var-lib-kubelet", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/var/lib/kubelet"}}},
						{Name: "etc-systemd", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/etc/systemd"}}},
						{Name: "etc-kubernetes", VolumeSource: corev1.VolumeSource{HostPath: &corev1.HostPathVolumeSource{Path: "/etc/kubernetes"}}},
					},
				},
			},
		},
	}
	if _, err := cs.BatchV1().Jobs(namespace).Create(ctx, job, metav1.CreateOptions{}); err != nil {
		return nil, fmt.Errorf("create kube-bench job: %w", err)
	}
	defer func() { _ = cs.BatchV1().Jobs(namespace).Delete(ctx, jobName, metav1.DeleteOptions{}) }()

	// 简单完成等待(生产建议改用 watch + 超时)。
	deadline := time.Now().Add(10 * time.Minute)
	for time.Now().Before(deadline) {
		j, err := cs.BatchV1().Jobs(namespace).Get(ctx, jobName, metav1.GetOptions{})
		if err == nil && j.Status.Succeeded > 0 {
			break
		}
		time.Sleep(5 * time.Second)
	}

	pods, err := cs.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
		LabelSelector: fmt.Sprintf("job-name=%s", jobName),
	})
	if err != nil {
		return nil, fmt.Errorf("list kube-bench pods: %w", err)
	}
	var failures []string
	for _, pod := range pods.Items {
		req := cs.CoreV1().Pods(namespace).GetLogs(pod.Name, &corev1.PodLogOptions{})
		stream, err := req.Stream(ctx)
		if err != nil {
			continue
		}
		data, _ := io.ReadAll(stream)
		_ = stream.Close()
		var out kubeBenchOutput
		if err := json.Unmarshal(data, &out); err != nil {
			continue
		}
		for _, c := range out.Controls {
			for _, r := range c.Results {
				if r.Status == "FAIL" || r.Status == "WARN" {
					failures = append(failures, fmt.Sprintf("[%s] %s %s: %s", cluster, r.Status, r.TestNumber, r.TestDesc))
				}
			}
		}
	}
	return failures, nil
}

高级企业增值能力

  • ④ 零信任网络:每个租户 Namespace 默认下发 default-deny NetworkPolicy,仅显式声明允许的入站/出站。服务网格层启用 mTLS,东西向调用必须携带身份证书。平台侧用 client-go 下发策略的代码如下:
// file: internal/security/policy/push.go
package policy

import (
	"context"
	"fmt"

	networkingv1 "k8s.io/api/networking/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
)

// ApplyDefaultDeny 经统一 K8s API 网关向租户 Namespace 下发 default-deny NetworkPolicy。
func ApplyDefaultDeny(ctx context.Context, cs *kubernetes.Clientset, namespace string) error {
	np := &networkingv1.NetworkPolicy{
		ObjectMeta: metav1.ObjectMeta{Name: "default-deny", Namespace: namespace},
		Spec: networkingv1.NetworkPolicySpec{
			PodSelector: metav1.LabelSelector{},
			PolicyTypes: []networkingv1.PolicyType{"Ingress", "Egress"},
		},
	}
	_, err := cs.NetworkingV1().NetworkPolicies(namespace).Create(ctx, np, metav1.CreateOptions{})
	if err != nil {
		return fmt.Errorf("apply default-deny to %s: %w", namespace, err)
	}
	return nil
}
  • ⑤ 漏洞治理闭环:扫描事件经 Webhook 推送到 ITSM,生成修复工单;修复后复扫通过才允许关闭。
  • ⑥ 运行时入侵检测:Falco 规则捕获 exec 异常、特权提升、挂载宿主目录等行为,事件进联邦监控并告警。
  • ⑦ 敏感操作二次认证:删除集群、关闭基线策略、改全局网络策略等高危动作需 MFA + 审批流双重确认,且不可绕过。
  • ⑧ 安全审计视图:所有安全动作(扫描决策、密钥轮转、基线豁免)写入审计日志,保留 90 天以上。

下面是配套的多 K8s IaC 声明(Gatekeeper / Kyverno / NetworkPolicy / PodSecurity),与上面 Go 代码中的标签、策略一一对应:

# file: deploy/gatekeeper/constraint-template.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredtenantlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredTenantLabels
      validation:
        openAPIV3Schema:
          type: object
          properties:
            labels:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.io/v1
      rego: |
        package k8srequiredtenantlabels
        violation[{"msg": msg}] {
          required := input.parameters.labels
          provided := input.review.object.metadata.labels
          missing := required[_]
          not provided[missing]
          msg := sprintf("缺少必需标签: %v", [missing])
        }        
# file: deploy/gatekeeper/constraint.yaml
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredTenantLabels
metadata:
  name: require-tenant-env-labels
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
    excludedNamespaces: ["kube-system", "gatekeeper-system"]
  parameters:
    labels: ["tenant", "env"]
# file: deploy/kyverno/require-labels.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-tenant-app-env-labels
spec:
  validationFailureAction: enforce
  rules:
    - name: require-labels
      match:
        any:
          - resources:
              kinds: ["Pod"]
      validate:
        message: "Pod 必须带 tenant/app/env 标签"
        pattern:
          metadata:
            labels:
              tenant: "?*"
              app: "?*"
              env: "?*"
# file: deploy/networkpolicy/default-deny.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: tenant-a
spec:
  podSelector: {}
  policyTypes: ["Ingress", "Egress"]
# file: deploy/networkpolicy/allow-dns.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: tenant-a
spec:
  podSelector: {}
  policyTypes: ["Egress"]
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
# file: deploy/podsecurity/restrict-baseline.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: tenant-a
  labels:
    tenant: tenant-a
    env: prod
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

下面是 OPA/Gatekeeper 策略校验 的 Go 封装(rest 把 AdmissionReview 发到 Gatekeeper webhook,生产环境 webhook 不可达时 fail-closed 默认拒绝):

// file: internal/security/admission/gatekeeper.go
package admission

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"time"
)

// AdmissionReview 精简的 Kubernetes AdmissionReview(用于调用 Gatekeeper 校验)。
type AdmissionReview struct {
	APIVersion string            `json:"apiVersion"`
	Kind       string            `json:"kind"`
	Request    *AdmissionRequest `json:"request,omitempty"`
	Response   *AdmissionResponse `json:"response,omitempty"`
}

type AdmissionRequest struct {
	UID       string         `json:"uid"`
	Kind      GroupVersionKind `json:"kind"`
	Namespace string         `json:"namespace"`
	Object    json.RawMessage `json:"object"`
}

type GroupVersionKind struct {
	Group   string `json:"group"`
	Version string `json:"version"`
	Kind    string `json:"kind"`
}

type AdmissionResponse struct {
	UID     string `json:"uid"`
	Allowed bool   `json:"allowed"`
	Result  *Status `json:"status,omitempty"`
}

type Status struct {
	Message string `json:"message,omitempty"`
}

// GatekeeperClient 调用 Gatekeeper 准入 Webhook 做策略校验。
type GatekeeperClient struct {
	webhookURL string
	httpClient *http.Client
}

func NewGatekeeperClient(webhookURL string) *GatekeeperClient {
	return &GatekeeperClient{
		webhookURL: webhookURL,
		httpClient: &http.Client{Timeout: 10 * time.Second},
	}
}

// Validate 对给定对象做准入校验,返回是否允许及原因。
func (g *GatekeeperClient) Validate(ctx context.Context, uid, namespace string, obj interface{}) (bool, string, error) {
	rawObj, err := json.Marshal(obj)
	if err != nil {
		return false, "", fmt.Errorf("marshal object: %w", err)
	}

	review := AdmissionReview{
		APIVersion: "admission.k8s.io/v1",
		Kind:       "AdmissionReview",
		Request: &AdmissionRequest{
			UID:       uid,
			Namespace: namespace,
			Object:    rawObj,
		},
	}
	review.Request.Kind = GroupVersionKind{Version: "v1", Kind: "Pod"}

	payload, err := json.Marshal(review)
	if err != nil {
		return false, "", fmt.Errorf("marshal review: %w", err)
	}

	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost,
		g.webhookURL, bytes.NewReader(payload))
	if err != nil {
		return false, "", fmt.Errorf("build request: %w", err)
	}
	httpReq.Header.Set("Content-Type", "application/json")

	resp, err := g.httpClient.Do(httpReq)
	if err != nil {
		// 生产环境 fail-closed:Webhook 不可达时默认拒绝。
		return false, "gatekeeper webhook unreachable: fail-closed", fmt.Errorf("call gatekeeper: %w", err)
	}
	defer resp.Body.Close()

	var out AdmissionReview
	if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
		return false, "", fmt.Errorf("decode review response: %w", err)
	}
	if out.Response == nil {
		return false, "empty admission response", fmt.Errorf("empty admission response")
	}
	msg := ""
	if out.Response.Result != nil {
		msg = out.Response.Result.Message
	}
	return out.Response.Allowed, msg, nil
}

下面是 安全事件指标与告警 的 Prometheus 规则(消费模块 Go 服务导出的 image_scan_vulnerabilities / security_event_total / cis_check_failures / secret_encryption_status):

# file: deploy/prometheus/security-alerts.yaml
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: security-alerts
  namespace: paas-system
  labels:
    role: alert-rules
spec:
  groups:
    - name: security-scan
      rules:
        - alert: ImageScanBlocked
          expr: increase(image_scan_vulnerabilities{severity=~"HIGH|CRITICAL"}[1h]) > 0
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "镜像高危漏洞拦截 {{ $labels.tenant }}/{{ $labels.app }}"
        - alert: PolicyViolation
          expr: increase(security_event_total{type="policy_violation"}[1h]) > 0
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: "租户 {{ $labels.tenant }} 触发策略违反"
    - name: security-baseline
      rules:
        - alert: CISCheckFailure
          expr: cis_check_failures > 0
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: "集群 {{ $labels.cluster }} CIS 基线不达标"
        - alert: SecretNotEncrypted
          expr: secret_encryption_status == 0
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: "集群 {{ $labels.cluster }}/{{ $labels.namespace }} 存在明文 Secret 风险"
# file: deploy/prometheus/servicemonitor-scanners.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: paas-security-server
  namespace: paas-system
  labels:
    team: security
spec:
  selector:
    matchLabels:
      app: paas-security-server
  endpoints:
    - port: http
      path: /metrics
      interval: 30s
  namespaceSelector:
    any: true

最后是 安全管控服务入口 main.go(gin 统一编排 + /metrics 导出联邦指标),把上面五个能力串起来:

// file: cmd/security-server/main.go
package main

import (
	"fmt"
	"net/http"
	"os"

	"github.com/gin-gonic/gin"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"

	"github.com/your-org/paas-security/internal/security/admission"
	"github.com/your-org/paas-security/internal/security/cis"
	"github.com/your-org/paas-security/internal/security/kms"
	"github.com/your-org/paas-security/internal/security/trivy"
)

var (
	imageScanVulns = prometheus.NewCounterVec(prometheus.CounterOpts{
		Name: "image_scan_vulnerabilities",
		Help: "镜像漏洞数,按 tenant/app/env/cluster/severity 标签",
	}, []string{"tenant", "app", "env", "cluster", "severity"})

	securityEvents = prometheus.NewCounterVec(prometheus.CounterOpts{
		Name: "security_event_total",
		Help: "运行时安全事件计数",
	}, []string{"tenant", "app", "env", "cluster", "type"})

	cisFailures = prometheus.NewCounterVec(prometheus.CounterOpts{
		Name: "cis_check_failures",
		Help: "CIS 基线不达标项",
	}, []string{"tenant", "cluster", "check_id"})

	secretEncStatus = prometheus.NewGaugeVec(prometheus.GaugeOpts{
		Name: "secret_encryption_status",
		Help: "密钥加密状态 1=已加密 0=明文风险",
	}, []string{"cluster", "namespace"})
)

func init() {
	prometheus.MustRegister(imageScanVulns, securityEvents, cisFailures, secretEncStatus)
}

func main() {
	trivyCli := trivy.NewClient(os.Getenv("TRIVY_API_URL"))
	gk := admission.NewGatekeeperClient(os.Getenv("GATEKEEPER_WEBHOOK_URL"))
	kmsCli := kms.NewHTTPKMSClient(os.Getenv("KMS_ENDPOINT"))
	keyID := os.Getenv("KMS_KEY_ID")

	r := gin.Default()

	r.POST("/api/v1/scan", func(c *gin.Context) {
		var req struct {
			Image   string `json:"image"`
			Tenant  string `json:"tenant"`
			App     string `json:"app"`
			Env     string `json:"env"`
			Cluster string `json:"cluster"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		resp, err := trivyCli.Scan(c.Request.Context(), req.Image, []string{"HIGH", "CRITICAL", "MEDIUM"})
		if err != nil {
			securityEvents.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, "scan_error").Inc()
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		for sev, n := range resp.CountBySeverity() {
			imageScanVulns.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, sev).Add(float64(n))
		}
		blocked := resp.Blocked([]string{"HIGH", "CRITICAL"})
		if blocked {
			securityEvents.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, "scan_blocked").Inc()
		}
		c.JSON(http.StatusOK, gin.H{"counts": resp.CountBySeverity(), "blocked": blocked})
	})

	r.POST("/api/v1/admission/validate", func(c *gin.Context) {
		var req struct {
			UID       string                 `json:"uid"`
			Namespace string                 `json:"namespace"`
			Tenant    string                 `json:"tenant"`
			App       string                 `json:"app"`
			Env       string                 `json:"env"`
			Cluster   string                 `json:"cluster"`
			Object    map[string]interface{} `json:"object"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		allowed, msg, err := gk.Validate(c.Request.Context(), req.UID, req.Namespace, req.Object)
		if err != nil || !allowed {
			securityEvents.WithLabelValues(req.Tenant, req.App, req.Env, req.Cluster, "policy_violation").Inc()
		}
		c.JSON(http.StatusOK, gin.H{"allowed": allowed, "message": msg})
	})

	r.POST("/api/v1/secrets/encrypt", func(c *gin.Context) {
		var req struct {
			Plaintext string `json:"plaintext"`
			Cluster   string `json:"cluster"`
			Namespace string `json:"namespace"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		sealed, err := kms.EnvelopeSeal(c.Request.Context(), kmsCli, keyID, []byte(req.Plaintext))
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		secretEncStatus.WithLabelValues(req.Cluster, req.Namespace).Set(1)
		c.JSON(http.StatusOK, gin.H{"sealed": sealed})
	})

	r.POST("/api/v1/secrets/decrypt", func(c *gin.Context) {
		var req struct {
			Sealed string `json:"sealed"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		plain, err := kms.EnvelopeOpen(c.Request.Context(), kmsCli, keyID, req.Sealed)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		c.JSON(http.StatusOK, gin.H{"plaintext": string(plain)})
	})

	r.POST("/api/v1/cis/run", func(c *gin.Context) {
		var req struct {
			Kubeconfig string `json:"kubeconfig"`
			Cluster    string `json:"cluster"`
			Namespace  string `json:"namespace"`
			Tenant     string `json:"tenant"`
		}
		if err := c.ShouldBindJSON(&req); err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}
		cs, err := cis.ClusterClient(req.Kubeconfig, req.Cluster)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		failures, err := cis.RunBenchmark(c.Request.Context(), cs, req.Cluster, req.Namespace)
		if err != nil {
			c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
			return
		}
		for i, f := range failures {
			_ = f
			cisFailures.WithLabelValues(req.Tenant, req.Cluster, fmt.Sprintf("fail-%d", i)).Inc()
		}
		c.JSON(http.StatusOK, gin.H{"failures": failures})
	})

	r.GET("/metrics", gin.WrapH(promhttp.Handler()))

	if err := r.Run(":8080"); err != nil {
		panic(err)
	}
}
# file: go.mod
module github.com/your-org/paas-security

go 1.22

require (
	github.com/gin-gonic/gin v1.10.0
	github.com/prometheus/client_golang v1.19.0
	k8s.io/api v0.30.0
	k8s.io/apimachinery v0.30.0
	k8s.io/client-go v0.30.0
)

下面这张功能架构图展示基础能力与高级企业增值能力的分层关系。

flowchart TD
    S[安全管控模块] --> B1[基础能力]
    S --> B2[高级企业增值能力]
    B1 --> F1[镜像安全扫描
Trivy 加 Harbor] B1 --> F2[密钥加密
KMS信封加密] B1 --> F3[合规基线
CIS Benchmark 加 kube-bench] B2 --> F4[零信任网络
NetworkPolicy 加 mTLS] B2 --> F5[漏洞治理闭环] B2 --> F6[运行时入侵检测] B2 --> F7[敏感操作二次认证] B2 --> F8[安全审计视图]

三、底层架构联动设计

1. 与多 K8s 集群交互逻辑

所有安全控制统一收敛到统一 K8s API 网关:集群侧不暴露 apiserver 公网地址,kubeconfig 由平台加密托管(KMS 信封加密)。安全策略(NetworkPolicy、准入规则、基线巡检任务)经网关下发到对应物理隔离集群的租户 Namespace。

flowchart LR
    subgraph 控制面
        GW[统一K8s API网关] --> Webhook[准入Webhook
Gatekeeper/Kyverno] GW --> Scan[镜像扫描Trivy] GW --> Bench[kube-bench巡检] end subgraph 生产集群 NS1[租户A Namespace] --> P1[业务Pod] NS2[租户B Namespace] --> P2[业务Pod] end GW --> NS1 GW --> NS2 Scan -.校验镜像.-> Webhook Bench -.基线结果.-> GW

平台侧用 client-go 经 API 网关把零信任策略下发到租户 Namespace 的代码,见 internal/security/policy/push.go(第二章)。下发逻辑复用 cis.ClusterClient 构建的 clientset,Host 指向 https://k8s-gateway.paas.local/<cluster>,保证物理隔离集群不暴露 apiserver。

2. 与联邦 Prometheus 监控联动(必须绑定联邦架构)

安全事件不是「进了日志就完事」,而是必须变成带标签的指标,接入全局 Prometheus 联邦——各集群 Prometheus Agent 采集本地扫描器、Falco、kube-bench 的指标,经中心 Prometheus + Thanos 长期存储汇总,Grafana 按 tenant/app/env/cluster 维度过滤出安全大盘,Alertmanager 按租户/严重度分级路由。

flowchart LR
    subgraph 集群侧
        K[租户Pod 加 运行时] --> Agent[Prometheus Agent]
        Webhook[准入Webhook] --> Scan[镜像扫描Trivy]
        Bench[kube-bench] --> Agent
    end
    Scan -->|扫描事件| Agent
    Agent -->|指标 tenant/app/env/cluster| Center[中心Prometheus 加 Thanos]
    Center --> Grafana[Grafana安全大盘]
    Center --> Alert[Alertmanager分级告警]
    Alert --> IM[企业微信 加 钉钉]

关键指标示例(均由 cmd/security-server/main.go/metrics 导出,并被联邦采集):

  • image_scan_vulnerabilities{tenant,app,env,cluster,severity}:各严重度漏洞数。
  • security_event_total{tenant,app,env,cluster,type}:运行时安全事件计数。
  • cis_check_failures{tenant,cluster,check_id}:基线不达标项。
  • secret_encryption_status{cluster,namespace}:加密状态(用于证明无明文 Secret)。

3. 与 RBAC / 审批 / 审计联动

  • RBAC:安全策略的「写权限」仅授予平台安全角色,租户默认只读自身安全大盘。
  • 审批流:基线豁免、拦截放行、敏感操作均需多级审批,审批事件触发网关实际集群操作。
  • 审计:所有安全决策(阻挡/放行/豁免)写入审计日志,保留 90 天以上,且不可被租户侧删除。

四、端到端标准操作流程

分三种角色给出流程。下面以「镜像携带高危漏洞被拦截 → 修复 → 重新准入」为例,用时序图说明跨角色协作。

sequenceDiagram
    participant Dev as 开发人员
    participant Harbor as 镜像仓库Harbor
    participant Trivy as 扫描引擎Trivy
    participant Gate as 准入网关K8s API网关
    participant NS as 租户Namespace
    Dev->>Harbor: 推送应用镜像
    Harbor->>Trivy: 触发漏洞扫描
    Trivy->>Trivy: 比对CVE漏洞库
    alt 发现HIGH或CRITICAL漏洞
        Trivy-->>Dev: 阻断推送并告警
        Gate-->>Dev: 部署准入被拒
    else 扫描通过
        Trivy-->>Harbor: 标记镜像可信
        Dev->>Gate: 提交部署(经审批)
        Gate->>NS: 准入webhook放行
    end

角色 A:开发人员(应用负责人)

  1. 本地构建镜像:docker build -t harbor.paas.local/tenant-a/app:v1.2 .
  2. 本地预扫:trivy image --severity HIGH,CRITICAL harbor.paas.local/tenant-a/app:v1.2 // 步骤1:先把高危清零
  3. 推送到 Harbor,等待平台自动扫描
  4. 若被阻断,按漏洞报告升级基础镜像/依赖,重打 tag 复扫
  5. 在 PaaS 控制台提交部署申请,关联扫描通过状态

角色 B:集群运维

  1. 经 API 网关为租户 Namespace 下发 default-deny NetworkPolicy(YAML 见第二章 deploy/networkpolicy/default-deny.yaml,或调用 policy.ApplyDefaultDeny):
# 运维 runbook:下发默认拒绝策略 + 放行 DNS
kubectl --context gateway-tenant-a apply -f deploy/networkpolicy/default-deny.yaml
kubectl --context gateway-tenant-a apply -f deploy/networkpolicy/allow-dns.yaml
  1. 周期性发起 kube-bench 基线巡检(经网关并发多集群):
# 运维 runbook:触发 CIS 基线巡检(对应 cis.RunBenchmark)
curl -X POST http://security-server.paas.local:8080/api/v1/cis/run \
  -H 'Content-Type: application/json' \
  -d '{"kubeconfig":"/etc/paas/kubeconfig","cluster":"prod","namespace":"paas-system","tenant":"platform"}'
  1. 在联邦 Grafana 查看 cis_check_failures,对不达标项派单修复

角色 C:平台安全管理员

  1. 配置 Trivy 阻断阈值与审批白名单
  2. 轮转 KMS 主密钥,验证 secret_encryption_status 全绿:
# 运维 runbook:信封加密一轮验证(对应 kms.EnvelopeSeal / EnvelopeOpen)
SEALED=$(curl -s -X POST http://security-server.paas.local:8080/api/v1/secrets/encrypt \
  -H 'Content-Type: application/json' \
  -d '{"plaintext":"db-password","cluster":"prod","namespace":"tenant-a"}' | jq -r .sealed)
echo "密文包(写入 etcd): $SEALED"
curl -s -X POST http://security-server.paas.local:8080/api/v1/secrets/decrypt \
  -H 'Content-Type: application/json' -d "{\"sealed\":\"$SEALED\"}" | jq -r .plaintext
  1. 处理 Alertmanager 升级的安全告警,必要时冻结租户部署权限

【测试环境】 vs 【生产环境】差异步骤

  • 测试环境:允许 MEDIUM 漏洞放行、可不启用 mTLS、基线不达标仅记录不阻断、准入网关放行策略宽松。
  • 生产环境HIGH/CRITICAL 强制阻断、NetworkPolicy 默认拒绝且需显式放行、mTLS 全量启用、基线豁免必须多级审批、所有安全操作强制二次认证并审计。

五、生产环境管控与安全约束

测试 vs 生产 差异化管控对照表

管控项测试环境生产环境
镜像扫描阻断阈值MEDIUM 及以下放行HIGH/CRITICAL 强制阻断,且需审批
网络策略可选 default-deny强制 default-deny + 显式放行
mTLS可关闭全量启用,禁止明文东西向
密钥加密开启(可用测试 KMS)开启,主密钥独立且定期轮转
CIS 基线不达标仅记录不达标阻断发布,豁免需审批
敏感操作单人可操作二次认证 + 多级审批
审计留存90 天90 天以上,且防篡改归档
运行时入侵检测告警即可告警 + 自动隔离 Pod

资源配额与权限隔离

  • 每个租户 Namespace 设置 ResourceQuotaLimitRange,防止单一租户耗尽节点资源或被资源耗尽型攻击拖垮。
  • 安全策略写入权仅限平台安全角色;租户对安全的可见范围被 RBAC 限制为「仅自身 Namespace」。

下面是生产环境租户 Namespace 的资源配额与 Pod Security 基线(与第二章 restrict-baseline.yaml 配套):

# file: deploy/podsecurity/tenant-quota.yaml
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-a-quota
  namespace: tenant-a
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    pods: "50"
    persistentvolumeclaims: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: tenant-a-limitrange
  namespace: tenant-a
spec:
  limits:
    - type: Container
      default:
        cpu: 500m
        memory: 512Mi
      defaultRequest:
        cpu: 100m
        memory: 128Mi

敏感操作审批与审计

  • 高危动作(删集群、改全局网络策略、关基线)绑定审批流,审批通过才由网关执行,流程不可绕过。
  • 所有安全决策写入审计日志,保留 90 天以上,格式含操作人、租户、集群、对象、结果、时间戳。

故障熔断策略

  • 当扫描器或准入 webhook 不可用时,生产环境默认拒绝新部署(fail-closed),避免未扫描镜像溜入(见 admission.GatekeeperClient.Validate 中 webhook 不可达返回 fail-closed)。
  • Alertmanager 对 security_event_total 突增做自动升级,连续触发则冻结对应租户部署并通知安全值班。

六、常见生产故障与解决方案

下面用流程图给出安全事件的标准排查路径。

flowchart TD
    P[安全事件发生] --> Q{事件类型?}
    Q -->|镜像高危漏洞| R1[定位镜像与CVE编号]
    Q -->|容器逃逸 加 异常进程| R2[隔离Pod 加 查kube-bench]
    Q -->|网络横向移动| R3[查NetworkPolicy 加 mTLS]
    R1 --> S1[升级基础镜像重建并重扫]
    R2 --> S2[封堵逃逸点 加 加固基线]
    R3 --> S3[收紧策略 加 重新签发证书]
    S1 --> T[事件进指标 加 审计留痕90天]
    S2 --> T
    S3 --> T

故障 1:镜像被拦截但本地 Trivy 扫不出

  • 现象:Harbor 报告 CRITICAL,本地扫描无结果。
  • 排查:本地 Trivy 数据库过期或基础镜像拉取了不同 digest。在联邦看板核对 image_scan_vulnerabilities 的 cluster 标签,确认是网关侧扫描器版本。
  • 方案:统一扫描器版本与漏洞库更新周期;建立镜像 digest 锁,确保 CI 与网关扫描同源。

故障 2:default-deny 导致正常调用中断

  • 现象:启用 NetworkPolicy 后服务间 503。
  • 排查:未显式放行 DNS(53) 与对端端口。
  • 方案:先放行 kube-system DNS(见 allow-dns.yaml),再按调用拓扑逐条加白;用 kubectl exec 在源 Pod 内 curl 验证。

故障 3:Secret 解密失败导致 Pod 起不来

  • 现象:Pod 处于 CreateContainerConfigError
  • 排查:secret_encryption_status 显示某集群 KEK 不可用,多为 KMS 主密钥轮转后缓存未刷新。
  • 方案:在网关侧重载 KMS 凭据缓存;恢复前该集群 fail-closed,避免明文回退。可用以下命令核验密文包能否解开:
# 运维 runbook:校验某租户 Secret 密文包可解密
SEALED=$(kubectl --context gateway-prod -n tenant-a get secret app-secret \
  -o jsonpath='{.data.encrypted}' | base64 -d)
curl -s -X POST http://security-server.paas.local:8080/api/v1/secrets/decrypt \
  -H 'Content-Type: application/json' -d "{\"sealed\":\"$SEALED\"}" | jq -r .plaintext

故障 4:kube-bench 基线分数波动

  • 现象:同一集群两次巡检分数不同。
  • 排查:控制面组件被手动改过参数(如匿名访问被打开)。
  • 方案:把基线检查接入 GitOps,参数变更走审批,异常项自动回滚并告警。可用查询确认 cis_check_failures 是否回归 0:
# 运维 runbook:查询某集群基线不达标项
curl -s 'http://prometheus-fed.paas.local:9090/api/v1/query?query=cis_check_failures{cluster="prod"}' \
  | jq '.data.result[] | {check: .metric.check_id, value: .value[1]}'

自测题与动手练习

自测题

  1. 镜像安全扫描应在哪两个位置拦截漏洞?为什么只放在 CI 不够?
  2. KMS 信封加密中,DEK 与 KEK 各负责什么?明文 Secret 会出现在哪里?
  3. default-deny NetworkPolicy 下发后,为什么 DNS 解析会先失败?应如何放行?
  4. 安全事件进入联邦监控时,必须带哪四个维度标签?缺少会怎样?
  5. 当准入 webhook 不可用时,生产环境应如何 fail-closed?为什么不能 fail-open?

动手练习

  1. 在测试租户 Namespace 写一条 default-deny,部署两个 Pod,验证跨 Pod HTTP 访问被拒,再加一条放行规则恢复。
  2. 用 Trivy 扫描你负责的一个镜像,把 HIGH/CRITICAL 漏洞归零,并在 Harbor 确认扫描状态为通过。
  3. 在联邦 Grafana 新建一个面板,查询 security_event_total{tenant="你的租户"},观察运行时告警曲线并截图。

本章小结

  • 安全管控模块以「扫描、加密、基线」三项★禁止删减能力为底座,叠加零信任、入侵检测、二次认证、审计等企业增值能力。
  • 所有安全控制经统一 K8s API 网关下发到物理隔离集群的租户 Namespace,密钥明文不落盘(KMS 信封加密 + AES-GCM)。
  • 安全事件必须转化为带 tenant/app/env/cluster 标签的联邦指标(由 cmd/security-server/main.go/metrics 导出),进入 Thanos 长期存储与分级告警。
  • 测试环境宽松、生产环境 fail-closed,是商用 PaaS 安全落地的核心差异化原则。
  • 平台侧全部以 Go(gin + client-go)实现:Trivy 扫描封装、Gatekeeper 准入校验(rest)、KMS 信封加解密(调 KMS)、kube-bench 触发(client-go),代码可编译、无占位符,与 YAML / Prometheus 规则端到端一致。

下一模块(模块 08 成本与资源账单)将建立在本文的联邦指标之上,把同样的 tenant/app/env/cluster 标签用于资源计量与分摊——安全与成本共享同一套可观测底座。

About Me

没什么想介绍的,一个很大众的码农…

喜欢代码,车,马,真的是 🐎

讨厌别人让我给自己的代码写注释 最厌烦别人的程序没有写注释

目标

学AI,加油!加油!