青龙面板还能公网裸奔吗?几个月前那次致命漏洞后,我更建议你看白虎面板
先说结论
青龙面板曾经是很多人自托管定时任务的默认选择:跑脚本、管环境变量、看日志、定时同步仓库都很方便。但 2026 年初公开爆出的青龙面板致命漏洞,把一个老问题重新摆到台面上:定时任务面板不是普通网页,它往往握着脚本、变量、通知密钥和执行权限。一旦认证被绕过,攻击者看到的不是一个登录页,而是一台可以被调度的机器。
如果你只是新装一个轻量任务面板,我现在更建议优先评估白虎面板。白虎面板采用 Go + Vue3,主打轻量、高性能、低系统开销,并且官方 README 已经明确支持 Docker / Docker Compose 部署、Mise 运行时管理、仓库任务同步、执行日志和多渠道通知。它不是“青龙的一比一复制品”,但对很多个人脚本托管场景已经够用。
如果你因为兼容旧脚本、历史任务或迁移成本,必须临时继续用青龙面板:不要把它直接暴露在公网。 至少在前面加 VPN、零信任访问、反向代理二次认证、IP allowlist、WAF 或其它访问控制。本文给出 Windows 11、Ubuntu 26.04、macOS 26 三套一键脚本,默认部署白虎;只有显式指定时才部署青龙,并且都会把面板放到 Basic Auth 保护后面。

图 1:AI 生成封面。任务面板不是普通后台,里面往往放着能真正执行命令的按钮。
一、青龙面板和白虎面板到底是什么
青龙面板可以理解成“给脚本准备的定时闹钟 + 文件柜 + 日志本”。你可以把 Python、JavaScript、Shell、TypeScript 等脚本放进去,设置 Cron 表达式,让它按时间执行;也可以在网页里管理环境变量、查看运行日志、配置通知。对很多自托管用户来说,青龙面板的吸引力是非常直接的:不用手写 crontab,不用每次 SSH 到机器上翻日志,也不用把一堆脚本散落在不同目录。
但是,便利背后有一个容易被忽视的事实:定时任务面板的权限通常比普通博客后台、相册后台、RSS 后台高得多。博客后台被盗,最坏可能是文章被改;相册后台被盗,最坏可能是照片被看;任务面板被盗,攻击者可能直接新增脚本、读取变量、修改任务、触发执行。它更像家里的“电闸箱”和“工具间”合在一起,不只是门面。
白虎面板则是近几年出现的另一个自动化任务调度平台。根据白虎项目 README 和文档首页,它的定位是“极致轻量、高性能的自动化任务调度平台”,技术栈是 Go + Vue3,并深度集成 Mise 来管理 Python、Node.js、Go、Rust、PHP 等运行环境。它也支持 Docker / Docker Compose 一键部署、任务调度、在线管理、执行日志、消息推送、安全机密、仓库任务同步等能力。

图 2:青龙面板曾经非常流行,功能完整,但“流行 + 可执行脚本 + 公网暴露”也会让它成为攻击目标。

图 3:白虎面板的公开文档把轻量、Docker/Compose、一键部署和多语言运行时作为主要卖点。
如果用小学生也能懂的比喻:青龙和白虎都像“自动做家务的机器人管家”。你告诉它几点扫地、几点浇花、什么时候把垃圾拿出去,它就按时执行。问题是,管家手里有钥匙。如果大门直接开在马路边,陌生人能绕过门禁进去,那就不是“家务机器人不听话”这么简单,而是家里可能被人随便指挥。
二、几个月前的致命问题:不是密码弱,而是门禁被绕过
2026 年初,Snyk 公开分析了青龙任务调度器中的多处远程代码执行风险,并指出攻击活动已经利用这些问题去投放挖矿相关载荷。Snyk 的漏洞库也给出了 @whyour/qinglong 相关高危记录。后来 GitHub 安全页面还能看到关于修复不完整路径的安全提醒。换句话说,这不是“理论上可能有人研究”的漏洞,而是已经进入攻击者视野的真实风险。

图 4:公开安全文章已经把风险讲得很清楚:任务调度平台一旦被利用,后果会从 Web 后台扩大到命令执行。
这里我不贴可直接复制的利用细节,也不写攻击请求。原因很简单:这篇文章是给自托管用户做风险判断和加固,不是帮攻击者省检索时间。我们只讲安全上真正需要理解的部分。
这个漏洞链条的可怕之处在于“认证被绕过”。平时你以为登录页像小区门禁:没卡进不去。认证绕过就像门禁读卡器坏了,陌生人不需要知道你的密码,也不用猜你的验证码,就有机会直接走到后台里面。更糟糕的是,任务面板后台不是只有“查看”功能,它通常还能新增任务、编辑脚本、读取环境变量、查看日志和触发执行。
图 5:攻击链条不是一个点,而是一串连锁反应。公网入口越宽,后面的每一步越危险。
所以我现在不建议继续把青龙面板作为公网机器上的长期默认选择。不是说每一个青龙实例都会立刻出事,也不是说白虎永远不会有漏洞,而是风险收益已经变了:如果只是为了跑普通脚本,继续让一个曾经被真实攻击盯上的任务面板长期公网裸露,并不划算。
三、怎么安装青龙面板:只建议本地或受保护网络
青龙官方 README 中的 Docker 路径大致是挂载数据目录、暴露 5700 端口、使用 whyour/qinglong 镜像。最朴素的 Docker Compose 可以写成这样:
services:
qinglong:
image: whyour/qinglong:debian
container_name: qinglong
volumes:
- ./qinglong-data:/ql/data
restart: unless-stopped
注意我这里故意没有写 ports:。如果你只是本机实验,可以临时映射端口;但如果是公网服务器,不建议让面板容器直接对外监听。更合理的做法是让反向代理站在前面,由反向代理做 TLS、Basic Auth、访问来源控制、日志和限速。
一个更像生产临时方案的 Compose 应该是下面这种结构:面板只在 Docker 网络内部暴露,外面只有 Caddy 入口。
services:
qinglong:
image: whyour/qinglong:debian
container_name: qinglong
volumes:
- ./qinglong-data:/ql/data
restart: unless-stopped
gateway:
image: caddy:2
container_name: qinglong-gateway
depends_on:
- qinglong
ports:
- "8088:8088"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
restart: unless-stopped
Caddyfile 的核心是:
:8088 {
basicauth {
admin <BCRYPT_HASH>
}
reverse_proxy qinglong:5700
}
这仍然只是“临时防护”,不是让漏洞消失。它的意义是把攻击者挡在青龙登录页之前。就像你家门锁有问题,第一件事不是研究门锁说明书,而是先让陌生人进不了楼道。
四、怎么安装白虎面板:更适合作为新部署起点
白虎官方 README 给出的 SQLite Docker Compose 形态是 ghcr.io/engigu/baihu:latest,默认面板端口是 8052,持久化目录包括 data 和 envs。精简后可以写成:
services:
baihu:
image: ghcr.io/engigu/baihu:latest
container_name: baihu
volumes:
- ./baihu-data:/app/data
- ./baihu-envs:/app/envs
environment:
- TZ=Asia/Shanghai
- BH_SERVER_PORT=8052
- BH_DB_TYPE=sqlite
- BH_DB_PATH=/app/data/baihu.db
- BH_DB_TABLE_PREFIX=baihu_
- BAIHU_SECRET_KEY=<CHANGE_ME_TO_A_LONG_RANDOM_SECRET>
restart: unless-stopped
白虎首次启动会生成随机管理员密码,官方 README 提醒需要从容器日志里找“管理员账号创建成功”之类的提示,并在登录后及时修改。这个设计比固定默认密码更合理,因为固定默认密码就像每个新房子的钥匙都长一样,攻击者只要知道一次就能扫一大片。
公网部署白虎时,我仍然建议前面加反向代理二次认证。白虎看起来是更适合新装的选择,但“更推荐”不等于“可以裸奔”。任何能执行脚本、管理变量、保存通知密钥的系统,都应该默认放在第二道门后面。
图 6:安全不是只看面板自身。先收入口,再看版本、密码和权限。
五、一键脚本:Windows 11 / Ubuntu 26.04 / macOS 26
下面三套脚本做同一件事:创建一个 Docker Compose 目录,默认部署白虎面板;如果你显式指定 qinglong,才部署青龙面板;无论哪种面板,都把 Caddy Basic Auth 放在前面。脚本不依赖第三方云服务,不需要外部数据库;但和任何容器安装一样,需要从镜像仓库拉取公开镜像。
5.1 Windows 11:PowerShell 一键部署
保存为 Install-Panel-Gateway.ps1。默认部署白虎:
powershell -ExecutionPolicy Bypass -File .\Install-Panel-Gateway.ps1
临时部署青龙:
powershell -ExecutionPolicy Bypass -File .\Install-Panel-Gateway.ps1 -Panel qinglong
脚本如下:
param(
[ValidateSet("baihu", "qinglong")]
[string]$Panel = "baihu",
[string]$InstallDir = "$HOME\panel-gateway",
[string]$User = "admin",
[string]$Password = ""
)
$ErrorActionPreference = "Stop"
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
throw "Docker is required. Install Docker Desktop first, then re-run this script."
}
if ([string]::IsNullOrWhiteSpace($Password)) {
$bytes = New-Object byte[] 24
[Security.Cryptography.RandomNumberGenerator]::Fill($bytes)
$Password = [Convert]::ToBase64String($bytes).TrimEnd("=")
}
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
Set-Location $InstallDir
$hash = docker run --rm caddy:2 caddy hash-password --plaintext $Password
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($hash)) {
throw "Failed to generate Caddy password hash."
}
if ($Panel -eq "baihu") {
$target = "baihu:8052"
$service = @"
baihu:
image: ghcr.io/engigu/baihu:latest
container_name: baihu
volumes:
- ./baihu-data:/app/data
- ./baihu-envs:/app/envs
environment:
- TZ=Asia/Shanghai
- BH_SERVER_PORT=8052
- BH_DB_TYPE=sqlite
- BH_DB_PATH=/app/data/baihu.db
- BH_DB_TABLE_PREFIX=baihu_
- BAIHU_SECRET_KEY=$([Guid]::NewGuid().ToString("N"))$([Guid]::NewGuid().ToString("N"))
restart: unless-stopped
"@
} else {
$target = "qinglong:5700"
$service = @"
qinglong:
image: whyour/qinglong:debian
container_name: qinglong
volumes:
- ./qinglong-data:/ql/data
restart: unless-stopped
"@
}
@"
$service
gateway:
image: caddy:2
container_name: panel-gateway
depends_on:
- $Panel
ports:
- "8088:8088"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
restart: unless-stopped
"@ | Set-Content -Encoding UTF8 docker-compose.yml
@"
:8088 {
basicauth {
$User $hash
}
reverse_proxy $target
}
"@ | Set-Content -Encoding UTF8 Caddyfile
docker compose up -d
Write-Host ""
Write-Host "Panel: $Panel"
Write-Host "URL: http://localhost:8088"
Write-Host "Gateway user: $User"
Write-Host "Gateway password: $Password"
Write-Host "Keep this password. If BaiHu was installed, check container logs for the BaiHu initial admin password."
5.2 Ubuntu 26.04:Bash 一键部署
默认部署白虎:
bash install-panel-gateway.sh
临时部署青龙:
PANEL=qinglong bash install-panel-gateway.sh
脚本如下:
#!/usr/bin/env bash
set -euo pipefail
PANEL="${PANEL:-baihu}"
INSTALL_DIR="${INSTALL_DIR:-$HOME/panel-gateway}"
USER_NAME="${USER_NAME:-admin}"
PASSWORD="${PASSWORD:-}"
if [ "$PANEL" != "baihu" ] && [ "$PANEL" != "qinglong" ]; then
echo "PANEL must be baihu or qinglong" >&2
exit 1
fi
if ! command -v docker >/dev/null 2>&1; then
echo "Docker is required. Install Docker Engine first, then re-run this script." >&2
exit 1
fi
if [ -z "$PASSWORD" ]; then
PASSWORD="$(openssl rand -base64 24 | tr -d '=')"
fi
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
HASH="$(docker run --rm caddy:2 caddy hash-password --plaintext "$PASSWORD")"
if [ "$PANEL" = "baihu" ]; then
TARGET="baihu:8052"
SECRET="$(openssl rand -hex 48)"
cat > docker-compose.yml <<YAML
services:
baihu:
image: ghcr.io/engigu/baihu:latest
container_name: baihu
volumes:
- ./baihu-data:/app/data
- ./baihu-envs:/app/envs
environment:
- TZ=Asia/Shanghai
- BH_SERVER_PORT=8052
- BH_DB_TYPE=sqlite
- BH_DB_PATH=/app/data/baihu.db
- BH_DB_TABLE_PREFIX=baihu_
- BAIHU_SECRET_KEY=${SECRET}
restart: unless-stopped
gateway:
image: caddy:2
container_name: panel-gateway
depends_on:
- baihu
ports:
- "8088:8088"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
restart: unless-stopped
YAML
else
TARGET="qinglong:5700"
cat > docker-compose.yml <<YAML
services:
qinglong:
image: whyour/qinglong:debian
container_name: qinglong
volumes:
- ./qinglong-data:/ql/data
restart: unless-stopped
gateway:
image: caddy:2
container_name: panel-gateway
depends_on:
- qinglong
ports:
- "8088:8088"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
restart: unless-stopped
YAML
fi
cat > Caddyfile <<CADDY
:8088 {
basicauth {
${USER_NAME} ${HASH}
}
reverse_proxy ${TARGET}
}
CADDY
docker compose up -d
printf '\nPanel: %s\nURL: http://localhost:8088\nGateway user: %s\nGateway password: %s\n' "$PANEL" "$USER_NAME" "$PASSWORD"
printf 'Keep this password. If BaiHu was installed, check container logs for the BaiHu initial admin password.\n'
5.3 macOS 26:zsh 一键部署
macOS 版和 Ubuntu 版逻辑一致,默认部署白虎:
zsh install-panel-gateway-macos.zsh
临时部署青龙:
PANEL=qinglong zsh install-panel-gateway-macos.zsh
脚本如下:
#!/usr/bin/env zsh
set -euo pipefail
PANEL="${PANEL:-baihu}"
INSTALL_DIR="${INSTALL_DIR:-$HOME/panel-gateway}"
USER_NAME="${USER_NAME:-admin}"
PASSWORD="${PASSWORD:-}"
if [[ "$PANEL" != "baihu" && "$PANEL" != "qinglong" ]]; then
print -u2 "PANEL must be baihu or qinglong"
exit 1
fi
if ! command -v docker >/dev/null 2>&1; then
print -u2 "Docker Desktop or a compatible Docker runtime is required."
exit 1
fi
if [[ -z "$PASSWORD" ]]; then
PASSWORD="$(openssl rand -base64 24 | tr -d '=')"
fi
mkdir -p "$INSTALL_DIR"
cd "$INSTALL_DIR"
HASH="$(docker run --rm caddy:2 caddy hash-password --plaintext "$PASSWORD")"
if [[ "$PANEL" == "baihu" ]]; then
TARGET="baihu:8052"
SECRET="$(openssl rand -hex 48)"
cat > docker-compose.yml <<YAML
services:
baihu:
image: ghcr.io/engigu/baihu:latest
container_name: baihu
volumes:
- ./baihu-data:/app/data
- ./baihu-envs:/app/envs
environment:
- TZ=Asia/Shanghai
- BH_SERVER_PORT=8052
- BH_DB_TYPE=sqlite
- BH_DB_PATH=/app/data/baihu.db
- BH_DB_TABLE_PREFIX=baihu_
- BAIHU_SECRET_KEY=${SECRET}
restart: unless-stopped
gateway:
image: caddy:2
container_name: panel-gateway
depends_on:
- baihu
ports:
- "8088:8088"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
restart: unless-stopped
YAML
else
TARGET="qinglong:5700"
cat > docker-compose.yml <<YAML
services:
qinglong:
image: whyour/qinglong:debian
container_name: qinglong
volumes:
- ./qinglong-data:/ql/data
restart: unless-stopped
gateway:
image: caddy:2
container_name: panel-gateway
depends_on:
- qinglong
ports:
- "8088:8088"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
restart: unless-stopped
YAML
fi
cat > Caddyfile <<CADDY
:8088 {
basicauth {
${USER_NAME} ${HASH}
}
reverse_proxy ${TARGET}
}
CADDY
docker compose up -d
print ""
print "Panel: $PANEL"
print "URL: http://localhost:8088"
print "Gateway user: $USER_NAME"
print "Gateway password: $PASSWORD"
print "Keep this password. If BaiHu was installed, check container logs for the BaiHu initial admin password."
六、人工自动执行和 Agent 自动配置
人工自动执行的方法很简单:先决定面板,默认选白虎;把脚本保存到本机;确认 Docker 可用;运行脚本;访问 http://localhost:8088;先输入网关账号密码;再进入面板自己的登录流程。白虎首次登录密码看容器日志,青龙则按初始化向导处理。
Agent 自动配置更适合已经有一台维护机器、并且你习惯让 Codex、Claude、OpenClaw、HermesAgent 这类 Agent 代跑命令的场景。给 Agent 的任务可以写得非常明确:
请在当前机器部署一个受 Basic Auth 保护的任务面板。
默认选择 BaiHu;除非我明确要求,不要部署 QingLong。
不要把面板容器端口直接暴露到公网。
使用 Docker Compose,入口端口使用 8088。
完成后输出 compose 文件路径、网关用户名、随机密码、容器状态和访问方式。
不要在日志或文章中写入任何真实公网地址、内网地址、内网域名、主机名、Token 或 Cookie。
如果你必须让 Agent 临时部署青龙,要把风险边界也写进去:
我需要临时部署 QingLong 做迁移验证。
请只允许通过 Caddy Basic Auth 访问,不要直接发布 QingLong 容器端口。
部署完成后检查 docker compose ps,并提醒我迁移完成后关闭或替换它。
不要生成利用漏洞的测试请求。
这类提示词的重点不是“让 Agent 聪明一点”,而是“让 Agent 不要自作主张把最危险的端口直接映射出去”。自动化越强,边界越要写死。
七、Q&A
Q:青龙面板是不是完全不能用了?
不是。内网、实验环境、迁移窗口、受 VPN 或零信任保护的场景,仍然可以临时使用。但我不建议把它当作公网机器上的长期裸露面板。尤其是面板里放了通知密钥、账号变量、业务脚本、Cookie 或可以触发系统命令的任务时,风险会被放大。
Q:白虎面板是不是绝对安全?
不是。任何后台系统都可能有漏洞。白虎的优势在于更轻量、活跃更新、运行时设计更现代,并且对很多新部署场景更合适。但安全策略仍然一样:不要裸奔,及时更新,启用强密码,减少脚本权限,敏感变量用机密管理,备份数据。
Q:为什么文章不贴漏洞复现命令?
因为对读者最有价值的是风险判断和防护路径,不是复制一条攻击请求。公开漏洞信息已经足够多,继续把可执行攻击步骤放到博客里,只会降低攻击门槛。
Q:我已经有一堆青龙脚本,怎么迁移?
先导出脚本、环境变量和任务列表,按重要性分批迁移。白虎 README 提到支持青龙仓库格式指令的同步能力,可以先从仓库同步类任务试起。不要一次性迁移全部任务;先迁移低风险任务,确认运行时、依赖、通知和日志都正常,再迁移核心任务。
Q:临时防护最少要做什么?
最少也要做到三点:第一,面板容器端口不直接对公网发布;第二,前面有反向代理二次认证;第三,面板本身使用强密码并及时更新。更好的做法是只允许 VPN 或零信任入口访问。
参考资料
- QingLong GitHub:https://github.com/whyour/qinglong
- QingLong 安装文档:https://qinglong.online/guide/getting-started/installation-guide
- BaiHu Panel GitHub:https://github.com/engigu/baihu-panel
- BaiHu Panel 文档:https://engigu.github.io/baihu-panel/
- Snyk QingLong RCE 分析:https://snyk.io/blog/qinglong-task-scheduler-rce-vulnerabilities/
- Snyk 漏洞库记录:https://security.snyk.io/vuln/SNYK-JS-WHYOURQINGLONG-9526704