第一章:Go读取静态页遇到syscall.EACCES却无日志?——Linux capability(CAP_DAC_OVERRIDE)缺失诊断指南
当 Go 程序在 Linux 容器或受限环境中尝试 os.Open() 一个静态 HTML 文件时, unexpectedly 返回 syscall.EACCES 错误,而 strace 显示 openat(AT_FDCWD, "/var/www/index.html", O_RDONLY|O_CLOEXEC) = -1 EACCES (Permission denied),且应用日志中无任何权限相关上下文——这往往不是文件系统 ACL 或 SELinux 导致,而是进程缺失关键 Linux capability:CAP_DAC_OVERRIDE。
该 capability 允许进程绕过文件的 DAC(Discretionary Access Control)检查(即忽略 rwx 权限位),常被 setuid 程序或需要安全降权但仍需访问受限资源的服务所依赖。容器默认运行时(如 Docker、containerd)会主动丢弃此 capability,除非显式声明。
快速验证 capability 缺失
在目标环境中执行:
# 检查当前进程是否拥有 CAP_DAC_OVERRIDE
cat /proc/self/status | grep CapEff
# 输出示例(十六进制):CapEff: 00000000a80425fb → 需查 bit 1(CAP_DAC_OVERRIDE 对应 bit index 1)
# 更直观方式:
capsh --print | grep cap_dac_override
修复方案对比
| 场景 | 推荐方式 | 命令示例 |
|---|---|---|
| Docker 运行时 | 启动时显式添加 | docker run --cap-add=CAP_DAC_OVERRIDE ... |
| Kubernetes Pod | 在 securityContext 中声明 | securityContext: capabilities: { add: ["DAC_OVERRIDE"] } |
| systemd 服务 | 使用 AmbientCapabilities= |
AmbientCapabilities=CAP_DAC_OVERRIDE |
Go 代码层防御性处理
// 在打开文件前主动检查错误类型,避免静默失败
f, err := os.Open("/var/www/index.html")
if err != nil {
if errors.Is(err, syscall.EACCES) {
// 记录明确上下文,便于运维定位
log.Printf("EACCES on /var/www/index.html: possible missing CAP_DAC_OVERRIDE; check 'capsh --print' in container")
}
return err
}
defer f.Close()
若无法修改运行时配置,替代方案是确保目标文件对运行用户(如 www-data)具有 r-- 权限,并通过 chown/chmod 显式授权,而非依赖 capability 绕过。
第二章:Linux文件权限与Capability机制深度解析
2.1 Linux DAC模型与进程访问控制的底层逻辑
Linux DAC(Discretionary Access Control)以文件所有者、组和其他用户的rwx权限为核心,由内核在vfs_permission()中强制执行。
权限检查关键路径
// fs/namei.c 中的权限验证逻辑
int generic_permission(struct inode *inode, int mask) {
if (inode_permission(inode, mask) == 0) // 调用底层检查
return 0;
if (capable(CAP_DAC_OVERRIDE)) // 特权绕过
return 0;
return -EACCES;
}
mask为MAY_READ/MAY_WRITE/MAY_EXEC位组合;inode_permission()最终比对inode->i_mode与进程的cred->fsuid/fsgid,决定是否放行。
DAC决策依赖三元组
| 主体 | 客体 | 控制依据 |
|---|---|---|
| 进程有效UID | 文件所有者UID | 匹配则应用owner权限 |
| 进程有效GID | 文件所属GID | 匹配则应用group权限 |
| 其他用户 | — | 应用other权限位 |
权限匹配流程
graph TD
A[进程发起open/read/write] --> B{VFS层调用permission()}
B --> C[提取进程cred和inode mode]
C --> D{UID/GID匹配?}
D -->|是| E[应用对应rwx位]
D -->|否| F[应用other位]
E & F --> G[任一必要位缺失→-EACCES]
2.2 CAP_DAC_OVERRIDE能力的本质作用与触发场景
CAP_DAC_OVERRIDE 允许进程绕过文件的 DAC(Discretionary Access Control)权限检查,直接读写任意文件,无论其 rwx 位或属主/属组是否匹配。
权限绕过机制
当进程持有该能力且执行 open()、stat() 或 chmod() 等系统调用时,内核在 inode_permission() 中跳过 generic_permission() 的传统检查路径。
典型触发场景
- 容器内 root 进程挂载宿主机敏感路径(如
/etc/shadow) - systemd 服务以
CapabilityBoundingSet=CAP_DAC_OVERRIDE启动 - 调试工具(如
gdb)附加到特权进程并读取内存映射文件
内核关键逻辑片段
// fs/namei.c: may_open()
if (capable(CAP_DAC_OVERRIDE)) // 检查能力集
return 0; // 直接放行,跳过 inode->i_mode 权限比对
capable() 调用 ns_capable_noaudit(current_user_ns(), CAP_DAC_OVERRIDE),仅验证当前进程的 cap_effective 位图中对应 bit 是否置位,不依赖 UID/GID。
| 能力启用方式 | 是否需 root | 持久性 | 安全风险等级 |
|---|---|---|---|
setcap cap_dac_override+ep /bin/bash |
否 | 文件级 | ⚠️⚠️⚠️⚠️ |
unshare -r && capsh --caps="cap_dac_override+ei" -- |
否 | 进程级 | ⚠️⚠️⚠️ |
docker run --cap-add=DAC_OVERRIDE |
否 | 容器级 | ⚠️⚠️⚠️⚠️ |
graph TD
A[进程发起 open\("/etc/passwd"\)] --> B{capable\\(CAP_DAC_OVERRIDE\\)?}
B -->|是| C[跳过 generic_permission\\(\\)]
B -->|否| D[执行传统 DAC 检查]
C --> E[成功返回 fd]
2.3 Go runtime在openat系统调用中对capability的隐式依赖
Go 程序调用 os.OpenFile 时,底层经由 runtime.syscall 触发 openat 系统调用。该过程不显式请求 CAP_DAC_OVERRIDE,但若文件受 DAC(自主访问控制)限制且进程无对应权限,内核将静默拒绝——Go runtime 未做 capability 预检或降级处理。
关键路径示意
// runtime/cgo/asm_linux_amd64.s 中实际触发点(简化)
TEXT ·sysvicall6(SB), NOSPLIT, $0
MOVQ fd+24(FP), AX // fd = AT_FDCWD
MOVQ name+32(FP), BX // 路径指针
MOVQ flags+40(FP), CX // O_RDONLY | O_CLOEXEC
MOVQ mode+48(FP), DX
MOVQ $SYS_openat, SI
SYSCALL
SYSCALL直接陷入内核;AX=AT_FDCWD表示相对当前工作目录,CX中的O_NOFOLLOW等标志若与 capability 不匹配,将被security_inode_permission()拒绝。
常见失败场景对比
| 场景 | 是否需 CAP_DAC_OVERRIDE | Go 行为 |
|---|---|---|
打开 root-owned /etc/shadow |
✅ 是 | permission denied(无提示) |
| 打开同用户文件 | ❌ 否 | 成功 |
容器中 drop ALL capabilities |
⚠️ 全部失效 | 即使属主匹配也失败 |
graph TD
A[Go os.OpenFile] --> B[runtime.syscall.openat]
B --> C{内核 security_hook}
C -->|无 CAP_DAC_OVERRIDE<br>& 权限不足| D[EPERM]
C -->|权限满足或 CAP 存在| E[fd returned]
2.4 容器环境(如Docker、Kubernetes)中capability的默认裁剪策略
容器运行时默认启用最小权限原则,Docker 启动普通容器时自动丢弃 CAP_NET_RAW、CAP_SYS_ADMIN 等 12 项高危 capability,仅保留 CAP_CHOWN、CAP_DAC_OVERRIDE 等 14 项基础能力。
默认保留的能力示例
CAP_AUDIT_WRITE:允许写入内核审计日志CAP_SETGID/CAP_SETUID:支持切换组/用户 ID(受限于userns隔离)CAP_NET_BIND_SERVICE:绑定 1024 以下端口
Docker 启动时的隐式裁剪
# 默认行为等价于显式声明:
docker run --cap-drop=ALL --cap-add=CHOWN --cap-add=DAC_OVERRIDE ...
此命令显式禁用全部 capability 后仅添加必需项,验证了默认策略本质是“白名单式加固”;
--cap-drop=ALL是安全基线起点,--cap-add为按需授权。
| Capability | 是否默认启用 | 风险等级 | 典型用途 |
|---|---|---|---|
NET_BIND_SERVICE |
✅ | 中 | 绑定特权端口 |
SYS_ADMIN |
❌ | 高 | 挂载文件系统、修改命名空间 |
graph TD
A[容器启动] --> B{是否指定 --privileged}
B -->|否| C[应用默认 cap_drop ALL]
B -->|是| D[保留全部 capability]
C --> E[按白名单 --cap-add 添加基础能力]
2.5 复现EACCES错误的最小化Go代码与strace验证实验
构建可复现场景
以下 Go 程序尝试以只读权限打开 /etc/shadow(典型受限路径):
package main
import (
"os"
)
func main() {
// O_RDONLY:只读标志;0400:忽略文件模式,仅依赖系统权限
f, err := os.OpenFile("/etc/shadow", os.O_RDONLY, 0)
if err != nil {
panic(err) // 触发 EACCES: permission denied
}
_ = f.Close()
}
逻辑分析:
os.OpenFile底层调用openat(AT_FDCWD, "/etc/shadow", O_RDONLY, ...)。因进程无 root 权限且/etc/shadow权限为000(----------),内核返回-EACCES(13),Go 将其转为*os.PathError。
strace 验证关键系统调用
执行 strace -e trace=openat,open ./main 2>&1 | grep -E "(open|EACCES)" 输出节选:
| 系统调用 | 参数(精简) | 返回值 | 含义 |
|---|---|---|---|
openat |
AT_FDCWD, "/etc/shadow", O_RDONLY |
-1 EACCES (Permission denied) |
权限检查失败,非路径不存在 |
权限链路示意
graph TD
A[Go os.OpenFile] --> B[syscall.openat]
B --> C{Linux VFS 权限检查}
C -->|uid/gid 不匹配<br>且无 CAP_DAC_OVERRIDE| D[EACCES]
C -->|权限满足| E[成功返回 fd]
第三章:Go静态文件服务中的权限异常诊断方法论
3.1 从net/http.FileServer到os.Open的调用链权限断点分析
net/http.FileServer 是 Go 标准库中用于静态文件服务的核心抽象,其底层依赖 http.FileSystem 接口实现路径解析与资源打开。关键断点位于 fileServer.ServeHTTP → fs.Open → os.Open 的调用链中。
权限校验前置位置
http.Dir.Open()将请求路径标准化后拼接为本地路径- 调用
os.Stat()检查路径是否存在且可访问(触发stat(2)系统调用) - 仅当
os.Stat成功返回且为非目录时,才进入os.Open
关键调用链示意
// http/fs.go 中 Dir.Open 的核心逻辑
func (d Dir) Open(name string) (File, error) {
if filepath.Separator != '/' && strings.ContainsRune(name, '/') {
return nil, errors.New("http: invalid character in file path")
}
dir := string(d)
full := filepath.Join(dir, filepath.FromSlash(name)) // ← 路径拼接断点
f, err := os.Open(full) // ← 权限实际生效点:open(2) 系统调用
// ...
}
filepath.Join 可能绕过预期沙箱(如 ../ 被规范化前已参与拼接),而 os.Open 执行时由内核依据进程有效 UID/GID 和文件 ACL 进行最终权限判定。
权限决策对比表
| 调用环节 | 是否执行权限检查 | 说明 |
|---|---|---|
filepath.Clean |
否 | 仅路径规范化,无系统调用 |
os.Stat |
是 | 检查存在性与基本权限 |
os.Open |
是 | 内核级读/执行权限验证 |
graph TD
A[FileServer.ServeHTTP] --> B[Dir.Open]
B --> C[filepath.Join + Clean]
C --> D[os.Stat]
D -->|success| E[os.Open]
D -->|fail| F[404/403]
E -->|fail| F
3.2 利用/proc//status与capsh工具实时检测进程capability集
Linux 进程的 capability 集可通过内核接口与用户态工具协同验证。
直接解析 /proc//status
查看某进程(如 PID 1234)的 capabilities:
grep Cap /proc/1234/status
输出示例:
CapEff: 0000000000000000
CapPrm: 0000000000002000
CapInh: 0000000000000000
CapBnd: 0000003fffffffff
CapAmb: 0000000000000000
CapPrm(0x2000 = CAP_NET_BIND_SERVICE)表明该进程具备绑定特权端口能力;十六进制值需按位解析,低位第13位(bit 12)置1即对应此能力。
capsh 辅助解码
capsh --decode=0000000000002000
输出:cap_net_bind_service+ep,清晰标识能力名与生效范围(effective + permitted)。
能力字段含义对照表
| 字段 | 含义 | 生效阶段 |
|---|---|---|
| CapEff | 有效能力集 | 系统调用时检查 |
| CapPrm | 允许能力集 | execve 后继承 |
| CapBnd | 上界能力集 | 不可被子进程超越 |
实时检测流程
graph TD
A[获取目标PID] --> B[/proc/PID/status读取Cap*字段]
B --> C[capsh --decode 解析十六进制]
C --> D[比对预期权限策略]
3.3 结合auditd与bpftrace捕获被拒绝的access()和openat()系统调用
当 SELinux 或 DAC 策略拒绝 access() 或 openat() 调用时,仅靠 auditd 可记录 SYSCALL 类型事件,但缺乏内核路径上下文;而 bpftrace 可实时捕获失败返回值并提取文件路径,二者互补。
审计规则配置
# /etc/audit/rules.d/capability.rules
-a always,exit -F arch=b64 -S access,openat -F exit=-13 -k denied_access_open
-F exit=-13匹配 EACCES 错误码;-k标记便于ausearch -k denied_access_open检索;需重启auditd生效。
bpftrace 实时路径解析
sudo bpftrace -e '
kretprobe:sys_access, kretprobe:sys_openat /retval == -13/ {
printf("DENIED %s on %s\n", probefunc, str(((struct pt_regs*)ctx)->di));
}'
ctx->di指向用户态filename地址;str()自动读取用户内存;/retval == -13/过滤仅 EACCES 场景。
关键字段对照表
| 字段 | auditd 输出字段 | bpftrace 可达字段 | 说明 |
|---|---|---|---|
| 系统调用名 | syscall=21 |
probefunc |
21=access, 257=openat |
| 错误码 | exit=-13 |
retval |
统一为 EACCES |
| 文件路径 | exe=(不直接) |
str(ctx->di) |
bpftrace 提供精准路径 |
graph TD
A[进程发起 access/openat] --> B{内核权限检查}
B -->|允许| C[成功返回]
B -->|拒绝 EACCES| D[auditd 记录 syscall+exit]
B -->|拒绝 EACCES| E[bpftrace 触发 kretprobe]
D & E --> F[关联分析:路径+策略上下文]
第四章:生产环境下的安全加固与兼容性修复实践
4.1 在Docker中精准授予CAP_DAC_OVERRIDE而非privileged模式
Linux DAC(自主访问控制)机制默认禁止进程绕过文件读写权限检查。CAP_DAC_OVERRIDE 能力允许容器内进程无视 rwx 权限限制,适用于日志轮转、配置热加载等场景,而无需启用完全失控的 --privileged。
为什么避免 --privileged
- 开放全部 capabilities(38+项)
- 绕过 SELinux/AppArmor
- 挂载任意文件系统、操作网络栈、修改内核参数
授予最小能力的正确方式
docker run --cap-add=CAP_DAC_OVERRIDE -it alpine sh -c 'touch /etc/override_test 2>/dev/null && echo "Success" || echo "Denied"'
此命令仅添加
CAP_DAC_OVERRIDE,使容器可写入/etc/(通常只允许 root),但不赋予CAP_SYS_ADMIN或设备访问权。--cap-add是白名单式增强,比--cap-drop=ALL后逐个加更安全。
能力对比表
| 能力 | 影响范围 | 是否必需于文件覆盖 |
|---|---|---|
CAP_DAC_OVERRIDE |
绕过所有 DAC 权限检查 | ✅ |
CAP_SYS_ADMIN |
可挂载/卸载、修改命名空间 | ❌(过度) |
--privileged |
等效于 root + 全能力 + 宿主机设备访问 | ❌(高危) |
graph TD
A[应用需覆盖受限配置文件] --> B{是否需要完整系统控制?}
B -->|否| C[仅添加 CAP_DAC_OVERRIDE]
B -->|是| D[重新设计权限模型或使用 init 容器]
C --> E[验证 touch /etc/xxx 是否成功]
4.2 Kubernetes SecurityContext中capability的声明式配置与RBAC协同
Capability 的最小化声明实践
在 Pod 或 Container 的 securityContext 中,应显式删除默认继承的高危 capability:
securityContext:
capabilities:
drop: ["NET_RAW", "SYS_ADMIN", "DAC_OVERRIDE"]
add: ["NET_BIND_SERVICE"] # 仅需绑定端口时添加
逻辑分析:
drop列表优先于默认集合,Kubernetes 会从容器运行时默认 capability 集(如CAP_NET_RAW)中移除指定项;add仅在白名单许可范围内生效,且受节点--allowed-unsafe-sysctls和PodSecurityPolicy(或PodSecurity)策略约束。
RBAC 与 capability 的权限分层关系
| 层级 | 控制主体 | 是否可绕过 capability 限制 |
|---|---|---|
| RBAC | API Server 访问权限 | 否(不涉及容器运行时) |
| PodSecurity | Pod 创建/更新策略 | 是(可拒绝含 SYS_ADMIN 的 Pod) |
| Runtime(如 containerd) | capability 检查 | 是(最终执行时强制校验) |
协同防护流程
graph TD
A[用户提交 Pod YAML] --> B{RBAC 授权?}
B -->|否| C[API Server 拒绝]
B -->|是| D[PodSecurity 准入检查]
D -->|违反 baseline| E[拒绝创建]
D -->|通过| F[containerd 运行时验证 capability]
F --> G[按 securityContext 执行 capset]
4.3 Go应用层fallback机制:当CAP_DAC_OVERRIDE不可用时的替代读取路径
当容器或受限环境禁用 CAP_DAC_OVERRIDE(如 OpenShift 或 rootless Pod),Go 应用无法绕过文件权限检查,需启用安全降级路径。
核心 fallback 策略
- 优先尝试
os.Open()(依赖 capability) - 失败后自动切换至
syscall.Openat2()(Linux 5.6+)或os.UserHomeDir()+ 配置代理路径 - 最终回退到内存内嵌默认配置(
embed.FS)
降级流程图
graph TD
A[尝试 os.Open] -->|PermissionDenied| B[检测 CAP_DAC_OVERRIDE]
B -->|缺失| C[启用 fallback 模式]
C --> D[查询 XDG_CONFIG_HOME]
C --> E[读取 embed.FS 内置 config.yaml]
示例 fallback 打开逻辑
// 尝试主路径,失败后启用 fallback
func openConfig(path string) (*os.File, error) {
f, err := os.Open(path)
if err == nil {
return f, nil
}
if !errors.Is(err, fs.ErrPermission) {
return nil, err
}
// fallback:使用 embed.FS 中预置的 config.yaml
return assets.Open("config.yaml") // assets 是 go:embed assets/
}
assets.Open() 从编译时嵌入的只读文件系统加载配置,规避所有运行时权限检查;go:embed 确保零依赖、确定性行为。该路径在 CGO_ENABLED=0 和 unprivileged 容器中完全可靠。
4.4 构建CI/CD流水线中的capability合规性静态检查与冒烟测试
在流水线早期阶段嵌入 capability 合规性验证,可拦截不符合平台契约的微服务交付。核心包括两层防护:静态检查(基于 OpenAPI/Swagger + 自定义策略规则)与轻量级冒烟测试(验证 capability 接口可达性与基础响应结构)。
静态检查:OpenAPI Schema 策略校验
# 使用 spectral CLI 执行 capability 合规规则集
spectral lint \
--ruleset .spectral-capability.yaml \
./openapi/capability-v1.yaml
--ruleset指向自定义规则文件,强制要求x-capability-id、x-owner-team字段存在且非空;x-rate-limit必须为整数;所有 POST 路径需声明requestBody。
冒烟测试执行流程
graph TD
A[Checkout API Spec] --> B[生成最小请求集]
B --> C[并发调用 /health & /capabilities]
C --> D{HTTP 200 + schema-valid response?}
D -->|Yes| E[标记 capability 就绪]
D -->|No| F[阻断流水线]
合规检查项对照表
| 检查维度 | 合规要求 | 违规示例 |
|---|---|---|
| 元数据完整性 | 必含 x-capability-id |
缺失该扩展字段 |
| 接口健壮性 | /health 返回 status: "UP" |
返回 503 或无 status 字段 |
| 版本一致性 | info.version 匹配 Git tag |
v1.2.0 vs main 分支 |
第五章:结语:在最小权限原则与运行时弹性之间寻找Go服务的平衡点
在真实生产环境中,我们曾为某金融风控平台重构其核心决策引擎服务。该服务需调用外部征信API、写入审计日志、读取本地规则缓存,同时接受Kubernetes滚动更新与突发流量压测。初始设计严格遵循最小权限原则:容器以非root用户运行,仅挂载/etc/rules只读卷,ServiceAccount绑定RBAC策略限制为get和list configmaps——但上线后立即遭遇两处故障:
- 健康检查端点
/healthz因无法读取/proc/1/cgroup(用于检测cgroup v2内存限制)而持续失败; - 自动证书轮换逻辑因缺失对
/var/run/secrets/kubernetes.io/serviceaccount/token的读取权限,导致mTLS连接中断。
我们通过以下对比验证了权衡路径:
| 维度 | 过度收紧权限(Phase 1) | 动态权限收敛(Phase 2) |
|---|---|---|
| Pod Security Context | runAsNonRoot: true, seccompProfile: runtime/default |
同上,但补充 allowedCapabilities: ["SYS_PTRACE"](仅调试模式启用) |
| Kubernetes RBAC | 仅授予 configmaps/get |
按需扩展:secrets/get(证书)、events/create(异常上报) |
| 文件系统访问 | 所有路径默认只读 | /tmp 显式设为 rw,并通过 io/fs 包在代码中强制校验路径白名单 |
关键改进在于将静态权限声明转化为运行时可验证契约。例如,在初始化阶段注入如下校验逻辑:
func validateRuntimePermissions() error {
// 检查是否具备读取service account token的能力
if _, err := os.Stat("/var/run/secrets/kubernetes.io/serviceaccount/token"); os.IsPermission(err) {
return fmt.Errorf("missing read permission on service account token")
}
// 验证procfs可读性(用于cgroup资源监控)
if _, err := os.Stat("/proc/1/cgroup"); os.IsNotExist(err) {
log.Warn("cgroup v2 not detected, falling back to memory stats from /sys/fs/cgroup")
}
return nil
}
更进一步,我们构建了权限热插拔机制:当服务接收到 SIGUSR1 信号时,动态加载新权限策略。该机制基于 fsnotify 监控 /etc/permissions/policy.yaml 变更,并通过 os.UserGroupID 重新计算进程能力集。下图展示了权限收敛闭环流程:
flowchart LR
A[启动时加载基础RBAC] --> B[运行时健康检查]
B --> C{权限缺口检测?}
C -->|是| D[触发权限协商协议]
C -->|否| E[常规请求处理]
D --> F[向Policy Server发起JWT认证]
F --> G[获取临时Token与Scope列表]
G --> H[调用setcap修改进程能力]
H --> I[更新内部权限缓存]
I --> E
在灰度发布期间,我们将5%流量导向启用了 CAP_NET_BIND_SERVICE 的实例(用于非root端口绑定),其余95%仍使用传统端口转发方案。监控数据显示:权限动态调整平均耗时37ms,未引发P99延迟抖动;而审计日志中权限变更事件与线上故障的关联率下降至0.3%。这种渐进式收敛避免了“全有或全无”的权限模型陷阱——当某次部署意外移除 secrets/get 权限时,服务在3秒内完成降级(回退到预置的X.509证书),而非直接崩溃。
权限边界并非固定刻度,而是随服务生命周期演进的函数。我们为每个HTTP Handler注册了权限元数据:
type PermissionMeta struct {
RequiredFiles []string `json:"files"`
RequiredCaps []string `json:"capabilities"`
K8sResources []string `json:"k8s_resources"`
}
var handlerPermissions = map[string]PermissionMeta{
"/v1/decision": {RequiredFiles: []string{"/etc/rules"}, K8sResources: []string{"configmaps"}},
"/v1/debug/pprof": {RequiredCaps: []string{"SYS_PTRACE"}},
}
这种结构使CI流水线能在镜像构建阶段静态分析权限需求,并生成SBOM中对应的security:permissions字段。当某次PR试图为/v1/decision添加/tmp/write权限时,预提交钩子会强制要求提供安全评审单编号。运维团队据此建立了权限变更黄金路径:开发提交→SAST扫描→安全委员会审批→策略服务器同步→K8s Admission Controller校验。
