学习目标
学完本模块,你应该能够:
- 说清镜像安全扫描如何在「镜像推送即拦截」模式下阻断高危漏洞上线,并能配置 Trivy/Harbor 的阻断阈值。
- 在租户 Namespace 内落地零信任网络:编写 NetworkPolicy、启用 mTLS,并验证东西向流量最小化。
- 使用 KMS 信封加密托管 Secret,确保密钥在 etcd、磁盘、配置分发链路中均不以明文落地。
- 通过 kube-bench / CIS Benchmark 对多套物理隔离集群做合规基线巡检,并对接联邦监控形成趋势看板。
- 把安全事件(漏洞、逃逸、异常进程、横向移动)转化为带
tenant/app/env/cluster标签的指标与告警,进入审计留痕 90 天以上的闭环。
前置知识:
- 熟悉 Kubernetes 核心对象(Namespace、Pod、Secret、Service、NetworkPolicy)。
- 了解镜像仓库(Harbor)与 CI/CD 基本流程。
- 读过本白皮书「模块 02 多 K8s 集群纳管」「模块 05 统一观测平台」,理解统一 K8s API 网关与 Prometheus 联邦架构。
- 基本了解对称/非对称加密、KMS、mTLS 的概念。
本章你会动手做的事:
- 给一个测试租户 Namespace 写一条默认拒绝(default-deny)的 NetworkPolicy,并用 curl 验证跨 Pod 访问被拦。
- 用 Trivy 对本团队镜像扫描一次,把 HIGH/CRITICAL 漏洞数量拉到 0 后再推送到 Harbor。
- 在联邦 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_total与image_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 扫描器,推送钩子触发。阻断策略按严重度分级——CRITICAL 与 HIGH 默认阻断入库;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-denyNetworkPolicy,仅显式声明允许的入站/出站。服务网格层启用 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:开发人员(应用负责人)
- 本地构建镜像:
docker build -t harbor.paas.local/tenant-a/app:v1.2 . - 本地预扫:
trivy image --severity HIGH,CRITICAL harbor.paas.local/tenant-a/app:v1.2// 步骤1:先把高危清零 - 推送到 Harbor,等待平台自动扫描
- 若被阻断,按漏洞报告升级基础镜像/依赖,重打 tag 复扫
- 在 PaaS 控制台提交部署申请,关联扫描通过状态
角色 B:集群运维
- 经 API 网关为租户 Namespace 下发
default-denyNetworkPolicy(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
- 周期性发起 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"}'
- 在联邦 Grafana 查看
cis_check_failures,对不达标项派单修复
角色 C:平台安全管理员
- 配置 Trivy 阻断阈值与审批白名单
- 轮转 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
- 处理 Alertmanager 升级的安全告警,必要时冻结租户部署权限
【测试环境】 vs 【生产环境】差异步骤
- 测试环境:允许
MEDIUM漏洞放行、可不启用 mTLS、基线不达标仅记录不阻断、准入网关放行策略宽松。 - 生产环境:
HIGH/CRITICAL强制阻断、NetworkPolicy 默认拒绝且需显式放行、mTLS 全量启用、基线豁免必须多级审批、所有安全操作强制二次认证并审计。
五、生产环境管控与安全约束
测试 vs 生产 差异化管控对照表
| 管控项 | 测试环境 | 生产环境 |
|---|---|---|
| 镜像扫描阻断阈值 | MEDIUM 及以下放行 | HIGH/CRITICAL 强制阻断,且需审批 |
| 网络策略 | 可选 default-deny | 强制 default-deny + 显式放行 |
| mTLS | 可关闭 | 全量启用,禁止明文东西向 |
| 密钥加密 | 开启(可用测试 KMS) | 开启,主密钥独立且定期轮转 |
| CIS 基线 | 不达标仅记录 | 不达标阻断发布,豁免需审批 |
| 敏感操作 | 单人可操作 | 二次认证 + 多级审批 |
| 审计留存 | 90 天 | 90 天以上,且防篡改归档 |
| 运行时入侵检测 | 告警即可 | 告警 + 自动隔离 Pod |
资源配额与权限隔离
- 每个租户 Namespace 设置
ResourceQuota与LimitRange,防止单一租户耗尽节点资源或被资源耗尽型攻击拖垮。 - 安全策略写入权仅限平台安全角色;租户对安全的可见范围被 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-systemDNS(见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]}'
自测题与动手练习
自测题
- 镜像安全扫描应在哪两个位置拦截漏洞?为什么只放在 CI 不够?
- KMS 信封加密中,DEK 与 KEK 各负责什么?明文 Secret 会出现在哪里?
- default-deny NetworkPolicy 下发后,为什么 DNS 解析会先失败?应如何放行?
- 安全事件进入联邦监控时,必须带哪四个维度标签?缺少会怎样?
- 当准入 webhook 不可用时,生产环境应如何 fail-closed?为什么不能 fail-open?
动手练习
- 在测试租户 Namespace 写一条 default-deny,部署两个 Pod,验证跨 Pod HTTP 访问被拒,再加一条放行规则恢复。
- 用 Trivy 扫描你负责的一个镜像,把 HIGH/CRITICAL 漏洞归零,并在 Harbor 确认扫描状态为通过。
- 在联邦 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标签用于资源计量与分摊——安全与成本共享同一套可观测底座。