mistermobilka-spec/k8s-manifest-guard
GitHub: mistermobilka-spec/k8s-manifest-guard
一款无需集群连接的 Kubernetes 清单离线安全与可靠性静态检查工具,旨在部署前拦截隐蔽的配置缺陷。
Stars: 1 | Forks: 0
# k8s-manifest-guard
[](https://github.com/mistermobilka-spec/k8s-manifest-guard/actions/workflows/ci.yml)
[](LICENSE)
通过从磁盘读取 YAML 来审计 Kubernetes 清单中的安全性和可靠性缺陷——无需 cluster,无需 kubeconfig,也无需访问 registry。
## 为什么开发此工具
我曾在 AWS EC2 上运行过一个单节点 k3s cluster:一个容器化的 Flask 应用部署在 PostgreSQL StatefulSet 前面,并通过内置的 Traefik ingress 发布。一切应用过程都很顺利。`kubectl get pods` 显示一片绿。
绿并不等于正确。一个清单可能完全合法,但仍然包含只有在以后才会显现的缺陷:
- 一个没有 `volumeClaimTemplates` 的 StatefulSet——该工作负载类型承诺提供稳定存储,但清单却悄悄地写入了 `emptyDir`,导致数据在第一次重新调度时就丢失了
- `image: app:latest`——相隔一周创建的两个副本可能运行着不同的代码,而回滚操作则成了碰运气
- 没有内存限制——某个容器会耗尽整个节点,并连累其他不相关的工作负载
- 没有 readiness probe——Service 会在进程能够提供服务之前就开始路由流量,导致每次 rollout 都会断开连接
这些情况都不会导致 `kubectl apply` 失败。它们只会在凌晨 03:00 进行节点排空时发生故障。
这个工具会在 apply 之前捕获这类缺陷。它解析 YAML 并对其进行静态推理,这正是它能安全地在 CI 中针对 manifest 运行的原因,即使运行环境没有目标集群的凭据——当 cluster 是私有的而 pipeline 不是时,这属于常见情况。
## 工作原理
审计仓库自带的未加固示例:
```
$ k8s-manifest-guard examples/flask-k3s/database.yaml --hide-remediation
HIGH
----
REL001 storefront/StatefulSet/postgres:postgres
Container has no resource requests: resources.requests does not declare cpu or memory, so the scheduler cannot reserve capacity and the pod may land on a node that is already saturated
source: examples/flask-k3s/database.yaml[0]
REL006 storefront/StatefulSet/postgres
StatefulSet stores data on ephemeral storage: no volumeClaimTemplates and no persistentVolumeClaim volume, so every pod restart discards the data this StatefulSet exists to keep
source: examples/flask-k3s/database.yaml[0]
SEC003 storefront/StatefulSet/postgres:postgres
Container may run as root: neither runAsNonRoot nor runAsUser is set, so the container runs as whichever user the image declares -- root for most base images
source: examples/flask-k3s/database.yaml[0]
MEDIUM
------
SEC007 storefront/StatefulSet/postgres:postgres
Secret is injected as an environment variable: secret values reach the process environment via envFrom secretRef postgres-credentials; environment variables are readable from /proc and are commonly copied into crash dumps and log output
source: examples/flask-k3s/database.yaml[0]
LOW
---
REL007 namespace/storefront
Namespace has no NetworkPolicy: no NetworkPolicy is defined for namespace 'storefront', so pod-to-pod traffic is unrestricted and any compromised pod can reach every other one
source: examples/flask-k3s/database.yaml[0]
Scanned 2 resource(s) with 18 rule(s).
Findings: 12 (0 critical, 4 high, 6 medium, 2 low).
4 finding(s) at or above the 'high' threshold.
```
*(已节选——完整运行会报告全部 12 个问题)*
修复后的相同工作负载,位于 `examples/flask-k3s-hardened/`:
```
$ k8s-manifest-guard examples/flask-k3s-hardened
No findings.
Scanned 9 resource(s) with 18 rule(s).
Findings: 0 (0 critical, 0 high, 0 medium, 0 low).
No findings at or above the 'high' threshold.
```
这两个示例目录树都会在每次推送时由测试套件和 CI 进行检查,因此可以保证“修复前”的目录树会持续产生问题,而“修复后”的目录树会保持整洁。这个加固的目录树是该项目自身的误报回归测试。
## 技术栈
- Python 3.12+
- 使用 PyYAML 进行解析
- 使用 pytest 进行测试,使用 ruff 进行 lint 和格式化,mypy 处于严格模式
- 使用 GitHub Actions 作为 CI,PowerShell 7 作为任务运行器
## 架构
分为五个阶段,每个阶段都可以独立测试。加载器将文件转换为 `Resource` 对象,并收集每个文档的失败信息,而不是直接中止。引擎会对每个适用的资源运行所有已注册的规则,并负责处理那些规则本身不应该关心的问题:忽略列表、将 namespace 作用域的规则合并为每个 namespace 一个问题,以及控制抛出异常的规则。规则本身是纯函数,用于生成描述问题的 `Issue` 值;引擎会为其附加身份和严重性。报告器负责渲染结果,CLI 将其转换为退出码。
最重要的设计决策是:**规则永远看不到文件**,且**加载器永远看不到规则**。规则接收的是一个已解码的 `Resource` 以及完整的 `Manifests` 集合——后者仅仅是因为存在跨资源的问题(是否有 PodDisruptionBudget 选择了此 Deployment?在此 namespace 中是否有 NetworkPolicy?)。这使得规则保持纯净,并可通过直接调用来对每个规则进行测试。
```
flowchart LR
A[YAML files] --> B[loader]
B -->|Resource objects| C[engine]
B -->|unparseable docs| F[AuditResult]
R[(rule registry
security · reliability · images)] --> C G[config
file → env → CLI] --> C C --> F F --> H[text reporter] F --> I[JSON reporter] H --> J[exit code] I --> J ``` ``` src/k8s_manifest_guard/ ├── loader.py discovery, multi-document decoding, error capture ├── models.py Resource, Container, Finding, safe navigation helpers ├── registry.py the @rule decorator and rule catalogue ├── engine.py execution, suppression, namespace dedup, error containment ├── config.py TOML file + K8S_GUARD_* env vars + CLI precedence ├── cli.py argument parsing and exit codes ├── rules/ │ ├── security.py SEC001-SEC009 │ ├── reliability.py REL001-REL007 │ └── images.py IMG001-IMG002 └── report/ ├── text.py grouped terminal output └── json_report.py versioned machine-readable output ``` ## 快速开始 **前提条件:** Python 3.12 或更高版本,以及 [uv](https://docs.astral.sh/uv/getting-started/installation/)。PowerShell 7 是可选的——它仅用于运行任务快捷方式。 ``` git clone https://github.com/mistermobilka-spec/k8s-manifest-guard.git cd k8s-manifest-guard uv venv --python 3.13 uv pip install -e ".[dev]" ``` 或者,使用 PowerShell 7: ``` ./tasks.ps1 install ``` 然后审计一些内容: ``` uv run k8s-manifest-guard examples/flask-k3s ``` 入门无需进行任何配置——每个设置都有默认值。配置的存在是为了针对特定仓库对工具进行调整。 ## 用法 以下示例使用裸命令名称。`uv venv` 不会激活环境,因此请在每个命令前加上 `uv run` 前缀,或者先激活环境: ``` source .venv/bin/activate # Linux, macOS .venv\Scripts\Activate.ps1 # Windows PowerShell ``` 审计目录树、单个文件或其中的几个: ``` k8s-manifest-guard ./manifests k8s-manifest-guard ./manifests/api.yaml ./manifests/db.yaml k8s-manifest-guard ./staging ./production ``` ### 退出码 该工具的核心在于对 pipeline 进行门控,因此退出码是契约的一部分: | 代码 | 含义 | |------|---------| | `0` | 没有达到或超过失败阈值的问题 | | `1` | 存在阻断性问题,**或者**有无法解析的文档 | | `2` | 审计无法运行:标志错误、缺少路径、配置不可用 | 一个悄悄跳过了一半输入的审计比不审计更糟糕,这就是为什么无法解析的文档会导致运行失败,而不是被忽略。 ### 调整阈值 默认值为 `high`。可以根据环境放宽或收紧: ``` k8s-manifest-guard ./manifests --fail-on critical # only block on the worst k8s-manifest-guard ./manifests --fail-on low # block on everything ``` ### 抑制规则 ``` k8s-manifest-guard ./manifests --ignore SEC008 --ignore IMG002 ``` 未匹配到任何内容的规则 ID 会触发错误,而不是静默无效——否则,拼写错误的抑制指令会让你误以为某个规则已被禁用,而实际上它却一直在导致你的 pipeline 失败。 ### 配置文件 在清单旁边放入一个 `.k8s-guard.toml` 并提交它,这样整个团队就可以使用相同的设置进行审计: ``` [k8s-guard] fail_on = "medium" ignore = ["IMG002"] # digest pinning is handled by our release pipeline ``` ### 环境变量 适用于在 CI 中难以传递标志参数的情况: ``` export K8S_GUARD_FAIL_ON=critical export K8S_GUARD_IGNORE=SEC008,IMG002 ``` **优先级,从高到低:** CLI 标志 → 环境变量 → 配置文件 → 默认值。这过程中不涉及任何机密信息,因此提交配置文件是安全的。 ### JSON 输出 用于仪表板、工单自动化或在运行之间进行 diff: ``` $ k8s-manifest-guard examples/flask-k3s --format json { "schemaVersion": 1, "summary": { "resourcesScanned": 5, "rulesRun": 18, "findingCount": 24, "blockingCount": 8, "failOn": "high", "countsBySeverity": { "critical": 0, "high": 8, "medium": 13, "low": 3 }, "loadErrorCount": 0, "ruleErrorCount": 0 }, "findings": [ { "ruleId": "IMG001", "title": "Container image uses a mutable tag", "severity": "high", "target": "storefront/Deployment/flask-api:api", "kind": "Deployment", "name": "flask-api", "namespace": "storefront", "container": "api", "detail": "image 'registry.example.com/storefront/flask-api:latest' uses mutable tag 'latest'; replicas created at different times can run different code, and rollback is not reproducible", "remediation": "Pin an explicit immutable version tag, ideally with a digest", "source": { "file": "examples/flask-k3s/app.yaml", "documentIndex": 0 } } ] } ``` 提供 `schemaVersion` 的目的是让消费者能够检测到破坏性变更,而不是静默地误读被重命名的字段。 ## 规则 ``` $ k8s-manifest-guard --list-rules ID SEVERITY CATEGORY TITLE IMG001 high images Container image uses a mutable tag IMG002 low images Container image is not pinned by digest REL001 high reliability Container has no resource requests REL002 high reliability Container has no resource limits REL003 medium reliability Container has no liveness probe REL004 medium reliability Container has no readiness probe REL005 medium reliability Replicated workload has no PodDisruptionBudget REL006 high reliability StatefulSet stores data on ephemeral storage REL007 low reliability Namespace has no NetworkPolicy SEC001 critical security Container runs in privileged mode SEC002 high security Pod shares a host namespace SEC003 high security Container may run as root SEC004 medium security Privilege escalation is not blocked SEC005 high security Container adds dangerous Linux capabilities SEC006 high security Pod mounts a hostPath volume SEC007 medium security Secret is injected as an environment variable SEC008 medium security Container root filesystem is writable SEC009 medium security Service account token is mounted unnecessarily ``` 规则仅限于对其有意义的工作负载类型。ConfigMap 永远不会被要求提供 liveness probe;Job 永远不会被要求提供 readiness probe;临时调试容器被豁免提供资源声明,因为 API 服务器会拒绝它们上面的 `resources` 块。 ## 在 CI 中使用 未发布到 PyPI,因此请从代码库安装: ``` - name: Audit manifests run: | pip install "git+https://github.com/mistermobilka-spec/k8s-manifest-guard.git@main" k8s-manifest-guard ./manifests --fail-on high ``` 当出现阻断性问题时,该步骤将导致任务失败。无需集群凭据,这正是其核心目的。 ## 测试 ``` ./tasks.ps1 test # or: uv run pytest ./tasks.ps1 coverage # with a coverage report ./tasks.ps1 check # lint + typecheck + test ./tasks.ps1 smoke # audit both example trees and assert their exit codes ``` CI 会依次运行 `check` 和 `smoke`,因此在本地运行这两者即可重现整个 pipeline。 在 Python 3.13 上的当前状态——188 个测试,98% 的分支覆盖率(行已节选): ``` $ ./tasks.ps1 coverage ==> pytest --cov 188 passed in 5.71s Name Stmts Miss Branch BrPart Cover --------------------------------------------------------------------------------- src\k8s_manifest_guard\cli.py 73 0 18 0 100% src\k8s_manifest_guard\engine.py 47 0 14 0 100% src\k8s_manifest_guard\loader.py 111 3 50 1 98% src\k8s_manifest_guard\models.py 151 4 30 3 96% src\k8s_manifest_guard\rules\images.py 58 0 22 0 100% src\k8s_manifest_guard\rules\reliability.py 73 0 40 1 99% src\k8s_manifest_guard\rules\security.py 91 0 46 0 100% --------------------------------------------------------------------------------- TOTAL 820 12 276 6 98% ``` 每个规则都经过了触发和保持静默两方面的测试,因为一个无法保持安静的 linter 最终只会被用户关闭。端到端测试会针对示例清单运行真实的入口点并断言退出码;其中一个测试会将 CLI 作为实际的子进程运行。 ## 已知局限性 明确说明此工具目前无法做到的事情: - **不渲染模板。** 包含 `{{ .Release.Name }}` 的文件将被报告为无法解析,并提示先运行 `helm template` 或 `kustomize build`。渲染是一个完全独立的关注点,做不好渲染比不渲染更糟糕。 - **无 schema 验证。** 这不会检查你的清单是否符合 Kubernetes OpenAPI schema——`kubeconform` 已经做得很好了。可能会被 API 服务器拒绝的清单在此处可能仍然会通过。 - **不评估 `matchExpressions` 选择器。** 当 PodDisruptionBudget 使用它时,REL005 会保持静默,而不是进行猜测。这是一种故意的漏报:如果对一个*已受保护*的工作负载产生误报,会让人们习惯于忽略该工具。 - **无 CRD 感知。** 自定义资源会被加载和计数,但没有任何规则适用于它们。 - **跨文件引用仅在审计集合内解析。** 如果 PodDisruptionBudget 位于你未传递的目录中,REL005 将报告该 Deployment 未受保护。 - **`--fail-on` 是全局性的。** 目前还没有针对特定规则的严重性覆盖。 ## 路线图 - 在配置文件中实现针对特定规则的严重性覆盖 - SARIF 输出,以便在 GitHub Security 选项卡中显示发现的问题 - 针对没有 TLS 的 `Ingress` 添加规则 - 行内抑制注释(`# k8s-guard: ignore SEC008`),用于属于清单旁边而非中心文件中的一次性例外情况 ## 许可证 MIT — 查看 [LICENSE](LICENSE)。
security · reliability · images)] --> C G[config
file → env → CLI] --> C C --> F F --> H[text reporter] F --> I[JSON reporter] H --> J[exit code] I --> J ``` ``` src/k8s_manifest_guard/ ├── loader.py discovery, multi-document decoding, error capture ├── models.py Resource, Container, Finding, safe navigation helpers ├── registry.py the @rule decorator and rule catalogue ├── engine.py execution, suppression, namespace dedup, error containment ├── config.py TOML file + K8S_GUARD_* env vars + CLI precedence ├── cli.py argument parsing and exit codes ├── rules/ │ ├── security.py SEC001-SEC009 │ ├── reliability.py REL001-REL007 │ └── images.py IMG001-IMG002 └── report/ ├── text.py grouped terminal output └── json_report.py versioned machine-readable output ``` ## 快速开始 **前提条件:** Python 3.12 或更高版本,以及 [uv](https://docs.astral.sh/uv/getting-started/installation/)。PowerShell 7 是可选的——它仅用于运行任务快捷方式。 ``` git clone https://github.com/mistermobilka-spec/k8s-manifest-guard.git cd k8s-manifest-guard uv venv --python 3.13 uv pip install -e ".[dev]" ``` 或者,使用 PowerShell 7: ``` ./tasks.ps1 install ``` 然后审计一些内容: ``` uv run k8s-manifest-guard examples/flask-k3s ``` 入门无需进行任何配置——每个设置都有默认值。配置的存在是为了针对特定仓库对工具进行调整。 ## 用法 以下示例使用裸命令名称。`uv venv` 不会激活环境,因此请在每个命令前加上 `uv run` 前缀,或者先激活环境: ``` source .venv/bin/activate # Linux, macOS .venv\Scripts\Activate.ps1 # Windows PowerShell ``` 审计目录树、单个文件或其中的几个: ``` k8s-manifest-guard ./manifests k8s-manifest-guard ./manifests/api.yaml ./manifests/db.yaml k8s-manifest-guard ./staging ./production ``` ### 退出码 该工具的核心在于对 pipeline 进行门控,因此退出码是契约的一部分: | 代码 | 含义 | |------|---------| | `0` | 没有达到或超过失败阈值的问题 | | `1` | 存在阻断性问题,**或者**有无法解析的文档 | | `2` | 审计无法运行:标志错误、缺少路径、配置不可用 | 一个悄悄跳过了一半输入的审计比不审计更糟糕,这就是为什么无法解析的文档会导致运行失败,而不是被忽略。 ### 调整阈值 默认值为 `high`。可以根据环境放宽或收紧: ``` k8s-manifest-guard ./manifests --fail-on critical # only block on the worst k8s-manifest-guard ./manifests --fail-on low # block on everything ``` ### 抑制规则 ``` k8s-manifest-guard ./manifests --ignore SEC008 --ignore IMG002 ``` 未匹配到任何内容的规则 ID 会触发错误,而不是静默无效——否则,拼写错误的抑制指令会让你误以为某个规则已被禁用,而实际上它却一直在导致你的 pipeline 失败。 ### 配置文件 在清单旁边放入一个 `.k8s-guard.toml` 并提交它,这样整个团队就可以使用相同的设置进行审计: ``` [k8s-guard] fail_on = "medium" ignore = ["IMG002"] # digest pinning is handled by our release pipeline ``` ### 环境变量 适用于在 CI 中难以传递标志参数的情况: ``` export K8S_GUARD_FAIL_ON=critical export K8S_GUARD_IGNORE=SEC008,IMG002 ``` **优先级,从高到低:** CLI 标志 → 环境变量 → 配置文件 → 默认值。这过程中不涉及任何机密信息,因此提交配置文件是安全的。 ### JSON 输出 用于仪表板、工单自动化或在运行之间进行 diff: ``` $ k8s-manifest-guard examples/flask-k3s --format json { "schemaVersion": 1, "summary": { "resourcesScanned": 5, "rulesRun": 18, "findingCount": 24, "blockingCount": 8, "failOn": "high", "countsBySeverity": { "critical": 0, "high": 8, "medium": 13, "low": 3 }, "loadErrorCount": 0, "ruleErrorCount": 0 }, "findings": [ { "ruleId": "IMG001", "title": "Container image uses a mutable tag", "severity": "high", "target": "storefront/Deployment/flask-api:api", "kind": "Deployment", "name": "flask-api", "namespace": "storefront", "container": "api", "detail": "image 'registry.example.com/storefront/flask-api:latest' uses mutable tag 'latest'; replicas created at different times can run different code, and rollback is not reproducible", "remediation": "Pin an explicit immutable version tag, ideally with a digest", "source": { "file": "examples/flask-k3s/app.yaml", "documentIndex": 0 } } ] } ``` 提供 `schemaVersion` 的目的是让消费者能够检测到破坏性变更,而不是静默地误读被重命名的字段。 ## 规则 ``` $ k8s-manifest-guard --list-rules ID SEVERITY CATEGORY TITLE IMG001 high images Container image uses a mutable tag IMG002 low images Container image is not pinned by digest REL001 high reliability Container has no resource requests REL002 high reliability Container has no resource limits REL003 medium reliability Container has no liveness probe REL004 medium reliability Container has no readiness probe REL005 medium reliability Replicated workload has no PodDisruptionBudget REL006 high reliability StatefulSet stores data on ephemeral storage REL007 low reliability Namespace has no NetworkPolicy SEC001 critical security Container runs in privileged mode SEC002 high security Pod shares a host namespace SEC003 high security Container may run as root SEC004 medium security Privilege escalation is not blocked SEC005 high security Container adds dangerous Linux capabilities SEC006 high security Pod mounts a hostPath volume SEC007 medium security Secret is injected as an environment variable SEC008 medium security Container root filesystem is writable SEC009 medium security Service account token is mounted unnecessarily ``` 规则仅限于对其有意义的工作负载类型。ConfigMap 永远不会被要求提供 liveness probe;Job 永远不会被要求提供 readiness probe;临时调试容器被豁免提供资源声明,因为 API 服务器会拒绝它们上面的 `resources` 块。 ## 在 CI 中使用 未发布到 PyPI,因此请从代码库安装: ``` - name: Audit manifests run: | pip install "git+https://github.com/mistermobilka-spec/k8s-manifest-guard.git@main" k8s-manifest-guard ./manifests --fail-on high ``` 当出现阻断性问题时,该步骤将导致任务失败。无需集群凭据,这正是其核心目的。 ## 测试 ``` ./tasks.ps1 test # or: uv run pytest ./tasks.ps1 coverage # with a coverage report ./tasks.ps1 check # lint + typecheck + test ./tasks.ps1 smoke # audit both example trees and assert their exit codes ``` CI 会依次运行 `check` 和 `smoke`,因此在本地运行这两者即可重现整个 pipeline。 在 Python 3.13 上的当前状态——188 个测试,98% 的分支覆盖率(行已节选): ``` $ ./tasks.ps1 coverage ==> pytest --cov 188 passed in 5.71s Name Stmts Miss Branch BrPart Cover --------------------------------------------------------------------------------- src\k8s_manifest_guard\cli.py 73 0 18 0 100% src\k8s_manifest_guard\engine.py 47 0 14 0 100% src\k8s_manifest_guard\loader.py 111 3 50 1 98% src\k8s_manifest_guard\models.py 151 4 30 3 96% src\k8s_manifest_guard\rules\images.py 58 0 22 0 100% src\k8s_manifest_guard\rules\reliability.py 73 0 40 1 99% src\k8s_manifest_guard\rules\security.py 91 0 46 0 100% --------------------------------------------------------------------------------- TOTAL 820 12 276 6 98% ``` 每个规则都经过了触发和保持静默两方面的测试,因为一个无法保持安静的 linter 最终只会被用户关闭。端到端测试会针对示例清单运行真实的入口点并断言退出码;其中一个测试会将 CLI 作为实际的子进程运行。 ## 已知局限性 明确说明此工具目前无法做到的事情: - **不渲染模板。** 包含 `{{ .Release.Name }}` 的文件将被报告为无法解析,并提示先运行 `helm template` 或 `kustomize build`。渲染是一个完全独立的关注点,做不好渲染比不渲染更糟糕。 - **无 schema 验证。** 这不会检查你的清单是否符合 Kubernetes OpenAPI schema——`kubeconform` 已经做得很好了。可能会被 API 服务器拒绝的清单在此处可能仍然会通过。 - **不评估 `matchExpressions` 选择器。** 当 PodDisruptionBudget 使用它时,REL005 会保持静默,而不是进行猜测。这是一种故意的漏报:如果对一个*已受保护*的工作负载产生误报,会让人们习惯于忽略该工具。 - **无 CRD 感知。** 自定义资源会被加载和计数,但没有任何规则适用于它们。 - **跨文件引用仅在审计集合内解析。** 如果 PodDisruptionBudget 位于你未传递的目录中,REL005 将报告该 Deployment 未受保护。 - **`--fail-on` 是全局性的。** 目前还没有针对特定规则的严重性覆盖。 ## 路线图 - 在配置文件中实现针对特定规则的严重性覆盖 - SARIF 输出,以便在 GitHub Security 选项卡中显示发现的问题 - 针对没有 TLS 的 `Ingress` 添加规则 - 行内抑制注释(`# k8s-guard: ignore SEC008`),用于属于清单旁边而非中心文件中的一次性例外情况 ## 许可证 MIT — 查看 [LICENSE](LICENSE)。
标签:AI合规, DevSecOps, Python安全, YAML, 上游代理, 安全基线, 安全库, 安全规则引擎, 恶意代码分类, 教学环境, 逆向工具, 静态检查