195 lines
5.6 KiB
Markdown
195 lines
5.6 KiB
Markdown
|
|
# vNext.4 Trusted-Subject 安全链部署指南
|
|||
|
|
|
|||
|
|
> 解决 2026-06-08 EXECUTION_BOARD.md 中记录的线上阻塞问题
|
|||
|
|
|
|||
|
|
## 问题描述
|
|||
|
|
|
|||
|
|
remote43 当前 nginx `/portal-admin-api/` 未注入 `X-CRM-Authenticated-Subject` / `X-CRM-Trusted-Proxy`,导致无法在现网安全完成新的 user-key 真验闭环。
|
|||
|
|
|
|||
|
|
## 解决方案
|
|||
|
|
|
|||
|
|
实施受信代理安全链:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
用户浏览器 ← → Portal 前端 ← → nginx (cookie→header 转换) ← → CRM
|
|||
|
|
↑ ↓
|
|||
|
|
设置 httpOnly cookie 验证并注入受信 header
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 所需变更
|
|||
|
|
|
|||
|
|
### 1. CRM 二进制更新 (已完成)
|
|||
|
|
|
|||
|
|
新增文件:
|
|||
|
|
|
|||
|
|
- `internal/app/portal_auth.go` - Portal user session 认证模块
|
|||
|
|
- `internal/app/portal_auth_test.go` - 测试用例
|
|||
|
|
|
|||
|
|
变更文件:
|
|||
|
|
|
|||
|
|
- `internal/app/http_api.go` - 添加 `/api/portal/session/*` 路由
|
|||
|
|
- `internal/app/bootstrap.go` - 传递 trusted proxy secret
|
|||
|
|
- `deploy/tksea-portal/index.html` - 添加 CRM session 登录/登出
|
|||
|
|
- `deploy/tksea-portal/nginx.sub.tksea.top.conf.example` - nginx 配置模板
|
|||
|
|
- `.env.example` - 环境变量模板
|
|||
|
|
|
|||
|
|
### 2. remote43 部署步骤
|
|||
|
|
|
|||
|
|
#### 步骤 1: 生成共享密钥
|
|||
|
|
|
|||
|
|
在 remote43 上执行:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
TRUSTED_PROXY_SECRET=$(openssl rand -hex 32)
|
|||
|
|
echo "Generated secret: $TRUSTED_PROXY_SECRET"
|
|||
|
|
# 保存此密钥,需要同时配置到 nginx 和 CRM
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 步骤 2: 更新 CRM 配置
|
|||
|
|
|
|||
|
|
编辑 `/home/ubuntu/.env.crm`(或实际运行目录):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 在文件末尾添加:
|
|||
|
|
# Trusted Subject Proxy Configuration
|
|||
|
|
SUB2API_CRM_TRUSTED_SUBJECT_HEADER=X-CRM-Authenticated-Subject
|
|||
|
|
SUB2API_CRM_TRUSTED_PROXY_SECRET_HEADER=X-CRM-Trusted-Proxy
|
|||
|
|
SUB2API_CRM_TRUSTED_PROXY_SECRET=<步骤1生成的64字符密钥>
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 步骤 3: 更新 nginx 配置
|
|||
|
|
|
|||
|
|
编辑 `/etc/nginx/sites-enabled/sub.tksea.top.conf`:
|
|||
|
|
|
|||
|
|
在 `server` 块内添加:
|
|||
|
|
|
|||
|
|
```nginx
|
|||
|
|
# 从 httpOnly cookie 提取 portal subject(放在 server 块内)
|
|||
|
|
map $http_cookie $portal_subject {
|
|||
|
|
default "";
|
|||
|
|
~*crm_session=([^;]+) $1;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
修改 `/portal-admin-api/` location:
|
|||
|
|
|
|||
|
|
```nginx
|
|||
|
|
location /portal-admin-api/ {
|
|||
|
|
proxy_pass http://127.0.0.1:18190/;
|
|||
|
|
proxy_set_header Host $host;
|
|||
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|||
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|||
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|||
|
|
|
|||
|
|
# 关键:从验证过的 cookie 提取并注入 subject
|
|||
|
|
proxy_set_header X-CRM-Authenticated-Subject $portal_subject;
|
|||
|
|
# 受信代理密钥(必须与 CRM 配置一致)
|
|||
|
|
proxy_set_header X-CRM-Trusted-Proxy "<步骤1生成的64字符密钥>";
|
|||
|
|
|
|||
|
|
proxy_http_version 1.1;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**注意**:
|
|||
|
|
|
|||
|
|
- 删除原来的 `proxy_set_header X-Portal-Subject "";` 行
|
|||
|
|
- 确保密钥替换为实际生成的 64 字符 hex 字符串
|
|||
|
|
|
|||
|
|
#### 步骤 4: 重启服务
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 测试 nginx 配置
|
|||
|
|
sudo nginx -t
|
|||
|
|
|
|||
|
|
# 重载 nginx
|
|||
|
|
sudo systemctl reload nginx
|
|||
|
|
|
|||
|
|
# 重启 CRM
|
|||
|
|
sudo systemctl restart crm
|
|||
|
|
# 或使用:
|
|||
|
|
# pkill -f "crm" && cd /home/ubuntu && ./server &
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### 步骤 5: 验证
|
|||
|
|
|
|||
|
|
浏览器测试:
|
|||
|
|
|
|||
|
|
1. 访问 `https://sub.tksea.top/portal/`
|
|||
|
|
2. 登录(会同时设置 CRM session cookie)
|
|||
|
|
3. 打开浏览器 DevTools → Application → Cookies
|
|||
|
|
4. 确认看到 `crm_session` cookie(httpOnly)和 `crm_subject` cookie
|
|||
|
|
5. 尝试创建/管理用户 Key,应该可以正常工作
|
|||
|
|
|
|||
|
|
API 测试:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# 1. 登录获取 session cookie
|
|||
|
|
curl -c cookies.txt -X POST https://sub.tksea.top/portal-admin-api/api/portal/session/login \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"email":"test@example.com"}'
|
|||
|
|
|
|||
|
|
# 2. 使用 cookie 访问 user-key API
|
|||
|
|
curl -b cookies.txt https://sub.tksea.top/portal-admin-api/api/keys
|
|||
|
|
|
|||
|
|
# 3. 创建新 key
|
|||
|
|
curl -b cookies.txt -X POST https://sub.tksea.top/portal-admin-api/api/keys \
|
|||
|
|
-H "Content-Type: application/json" \
|
|||
|
|
-d '{"key_name":"test-key","logical_group_id":"gpt-shared"}'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 3. 故障排除
|
|||
|
|
|
|||
|
|
#### CRM 返回 `unauthorized` / `trusted proxy authentication required`
|
|||
|
|
|
|||
|
|
- 检查 `.env.crm` 中的 `SUB2API_CRM_TRUSTED_PROXY_SECRET` 是否正确设置
|
|||
|
|
- 检查 nginx 中的 `X-CRM-Trusted-Proxy` header 值是否一致
|
|||
|
|
- 检查两个密钥是否完全匹配(无多余空格)
|
|||
|
|
|
|||
|
|
#### CRM 返回 `trusted subject header required`
|
|||
|
|
|
|||
|
|
- 检查 nginx 是否正确添加了 `map $http_cookie $portal_subject`
|
|||
|
|
- 检查浏览器是否有 `crm_session` cookie(登录后应该自动设置)
|
|||
|
|
- 检查 cookie 是否被浏览器阻止(SameSite/Secure 设置)
|
|||
|
|
|
|||
|
|
#### Portal 前端无法登录 CRM session
|
|||
|
|
|
|||
|
|
- 检查浏览器 console 是否有 CORS 错误
|
|||
|
|
- 检查 `/portal-admin-api/` location 是否正确配置
|
|||
|
|
- 确认 CRM 服务正在监听 `127.0.0.1:18190`
|
|||
|
|
|
|||
|
|
## 安全配置建议
|
|||
|
|
|
|||
|
|
1. **密钥管理**
|
|||
|
|
- 使用 `openssl rand -hex 32` 生成强密钥
|
|||
|
|
- 不要在任何地方记录或提交密钥
|
|||
|
|
- 考虑使用 HashiCorp Vault 或 AWS Secrets Manager
|
|||
|
|
|
|||
|
|
2. **HTTPS**
|
|||
|
|
- 生产环境必须启用 HTTPS
|
|||
|
|
- 设置 `Secure` flag 在 cookies 上
|
|||
|
|
|
|||
|
|
3. **Cookie 设置**
|
|||
|
|
- `crm_session`: httpOnly, SameSite=Lax, Secure (HTTPS only)
|
|||
|
|
- `crm_subject`: SameSite=Lax, Secure (HTTPS only)
|
|||
|
|
|
|||
|
|
## 回滚计划
|
|||
|
|
|
|||
|
|
如果需要回滚:
|
|||
|
|
|
|||
|
|
1. 还原 nginx 配置(删除 map 和 header 设置)
|
|||
|
|
2. 还原 `.env.crm`(移除 TRUSTED\_\* 配置)
|
|||
|
|
3. 重载 nginx / 重启 CRM
|
|||
|
|
|
|||
|
|
portal 会回退到之前的 bearer token 认证模式。
|
|||
|
|
|
|||
|
|
## 验证清单
|
|||
|
|
|
|||
|
|
- [ ] 生成并记录了 64 字符 hex secret
|
|||
|
|
- [ ] 更新了 `.env.crm` 配置
|
|||
|
|
- [ ] 更新了 nginx 配置
|
|||
|
|
- [ ] 重载了 nginx 配置
|
|||
|
|
- [ ] 重启了 CRM 服务
|
|||
|
|
- [ ] 浏览器测试通过(可以看到 crm_session cookie)
|
|||
|
|
- [ ] API 测试通过(可以创建 user-key)
|
|||
|
|
- [ ] 完整链路测试通过(create → chat → pause → resume → delete)
|