---
name: vless-ws-tls-cf-vps
description: 在新 VPS 上配置整套 VLESS+WS+TLS+Cloudflare CDN+伪装域名代理(3x-ui 承载入站 + Nginx Proxy Manager 终结 TLS + v2rayN 客户端),适用于 IP 被墙需 CDN 前置的机器。触发:新 VPS 配置 vless ws、websocket+tls、cdn、cloudflare、伪装域名、npm、nginx proxy manager、3x-ui 面板反代,或 /vless-ws-tls-cf-vps
---

# 新 VPS 配置 ws+tls+Cloudflare+伪装域名(3x-ui + NPM + v2rayN)

适用:IP 已被墙(或预判会被墙)的 VPS,流量经 CF 边缘中转,国内可达。1G 内存即可。

占位符:`<DOMAIN>`(裸域) `<SSH_ALIAS>` `<ORIGIN_IP>` `<PPORT>`(3x-ui 面板口) `<WSPORT>`(ws 入站口,随机高位)。

## 链路

```
v2rayN → CF 边缘(<DOMAIN>,橙云,Full strict)
       → NPM(docker,443,Origin CA 证书)
       → 172.17.0.1:<WSPORT>(3x-ui 主 xray 的 VLESS+WS 入站,无 TLS)
```

根路径直接落到 xray(未知 path 返 400/404)。NPM 模板对 location 硬编码 `Host $host` 无法改写,做不了 bing 类站点伪装;要真站点需上游忽略 Host(勿浪费时间尝试)。

## 流程

### 0. 前置(CF 侧,人工/用户配合)

1. 域名托管 CF,DNS 两条:A `<DOMAIN>` → `<ORIGIN_IP>`、A `*.<DOMAIN>` → `<ORIGIN_IP>`,均 **Proxied(橙云)**。
2. SSL/TLS → 加密模式 = **Full (strict)**。
3. SSL/TLS → Origin Server → Create Certificate:主机名默认含裸域+一级泛域,有效期拉满 15 年;证书块与私钥块存本地 `certs/<DOMAIN>/{fullchain,privkey}.pem`(600)。**必须在当前账号签**——旧账号/历史存档的 Origin 证书在 strict 下会 526(Invalid SSL certificate),跨账号无豁免。
4. 本地校验配对:`openssl x509 -in fullchain.pem -noout -modulus | md5` 与 `openssl rsa -in privkey.pem -noout -modulus | md5` 相等。
5. 服务器基础加固(BBR、ufw、fail2ban)与 3x-ui 安装:面板端口用 `<PPORT>`、basePath 随机化,密码只落 `/root/.panel-pass`。

### 1. NPM(docker compose)

```bash
mkdir -p /opt/nginx-proxy-manager && cd /opt/nginx-proxy-manager
cat > docker-compose.yml <<'EOF'
services:
  npm:
    image: jc21/nginx-proxy-manager:latest
    container_name: nginx-proxy-manager
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "127.0.0.1:81:81"
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
EOF
docker compose up -d
```

### 2. NPM v3 管理员 bootstrap(三表,缺一不可)

:latest(2026-10 实测)**不再 seed 默认管理员**,admin@example.com/changeme 登不进。且仅建 user+auth 能登录但 API 全 403(log 报 "reading 'visibility'")——`user_permission` 行缺失是根因(HasOne 关联,access.js 构造校验数据时空引用)。三件套:

```bash
DB=/opt/nginx-proxy-manager/data/database.sqlite
NPW=$(openssl rand -base64 18)
HASH=$(docker exec -e PW="$NPW" nginx-proxy-manager node -e 'console.log(require("bcrypt").hashSync(process.env.PW,10))')
NOW=$(date '+%Y-%m-%d %H:%M:%S')
sqlite3 $DB "insert into user (created_on,modified_on,is_deleted,is_disabled,email,name,nickname,avatar,roles) values ('$NOW','$NOW',0,0,'admin@<DOMAIN>','admin','admin','','[\"admin\"]');"
NEWUID=$(sqlite3 $DB "select max(id) from user;")   # 勿用 UID,bash 只读变量
sqlite3 $DB "insert into auth (created_on,modified_on,user_id,type,secret,meta,is_deleted) values ('$NOW','$NOW',$NEWUID,'password','$HASH','{}',0);"
sqlite3 $DB "insert into user_permission (created_on,modified_on,user_id,visibility,proxy_hosts,redirection_hosts,dead_hosts,streams,access_lists,certificates) values ('$NOW','$NOW',$NEWUID,'all','manage','manage','manage','manage','manage','manage');"
umask 077; printf 'NPM admin (http://127.0.0.1:81)\nemail: admin@<DOMAIN>\npassword: %s\n' "$NPW" > /root/npm-creds.txt
# 验证登录(只打 OK/FAIL,不回显 token)
curl -s -X POST http://127.0.0.1:81/api/tokens -H "Content-Type: application/json" -d "{\"identity\":\"admin@<DOMAIN>\",\"secret\":\"$NPW\"}" | jq -r 'if .token then "OK" else "FAIL" end'
```

### 3. VLESS+WS 入站(3x-ui,停服直写 db)

密钥服务器侧生成:

```bash
XRAY=/usr/local/x-ui/bin/$(ls /usr/local/x-ui/bin/ | grep -i '^xray' | head -1)
UUID=$($XRAY uuid); EMAIL=$(openssl rand -hex 5); WSPATH=/ws-$(openssl rand -hex 6); SUBID=$(openssl rand -hex 8)
umask 077; printf 'UUID=%s\nWSPATH=%s\nSUBID=%s\n' "$UUID" "$WSPATH" "$SUBID" > /root/ws-setup.env
```

此流程用直接写库配置入站,分享链接仍由 3x-ui 原生生成;写库前用 SQLite backup API 备份 `/etc/x-ui/x-ui.db`(备份权限 600)。已有入站排障时先读数据库和面板 API,保留现有字段,仅修改确认缺失或错误的值。

`systemctl stop x-ui` 后,python 参数化写库(**杜绝 bash 双引号吞 JSON 双引号**;**node_id 必须 NULL**):

```bash
UUID="$UUID" WSPATH="$WSPATH" EMAIL="$EMAIL" SUBID="$SUBID" python3 - <<'PY'
import sqlite3, json, os, time
U, WS, EM, SUB = os.environ['UUID'], os.environ['WSPATH'], os.environ['EMAIL'], os.environ['SUBID']
now = int(time.time()*1000)
db = sqlite3.connect('/etc/x-ui/x-ui.db'); cur = db.cursor()
if cur.execute("SELECT id FROM inbounds WHERE port=<WSPORT>").fetchone(): raise SystemExit("port exists")
settings = json.dumps({"clients":[{"id":U,"email":EM,"flow":"","enable":True,"expiryTime":0,"limitIp":0,"totalGB":0,"reset":0,"subId":SUB,"tgId":0,"security":"auto","comment":""}],"decryption":"none","fallbacks":[],"insecure":False})
stream = json.dumps({"network":"ws","security":"none","wsSettings":{"path":WS,"host":"<DOMAIN>","headers":{"Host":"<DOMAIN>"}},"externalProxy":[{"dest":"<DOMAIN>","port":443,"forceTls":"tls","sni":"<DOMAIN>","hostHeader":"<DOMAIN>","remark":"cf-cdn"}]})
sniff = json.dumps({"enabled":True,"destOverride":["http","tls","quic"]})   # 禁 fingerprint
cur.execute("INSERT INTO inbounds (user_id,up,down,total,remark,enable,listen,port,protocol,settings,stream_settings,tag,sniffing,node_id,share_addr_strategy,share_addr,disable_flow) VALUES (1,0,0,0,'vless-ws-cf',1,'',<WSPORT>,'vless',?,?,?,'inbound-<WSPORT>-ws',NULL,'node','',0)", (settings,stream,sniff))
iid = cur.lastrowid
cur.execute("INSERT INTO clients (email,sub_id,uuid,security,enable,created_at,updated_at) VALUES (?,?,?,'auto',1,?,?)", (EM,SUB,U,now,now))
cid = cur.lastrowid
cur.execute("INSERT INTO client_inbounds (client_id,inbound_id,flow_override,created_at) VALUES (?,?,'',?)", (cid,iid,now))
cur.execute("INSERT INTO client_traffics (inbound_id,enable,email,up,down,expiry_time,total) VALUES (?,1,?,0,0,0,0)", (iid,EM))
db.commit(); print("inbound=%d client=%d" % (iid,cid))
PY
systemctl start x-ui; sleep 8
ss -tlnp | grep ":<WSPORT> "          # 应为 xray-linux 主进程
jq -c '[.inbounds[]|select(.port==<WSPORT>)|{tag,clients:(.settings.clients|length)}]' /usr/local/x-ui/bin/config.json
```

### 4. NPM:证书 + proxy host(API)

上传证书(**字段名 certificate / certificate_key**,不是 cert_file):

```bash
TOK=$(curl -s -X POST http://127.0.0.1:81/api/tokens -H "Content-Type: application/json" -d "{\"identity\":\"admin@<DOMAIN>\",\"secret\":\"$(grep ^password /root/npm-creds.txt | cut -d' ' -f2)\"}" | jq -r .token)
CID=$(curl -s -X POST http://127.0.0.1:81/api/nginx/certificates -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" -d '{"provider":"other","nice_name":"<DOMAIN>-cf-origin"}' | jq -r .id)
curl -s -X POST http://127.0.0.1:81/api/nginx/certificates/$CID/upload -H "Authorization: Bearer $TOK" -F certificate=@certs/<DOMAIN>/fullchain.pem -F certificate_key=@certs/<DOMAIN>/privkey.pem
```

三个 host(证书全挂 `<CID>`;**先传证书再建 host**,顺序反了 conf 会写成 `.conf.err`):

```bash
mkhost(){ curl -s -X POST http://127.0.0.1:81/api/nginx/proxy-hosts -H "Authorization: Bearer $TOK" -H "Content-Type: application/json" -d "$1" | jq -r 'if .id then "OK "+(.domain_names|join(",")) else (.error.message) end'; }
# 1) 裸域 → xray ws(websocket 升级头靠 host 级 allow_websocket_upgrade)
mkhost "$(jq -nc --argjson cid $CID '{domain_names:["<DOMAIN>"],forward_scheme:"http",forward_host:"172.17.0.1",forward_port:<WSPORT>,certificate_id:$cid,ssl_forced:true,http2_support:true,hsts_enabled:false,caching_enabled:false,block_exploits:true,allow_websocket_upgrade:true,meta:{}}')"
# 2) nginx.<DOMAIN> → NPM 自身 UI(容器内 81)
mkhost "$(jq -nc --argjson cid $CID '{domain_names:["nginx.<DOMAIN>"],forward_scheme:"http",forward_host:"127.0.0.1",forward_port:81,certificate_id:$cid,ssl_forced:true,http2_support:true,hsts_enabled:false,caching_enabled:false,allow_websocket_upgrade:true,meta:{}}')"
# 3) xui.<DOMAIN> → 3x-ui 面板(容器→宿主机必须 172.17.0.1)
mkhost "$(jq -nc --argjson cid $CID '{domain_names:["xui.<DOMAIN>"],forward_scheme:"http",forward_host:"172.17.0.1",forward_port:<PPORT>,certificate_id:$cid,ssl_forced:true,http2_support:true,hsts_enabled:false,caching_enabled:false,allow_websocket_upgrade:true,meta:{}}')"
```

订阅可选(面板二维码外发需要):

```bash
# sub.<DOMAIN> → 订阅服务(2096)
mkhost "$(jq -nc --argjson cid $CID '{domain_names:["sub.<DOMAIN>"],forward_scheme:"http",forward_host:"172.17.0.1",forward_port:2096,certificate_id:$cid,ssl_forced:true,http2_support:true,hsts_enabled:false,caching_enabled:false,allow_websocket_upgrade:false,meta:{}}')"
ufw allow from 172.16.0.0/12 to any port 2096 proto tcp comment 'docker bridges to x-ui sub'
sqlite3 /etc/x-ui/x-ui.db "INSERT OR REPLACE INTO settings (key,value) VALUES ('subURI','https://sub.<DOMAIN>');"
```

若 conf 已成 `.conf.err`(证书晚到):上传成功后 `mv N.conf.err N.conf && docker exec nginx-proxy-manager nginx -s reload`(NPM 只在 host 增改删时重写 conf,重启不重生成)。

### 5. 防火墙

```bash
ufw allow 80/tcp comment 'NPM http'; ufw allow 443/tcp comment 'NPM https'
# 容器(compose 网在 172.18.x,docker0 是 172.17.0.1)访问宿主机服务必须放行整个私网段
ufw allow from 172.16.0.0/12 to any port <WSPORT> proto tcp comment 'docker bridges to x-ui xray ws'
ufw allow from 172.16.0.0/12 to any port <PPORT> proto tcp comment 'docker bridges to x-ui panel'
```

`<WSPORT>`/`<PPORT>` 公网默认 deny 即被挡(ufw default deny)。外部验证从干净视角测,勿在本机 `/dev/tcp/<ORIGIN_IP>/port` 自测——走 lo 恒通,结果失真。

### 6. 客户端与验收

先刷新面板,从 3x-ui 的复制/二维码获取实际分享链接,并读取订阅内容核对。下方仅为期望格式,手工拼出正确链接不能替代面板生成验收。通过后保存实际生成的直链(600 权限,`/root/vless-ws-link.txt` + 本地 `certs/<DOMAIN>/`):

```
vless://<UUID>@<DOMAIN>:443?encryption=none&security=tls&sni=<DOMAIN>&type=ws&host=<DOMAIN>&path=<WSPATH>#<DOMAIN>-ws-cf
```

- **分享生成验收**:解析面板实际复制的链接(也可用浏览器调用当前部署版本的原生生成函数;不要硬编码带 hash 的 JS 文件名),核对地址 `<DOMAIN>`、端口 `443`、`security=tls`、SNI/WS Host 均 `<DOMAIN>`、原 UUID 和 WS path。订阅输出同样核对;未通过不能宣称面板分享已修复。
- **TLS 分层**:后端入站仍是 `security=none`,公网客户端通过 NPM/CF 使用 TLS;`externalProxy` 描述公网入口。WS Host 从 `wsSettings.host` / `headers.Host` 读取,不能假定 `externalProxy.hostHeader` 会被分享生成器采用。
- v2rayN(Mac 或任意 Linux):剪贴板导入直链,或订阅分组导入 `https://sub.<DOMAIN><subPath><subId>`(base64 内容,面板二维码即此 URL)。
- CF 边缘:`curl -s -o /dev/null -w "%{http_code}\n" https://<DOMAIN>/`(400/404)、`https://nginx.<DOMAIN>/`(200)、`https://xui.<DOMAIN>/<basePath>/`(200)。
- ws 握手探测:带 `Connection: Upgrade`+`Upgrade: websocket` 打 `https://<DOMAIN><WSPATH>`,返 400 = 链路通(xray 拒假握手属预期)。
- **硬验收(唯一可信)**:目标网络环境跑真实内核,`https://www.google.com` 200 且出口 IP = `<ORIGIN_IP>`。无头复用法:取 v2rayN 的 xray 二进制,按链接参数生成 socks 入站 10888 的 client config,`curl --socks5-hostname 127.0.0.1:10888 https://www.google.com` + `https://ifconfig.me`。

### 7. 落盘与归档

凭据合并:`certs/<DOMAIN>/panel-creds.md` + 服务器 `/root/panel-creds.txt`(NPM 账密、3x-ui 账密、各 URL),600。为机器落档案 `docs/<主机名>-ws-tls-cf.md`(含密 → 当场追加 `.gitignore`)。

## 避坑点(全实测,源码级)

| # | 坑 | 解 |
|---|---|---|
| 1 | **3x-ui 手工插库被静默跳过、无日志**:`GetXrayConfig`(xray.go:197-206)对 `NodeID != nil` 的入站直接 continue,`node_id=0` 是非 nil 指针 | `node_id` 必须 **NULL**(=本机 xray 承载);建后核验 `ss` 监听 + config.json 含该入站 |
| 2 | 3x-ui 面板 API POST 一律 403 空体、账密对错都一样 | CSRF 中间件(security.go:52)在密码校验前拒;token 在 `/csrf-token` 响应的 **`.obj` 字段**不是 `.token`,取错等于没带 |
| 3 | NPM :latest 不 seed 管理员;只建 user+auth 则 API 全 403 | 三表:user + auth(bcrypt $2b$) + user_permission(visibility=all 全 manage) |
| 4 | NPM 证书上传报 "Certificate file was not provided" | multipart 字段名 `certificate`/`certificate_key` |
| 5 | host conf 写成 `.conf.err` 且重启不恢复 | 证书先于 host 上传;已成 .err 则传完后 rename + reload |
| 6 | nginx→宿主机服务超时 | 容器内目标写 `172.17.0.1`(非 127.0.0.1);ufw 放行 172.16.0.0/12 |
| 7 | CF 526(Invalid SSL certificate) | strict 下证书须当前账号签发/未吊销/SAN 匹配;旧账号证书重签即愈,或临时切 Full(非 strict) |
| 8 | NPM 反代 bing 类伪装站 502/400 | 模板硬编码 `Host $host` 改不了 + 变量式 proxy_pass 先试 AAAA(容器无 v6);放弃站点伪装,根路径落 xray |
| 9 | bash 双引号包 SQL 吞 JSON 双引号 → 语句截断 | python sqlite3 参数化,或 quoted heredoc 文件 |
| 10 | VLESS 入站用户列表来自 clients JOIN client_inbounds,settings JSON 内嵌 clients 会被覆写 | 三表插齐,email/uuid 一致;缺行=握手静默失败零日志 |
| 11 | 面板客户端二维码报 "no subId, no shareable link" | clients 表 `sub_id` 列 + settings JSON 的 subId 都要填;外链可达还需 `subURI` 指向经 NPM 的 sub 子域(2096 仅桥段放行)。订阅 URL 形状:`https://sub.<DOMAIN><subPath><subId>`(**无尾斜杠**,带尾斜杠 301) |
| 12 | 面板分享出现面板域名、后端端口、`security=none` 或空 WS Host,与 CDN 公网入口不符 | 先核对当前 DB、面板 API 和刷新后的实际链接;缺失时补 `externalProxy` 的 `dest/port/forceTls/sni/remark`,WS Host 配在 `wsSettings.host` 和 `headers.Host`。已有 externalProxy 时不要盲目重写,也不能未复现就断言页面缓存是原因。以第 6 节实际分享生成与端到端验收为准 |
| 13 | 用户在面板 UI 里重置/改了 UUID → 已落盘的直链失效(握手 403/失败) | 落盘链接以 db 为准刷新:`SELECT uuid FROM clients WHERE email=...` 重建 link 文件;或引导用户走订阅链接(自动跟随) |

## 运维备忘

- 证书续期:Origin CA 15 年免管;换证书只需重传 API + reload
- ws path/UUID 换新:改 db(stream_settings/settings + clients 表)→ 重启 x-ui → 重发链接
- 面板访问:`https://xui.<DOMAIN>/<basePath>/`(经 NPM)或 SSH 隧道直连 `<PPORT>`
- CF 侧排查顺序:526(证书)→ 521/522(源站 80/443 不通)→ 客户端 502(Flexible 模式,务必 Full 以上)
- WS 经 CF 有空闲超时(免费档不可调),客户端开心跳;CF 重启断连属正常

---

*公开分享版:已去除原私有环境引用,全部敏感值为占位符,替换后即可使用。原始版本以 agent skill 形式维护。*
