yakohhhh/portcullis
GitHub: yakohhhh/portcullis
Portcullis 是一款本地运行的 docker-compose 安全审计工具,通过静态分析服务暴露面和配置缺陷,为自托管基础设施生成带 A-F 评级的优先级修复报告。
Stars: 2 | Forks: 2
# Portcullis
[](https://github.com/yakohhhh/portcullis/actions/workflows/ci.yml)
[](https://github.com/yakohhhh/portcullis/actions/workflows/codeql.yml)
[](https://scorecard.dev/viewer/?uri=github.com/yakohhhh/portcullis)
[](LICENSE)
[](pyproject.toml)
[](#roadmap)
Portcullis 是一款专为自托管基础设施设计的安全审计工具。它会静态分析您的 docker-compose 文件,查明每个服务实际暴露的情况,执行“走火”(自残式风险)检查,并生成一份带有 A-F 评分的优先级报告。100% 本地运行:不会有任何数据离开您的设备。
## 为什么需要它
家庭实验室(Homelab)通常会在 Traefik 或 Caddy 背后运行数十个 compose 服务,且没有任何安全团队对其进行审查。这种故障模式是残酷且悄无声息的:只要一个被发布的数据库端口、一个被挂载的 Docker socket、或者一个局域网可访问服务上的默认密码——攻击者就能控制宿主机,而不仅仅是容器。
现有的扫描工具在这里起不到多大作用。它们逐个文件地进行扫描,专为云和 Kubernetes 资产环境构建,并且会用数百个无关紧要的发现将那唯一一个关键发现淹没。Portcullis 将 compose 堆栈作为分析单元,并提出一个自托管者真正关心的问题:*什么可以访问此服务,如果它被攻破会发生什么?*
## 它的功能
- **暴露引擎** - 通过交叉比对三个信号:发布的端口 × 反向代理路由 × `internal: true` 网络,将每个服务分类为 INTERNAL / HOST / LAN / INTERNET。路由信息从 compose 标签(Traefik、caddy-docker-proxy、nginx-proxy)**以及**反向代理文件配置中读取——包括 Traefik(`traefik.yml`/`.toml`、`command:` 标志、动态文件提供程序、`exposedByDefault`、入口点绑定地址)、纯 Caddyfile(站点地址和 `reverse_proxy` 上游,包括代码片段导入)、原始 nginx `server` 块以及 Nginx Proxy Manager(其 `database.sqlite` 或生成的配置)。
- **应用知识库** - YAML 条目将容器镜像映射到应用的实际属性(类别、敏感度、建议的暴露级别),从而将以不同方式对待暴露密码管理器和暴露博客的行为。
- **“走火”规则** - 12 项 compose 级别的检查 (PC-001..PC-012),专门针对那些实际上会轻易将家庭实验室拱手送人的配置错误。每个发现都会解释查出了什么问题、为什么这很重要,以及如何修复它。
- **A-F 评级** - 一个简单且文档完善的评分系统(从 100 分开始,根据发现的严重程度进行扣分),您可以随时跟踪或将其作为 CI 的拦截标准。
- **可选的 Trivy 委托** - 当安装了 `trivy` 二进制文件时,镜像的 CVE 将被合并到报告中,并聚合为每个镜像一条发现。
### 检查规则
| ID | 检查项 | 严重程度 |
| --- | --- | --- |
| PC-001 | Docker socket 挂载到容器中 | CRITICAL |
| PC-002 | 容器在特权模式下运行 | CRITICAL |
| PC-003 | 容器使用宿主网络 | HIGH |
| PC-004 | 授予了危险的 Linux capability (`cap_add`) | HIGH 或 MEDIUM(取决于具体的 capability) |
| PC-005 | 镜像没有标签或使用 `latest` | LOW |
| PC-006 | 容器明确以 root 身份运行 | LOW |
| PC-007 | 容器共享宿主的 PID namespace | HIGH |
| PC-008 | 环境变量中存在弱密码、默认密码或空密码 | CRITICAL 或 HIGH(取决于暴露程度) |
| PC-009 | 敏感应用的暴露程度超出了推荐水平 | CRITICAL 或 HIGH(取决于敏感度) |
| PC-010 | 数据库端口发布在宿主上 | HIGH |
| PC-011 | 经过代理的服务同时也直接发布了端口(绕过代理) | MEDIUM |
| PC-012 | 在堆栈使用 Docker `secrets:` 的情况下,通过 `environment:` 传递机密信息 | LOW |
完整详情请参阅 [docs/checks.md](docs/checks.md)。
## 暴露程度是如何分类的
- **INTERNAL** - 只能由同一个 Docker 网络上的其他容器访问。
- **HOST** - 已发布,但绑定到环回地址:仅可从宿主机访问。
- **LAN** - 在所有接口上发布:可从本地网络访问。
- **INTERNET** - 由反向代理进行路由,这通常是互联网的入口点。
诚实的说明:静态分析无法得知您的路由器是否转发了某个端口,因此在所有接口上发布的服务会被归类为 LAN——这是最安全、无可辩驳的答案。如果该端口*确实*被转发,那它就是面向互联网的,并且相关发现只会变得更加紧急。详细信息和局限性请参阅 [docs/exposure-model.md](docs/exposure-model.md)。
## 快速开始
```
# from PyPI(发布后)
pipx install portcullis
# 来自 git 的最新版本
pipx install git+https://github.com/yakohhhh/portcullis
# 或者使用 pip,从源码
git clone https://github.com/yakohhhh/portcullis
cd portcullis && pip install .
```
然后将其指向一个 compose 文件或目录树(例如您的 homelab Git 仓库):
```
portcullis scan .
# 或者是交互式 HTML 报告(可过滤的发现结果、暴露图表、分数趋势):
portcullis report . -o report.html # open report.html in a browser
portcullis report . --serve # or serve it on http://127.0.0.1:8765
# 可选地,确认实际响应的内容(可选,仅为 stack 声明的端口):
portcullis probe . # local check of 127.0.0.1
portcullis probe . --host my-server.example.com # from your own external vantage point
```
需要 Python 3.10+。可在 Linux(主要目标平台)、macOS 和 Windows 上运行——测试套件在 CI 中涵盖了这三个平台。
| 选项 | 默认值 | 描述 |
| --- | --- | --- |
| `PATH` | `.` | 一个 compose 文件,或一个递归遍历的目录。 |
| `--format terminal\|markdown\|html\|json` | `terminal` | 报告格式。 |
| `-o, --output FILE` | stdout | 将 markdown/html/json 报告写入文件。 |
| `--min-severity LEVEL` | `info` | 隐藏低于 `info`/`low`/`medium`/`high`/`critical` 的发现。 |
| `--fail-on LEVEL` | `never` | 如果任何发现处于或高于 LEVEL 级别,则返回退出代码 1(用于 CI 拦截)。 |
| `--trivy` / `--no-trivy` | auto | 强制开启或禁用 Trivy(默认:找到该二进制文件时使用)。 |
| `--rules DIR` | - | 加载社区规则包目录(可重复使用)。详见 [docs/rule-packs.md](docs/rule-packs.md)。 |
| `--suggest-patches [FILE]` | - | 将机械化的修复方案输出为统一差异文件(默认为 `portcullis.patch`)。请自行审查并执行 `git apply`——Portcullis 绝不会修改您的文件。 |
### 输出示例
这是一个包含三个服务的堆栈的实际输出——vaultwarden 由 Traefik 代理但同时也发布了一个端口,postgres 绑定到环回地址并使用了默认密码,以及拥有 Docker socket 的 watchtower:
```
╭────────────────────────────────────────────────────────────────╮
│ Portcullis - security report for /home/user/homelab │
│ Grade: C (score 60/100, 3 services, 4 findings) │
╰────────────────────────────────────────────────────────────────╯
Service exposure
┏━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┓
┃ Service ┃ Image ┃ Exposure ┃
┡━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━┩
│ db │ postgres:16 │ HOST │
│ vaultwarden │ vaultwarden/server:latest │ INTERNET │
│ watchtower │ containrrr/watchtower:1.7.1 │ INTERNAL │
└─────────────┴─────────────────────────────┴──────────┘
╭────────── CRITICAL Docker socket mounted into 'watchtower' ──────────╮
│ The container 'watchtower' mounts /var/run/docker.sock. Whoever │
│ controls this socket controls the Docker daemon. │
│ │
│ Why it matters: Any code execution inside this container (a │
│ vulnerability in the app is enough) can start a privileged container │
│ and take over the whole host - data, other services, everything. │
│ │
│ Fix: Remove the mount if the app does not truly need it. If it does │
│ (reverse proxy auto-discovery, dashboards, updaters), put a socket │
│ proxy in front (e.g. tecnativa/docker-socket-proxy) and grant only │
│ the API sections the app requires. │
╰──────────────────────────────────────── PC-001 · exposure: INTERNAL ───╯
… followed by the 3 remaining findings in the same format:
HIGH PC-008 (default POSTGRES_PASSWORD), MEDIUM PC-011 (vaultwarden
bypasses the proxy via port 8081), LOW PC-005 (mutable `latest` tag).
```
将其用作 CI 拦截标准,或生成可共享的报告:
```
portcullis scan . --format markdown -o report.md --fail-on high
portcullis scan . --format html -o report.html # self-contained, no external requests
```
## GitHub Action
在每次推送代码和拉取请求时运行 Portcullis。该操作会将报告写入作业摘要,可选择将其作为评论发布在 PR 上,并在严重程度超过阈值时使作业失败。
```
# .github/workflows/portcullis.yml
name: Portcullis
on: [push, pull_request]
permissions:
contents: read
pull-requests: write # only needed for comment-on-pr
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: yakohhhh/portcullis@v1
with:
path: . # a compose file or a directory tree
fail-on: high # critical | high | medium | low | info | never
comment-on-pr: true
```
| 输入项 | 默认值 | 描述 |
| --- | --- | --- |
| `path` | `.` | 要扫描的 compose 文件或目录。 |
| `fail-on` | `high` | 在达到或超过此严重程度时使作业失败(设为 `never` 则永不失败)。 |
| `min-severity` | `info` | 在报告中隐藏低于此严重程度的发现。 |
| `trivy` | `false` | 在可用时同时运行 Trivy。 |
| `comment-on-pr` | `false` | 将报告作为评论发布在 PR 中。 |
输出项:`grade` (A-F) 和 `score` (0-100)。
## Trivy 集成
Portcullis 有意不去重新实现 [Trivy](https://github.com/aquasecurity/trivy) 已经做得很好的功能。分工如下:
| Trivy 的工作 | Portcullis 的工作 |
| --- | --- |
| 镜像 CVE 扫描 | 将 compose 堆栈作为首要目标 |
| 文件中提交的机密信息 | 暴露引擎(端口 × 代理 × 网络) |
| Dockerfile 分析 | 应用知识库 |
| | Compose “走火”规则 (PC-001..PC-012) |
| | 具有教育意义、按优先级排序并带有评分的报告 |
当 `trivy` 处于您的 PATH 中时,Portcullis 会运行其三个相关的扫描程序,并将结果作为常规发现合并进来:
- `trivy image` - 已知的镜像漏洞 (CVE),每个镜像生成一条发现;
- `trivy fs --scanners secret` - 提交到目录树中的机密信息,每个文件生成一条发现;
- `trivy config` - Dockerfile 配置错误,每个 Dockerfile 生成一条发现。
结果会进行聚合(按镜像 / 文件 / Dockerfile,绝不会每次出现都生成一条发现),从而保持报告的可读性,并且已被 Portcullis 自身的 PC-008 规则标记过的机密信息不会被重复报告。如果在没有安装 Trivy 的情况下,其他所有功能的工作方式完全相同:功能降级,但绝不出错。
## 隐私
Portcullis 是 100% 本地运行的。它读取您的配置文件,打印报告,然后就结束了:没有网络调用,没有遥测,不需要账号。(可选的 Trivy 集成是一个独立的二进制文件,具有其自身的行为,完全可以通过 `--no-trivy` 选择退出。)
## 路线图
- **M1 - 核心扫描**(已完成,v0.1):compose 发现与解析、暴露引擎、规则 PC-001..PC-011、应用知识库、A-F 评级、终端与 markdown 报告、Trivy 合并、`--fail-on` CI 拦截。
- **M2 - 反向代理配置文件**(已完成):Traefik(静态 `traefik.yml`/`.toml`,`command:` 标志,动态文件提供程序)和 Caddyfile 解析,以及扩展到 90 多个应用的知识库。
- **M3 - 报告与丰富化**(已完成):独立的 HTML 报告 (`--format html`) 和更丰富的 Trivy 合并(镜像 CVE、提交的机密、Dockerfile 配置错误)。
- **M4 - 工业化**(已完成):机器可读的 JSON 输出 (`--format json`)、GitHub Action、macOS/Windows CI,以及通过受信任发布方式自动发布到 PyPI(详见 [RELEASING.md](RELEASING.md))。
- **v2**(已完成):nginx 和 Nginx Proxy Manager 路由、社区规则包 (`--rules`)、补丁建议 (`--suggest-patches`)、交互式 Web 报告 (`portcullis report`),以及可选的实时可达性探测 (`portcullis probe`),用于确认哪些发布的端口实际上会响应——无论是在本地测试,还是从您自己的外部观察点进行测试。
## 贡献
请参阅 [CONTRIBUTING.md](CONTRIBUTING.md)。最简单且影响最大的贡献方式是提供**知识库条目**:只需一个用于描述您所熟知的一款自托管应用的小型 YAML 文件——完全不需要掌握 Python。
## 许可证
[PolyForm Noncommercial 1.0.0](LICENSE) - **对个人免费**(家庭实验室、业余项目、研究、学习)**且对非商业组织免费**(慈善机构、学校、公共机构)。**公司及任何其他商业用途需要购买付费的商业许可证**:详见 [COMMERCIAL.md](COMMERCIAL.md)。
准确地说:源代码是公开的,并且欢迎贡献,但这是一种*源码可见*的许可证,而不是 OSI 批准的开源许可证。在许可证切换之前发布的版本仍然保持 MIT 许可证(参见 git 历史记录)。
标签:DevSecOps, Docker Compose, Python, 上游代理, 云安全监控, 无后门, 本地部署, 聊天机器人, 请求拦截, 逆向工具, 静态分析