antoan-a-ignatov/devsecops-k3s-demo
GitHub: antoan-a-ignatov/devsecops-k3s-demo
一个 DevSecOps 实践演示项目,展示了三层应用从 Docker Compose 迁移至 K3s 的过程,并融入了容器加固、机密管理和 CI/CD 安全门禁。
Stars: 1 | Forks: 0
# DevSecOps K3s 迁移演示
这是一个小型的多层应用,从 Docker Compose 迁移到了 K3s,旨在展示实用的 DevSecOps 工作:容器化、编排、机密管理以及安全加固的部署。
该项目目前正在进行中。未来几天内将添加 CI/CD pipeline 以及更多安全工具(SAST、镜像扫描、签名)。请参阅路线图部分了解即将推出的内容。
## 架构
```
Browser
|
v
Traefik Ingress (built into K3s)
|
v
frontend (Nginx, static HTML)
|
v K8s Service DNS
api (Flask)
|
v NetworkPolicy restricts this hop to api only
db (PostgreSQL 16)
```
所有三层均作为独立的 Deployment 和 Service 运行在单节点 K3s 集群(WSL2/Ubuntu)上。数据库密码来自 Kubernetes Secret,而不是硬编码的值。
## 本项目展示的内容
| 技能/技术 | 仓库中的内容 |
|---|---|
| Docker 和 Docker Compose | 三层应用,首先使用 Compose 在本地构建和测试 |
| 从 Compose 迁移到 Kubernetes | 使用 Kompose 转换,然后进行人工检查和修复(详见下文) |
| K3s | 真实的部署、调试以及一次完整的故障恢复 |
| 容器加固 | 多阶段 Dockerfile、非 root 用户、丢弃 Linux 权能、K8s securityContext |
| 机密管理 | 自动化转换引入的明文密码,已被捕获并替换为适当的 Secret |
| 网络安全 | NetworkPolicy 将数据库访问限制为仅限 API pod,并使用真实流量进行了测试 |
| 部署自动化基础 | 每个容器上的存活/就绪探针与资源限制 |
| CI/CD 安全门禁 | Gitleaks、Semgrep 和 Trivy 作为真实的 pipeline 门禁,并记录了对每个发现的分类处理 |
| 故障排除 | 一次真实的 K3s 集群故障,已诊断并修复(见下文) |
## 快速开始
您将需要一个 K3s 集群、kubectl 和 Docker。
```
# 构建镜像
docker build -t api:latest ./app/api
docker build -t frontend:latest ./app/frontend
# 导入到 K3s 的 containerd 存储中(暂无 registry,将会随 CI/CD pipeline 一起配置)
docker save -o api.tar api:latest && sudo k3s ctr images import api.tar
docker save -o frontend.tar frontend:latest && sudo k3s ctr images import frontend.tar
rm api.tar frontend.tar
# 创建 db secret(未提交到 repo,见 Secrets 部分)
kubectl create secret generic db-credentials \
--from-literal=DB_PASSWORD='your-password-here' \
--dry-run=client -o yaml | kubectl apply -f -
# 部署
kubectl apply -f k8s/
# 检查是否已启动
kubectl get pods
kubectl get ingress
```
使用 `kubectl get nodes -o wide` 查找 K3s 节点的 IP,并在浏览器中打开它。
## 机密
`k8s/db-secret.yaml` 是刻意被 gitignore 忽略的。Kubernetes Secret 是 base64 编码的,并未加密,因此按原样提交它实际上并不安全,只是看起来安全而已。Sealed Secrets 已列入路线图,以妥善解决此问题。
## 安全加固
- **非 root 容器。** API 和数据库以其真实的非 root UID 运行(已人工核对),并通过 `runAsNonRoot` 强制执行。
- **CI 例外情况(已记录)。** Semgrep 的 `run-as-non-root` 规则标记了 `frontend-deployment.yaml` 未在 pod 层级设置 `runAsNonRoot`。这在 CI pipeline 中被排除(`--exclude-rule`)而不是被修复,因为原样是正确的,参见上文的 Nginx 启动说明。此处记录是因为 Semgrep 的精确匹配行使得内联 YAML 注释变得不切实际。
- **丢弃权能。** 每个容器默认丢弃所有 Linux 权能,然后只加回它实际需要的。Nginx 在其正常的启动序列中需要 `NET_BIND_SERVICE`、`CHOWN`、`SETUID` 和 `SETGID`(它以 root 身份绑定 80 端口,然后切换到非特权用户)。第一次尝试时丢弃所有权能且没有例外导致了崩溃,这很好地提醒了我们:这些控制需要针对每个容器进行应用。
- **资源限制。** 每个容器都有 CPU 和内存的 requests 和 limits。
- **健康探针。** 所有三层上都有存活和就绪检查,前端和 API 使用 HTTP,Postgres 使用 `pg_isready` exec 探针。
- **Network policy。** 只有 API pod 可以在端口 5432 上访问数据库。值得注意的是:K3s 默认的 Flannel CNI 本身并不强制执行 NetworkPolicy,但 K3s 附带了一个单独的基于 kube-router 的控制器来实现该功能,因此无需安装任何东西即可开箱即用。
## 来自 pipeline 的安全发现及其处理方式
对真实代码和真实基础镜像运行真实的扫描器揭示了一些真实的发现。每一个发现都在这里进行了记录,而不是悄悄地修复或抑制,因为正确地对发现进行分类处理才是 DevSecOps pipeline 的真正意义所在,而不仅仅是获得绿色的勾号。
### Semgrep (SAST)
**发现:`python.flask.security.audit.app-run-param-config.avoid_app_run_with_bad_host`**
标记了 Flask API 中的 `app.run(host="0.0.0.0", port=5000)`。存在此规则是因为绑定到所有接口可能会导致应用在共享主机上过度暴露。在此特定上下文中评估为误报:容器运行在其独立的 Kubernetes pod 网络命名空间中,而不是共享主机,并且绑定到 `0.0.0.0` 是让 Service/Ingress 能够访问容器的必要条件。已在 `app.py` 中使用 `nosemgrep` 注释以及该行正上方的书面说明进行了内联抑制。
**发现:`yaml.kubernetes.security.run-as-non-root.run-as-non-root`**
标记了 `frontend-deployment.yaml` 未在 pod 层级设置 `runAsNonRoot`。按其编写的方式是正确且有意为之的:官方 `nginx:alpine` 镜像的 master 进程必须以 root 身份启动以绑定端口 80(特权端口),然后在内部自行切换到非特权的 `nginx` 用户。在这里强制设置 `runAsNonRoot: true` 将完全阻止 pod 的启动。在 Semgrep CI 步骤中使用 `--exclude-rule` 在 pipeline 层级将其排除,因为该规则的匹配位置使得内联注释变得不切实际,因此在此处进行记录。
### Trivy(容器镜像扫描)
扫描了三个镜像,每个镜像都需要不同的应对措施,这本身就值得关注:处理 CVE 发现没有单一的正确方法,正确的举措完全取决于该发现的实际状态。
**API 镜像 (python:3.12-slim)**:11 个发现,全部位于应用程序不直接使用的操作系统级别包中(perl-base、libncursesw6、libsqlite3-0、ncurses),无论应用程序需要什么,这些都是由 Debian 捆绑的。每个发现的 Trivy 状态均为 `affected` 或 `fix_deferred`,这意味着上游任何地方尚不存在修补版本。已使用 Trivy 的 `ignore-unfixed: true` 标志进行处理,该标志会过滤掉没有可用修复的发现,同时仍然会对任何有修复的发现进行门禁控制。曾考虑过切换到 Alpine 以完全避免这些包,但后来被拒绝:psycopg2-binary 仅提供 glibc 版本的 wheel,而 Alpine 的 musl libc 很可能会强制执行较慢、较脆弱的源码构建,却无法带来真正的安全收益。
**前端镜像 (nginx:1.27-alpine)**:33 个发现(OpenSSL、libxml2、libpng、zlib、musl 等),全部具有 Trivy 状态 `fixed` 以及列出的实际修复版本。这意味着基础镜像本身相对于当前可用的 Alpine 包而言仅仅是过时了。通过在 Dockerfile 中添加 `RUN apk update && apk upgrade --no-cache` 进行了修复,在构建时拉取当前打过补丁的包,而不是依赖镜像发布时冻结的任何内容。结果:干净的扫描,零发现。
**数据库镜像 (postgres:16-alpine,官方,未修改)**:16 个发现,全部位于由其上游维护者捆绑在镜像中的 Go 标准库内(crypto/tls、net/url、crypto/x509),其中包括一个严重 (CRITICAL) 的 TLS 证书验证错误。与其他两个镜像不同,这些无法通过 apk upgrade 修复(Go 运行时不是 Alpine 包),而且我们也没有自己的 Dockerfile 可供修补,因为这是按原样使用的官方镜像。postgres:16-alpine 标签得到了积极维护并定期重新构建,因此这并不是使用陈旧标签的情况,这只是目前上游捆绑的 Go 工具链的当前状态。在 pipeline 中标记了 `continue-on-error: true`:发现的问题会被扫描并显示在 CI 输出中,但不会阻止部署,因为对于我们既无法构建也无法修补的内容无限期地阻止部署并不是一个有意义的安全门禁。这被作为已知、已接受、已监控的风险进行跟踪,直到上游镜像更新为止,而不是疏忽。
### Gitleaks(机密检测)
没有发现。值得明确指出,而不是保持沉默:数据库密码在任何时候都刻意未提交到 git 历史记录中,它仅存在于本地 gitignore 的 .env 文件以及集群中的 Kubernetes Secret 中,这正是 Gitleaks 旨在验证的场景。
## 从 Compose 迁移到 K3s
Kompose 自动完成了大部分工作,但它需要检查和修复,而不仅仅是盲目地执行 `kubectl apply`:
1. **明文密码。** Kompose 在转换时从 Compose 文件中解析了 `.env` 变量,并将实际密码直接写入了生成的 Deployment YAML 中。立即发现了这一点并用 Secret 进行了替换。
2. **缺失的 Service。** Kompose 仅为原始 Compose 文件中具有 `ports:` 块的容器创建 Service。API 和数据库在 Compose 中从不需要它(它们只是使用了 Docker 的内部 DNS),因此 Kompose 跳过了它们。手动添加了这两者。
3. **暂无镜像仓库。** Kompose 假定您的镜像来自镜像仓库。目前,镜像是在本地构建的,并通过 `imagePullPolicy: Never` 直接导入 K3s。一旦 CI/CD pipeline 开始推送到 GHCR,这就会改变。
## 值得一提的故障
在中途,K3s 集群在 WSL2 重启后开始出现 crash-looping,在内部的 RBAC 引导步骤失败,并给出了一个刻意模糊的错误消息。排除了磁盘空间不足和数据存储损坏的可能性,然后将其追溯到过时的 WSL2 内核。通过 `wsl --update`、重新安装干净的 K3s 以及刷新 kubeconfig 完成了修复。
## 路线图
- CI/CD pipeline(GitHub Actions,在 Azure DevOps 和 GitLab CI/CD 中镜像),包含 SAST、依赖项扫描和镜像扫描
- Helm chart
- Sealed Secrets
- Cosign 镜像签名
- SBOM 生成
- 使用 OPA 或 Kyverno 进行策略强制执行
- 具有真正提升流程的 staging 和 production 环境命名空间
- 部署到 AKS 以证明云迁移路径
## 技术栈(目前)
Docker、Docker Compose、Kompose、Kubernetes、K3s、Traefik、PostgreSQL、Flask、Nginx、GitHub Actions、GHCR、Gitleaks、Semgrep、Trivy
标签:DevSecOps, Docker Compose, GitHub Advanced Security, K3s, NIDS, 上游代理, 子域名突变, 安全加固, 容器化, 测试用例, 版权保护, 请求拦截