中文 English

青龙面板还能公网裸奔吗?几个月前那次致命漏洞后,我更建议你看白虎面板

发布时间: 2026-07-09
青龙面板 白虎面板 Docker 自动化 安全 定时任务 运维

先说结论

青龙面板曾经是很多人自托管定时任务的默认选择:跑脚本、管环境变量、看日志、定时同步仓库都很方便。但 2026 年初公开爆出的青龙面板致命漏洞,把一个老问题重新摆到台面上:定时任务面板不是普通网页,它往往握着脚本、变量、通知密钥和执行权限。一旦认证被绕过,攻击者看到的不是一个登录页,而是一台可以被调度的机器。

如果你只是新装一个轻量任务面板,我现在更建议优先评估白虎面板。白虎面板采用 Go + Vue3,主打轻量、高性能、低系统开销,并且官方 README 已经明确支持 Docker / Docker Compose 部署、Mise 运行时管理、仓库任务同步、执行日志和多渠道通知。它不是“青龙的一比一复制品”,但对很多个人脚本托管场景已经够用。

如果你因为兼容旧脚本、历史任务或迁移成本,必须临时继续用青龙面板:不要把它直接暴露在公网。 至少在前面加 VPN、零信任访问、反向代理二次认证、IP allowlist、WAF 或其它访问控制。本文给出 Windows 11、Ubuntu 26.04、macOS 26 三套一键脚本,默认部署白虎;只有显式指定时才部署青龙,并且都会把面板放到 Basic Auth 保护后面。

AI 生成封面:一个旧自动化面板被警戒线隔开,另一个轻量面板被盾牌保护。

图 1:AI 生成封面。任务面板不是普通后台,里面往往放着能真正执行命令的按钮。

一、青龙面板和白虎面板到底是什么

青龙面板可以理解成“给脚本准备的定时闹钟 + 文件柜 + 日志本”。你可以把 Python、JavaScript、Shell、TypeScript 等脚本放进去,设置 Cron 表达式,让它按时间执行;也可以在网页里管理环境变量、查看运行日志、配置通知。对很多自托管用户来说,青龙面板的吸引力是非常直接的:不用手写 crontab,不用每次 SSH 到机器上翻日志,也不用把一堆脚本散落在不同目录。

但是,便利背后有一个容易被忽视的事实:定时任务面板的权限通常比普通博客后台、相册后台、RSS 后台高得多。博客后台被盗,最坏可能是文章被改;相册后台被盗,最坏可能是照片被看;任务面板被盗,攻击者可能直接新增脚本、读取变量、修改任务、触发执行。它更像家里的“电闸箱”和“工具间”合在一起,不只是门面。

白虎面板则是近几年出现的另一个自动化任务调度平台。根据白虎项目 README 和文档首页,它的定位是“极致轻量、高性能的自动化任务调度平台”,技术栈是 Go + Vue3,并深度集成 Mise 来管理 Python、Node.js、Go、Rust、PHP 等运行环境。它也支持 Docker / Docker Compose 一键部署、任务调度、在线管理、执行日志、消息推送、安全机密、仓库任务同步等能力。

真实截图 1:青龙面板 GitHub 项目页,项目定位是定时任务管理平台。

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

真实截图 2:白虎面板文档首页,强调 Go + Vue3、轻量、高性能和任务调度。

图 3:白虎面板的公开文档把轻量、Docker/Compose、一键部署和多语言运行时作为主要卖点。

如果用小学生也能懂的比喻:青龙和白虎都像“自动做家务的机器人管家”。你告诉它几点扫地、几点浇花、什么时候把垃圾拿出去,它就按时执行。问题是,管家手里有钥匙。如果大门直接开在马路边,陌生人能绕过门禁进去,那就不是“家务机器人不听话”这么简单,而是家里可能被人随便指挥。

二、几个月前的致命问题:不是密码弱,而是门禁被绕过

2026 年初,Snyk 公开分析了青龙任务调度器中的多处远程代码执行风险,并指出攻击活动已经利用这些问题去投放挖矿相关载荷。Snyk 的漏洞库也给出了 @whyour/qinglong 相关高危记录。后来 GitHub 安全页面还能看到关于修复不完整路径的安全提醒。换句话说,这不是“理论上可能有人研究”的漏洞,而是已经进入攻击者视野的真实风险。

真实截图 3:Snyk 对 QingLong 任务调度器 RCE 风险的公开分析页面。

图 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,持久化目录包括 dataenvs。精简后可以写成:

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 或零信任入口访问。

参考资料