rootHytx/NervCTF
GitHub: rootHytx/NervCTF
NervCTF 是一套基于 CTFd 的 CTF 赛事管理工具链,通过 YAML 一键部署题目、分配容器并检测 flag 共享。
Stars: 3 | Forks: 0
# NervCTF
一个用于在 CTFd 上运行 CTF 比赛的 CLI + 服务器工具链。只需一条命令,即可从 YAML 部署题目、为每支队伍分配临时的容器,并检测 flag 共享行为,全程无需手动调用 CTFd API。
## 工作原理
NervCTF 包含两个组件,部署一次后即可保持运行:
```
Your machine CTFd host (single-machine mode)
───────────── ─────────────────────────────────────────────────
┌─ Docker Compose stack ────────────────────────┐
nervctf CLI ─── Token ──▶ │ remote-monitor:33133 ─── SQL ──▶ MariaDB │
│ │ └──▶ uploads dir │
│ instance manager │
│ (docker daemon, local) │
│ CTFd (nginx+gunicorn)│
│ nervctf plugin │
└───────────────────────────────────────────────┘
```
**`nervctf` (CLI)** — 在你的本地计算机上运行。负责读取 `challenge.yml` 文件,对其进行校验,并同步到远程的 monitor。它还可以运行 `nervctf setup` 来初始化服务器。
**`remote-monitor` (server)** — 在 CTFd 宿主机的 Docker 中运行。它直接写入 CTFd 的 MariaDB(无需 CTFd API key),为每支队伍的题目管理容器实例,并提供管理仪表板。
**`nervctf_instance` (CTFd 插件)** — 由 `nervctf setup` 部署。接入 CTFd 的题目系统,使得玩家可以直接在 CTFd 界面中请求、续期和停止容器。
在**分离式机器模式**下,容器将在单独的 worker 节点上运行。CLI 会将题目文件直接 rsync 到 runner 上;monitor 则通过 SSH 控制容器。
```
Your machine CTFd host Runner node
──────────── ────────────────────────────── ──────────────────
nervctf CLI ──▶ remote-monitor ─── SSH ──────▶ docker daemon
│ │
└── rsync ──────┘ (challenge files)
```
## 安装说明
### 预编译二进制文件(推荐)
从 [GitHub Releases](https://github.com/rootHytx/NervCTF/releases) 下载:
- `nervctf-linux-x86_64-static` — 适用于 Linux 的 CLI(静态编译,无依赖)
- `nervctf-linux-aarch64` — 适用于 ARM64 的 CLI
- `nervctf-windows-x86_64.exe` — 适用于 Windows 的 CLI
- `remote-monitor-linux-x86_64-static` — 服务器二进制文件(部署到 CTFd 宿主机)
将它们重命名为 `nervctf` 和 `remote-monitor`,并放置在你的 `PATH` 路径中。
### 从源码构建
```
# 使用 Nix(提供所有依赖)
nix develop .# --command cargo build --release
# 不使用 Nix (Debian/Ubuntu)
sudo apt install build-essential pkg-config libssl-dev
cargo build --release
# 静态 musl 构建(releases 所使用的方式)
nix develop .# --command cargo build --release --target x86_64-unknown-linux-musl
```
交叉编译目标:`make release-musl`(静态编译),`make release-arm64`,`make release-windows`。运行 `make help` 查看所有目标。
## 快速开始
```
# 1. 运行 setup 向导 — 在远程主机上配置 Docker、CTFd、plugin、monitor
nervctf setup
# 2. 编写你的 challenges
mkdir -p challenges/web/sqli challenges/pwn/overflow
cat > challenges/web/sqli/challenge.yml <<'EOF'
name: SQL Injection 101
category: web
value: 100
type: standard
description: Find the flag in the database.
flags:
- flag{sql_is_fun}
EOF
# 3. 本地验证(无需网络)
nervctf validate
# 4. 部署到 CTFd
nervctf deploy
# 5. 检查与线上 CTFd 实例的兼容性
nervctf probe
```
## 配置说明
### `.nervctf.yml`
由 `nervctf setup` 创建。会从工作目录开始向上搜索(一直查找到文件系统根目录)。你可以将其放在仓库根目录下,然后在任何子目录中运行 `nervctf`。
```
# ── Monitor 连接 ─────────────────────────────────────────────────────────
monitor_ip: 1.2.3.4 # IP of the CTFd/monitor host
monitor_port: 33133 # default: 33133
# ── 认证 ────────────────────────────────────────────────────────────
monitor_token: # 64-char hex token; auto-generated by nervctf setup
# This is the token for the remote-monitor, NOT CTFd.
# ── 部署(仅由 nervctf setup / setup --upgrade 使用)─────────────────
monitor_user: root # SSH user on the CTFd host (must have sudo/docker)
monitor_ctfd_path: /home/root/CTFd # CTFd install path on the remote host
# default: /home//CTFd
ssh_key_path: ~/.ssh/id_rsa # private key for SSH access to monitor + runner hosts
# ── 本地 challenges ──────────────────────────────────────────────────
challenges_path: ./challenges # where nervctf looks for challenge.yml files
# ── 调优(在 setup 时内置;更改需要运行 setup --upgrade)────────────
max_concurrent_provisions: 4 # max parallel container provisions
max_instances_per_team: 3 # max active instances per team across all challenges
# 0 = unlimited
# ── 显示 ───────────────────────────────────────────────────────────
ctfd_domain: ctfd.example.com # CTFd URL shown in admin dashboard links
# defaults to http://
# ── 分机模式(可选)────────────────────────────────────────────────────
# 设置 runner_ip 以在单独的节点上运行 containers。
# 留空则在与 CTFd 相同的机器上运行 containers。
runner_ip: 192.168.1.50
runner_user: docker
runner_domain: challenges.example.com # hostname shown to players in connection strings
# defaults to runner_ip
```
### 配置优先级(优先级从高到低)
```
CLI flags > environment variables > .nervctf.yml
```
| CLI flag | 环境变量 | 描述 |
|---|---|---|
| `--monitor-url` | `MONITOR_URL` | 远程 monitor 的完整 URL (`http://host:port`) |
| `--monitor-token` | `MONITOR_TOKEN` | Monitor 认证 token |
`--monitor-url` 标志会同时覆盖配置文件中的 `monitor_ip` 和 `monitor_port`。
## 命令
所有命令均接受位于子命令名称**之前**的全局标志:
```
nervctf [GLOBAL FLAGS] [SUBCOMMAND FLAGS]
```
### 全局标志
| 标志 | 默认值 | 描述 |
|---|---|---|
| `-c, --challenges-dir ` | `.`(当前目录) | 搜索 `challenge.yml` 文件的根目录 |
| `-v, --verbose` | false | 开启详细输出模式 |
| `--monitor-url ` | 取自配置/环境变量 | 覆盖本次运行的 monitor URL |
| `--monitor-token ` | 取自配置/环境变量 | 覆盖本次运行的 monitor token |
### `nervctf setup`
在远程主机上配置完整的服务器环境。每场比赛只需运行一次。
```
nervctf setup
nervctf setup --upgrade
```
**执行内容(首次运行):**
1. 以交互方式提示输入:主机 IP、SSH 用户名、CTFd 路径、monitor 端口、token、SSH 密钥
2. 将所有信息保存到 `.nervctf.yml`
3. 运行内嵌的 Ansible playbook,执行以下操作:
- 安装 Docker + Docker Compose(如果尚未安装)
- 克隆 CTFd 3.7.3 并启动
- 构建并启动 `remote-monitor` 容器
- 将 `nervctf_instance` 插件 Rsync 到 CTFd 的插件目录
- 将带有 monitor 环境变量的配置写入 `docker-compose.override.yml`
- 重启 CTFd 以加载插件
- 轮询健康检查 endpoint 直到所有服务就绪
4. 打印管理仪表板的 URL 和 token
**`--upgrade` 的作用:**
- 将新的 `remote-monitor` 二进制文件和插件文件推送到服务器
- 重新构建 monitor Docker 镜像
- 重启 CTFd 和 monitor
- 检查已安装的 CTFd 版本是否与已测试版本 (3.7.3) 一致
- **不会**重新配置 CTFd 或重新生成 token
| 标志 | 描述 |
|---|---|
| `--upgrade` | 在现有部署上升级插件 + monitor 二进制文件 |
### `nervctf deploy`
在本地校验所有题目,将其与 CTFd 上的当前状态进行比对,并应用更改。
```
nervctf deploy
nervctf deploy --dry-run
nervctf deploy --recreate
nervctf deploy --recreate --prune
```
**Deploy 分为四个有序阶段执行:**
| 阶段 | 执行内容 |
|---|---|
| 1 — 核心 | 创建新题目并更新已更改的题目。上传 flag、tag、topic、hint。检测更改的文件(标记为第 2 阶段处理)。 |
| 2 — 文件 | 为新增/更改的题目上传附件文件。 |
| 3 — 前置要求 | 将前置题目的名称解析为 CTFd ID 并修补 `requirements`。在第 1 阶段之后执行,以确保所有 ID 均已存在。 |
| 4 — Next 指针 | 修补 `next_id` 链接。原因与第 3 阶段相同。 |
在第 1 阶段之前,CLI 会获取已存储的兼容性探针结果,并执行以下操作:
- 如果动态题目计分处于损坏状态(CTFd 迁移不完整),则**中止部署**
- 如果 CTFd 处于 user-mode,则发出**警告**(玩家实例认证将失败)
- **记录**任何降级的功能(例如 Redis 缓存未失效)
- 如果当前的 CTFd 版本与已测试版本 (3.7.3) 不同,则发出**警告**
如果某题目的以下任何内容与远端不同,则被视为已**更改**:`category`、`description`、`state`、`connection_info`、`attempts`、`extra` 字段、flag `(content, type, data)`、hint `(content, cost)`、tag、topic 或文件。
**类型变更**(例如 `standard` → `dynamic`)始终会触发删除 + 重建,因为 CTFd 的 PATCH endpoint 不会创建所需的关联表行。
| 标志 | 描述 |
|---|---|
| `-d, --dry-run` | 仅打印将要执行的更改,而不实际应用 |
| `--recreate` | 强制重新部署所有题目(即使已是最新);将文件重新同步到 runner 并重新构建镜像 |
| `--prune` | 删除本地不再存在的远端题目(在第 4 阶段后执行) |
### `nervctf validate`
在不连接服务器的情况下校验题目 YAML 文件。如果发现任何错误,则以退出代码 1 退出。
```
nervctf validate
nervctf validate --debug
```
校验会检查每种题目类型的所有字段以及跨题目的规则(例如:名称重复、自引用的 `requirements`)。请参阅 [题目规范](#challenge-specification) 获取完整的规则表。
| 标志 | 描述 |
|---|---|
| `--debug` | 打印每个题目的完整解析字段字典 |
### `nervctf probe`
查询远程 monitor 当前的 CTFd 兼容性状态,并显示能力矩阵。
```
nervctf probe
nervctf probe --refresh
nervctf probe --json
```
Monitor 在启动时会存储一次探针结果,并支持按需刷新。探针会查询 CTFd 的 MariaDB 以检测已安装的版本、团队模式还是用户模式、存在哪些可选 schema 列,以及是否安装了插件数据表。
**输出示例:**
```
NervCTF <-> CTFd Compatibility Report
══════════════════════════════════════
CTFd version : 3.7.3 (source: configs_table)
CTFd mode : team
Probed at : 2026-05-26 14:32:11 UTC
Capability Status
───────────────────────── ──────────
Challenge CRUD [ok]
Dynamic scoring [ok]
Player authentication [ok]
Instance flag lifecycle [ok]
Redis cache invalidation [DEGRADED]
Schema fingerprint:
challenges : attribution, category, connection_info, decay, description,
function, id, initial, logic, max_attempts, minimum, name,
next_id, position, requirements, state, type, value
dynamic_challenge: id (stub-only — scoring is inline in challenges)
Warnings:
• Direct MariaDB writes bypass CTFd Redis cache — stale data may be served
until CTFd restart or TTL expiry
```
**能力状态:**
| 状态 | 含义 |
|---|---|
| `[ok]` | 完全支持,无任何限制 |
| `[DEGRADED]` | 在已知限制下可用 — 会向运维人员发出警告 |
| `[BROKEN]` | 无法工作;部署被阻止或严重受损 |
**退出代码:** 如果所有能力均为 `ok` 或 `degraded` 则为 0;如果任何能力为 `broken` 则为 1。适用于 CI 门禁。
注意:`Redis cache invalidation` 始终为 `DEGRADED` — NervCTF 直接写入 MariaDB,无法清除 CTFd 的 Redis 缓存。在进行大批量部署后,请重启 CTFd 或调整 `CACHE_DEFAULT_TIMEOUT`。
| 标志 | 描述 |
|---|---|
| `--refresh` | 强制从 MariaDB 发起全新探测(忽略 monitor 上的缓存结果) |
| `--json` | 输出原始 JSON 而非格式化的表格 |
### `nervctf list`
列出在 `--challenges-dir` 下找到的所有题目。
```
nervctf list
nervctf list --detailed
```
| 标志 | 描述 |
|---|---|
| `-d, --detailed` | 显示每个题目的完整字段值 |
### `nervctf scan`
扫描题目目录并打印统计信息(按类型、类别统计的数量,总分数等)。
```
nervctf scan
nervctf scan --detailed
```
| 标志 | 描述 |
|---|---|
| `-d, --detailed` | 显示每个题目的细分数据 |
### `nervctf fix`
以交互方式扫描题目 YAML 文件,并提供修补常见缺失字段(如 `state`、`version`、`description` 等)的选项。
```
nervctf fix
nervctf fix --dry-run
```
| 标志 | 描述 |
|---|---|
| `-d, --dry-run` | 仅显示将要修补的内容,而不修改文件 |
## 题目规范
题目存放在名为 `challenge.yml`(或 `challenge.yaml`)的文件中。NervCTF 会在 `--challenges-dir` 下递归搜索,最大深度为 5。目录结构是任意的 — 使用适合你比赛的任何布局即可。
```
challenges/
├── web/
│ └── sqli/
│ ├── challenge.yml
│ └── dist/source.py ← referenced in files:
└── pwn/
└── overflow/
└── challenge.yml
```
### 标准题目
固定分数的题目。玩家提交一个 flag,结果要么匹配要么不匹配。
```
# ── 身份 ──────────────────────────────────────────────────────────
name: SQL Injection 101 # required; must be unique across all challenges
category: web # required
version: "0.3" # optional; local metadata only, not sent to CTFd
author: alice # optional; local metadata only
# ── 计分 ───────────────────────────────────────────────────────────
type: standard # standard | dynamic | instance
value: 100 # required for standard; must be > 0
state: visible # visible (default) | hidden
# ── 内容 ───────────────────────────────────────────────────────────
description: |
Find the vulnerability and retrieve the flag.
The server is at http://challenge.example.com
connection_info: "http://challenge.example.com" # optional; shown on the challenge page
attempts: 5 # max wrong guesses; 0 or omitted = unlimited
# ── Flags(非 instance challenges 至少需要一个)────────────────
flags:
- flag{simple_string} # shorthand: static type, case-sensitive
- type: static
content: "flag{alternate}"
data: case_insensitive # optional modifier; default: case-sensitive
- type: regex
content: "flag\\{[a-z]+\\}" # regex pattern
# ── 组织 ──────────────────────────────────────────────────────────────
tags: [web, sql-injection, beginner]
topics: [owasp-top-10, database] # freeform topic labels (separate from tags in CTFd)
# ── 提示 ─────────────────────────────────────────────────────────────
hints:
- "Try single-quote injection" # free hint
- content: "The login form is vulnerable"
cost: 50 # costs 50 points to unlock
# ── 文件 ─────────────────────────────────────────────────────────────
files:
- dist/source.py # path relative to challenge.yml
- dist/Dockerfile
# ── 前置条件 ─────────────────────────────────────────────────────────────
requirements:
- "Warmup" # other challenge name; must exist locally or on CTFd
- "Web Intro"
# ── 导航 ────────────────────────────────────────────────────────────
next: "Advanced SQLi" # name of the challenge to show as "next" in CTFd UI
```
### 动态计分题目
随着更多队伍解出题目,其分数会逐渐降低。
```
name: Crypto Hard
category: crypto
type: dynamic
value: 0 # ignored for dynamic; set initial/minimum below
extra:
initial: 500 # starting point value (required, must be > 0)
decay: 50 # number of solves at which value reaches minimum (required, must be > 0)
minimum: 100 # floor value (optional; defaults to 0)
decay_function: linear # linear (default) | logarithmic
flags:
- flag{crypto_hard}
```
### 实例题目
为每个队伍分配一个临时容器。玩家在 CTFd 中点击“Request Instance”,即可获得一个主机/端口,并直接与他们相互隔离的环境进行交互。
```
name: Pwn Me
category: pwn
type: instance
value: 0
extra: # optional dynamic scoring on top of instance
initial: 500
decay: 50
minimum: 100
decay_function: linear
description: |
Connect to your instance and get root.
instance:
# ── Backend ─────────────────────────────────────────────────────────────────
backend: docker # docker | compose | lxc | vagrant
# ── Docker backend ───────────────────────────────────────────────────────────
image: . # "." = build from Dockerfile in challenge dir
# or a registry image: "ubuntu:22.04"
# ── Compose backend (mutually exclusive with docker image) ───────────────────
# compose_file: docker-compose.yml
# compose_service: app # which service exposes the port
# ── LXC backend ──────────────────────────────────────────────────────────────
# lxc_image: ubuntu:22.04
# ── Ports ────────────────────────────────────────────────────────────────────
# Single port (most common):
internal_ports: [1337]
# Multi-port: each gets a separately allocated random host port.
# The first port is the "primary" shown in the connection string.
# internal_ports: [80, 443]
# Compose multi-service (mutually exclusive with internal_ports):
# service_ports:
# app: [80]
# admin: [8080]
connection: nc # nc | http | ssh
# Controls the connection string shown to players.
# ── Lifecycle ────────────────────────────────────────────────────────────────
timeout_minutes: 45 # instance auto-expires after this many minutes
max_renewals: 3 # how many times players can extend the timer
# ── Flag ─────────────────────────────────────────────────────────────────────
flag_mode: random # random | static
# random: a unique flag is generated per instance and
# registered in CTFd's flags table at provision time.
# static: uses the flags: list above; one flag shared by all teams.
flag_prefix: "CTF{" # optional; wraps the random part
flag_suffix: "}"
random_flag_length: 16 # characters in the random part (default: 16)
# How the flag is delivered into the container:
flag_delivery: env # env (default) | file
# env: injected as $FLAG environment variable
# file: written to a bind-mounted file at flag_file_path
# flag_file_path: /challenge/flag # required when flag_delivery: file
# flag_service: app # compose only: which service gets the file mount
# ── Optional ─────────────────────────────────────────────────────────────────
command: null # override the container's CMD/entrypoint
```
#### 静态 flag 实例题目
使用 `flag_mode: static`(或省略该项)并在顶层的 `flags:` 列表中定义 flag。所有队伍共享同一个 flag。
```
type: instance
instance:
backend: docker
image: .
internal_ports: [1337]
connection: nc
flag_mode: static
flags:
- flag{shared_static_flag}
```
#### 实例题目的校验规则
| 检查项 | 严重程度 |
|---|---|
| 必须包含 `instance` 块 | Error |
| `instance.internal_ports` 必须至少包含一个条目(除非设置了 `service_ports`) | Error |
| 端口值必须为 1–65535 | Error |
| `service_ports` 和 `internal_ports` 互斥 | Warning |
| `instance.connection` 不能为空 | Error |
| `instance.timeout_minutes == 0` | Warning |
| Docker 后端:必须包含 `image` | Error |
| Docker 后端:本地路径缺少 Dockerfile | Warning |
| Compose 后端:磁盘上找不到 `compose_file` | Warning |
| LXC 后端:必须包含 `lxc_image` | Error |
| `flag_mode: random` 缺少 `flag_prefix`/`flag_suffix` | — (使用默认值) |
| `flag_delivery: file` 缺少 `flag_file_path` | Error |
| `flag_file_path` 不是绝对路径 | Warning |
| `flag_mode: static` 未定义任何 flag | Error |
## 校验规则(所有类型)
| 字段 | 检查项 | 严重程度 |
|---|---|---|
| `name` | 为空或全是空格 | Error |
| `name` | 在不同题目文件中重复 | Error |
| `category` | 为空或全是空格 | Error |
| `value` | `standard` 类型下为 `== 0` | Error |
| `extra` | `dynamic` 类型下缺失 | Error |
| `extra.initial` | `dynamic` 类型下缺失或为 `== 0` | Error |
| `extra.decay` | `dynamic` 类型缺失或为 `== 0` | Error |
| `extra.minimum` | `dynamic` 类型下未设置 | Warning |
| `extra.decay_function` | 设置时非 `"linear"` 或 `"logarithmic"` | Error |
| `flags` | 必须至少包含一个 flag (非实例题目且非随机 flag) | Error |
| `flags[].content` | 为空或全是空格 | Error |
| `flags` | 重复的 content 值 | Warning |
| `hints[].content` | 为空或全是空格 | Error |
| `files` | 引用的文件在磁盘上未找到 | Error |
| `requirements` | 题目列表包含自身 | Error |
| `requirements` | 在本地未找到指定的题目名称 | Warning |
| `next` | 指向自身 | Error |
| `next` | 在本地未找到指定的题目名称 | Warning |
| `attempts` | 设置为 `0`(等同于无限次 — 很可能是笔误) | Warning |
| 未知的 YAML 键 | 不在规范中 | Warning |
## 远程 Monitor
Monitor 作为 Docker 容器在 CTFd 宿主机上运行,由 `nervctf setup` 写入的 `docker-compose.override.yml` 进行管理。
### 管理仪表板
```
http://:/admin?token=
```
或者也可以在 `http://:/` 登录一次,会话 cookie 即会被设置。
仪表板会显示:
- 每支队伍的所有活动容器实例
- Flag 提交尝试(包括 flag 共享警报)
- 正确的解答日志
- 运行时配置(公共主机、runner 模式、基础目录等)
- CTFd 兼容性探针结果
### 环境变量
这些变量由 `nervctf setup` 在 `docker-compose.override.yml` 中设置,通常不需要手动编辑。
| 变量 | 必填 | 默认值 | 描述 |
|---|---|---|---|
| `CTFD_DB_URL` | **是** | — | MariaDB URL: `mysql://user:pass@host/db` |
| `MONITOR_TOKEN` | **是** | — | 管理 token(启动时计算 SHA-256 哈希;绝不存储明文) |
| `PUBLIC_HOST` | **是** | — | 在连接字符串中返回给玩家的主机名或 IP |
| `MONITOR_PORT` | 否 | `33133` | 监听的 TCP 端口 |
| `MONITOR_BIND` | 否 | `0.0.0.0` | 绑定的地址 |
| `DB_PATH` | 否 | `./monitor.db` | SQLite 数据库路径 |
| `CHALLENGES_BASE_DIR` | 否 | `/opt/nervctf/challenges` | 服务器上解压题目文件的存放位置 |
| `CTFD_UPLOADS_DIR` | 否 | `""` | CTFd 上传目录的绝对路径(用于写入文件附件) |
| `RUNNER_SSH_TARGET` | 否 | `""` | 分离式机器模式下的 SSH 目标,例如 `docker@192.168.1.50` |
| `MAX_CONCURRENT_PROVISIONS` | 否 | `4` | 并发容器分配数 (信号量) |
| `MAX_INSTANCES_PER_TEAM` | 否 | `0` | 每支队伍在所有题目中允许的最大活动实例数 (`0` = 无限制) |
| `CTFD_DB_SYNC_INTERVAL` | 否 | `30` | CTFd 解答/用户同步周期的间隔秒数 |
| `CTFD_DOMAIN` | 否 | `http://` | 管理仪表板链接中显示的 CTFd URL |
### 后台任务
Monitor 运行两个独立于 HTTP 流量的后台循环:
**同步循环**(每隔 `CTFD_DB_SYNC_INTERVAL` 秒):
- 读取 CTFd `submissions` 并将正确的解答缓存到 SQLite 中
- 读取 CTFd `teams` 和 `users` 用于名称显示
- 如果 CTFd 管理员删除了解答记录,则将实例恢复为运行状态
- 清除过期的 flag 尝试记录
**过期 + 健康检查循环**(每 30 秒):
- 销毁已过 `expires_at` 时间的实例
- 清理超过 30 分钟卡住的分配存根
- 检测并销毁未在 SQLite 中记录的孤立 Docker Compose 项目
- 对已跟踪的实例进行健康检查,并清理被外部强行终止的容器
### Flag 共享检测
当玩家提交 flag 时,CTFd 插件会调用 monitor 的 `/api/v1/plugin/attempt` endpoint。Monitor 会检查提交的 flag 是否最初分配给了**另一支队伍**。如果是,它会记录一条 flag 共享警报,该警报可在管理仪表板的 "Attempts (alerts only)" 下查看。
Flag 的归属权永久存储在 `team_flags` SQLite 表中 — 即使实例过期后,monitor 也会记住哪支队伍拥有哪个 flag。
## 分离式机器模式
在 `.nervctf.yml` 中设置 `runner_ip` 和 `runner_user`,即可在单独的节点上运行容器。
**工作原理:**
1. `nervctf deploy` 会使用 `ssh_key_path`,将每个题目的构建上下文从你的计算机直接 rsync 到 `runner_user@runner_ip:~/challenges//`
2. Monitor 通过 SSH 连接到 runner 以构建 Docker 镜像并启动容器
3. Monitor 在设置阶段会生成一对专用的 SSH 密钥对 (`monitor_ssh_key`),并 bind-mount 到 monitor 容器中
**对 runner 节点的要求:**
- 已安装 Docker + Docker Compose
- 允许 monitor 进行 SSH 访问(公钥需添加到 `~/.ssh/authorized_keys`)
- 无需 CTFd — 它被隔离在 CTFd 宿主机上
## 故障排除
| 症状 | 原因 | 解决方法 |
|---|---|---|
| 未找到题目 | 文件名不是 `challenge.yml` 或层级超过了 5 个目录 | 检查路径;最大深度为 5 |
| `state: Field may not be null` | CTFd 需要 `state` 字段 | 运行 `nervctf fix` |
| 文件上传出现 500 错误 | 上传目录的所有权不正确 | `chown -R 1001:1001 /.data/CTFd/uploads` |
| Monitor 返回 401 | CLI 和服务器之间的 token 不匹配 | 检查 `.nervctf.yml` 中的 `monitor_token` 是否与服务器上的 `MONITOR_TOKEN` 一致 |
| `ansible-playbook: not found` | 工具不在 PATH 中 | 在 `nix develop .#` 环境中运行或安装 ansible |
| 玩家请求实例时遇到 403 | CTFd 处于 user-mode | 将 CTFd 切换为 team mode;可通过 `nervctf probe` 确认 |
| 动态题目在 CTFd 管理界面报 500 错误 | CTFd schema 迁移不完整 | 运行 `nervctf probe --refresh` 进行诊断 |
| rsync 权限拒绝 | 未将 SSH 密钥传递给 rsync | 在 `.nervctf.yml` 中设置 `ssh_key_path` |
| 实例向玩家显示了错误的主机 | `runner_domain` 或 `PUBLIC_HOST` 错误 | 在 `.nervctf.yml` 中设置 `runner_domain` 并重新运行 `nervctf setup --upgrade` |
| 提交 flag 显示 "Incorrect" 但 flag 匹配 | CTFd `attempt()` 返回类型不匹配 | 升级 NervCTF;已在 v2.4.0 版本中修复 |
## 开发说明
```
# 所有开发命令使用 Nix devshell
nix develop .# --command cargo check
nix develop .# --command cargo test
nix develop .# --command cargo fmt
# 构建静态 release binaries
nix develop .# --command cargo build --release --target x86_64-unknown-linux-musl
# 构建后复制到 dist/(CLAUDE.md 所需)
cp target/x86_64-unknown-linux-musl/release/remote-monitor dist/remote-monitor-linux-x86_64-static
cp target/x86_64-unknown-linux-musl/release/nervctf dist/nervctf-linux-x86_64-static
```
有关各个模块的文档,请参阅 [`docs/`](docs/):
| 文档 | 内容 |
|---|---|
| [`docs/instance-challenges.md`](docs/instance-challenges.md) | 完整的实例题目 YAML 参考、后端详细信息、多端口设置 |
| [`docs/remote-monitor.md`](docs/remote-monitor.md) | 所有 API 路由、环境变量、schema 检测、兼容性探针 |
| [`docs/challenge_manager.md`](docs/challenge_manager.md) | 同步逻辑、needs_update 字段、子资源策略 |
| [`docs/validator.md`](docs/validator.md) | 所有包含严重级别的校验规则 |
| [`docs/ctfd_api.md`](docs/ctfd_api.md) | HTTP API 概览、版本兼容性表 |
| [`docs/ctfd-dependency-audit.md`](docs/ctfd-dependency-audit.md) | 完整的 CTFd 依赖关系图、风险登记表 |
| [`docs/dev-notes.md`](docs/dev-notes.md) | 构建环境、交叉编译、架构说明 |
完整的系统参考说明请查阅 [`ARCHITECTURE.md`](ARCHITECTURE.md)。
## 开源协议
MIT License。详情请参阅 [LICENSE](LICENSE)。
标签:CTFd, Docker, 内存分配, 可视化界面, 安全防御评估, 版权保护, 特权提升, 系统提示词, 自动化部署, 请求拦截, 运维工具, 通知系统