绿联 NAS 公网 CLI、SFTP 与 AI 文件索引部署教程

# 绿联 NAS 公网 CLI、SFTP 与 AI 文件索引部署教程

> 部署日期:2026-07-21
> NAS:UGREEN DX4600,UGOS Pro 1.17.0.0095
> 公网服务器:Ubuntu 24.04,`www.xunl.net`
> 状态:已部署并通过 SSH、SFTP、只读访问、受限写入、自动索引测试

## 1. 这套系统解决了什么问题

目标是让不在 NAS 局域网内的公网 Ubuntu 服务器能够:

1. 使用命令行浏览 NAS 文件;
2. 使用 SSH/SFTP 下载素材;
3. 为 AI 建立可搜索的 NAS 文件索引;
4. 允许 AI 把生成结果上传到指定目录;
5. 不把 NAS 的 SSH 端口直接暴露在公网;
6. 尽量避免 AI 误删、误改原始素材。

最终采用:

```text
FRP STCP + Docker 内的密钥 SSH/SFTP + SQLite/JSONL 索引
```

花生壳和蒲公英没有作为主方案:花生壳更偏端口映射,蒲公英更偏多设备虚拟局域网;当前核心需求是“公网服务器上的 AI 使用 CLI 访问 NAS”,已有服务器配合 FRP STCP 更直接。

## 2. 最终架构

```mermaid
flowchart LR
AI["Ubuntu 服务器上的 AI/脚本"]
RO["127.0.0.1:16022\nnas-private\n全部 NAS 卷只读"]
RW["127.0.0.1:16023\nnas-output\n仅 AI 输出目录可写"]
FRPS["公网服务器 frps\nTCP 7000 + TLS + Token"]
FRO["NAS frpc\nnas-cli-private"]
FRW["NAS frpc\nnas-output-private"]
SSHRO["NAS Docker SSH/SFTP\n/volume1、/volume2 只读"]
SSHRW["NAS Docker SSH/SFTP\n06_AI_Generated 可读写"]
IDX["SQLite + JSONL 索引\n每小时刷新"]

AI --> RO
AI --> RW
AI --> IDX
RO --> FRPS --> FRO --> SSHRO
RW --> FRPS --> FRW --> SSHRW
```

重要安全边界:

- `16022` 和 `16023` 只监听服务器的 `127.0.0.1`;
- 公网没有开放 NAS SSH 端口;
- SSH 只允许密钥登录;
- 只读入口不能修改 NAS 文件;
- 写入口只挂载 `06_AI_Generated`;
- FRP 使用 STCP,访问方必须同时拥有 Token 和 STCP 密钥。

## 3. 当前部署清单

### 3.1 NAS Docker 项目

| 项目 | 容器数量 | 用途 | 状态 |
|---|---:|---|---|
| `nas-cli` | 3 | 只读 SSH/SFTP、FRP 客户端,以及早期原生 SSH 代理 | Running |
| `nas-output` | 2 | AI 输出目录的受限写入 SSH/SFTP、FRP 客户端 | Running |

### 3.2 公网服务器服务

| 服务 | 作用 |
|---|---|
| `frps.service` | FRP 服务端 |
| `frpc-nas-visitor.service` | 连接只读 STCP 服务,本地监听 `16022` |
| `frpc-nas-output-visitor.service` | 连接输出 STCP 服务,本地监听 `16023` |
| `nas-index.service` | 扫描 NAS 并生成索引 |
| `nas-index.timer` | 每小时触发一次索引 |

### 3.3 服务器关键文件

```text
/etc/frp/frps.toml
/etc/frp/frpc-nas-visitor.toml
/etc/frp/frpc-nas-output-visitor.toml
/etc/systemd/system/frpc-nas-visitor.service
/etc/systemd/system/frpc-nas-output-visitor.service
/etc/ssh/ssh_config.d/90-nas-private.conf
/root/.ssh/id_ed25519_nas_indexer
/root/.ssh/id_ed25519_nas_indexer.pub
/opt/nas-indexer/nas_indexer.py
/etc/systemd/system/nas-index.service
/etc/systemd/system/nas-index.timer
/var/lib/nas-index/files.sqlite3
/var/lib/nas-index/files.jsonl
/usr/local/bin/nas-search
```

## 4. 实际部署过程

以下顺序既记录本次实际操作,也可以作为重新部署教程。

### 第一步:检查公网服务器已有 FRP

检查版本和服务:

```bash
frps --version
systemctl status frps --no-pager
ss -lntp | grep -E '7000|7500'
```

本次服务器已有 FRP 0.69.1,服务端监听 TCP `7000`。FRP Dashboard 原本监听 `7500`,它不参与 NAS CLI 通道。

`/etc/frp/frps.toml` 核心配置结构如下:

```toml
bindPort = 7000

auth.method = "token"
auth.token = "<FRPS_TOKEN>"

transport.tls.force = true
```

注意:`<FRPS_TOKEN>` 必须替换为强随机值,不能把真实值写入教程或代码仓库。

如果服务器没有 `frpc`,安装与 `frps` 相同版本:

```bash
frpc --version
# 确保 frpc 与 frps 都是 0.69.1,或至少使用相互兼容的版本。
```

### 第二步:在 NAS 开启 Docker

在 UGOS Pro 中:

1. 打开“应用中心”;
2. 安装 Docker;
3. 接受 Docker 使用提示;
4. 确认 Docker 应用可以正常打开。

### 第三步:开启并测试 NAS 原生 SSH

在“控制面板 → 终端”中开启 SSH,端口为 `22`。

本次还创建了 NAS 标准用户:

```text
用户名:ai-indexer
说明:Read-only NAS indexing and AI asset transfer
```

权限规划:

- 业务共享目录:只读;
- `06_AI_Generated`:读写;
- `docker`:拒绝访问。

实际测试发现:UGOS 创建的标准用户 Shell 是不可登录状态,而且没有 `/home/ai-indexer`,出现:

```text
Could not chdir to home directory /home/ai-indexer
This account is currently not available.
```

因此生产方案没有依赖 NAS 原生标准用户 SSH,而改成了 Docker 内的 SSH/SFTP 服务。这样还能把可见目录限制在明确的 Docker 挂载范围内。

### 第四步:生成服务器专用 SSH 密钥

在公网服务器执行:

```bash
ssh-keygen -t ed25519 \
-f /root/.ssh/id_ed25519_nas_indexer \
-C 'nas-ai-indexer@www.xunl.net'

chmod 600 /root/.ssh/id_ed25519_nas_indexer
chmod 644 /root/.ssh/id_ed25519_nas_indexer.pub
```

查看公钥:

```bash
cat /root/.ssh/id_ed25519_nas_indexer.pub
```

只把 `.pub` 公钥放进 NAS Docker 配置,私钥永远保留在服务器。

### 第五步:准备 FRP STCP 参数

生成独立的 STCP 密钥,例如:

```bash
openssl rand -hex 32
```

需要准备两个秘密:

```text
<FRPS_TOKEN> FRP 服务端 Token
<STCP_SECRET> STCP 访问密钥
```

这两个值不要提交到 Git,也不要复制到公开文档。

### 第六步:解决 NAS 无法访问 Docker Hub

本次 NAS Docker 拉取 `fatedier/frpc:v0.69.1` 时出现:

```text
Client.Timeout exceeded while awaiting headers
```

处理办法是在公网服务器临时建立一个 HTTPS Docker Registry,只用于引导 NAS 拉取镜像。

先在服务器拉取镜像:

```bash
docker pull fatedier/frpc:v0.69.1
docker pull lscr.io/linuxserver/openssh-server:latest
docker pull registry:2
```

启动只监听服务器本机的 Registry:

```bash
docker run -d \
--restart unless-stopped \
--name nas-temp-registry \
-p 127.0.0.1:5000:5000 \
registry:2
```

重新打标签并推送:

```bash
docker tag fatedier/frpc:v0.69.1 \
127.0.0.1:5000/fatedier/frpc:v0.69.1
docker push 127.0.0.1:5000/fatedier/frpc:v0.69.1

docker tag lscr.io/linuxserver/openssh-server:latest \
127.0.0.1:5000/linuxserver/openssh-server:latest
docker push 127.0.0.1:5000/linuxserver/openssh-server:latest
```

在 `www.xunl.net` 的 Nginx HTTPS `server` 块中临时加入:

```nginx
# Temporary read-only Docker registry for NAS bootstrap
location /v2/ {
proxy_pass http://127.0.0.1:5000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Docker-Distribution-Api-Version registry/2.0;
proxy_request_buffering off;
client_max_body_size 0;
limit_except GET HEAD { deny all; }
}
```

验证:

```bash
nginx -t
systemctl reload nginx
curl -fsS https://www.xunl.net/v2/
```

预期返回:

```json
{}
```

NAS Compose 中临时使用:

```yaml
image: www.xunl.net/fatedier/frpc:v0.69.1
```

以及:

```yaml
image: www.xunl.net/linuxserver/openssh-server:latest
```

镜像拉取完成后,必须移除 Nginx 的 `/v2/` 配置并删除临时 Registry:

```bash
nginx -t && systemctl reload nginx
docker rm -f nas-temp-registry
curl -sS -o /dev/null -w '%{http_code}\n' https://www.xunl.net/v2/
```

本次清理后返回 `404`,说明临时入口已经关闭。

## 5. NAS 只读 CLI 项目

在 NAS Docker 中创建项目 `nas-cli`。

推荐的精简版 Compose 如下。当前线上项目还保留了一个早期用于原生 SSH 的 FRP 容器,所以显示 3 个容器;未来重建时使用下面两个服务即可。

```yaml
services:
nas_cli:
image: www.xunl.net/linuxserver/openssh-server:latest
container_name: nas-cli-sshd
restart: unless-stopped
environment:
- PUID=0
- PGID=0
- TZ=Asia/Shanghai
- PUBLIC_KEY=<SSH_PUBLIC_KEY>
- SUDO_ACCESS=false
- PASSWORD_ACCESS=false
- USER_NAME=ai-indexer
- LOG_STDOUT=true
volumes:
- ./sshd-config:/config
- /volume1:/nas/volume1:ro
- /volume2:/nas/volume2:ro
ports:
- "127.0.0.1:2222:2222"

frpc_cli:
image: www.xunl.net/fatedier/frpc:v0.69.1
container_name: nas-frpc-cli
network_mode: host
restart: unless-stopped
entrypoint: ["/bin/sh", "-c"]
command:
- |
cat > /tmp/frpc.toml <<'EOF'
serverAddr = "www.xunl.net"
serverPort = 7000
auth.method = "token"
auth.token = "<FRPS_TOKEN>"
transport.tls.enable = true
[[proxies]]
name = "nas-cli-private"
type = "stcp"
secretKey = "<STCP_SECRET>"
localIP = "127.0.0.1"
localPort = 2222
EOF
exec /usr/bin/frpc -c /tmp/frpc.toml
```

说明:

- NAS 主机的 `volume1` 和 `volume2` 映射到容器 `/nas`;
- 两个挂载都带 `:ro`,因此即使容器里的用户 UID 是 0,也不能修改 NAS 卷;
- `PASSWORD_ACCESS=false` 禁用密码登录;
- 只有服务器对应私钥可以登录;
- `2222` 仅绑定 NAS 主机的 `127.0.0.1`,局域网其他机器也不能直接连接。

## 6. NAS 受限输出项目

创建 Docker 项目 `nas-output`:

```yaml
services:
output_ssh:
image: www.xunl.net/linuxserver/openssh-server:latest
container_name: nas-output-sshd
restart: unless-stopped
environment:
- PUID=0
- PGID=0
- TZ=Asia/Shanghai
- PUBLIC_KEY=<SSH_PUBLIC_KEY>
- SUDO_ACCESS=false
- PASSWORD_ACCESS=false
- USER_NAME=ai-output
- LOG_STDOUT=true
volumes:
- ./sshd-config:/config
- /volume2/06_AI_Generated:/output:rw
ports:
- "127.0.0.1:2223:2222"

frpc_output:
image: www.xunl.net/fatedier/frpc:v0.69.1
container_name: nas-frpc-output
network_mode: host
restart: unless-stopped
entrypoint: ["/bin/sh", "-c"]
command:
- |
cat > /tmp/frpc.toml <<'EOF'
serverAddr = "www.xunl.net"
serverPort = 7000
auth.method = "token"
auth.token = "<FRPS_TOKEN>"
transport.tls.enable = true
[[proxies]]
name = "nas-output-private"
type = "stcp"
secretKey = "<STCP_SECRET>"
localIP = "127.0.0.1"
localPort = 2223
EOF
exec /usr/bin/frpc -c /tmp/frpc.toml
```

这个入口只看得到 `/output`,不能访问其他 NAS 目录。

## 7. 公网服务器的 STCP Visitor

### 7.1 只读入口

`/etc/frp/frpc-nas-visitor.toml`:

```toml
serverAddr = "127.0.0.1"
serverPort = 7000

auth.method = "token"
auth.token = "<FRPS_TOKEN>"
transport.tls.enable = true

[[visitors]]
name = "nas-cli-visitor"
type = "stcp"
serverName = "nas-cli-private"
secretKey = "<STCP_SECRET>"
bindAddr = "127.0.0.1"
bindPort = 16022
```

`/etc/systemd/system/frpc-nas-visitor.service`:

```ini
[Unit]
Description=FRP STCP visitor for private NAS CLI
After=network-online.target frps.service
Wants=network-online.target

[Service]
Type=simple
ExecStart=/usr/bin/frpc -c /etc/frp/frpc-nas-visitor.toml
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

### 7.2 输出入口

`/etc/frp/frpc-nas-output-visitor.toml`:

```toml
serverAddr = "127.0.0.1"
serverPort = 7000

auth.method = "token"
auth.token = "<FRPS_TOKEN>"
transport.tls.enable = true

[[visitors]]
name = "nas-output-visitor"
type = "stcp"
serverName = "nas-output-private"
secretKey = "<STCP_SECRET>"
bindAddr = "127.0.0.1"
bindPort = 16023
```

启用服务:

```bash
systemctl daemon-reload
systemctl enable --now frpc-nas-visitor.service
systemctl enable --now frpc-nas-output-visitor.service
```

验证:

```bash
systemctl is-active frps frpc-nas-visitor frpc-nas-output-visitor
ss -lntp | grep -E '16022|16023'
```

必须看到两个端口只绑定:

```text
127.0.0.1:16022
127.0.0.1:16023
```

不能是 `0.0.0.0:16022` 或 `0.0.0.0:16023`。

## 8. SSH 别名

服务器文件 `/etc/ssh/ssh_config.d/90-nas-private.conf`:

```sshconfig
Host nas-private
HostName 127.0.0.1
Port 16022
User ai-indexer
IdentityFile /root/.ssh/id_ed25519_nas_indexer
IdentitiesOnly yes
StrictHostKeyChecking yes

Host nas-output
HostName 127.0.0.1
Port 16023
User ai-output
IdentityFile /root/.ssh/id_ed25519_nas_indexer
IdentitiesOnly yes
StrictHostKeyChecking accept-new
```

现在可以直接运行:

```bash
ssh nas-private
sftp nas-private
sftp nas-output
```

## 9. 日常文件操作

### 9.1 浏览 NAS

```bash
ssh nas-private
ls -lah /nas/volume1
ls -lah /nas/volume2
find /nas/volume2 -type f -iname '*EC280*' | head
```

### 9.2 下载文件

```bash
scp 'nas-private:/nas/volume2/某共享目录/某文件.jpg' ./
```

路径含空格或中文时要加引号。

SFTP:

```bash
sftp nas-private
sftp> ls /nas/volume2
sftp> get "/nas/volume2/某共享目录/某文件.mp4"
sftp> bye
```

### 9.3 上传 AI 结果

```bash
scp result.mp4 nas-output:/output/
```

或:

```bash
sftp nas-output
sftp> put result.mp4 /output/result.mp4
sftp> bye
```

不要尝试通过 `nas-private` 上传,它是只读入口。

## 10. 文件索引

### 10.1 当前结果

首轮索引结果:

```text
文件数:367,909
SQLite:/var/lib/nas-index/files.sqlite3,约 338 MB
JSONL:/var/lib/nas-index/files.jsonl,约 132 MB
```

索引字段:

```text
path
name
extension
size
mtime
mtime_iso
share
```

SQLite 还建立了名称、扩展名、共享目录、修改时间索引和 FTS5 表。

### 10.2 搜索命令

```bash
nas-search 离心风机
nas-search EC280 白底图 -n 20
nas-search 管道风机 mp4 -n 50
nas-search 产品图片 PSD -n 100
```

输出格式:

```text
文件大小 UTC修改时间 NAS完整路径
```

### 10.3 定时任务

```bash
systemctl status nas-index.timer --no-pager
systemctl list-timers nas-index.timer --no-pager
```

手动刷新:

```bash
systemctl start nas-index.service
journalctl -u nas-index.service -f
```

检查索引:

```bash
python3 - <<'PY'
import sqlite3
c = sqlite3.connect('/var/lib/nas-index/files.sqlite3')
print(c.execute('select count(*) from files').fetchone()[0])
for row in c.execute('select share,count(*) from files group by share order by count(*) desc'):
print(row)
PY
```

### 10.4 当前索引能力边界

当前版本是“文件元数据索引”,可以让 AI 知道:

- 文件名;
- 所在目录和业务共享区;
- 扩展名;
- 文件大小;
- 修改时间;
- 如何通过 SFTP 获取原文件。

当前还没有自动提取:

- Word、PDF、Excel 的正文;
- 图片视觉描述和 OCR;
- 视频语音、镜头和字幕;
- 向量 Embedding;
- 重复文件哈希。

因此“从 36 万文件中按名称和目录找素材”已经可用;“按画面内容或文档语义搜索”属于下一阶段。

## 11. 推荐的 AI 索引二期

下一个 AI 可以在现有基础上继续:

1. 对 PDF、DOCX、XLSX、PPTX、TXT 提取正文;
2. 对图片生成缩略图、OCR、视觉描述和 Embedding;
3. 对视频使用 `ffprobe` 获取时长和分辨率;
4. 视频每隔一定时间抽帧,再做 OCR/视觉描述;
5. 对音视频生成语音转写;
6. 使用 SHA-256 或快速哈希识别重复素材;
7. 将元数据、正文和向量写入 SQLite/PostgreSQL/Qdrant;
8. 提供一个 API,例如:

```text
GET /search?q=EC280+白底图
GET /file/{id}
POST /output
```

9. AI 搜索后,通过 `sftp nas-private` 下载原素材;
10. AI 生成结果后,通过 `sftp nas-output` 上传至 `/output`。

建议先对高价值目录增量处理,不要一次对 36 万文件全部做视觉模型推理。

## 12. 故障排查

### 12.1 `ssh nas-private` 失败

依次检查:

```bash
systemctl status frps --no-pager
systemctl status frpc-nas-visitor --no-pager
ss -lntp | grep 16022
journalctl -u frps -u frpc-nas-visitor -n 100 --no-pager
```

NAS Docker 中检查:

- `nas-cli` 项目是否 Running;
- `nas-cli-sshd` 是否 Running;
- `nas-frpc-cli` 是否 Running;
- FRP 日志是否显示 `nas-cli-private` 注册成功。

### 12.2 SSH 主机指纹变化

重建 SSH 容器后可能出现:

```text
WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!
```

先确认确实是自己重建容器,然后执行:

```bash
ssh-keygen -f /root/.ssh/known_hosts -R '[127.0.0.1]:16022'
ssh nas-private
```

输出入口对应:

```bash
ssh-keygen -f /root/.ssh/known_hosts -R '[127.0.0.1]:16023'
ssh nas-output
```

### 12.3 FRP 显示代理不存在

检查两端名称必须完全一致:

```text
NAS frpc name = nas-cli-private
服务器 serverName = nas-cli-private
```

输出通道:

```text
NAS frpc name = nas-output-private
服务器 serverName = nas-output-private
```

同时检查 `<STCP_SECRET>` 是否一致。

### 12.4 Docker 容器反复 Restarting

本次遇到过:

```text
open /etc/frp/frpc.toml: no such file or directory
```

原因是 UGOS Docker Compose 对 `configs: content:` 的行为不符合预期,没有生成目标文件。

修复办法是使用:

```yaml
entrypoint: ["/bin/sh", "-c"]
command:
- |
cat > /tmp/frpc.toml <<'EOF'
...
EOF
exec /usr/bin/frpc -c /tmp/frpc.toml
```

### 12.5 索引失败

```bash
systemctl status nas-index.service --no-pager -l
journalctl -u nas-index.service -n 100 --no-pager
ssh nas-private 'find /nas -maxdepth 2 -type d | head'
```

注意 Shell `case` 中以 `#` 开头的模式要加引号,例如:

```bash
case "$b" in @*|docker|"#recycle") continue ;; esac
```

否则 `#` 会被解析成注释并造成语法错误。

## 13. 安全维护

### 必须做

1. 本次服务器 root 密码曾通过聊天发送,应该立即更换;
2. 尽快把服务器 root 登录改为密钥认证;
3. 确认 SSH 配置后关闭 root 密码登录;
4. 定期轮换 `<FRPS_TOKEN>` 和 `<STCP_SECRET>`;
5. 不要把私钥、Token、密码提交到 Git;
6. 定期检查:

```bash
ss -lntp
systemctl --failed
docker ps
journalctl -u frps --since today
```

### 建议优化

- 当前只读 SSH 容器使用 `PUID=0`、`PGID=0`,但 NAS 卷全部 `:ro`;未来可以查出合适的 NAS UID/GID 后改为非 root;
- 当前 `nas-cli` 还保留一个早期原生 SSH FRP 容器,可在维护窗口删除,只保留精简版的两个服务;
- 如果不再需要 NAS 原生 SSH,可以在确认容器化 SSH 稳定后关闭 UGOS 原生 SSH;
- FRP Dashboard 如果不需要公网访问,应绑定 `127.0.0.1` 或用防火墙限制来源;
- 数据库和 JSONL 较大,建议为 `/var/lib/nas-index` 设置磁盘监控。

## 14. 给下一个 AI 的交接指令

可以把下面内容原样发给下一个 AI:

```text
这是一个已经运行的绿联 NAS 公网 CLI 系统。

公网服务器:www.xunl.net,Ubuntu 24.04。
不要要求用户再次提供密码,不要在输出中展示任何已有 Token、STCP Secret 或私钥。

只读 NAS:
- ssh nas-private
- sftp nas-private
- NAS 路径在 /nas/volume1 和 /nas/volume2
- 两个卷是只读挂载

AI 输出:
- sftp nas-output
- 唯一可写路径是 /output
- /output 对应 NAS 的 06_AI_Generated

索引:
- nas-search <关键词> -n <数量>
- SQLite:/var/lib/nas-index/files.sqlite3
- JSONL:/var/lib/nas-index/files.jsonl
- systemd timer:nas-index.timer,每小时运行
- 当前约 367,909 个文件

FRP:
- frps.service
- frpc-nas-visitor.service,本地 127.0.0.1:16022
- frpc-nas-output-visitor.service,本地 127.0.0.1:16023
- 使用 STCP,不能把两个本地端口改成 0.0.0.0

NAS Docker:
- nas-cli:只读 CLI
- nas-output:仅 AI 输出目录可写

执行任何修改前先做只读检查。
不要扩大可写目录,不要开放公网 SSH,不要删除用户文件。
下一阶段目标是正文提取、OCR、图片/视频描述、Embedding 和语义检索。
```

## 15. 验收命令

以后维护完成后,至少运行:

```bash
systemctl is-active \
frps \
frpc-nas-visitor \
frpc-nas-output-visitor

ss -lntp | grep -E '127.0.0.1:16022|127.0.0.1:16023'

ssh -o BatchMode=yes nas-private \
'id; test -d /nas/volume2 && echo READ_OK'

ssh -o BatchMode=yes nas-output \
'touch /output/.write-test && rm /output/.write-test && echo WRITE_OK'

nas-search 离心风机 -n 3

systemctl list-timers nas-index.timer --no-pager
```

预期关键输出:

```text
active
READ_OK
WRITE_OK
```

---

本文档已将服务器密码、FRP Token、STCP Secret 和 SSH 私钥全部脱敏。恢复或迁移时应生成新的秘密,而不是从聊天记录中复制旧值。

个人微信:ssevening

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注