写在前面:vllm 潜在的高危漏洞 先说结论:不要直接把 LLM 服务(vLLM、Ollama、Jupyter 等)通过 FRP 的 tcp 模式暴露到公网 :
vLLM 默认无认证 :只要能连上端口就能调 API。
vLLM 历史 RCE 漏洞 :CVE-2025-62164、CVE-2025-9141、CVE-2025-66448 等,CVSS 都在 8.0+,通过 prompt embedding、tool call、auto_map 都能 RCE。
FRP tcp 模式直接在公网开端口 :扫描器几小时内就能扫到 OpenAI 兼容 API。
任何一个单独存在都还能扛,三个叠在一起就是灾难。
整体架构 我们用 FRP 的 stcp(secret tcp) 模式,而不是 tcp 模式。区别:
模式
公网端口
谁能访问
安全性
tcp
中转服务器开放固定端口(如 10001)
任何能扫到这个端口的人
❌ 等于裸奔
stcp
不开任何公网端口
必须持有 secretKey 的 visitor
✅ 公网零暴露
最终的访问链路:
1 2 3 4 5 6 7 8 9 10 11 [你的笔记本/工作机] └─ frpc visitor (持有 secretKey) │ 通过中转服务器建立加密隧道 ↓ [中转服务器 frps] ← 只开 SSH (22) 和 FRP 控制端口 (7000),无业务端口 ↑ │ vLLM 主机主动连出 [GPU 主机 vLLM] └─ frpc (持有同样的 secretKey) └─ vLLM 监听 127.0.0.1:8888 (不对外) └─ vLLM 启用 API key
整条路径上有四把锁:
SSH 密钥登录(防爆破)
FRP token(防恶意客户端注册)
stcp secretKey(防未授权访问 LLM)
vLLM API key(即便前面都漏了,API 层还有一道)
一、客户端部署脚本(GPU 主机) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 #!/bin/bash set -euo pipefail : "${FRP_SERVER_ADDR:?need FRP_SERVER_ADDR} " : "${FRP_AUTH_TOKEN:?need FRP_AUTH_TOKEN (openssl rand -hex 32)} " : "${FRP_STCP_SECRET:?need FRP_STCP_SECRET (openssl rand -hex 32)} " FRP_VERSION="0.61.0" FRP_TAR_FILE="frp_${FRP_VERSION} _linux_amd64.tar.gz" FRP_DIR="frp_${FRP_VERSION} _linux_amd64" DOWNLOAD_URL="https://github.com/fatedier/frp/releases/download/v${FRP_VERSION} /${FRP_TAR_FILE} " EXPECTED_SHA256="<paste-sha256-from-github-release-here>" SERVER_PORT=7000 PROXY_NAME="llm-service" LOCAL_IP="127.0.0.1" LOCAL_PORT=8000if [ ! -d "$FRP_DIR " ]; then echo "[+] Downloading FRP ${FRP_VERSION} ..." wget -q --show-progress "$DOWNLOAD_URL " if [ "$EXPECTED_SHA256 " != "<paste-sha256-from-github-release-here>" ]; then echo "[+] Verifying SHA256..." echo "${EXPECTED_SHA256} ${FRP_TAR_FILE} " | sha256sum -c - else echo "[!] SHA256 not set. Strongly recommend pinning EXPECTED_SHA256." fi tar -zxf "$FRP_TAR_FILE " else echo "[=] FRP directory already exists, skipping download." fi cd "$FRP_DIR " echo "[+] Generating frpc.toml..." cat > frpc.toml <<EOF serverAddr = "${FRP_SERVER_ADDR}" serverPort = ${SERVER_PORT} auth.method = "token" auth.token = "${FRP_AUTH_TOKEN}" # 强制 TLS,加密 frpc <-> frps 的控制信道 transport.tls.enable = true transport.tls.disableCustomTLSFirstByte = false # 日志(限制级别和大小,避免被攻击者翻历史请求 / 被日志炸盘) log.to = "./frpc.log" log.level = "info" log.maxDays = 7 [[proxies]] name = "${PROXY_NAME}" # 关键:用 stcp(secret tcp)而不是 tcp # 公网不会有任何端口开放,只有持有 secretKey 的 visitor 能连上 type = "stcp" secretKey = "${FRP_STCP_SECRET}" localIP = "${LOCAL_IP}" localPort = ${LOCAL_PORT} EOF chmod 600 frpc.tomlecho "[+] Stopping existing frpc instances..." pkill -f "frpc -c ./frpc.toml" 2>/dev/null || true echo "[+] Starting frpc..." nohup ./frpc -c ./frpc.toml > /dev/null 2>&1 &cat <<EOF ------------------------------------------------------ Deployment complete (stcp mode). The service is NOT exposed on any public port. To access it from another machine, run an frpc visitor with the same secretKey on that client. Local logs: $(pwd)/frpc.log Config (600): $(pwd)/frpc.toml ------------------------------------------------------ EOF
几个关键变化:
secret 不再硬编码到脚本里 ,从环境变量读取。脚本可以放进 Git,密钥不会泄露。
FRP 版本升级到 0.61.0 ,并加 SHA256 校验防止下载被劫持。
type = "tcp" → type = "stcp" ,删除了 remotePort,公网零端口暴露。
强制 TLS ,加密控制信道。
localIP 必须配合 vLLM 的 --host 127.0.0.1 ,否则等于白配。
二、访问端的 visitor 配置(你的工作机/笔记本) stcp 模式需要在访问 LLM 的那台机器 上跑一个 visitor。这是新增的、原来没有的步骤。
一键部署脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 #!/bin/bash set -euo pipefail : "${FRP_SERVER_ADDR:?need FRP_SERVER_ADDR} " : "${FRP_AUTH_TOKEN:?need FRP_AUTH_TOKEN} " : "${FRP_STCP_SECRET:?need FRP_STCP_SECRET} " LOCAL_BIND_PORT="${LOCAL_BIND_PORT:-8888} " FRP_VERSION="0.61.0" FRP_TAR_FILE="frp_${FRP_VERSION} _linux_amd64.tar.gz" DOWNLOAD_URL="https://github.com/fatedier/frp/releases/download/v${FRP_VERSION} /${FRP_TAR_FILE} " EXPECTED_SHA256="<paste-sha256-from-github-release-here>" FRP_DIR="${HOME} /frp" BIN_DIR="${FRP_DIR} /bin" CONFIG_FILE="${FRP_DIR} /frpc-visitor.toml" LOG_FILE="${FRP_DIR} /frpc-visitor.log" mkdir -p "${BIN_DIR} " if [ ! -f "${BIN_DIR} /frpc" ]; then echo "[+] Downloading FRP ${FRP_VERSION} ..." wget -q --show-progress -O "/tmp/${FRP_TAR_FILE} " "${DOWNLOAD_URL} " if [ "${EXPECTED_SHA256} " != "<paste-sha256-from-github-release-here>" ]; then echo "[+] Verifying SHA256..." echo "${EXPECTED_SHA256} /tmp/${FRP_TAR_FILE} " | sha256sum -c - else echo "[!] SHA256 not set — strongly recommend pinning EXPECTED_SHA256." fi tar -zxf "/tmp/${FRP_TAR_FILE} " -C /tmp cp "/tmp/frp_${FRP_VERSION} _linux_amd64/frpc" "${BIN_DIR} /frpc" chmod 755 "${BIN_DIR} /frpc" rm -rf "/tmp/frp_${FRP_VERSION} _linux_amd64" "/tmp/${FRP_TAR_FILE} " echo "[+] frpc installed to ${BIN_DIR} /frpc" else echo "[=] frpc already at ${BIN_DIR} /frpc, skipping download." fi echo "[+] Generating ${CONFIG_FILE} ..." cat > "${CONFIG_FILE} " <<EOF serverAddr = "${FRP_SERVER_ADDR}" serverPort = 7000 auth.method = "token" auth.token = "${FRP_AUTH_TOKEN}" transport.tls.enable = true log.to = "${LOG_FILE}" log.level = "info" log.maxDays = 7 [[visitors]] name = "llm-visitor" type = "stcp" serverName = "llm-service" # 必须和 GPU 主机 proxy name 一致 secretKey = "${FRP_STCP_SECRET}" bindAddr = "127.0.0.1" bindPort = ${LOCAL_BIND_PORT} EOF chmod 600 "${CONFIG_FILE} " SYSTEMD_USER_DIR="${HOME} /.config/systemd/user" mkdir -p "${SYSTEMD_USER_DIR} " cat > "${SYSTEMD_USER_DIR} /frpc-visitor.service" <<EOF [Unit] Description=FRP Client Visitor (stcp → llm-service) After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=${BIN_DIR}/frpc -c ${CONFIG_FILE} Restart=on-failure RestartSec=5 [Install] WantedBy=default.target EOF loginctl enable-linger "$(whoami) " 2>/dev/null || true systemctl --user daemon-reload systemctl --user enable --now frpc-visitorcat <<EOF ------------------------------------------------------ Visitor 部署完成。 访问 LLM: http://127.0.0.1:${LOCAL_BIND_PORT} 配置文件: ${CONFIG_FILE} 日志: ${LOG_FILE} 常用命令: systemctl --user status frpc-visitor systemctl --user restart frpc-visitor journalctl --user -u frpc-visitor -f ------------------------------------------------------ EOF
最终生成的 ~/frp/frpc-visitor.toml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 serverAddr = "your.server.ip" serverPort = 7000 auth.method = "token" auth.token = "和 GPU 主机相同的 FRP_AUTH_TOKEN" transport.tls.enable = true log.to = "/home/you/frp/frpc-visitor.log" log.level = "info" log.maxDays = 7 [[visitors]] name = "llm-visitor" type = "stcp" serverName = "llm-service" secretKey = "和 GPU 主机相同的 FRP_STCP_SECRET" bindAddr = "127.0.0.1" bindPort = 8888
启动后,本地访问 http://127.0.0.1:8888 就等于访问 GPU 主机的 vLLM。整条链路对公网完全不可见。
三、服务端(中转 VPS) 一键部署脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 #!/bin/bash set -euo pipefail : "${FRP_AUTH_TOKEN:?need FRP_AUTH_TOKEN (openssl rand -hex 32)} " : "${FRP_DASHBOARD_PASSWORD:?need FRP_DASHBOARD_PASSWORD (openssl rand -hex 16)} " ALLOWED_GPU_IP="${ALLOWED_GPU_IP:-} " ALLOWED_SSH_IP="${ALLOWED_SSH_IP:-} " FRP_VERSION="0.61.0" FRP_TAR_FILE="frp_${FRP_VERSION} _linux_amd64.tar.gz" DOWNLOAD_URL="https://github.com/fatedier/frp/releases/download/v${FRP_VERSION} /${FRP_TAR_FILE} " EXPECTED_SHA256="<paste-sha256-from-github-release-here>" INSTALL_DIR="/opt/frp" if [ ! -f "${INSTALL_DIR} /frps" ]; then echo "[+] Downloading FRP ${FRP_VERSION} ..." wget -q --show-progress -O "/tmp/${FRP_TAR_FILE} " "${DOWNLOAD_URL} " if [ "${EXPECTED_SHA256} " != "<paste-sha256-from-github-release-here>" ]; then echo "[+] Verifying SHA256..." echo "${EXPECTED_SHA256} /tmp/${FRP_TAR_FILE} " | sha256sum -c - else echo "[!] SHA256 not set — strongly recommend pinning EXPECTED_SHA256." fi tar -zxf "/tmp/${FRP_TAR_FILE} " -C /tmp mkdir -p "${INSTALL_DIR} " cp "/tmp/frp_${FRP_VERSION} _linux_amd64/frps" "${INSTALL_DIR} /frps" chmod 755 "${INSTALL_DIR} /frps" rm -rf "/tmp/frp_${FRP_VERSION} _linux_amd64" "/tmp/${FRP_TAR_FILE} " echo "[+] frps installed to ${INSTALL_DIR} /frps" else echo "[=] frps already at ${INSTALL_DIR} /frps, skipping download." fi if ! id frps &>/dev/null; then echo "[+] Creating frps system user..." useradd -r -s /usr/sbin/nologin -d /nonexistent frpsfi echo "[+] Generating ${INSTALL_DIR} /frps.toml..." cat > "${INSTALL_DIR} /frps.toml" <<EOF bindPort = 7000 auth.method = "token" auth.token = "${FRP_AUTH_TOKEN}" # 拒绝非 TLS 连接 transport.tls.force = true # stcp 不需要 allowPorts,保留以防万一以后加 tcp 代理 allowPorts = [ { start = 10000, end = 10010 } ] # Dashboard 只监听本地,通过 SSH 隧道访问 webServer.addr = "127.0.0.1" webServer.port = 7500 webServer.user = "admin" webServer.password = "${FRP_DASHBOARD_PASSWORD}" log.to = "/var/log/frps.log" log.level = "info" log.maxDays = 30 EOF chown -R frps:frps "${INSTALL_DIR} " chmod 600 "${INSTALL_DIR} /frps.toml" touch /var/log/frps.logchown frps:frps /var/log/frps.logecho "[+] Installing /etc/systemd/system/frps.service..." cat > /etc/systemd/system/frps.service <<EOF [Unit] Description=FRP Server After=network.target [Service] Type=simple User=frps Group=frps WorkingDirectory=${INSTALL_DIR} ExecStart=${INSTALL_DIR}/frps -c ${INSTALL_DIR}/frps.toml Restart=always RestartSec=3 # 安全加固 NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/log CapabilityBoundingSet=CAP_NET_BIND_SERVICE AmbientCapabilities=CAP_NET_BIND_SERVICE [Install] WantedBy=multi-user.target EOF systemctl daemon-reload systemctl enable --now frpsif command -v ufw &>/dev/null; then echo "[+] Configuring UFW..." ufw default deny incoming ufw default allow outgoing if [ -n "${ALLOWED_SSH_IP} " ]; then ufw allow from "${ALLOWED_SSH_IP} " to any port 22 proto tcp else ufw allow 22/tcp fi if [ -n "${ALLOWED_GPU_IP} " ]; then ufw allow from "${ALLOWED_GPU_IP} " to any port 7000 proto tcp else echo "[!] ALLOWED_GPU_IP not set — opening port 7000 to all (not ideal)." ufw allow 7000/tcp fi ufw --force enable echo "[+] UFW rules applied." else echo "[!] ufw not found — please configure your firewall manually." echo " Open ports: 22/tcp (SSH), 7000/tcp (FRP control)" fi cat <<EOF ------------------------------------------------------ FRP Server 部署完成。 状态检查: systemctl status frps tail -f /var/log/frps.log Dashboard(本地只读,需先建 SSH 隧道): ssh -L 7500:127.0.0.1:7500 user@<your-server-ip> 然后访问 http://127.0.0.1:7500 配置文件(chmod 600):${INSTALL_DIR}/frps.toml ------------------------------------------------------ EOF
最终生成的 /opt/frp/frps.toml frps.toml 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 bindPort = 7000 auth.method = "token" auth.token = "和客户端相同的 FRP_AUTH_TOKEN" transport.tls.force = true allowPorts = [ { start = 10000 , end = 10010 } ]webServer.addr = "127.0.0.1" webServer.port = 7500 webServer.user = "admin" webServer.password = "<another-strong-random-password>" log.to = "/var/log/frps.log" log.level = "info" log.maxDays = 30
注意点:
auth.token 必须用 openssl rand -hex 32 生成的真随机串,不要用 your_secure_token 这种占位符(我之前就是这么挂的)。
webServer.addr = "127.0.0.1":dashboard 只听本地,如果你想看,本地起 SSH 隧道:ssh -L 7500:127.0.0.1:7500 user@your.server.ip。
transport.tls.force = true:拒绝任何明文连接。
最终生成的 /etc/systemd/system/frps.service 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 [Unit] Description =FRP ServerAfter =network.target[Service] Type =simpleUser =frps Group =frpsWorkingDirectory =/opt/frpExecStart =/opt/frp/frps -c /opt/frp/frps.tomlRestart =alwaysRestartSec =3 NoNewPrivileges =true PrivateTmp =true ProtectSystem =strictProtectHome =true ReadWritePaths =/var/logCapabilityBoundingSet =CAP_NET_BIND_SERVICEAmbientCapabilities =CAP_NET_BIND_SERVICE[Install] WantedBy =multi-user.target
以上所有配置均由上方的部署脚本自动生成,手动调整时参考这两份文件。
中转服务器防火墙(关键!) 1 2 3 4 5 6 7 8 9 10 11 sudo ufw default deny incomingsudo ufw default allow outgoingsudo ufw allow from <your-home-ip> to any port 22sudo ufw allow from <gpu-host-ip> to any port 7000sudo ufw enable
stcp 模式下不需要开放业务端口 ,这是它最大的安全优势。
四、配套的 vLLM 启动方式 FRP 只是网络层。LLM 服务本身的安全配置同样关键,否则前面的加固都白费:
1 2 3 4 5 6 7 8 9 10 11 12 pip install --upgrade vllmsudo useradd -m -s /bin/bash vllmusersudo -u vllmuser bash -c ' python -m vllm.entrypoints.openai.api_server \ --model your_model \ --host 127.0.0.1 \ --port 8000 \ --api-key "$(openssl rand -hex 32 | tee ~/.vllm_api_key)" '
三个要点:
--host 127.0.0.1 :只监听本地,所有外部访问必须经过 FRP,多一道控制。
--api-key :vLLM 默认无认证,必须显式启用。
专用用户启动 :万一 vLLM 出新 CVE 被 RCE,攻击者拿到的也只是 vllmuser 权限,不是 root。
五、安全隐患说明 如果用 tcp 模式,每个隐患列出来对比参考:
隐患 1:type = "tcp" 直接在公网开端口 如果用 tcp 模式,中转服务器会监听 remotePort = 10001。互联网上的端口扫描器(Shodan、Censys、各种自动化爬虫)几小时内就能发现这个端口。一旦识别出是 OpenAI 兼容 API,就会被自动化工具尝试漏洞利用。
修复 :改用 stcp,公网零端口。
隐患 2:vLLM 自身无认证 + 已知 RCE vLLM 默认不需要任何认证。即使你信任所有能扫到端口的人不会乱用算力,也防不住已知漏洞:
CVE-2025-62164 (CVSS 8.8):通过 prompt embedding 触发 torch.load() RCE,影响 0.10.2 ~ 0.11.0
CVE-2025-9141 :tool call 反序列化 RCE,影响 0.10.0 ~ 0.10.1.1
CVE-2025-66448 :auto_map 配置 RCE,影响所有 < 0.11.1
ShadowMQ 系列 :ZMQ + pickle 反序列化 RCE
修复 :升级到 ≥ 0.11.1,启用 --api-key,绑定 127.0.0.1,用低权限用户跑。
隐患 3:硬编码的弱 token 如果用 AUTH_TOKEN="your_secure_token" 这种占位符,任何人都能注册 frpc 客户端连到你的 frps。
修复 :从环境变量读取,openssl rand -hex 32 生成强随机串,配置文件 chmod 600。
隐患 4:明文 TCP 控制信道 如果用默认配置不启用 TLS,frpc 和 frps 之间的控制信道明文传输,中间人可以嗅探 token、看到代理元数据。
修复 :客户端 transport.tls.enable = true,服务端 transport.tls.force = true。
隐患 5:nohup 启动 + root 跑 如果用 nohup ./frpc &,问题:进程崩了不会重启、日志无管理、跟 shell 生命周期耦合、通常是以当前用户(可能是 root)跑。
修复 :systemd 服务 + 专用低权限用户 + 资源/能力限制。
隐患 6:FRP 版本过旧 如果用旧版本(如 0.56.0),FRP 本身偶尔也出安全更新。
修复 :跟进新版本,定期 wget 新 release + SHA256 校验。
隐患 7:中转服务器无防火墙 如果中转 VPS 上还有别的服务(数据库管理面板、redis、什么测试 API),它们也跟 frps 一起暴露在公网。
修复 :默认 deny incoming,只白名单放行必要端口。
总结:四道锁的成本 一次配好可以用很久:
加固项
一次性成本
防住的事
stcp 模式
改几行配置 + 客户端配 visitor
所有公网扫描和未授权访问
FRP TLS
改两行配置
token 嗅探、中间人
vLLM API key + 127.0.0.1
启动加两个参数
API 层未授权调用
vLLM 低权限用户
创建一个用户
RCE 时不直接 root
systemd + 防火墙
几分钟
进程稳定性、其他服务暴露