Nginx 与网络配置¶
适用于所有环境。完成 服务器基础设施初始化 后执行。
域名规划¶
| 服务 | Beta(当前使用正式域名) | Staging | Prod(待上线) |
|---|---|---|---|
| UserPortal(浏览器入口,BFF) | turingfocus.cn | user-staging.turingfocus.cn | turingfocus.cn |
| AdminPortal(浏览器入口,BFF) | admin.turingfocus.cn | admin-staging.turingfocus.cn | admin.turingfocus.cn |
| API 直通(机器客户端入口) | api.turingfocus.cn | api-staging.turingfocus.cn | api.turingfocus.cn |
| Infisical | secret.turingfocus.cn | secret-staging.turingfocus.cn | secret.turingfocus.cn |
| OTLP Receiver(计费事件入口,TFRM-119) | metrics.turingfocus.cn | metrics-staging.turingfocus.cn | metrics.turingfocus.cn |
| K8s 集群 | enaic.turingfocus.cn | staging.turingfocus.cn | 待定 |
| Grafana | grafana.enaic.turingfocus.cn | grafana.staging.turingfocus.cn | 待定 |
| Jaeger | jaeger.enaic.turingfocus.cn | jaeger.staging.turingfocus.cn | 待定 |
| 管理域 OIDC issuer(TFRM-143) | admin.turingfocus.cn/oidc |
admin-staging.turingfocus.cn/oidc |
admin.turingfocus.cn/oidc |
| 管理域 OIDC 登录入口 auth host(TFRO-61) | auth.enaic.turingfocus.cn | auth.staging.turingfocus.cn | 待定 |
注意: Prod 上线后,正式域名将从 Beta 迁移到 Prod,Beta 将切换为
*-beta.turingfocus.cn域名(包括api-beta.turingfocus.cn)。管理域 OIDC issuer 不新增域名:admin-service 在 admin 域名的
/oidc路径下暴露 OIDC Provider(TFRM-143,支撑被纳管集群 oauth2-proxy 对管理域 Jaeger 的网关鉴权,见 TFRO-57 方案 B)。issuer =https://admin{,-staging}.turingfocus.cn/oidc,须经 Nginx 直连 admin-service、绕过 BFF(见 §2.2)。管理域 OIDC 登录入口 auth host 是独立子域:oauth2-proxy 的
/oauth2/*(登录入口、callback)经 K8s 集群侧 Istio 网关暴露在auth.<env-domain>(与 Grafana/Jaeger 同集群入口、同 CLB),不在 admin 域名下。须为该子域单独配 DNS + TLS 证书 + CLB 转发规则(见 §2.7)。端到端落地清单见 §2.7。
入口职责区分(强制约束)¶
平台对外暴露两条独立的接入链路,浏览器与非浏览器客户端必须分流:
| 入口类型 | 域名形态 | 鉴权方式 | 适用客户端 |
|---|---|---|---|
| BFF 链路(浏览器) | user-* / admin-* / turingfocus.cn |
HttpOnly Cookie → BFF 注入 Authorization: Bearer 转发上游 |
浏览器(Web Portal) |
| API 直通链路(机器) | api-* / api.turingfocus.cn |
客户端直接携带 Authorization: Bearer <jwt> |
Tauri 桌面客户端、第三方集成、Webhook 回调、自动化脚本 |
背景:BFF 的 proxy 实现(
TFRobotFrontPortal/src/app/api/_lib/proxy.ts、TFRobotAdminPortal/src/api/base/ApiClient.ts)会重写 outbound headers——只读 cookie 注入Authorization,外部客户端发送的Authorization会被丢弃。同时 BFF 是按路由手工挂载的(非 catch-all),后端新接口不会自动暴露到 BFF 域名上。 因此凡是非浏览器场景,必须走 api 子域直连 user-service,不能走 user-* 域名。 历史背景:TFRB-124。
端口映射¶
所有服务端口绑定 127.0.0.1,仅通过 Nginx 反向代理对外暴露。
| 端口 | 服务 | 说明 |
|---|---|---|
| 80 / 443 | Nginx | 对外入口(0.0.0.0) |
| 3000 | UserPortal | Next.js 前端 |
| 3001 | AdminPortal | Next.js 前端 |
| 8080 | user-service | Go 后端 API |
| 8081 | admin-service | Go 后端 API(容器内 0.0.0.0) |
| 5432 | PostgreSQL | 数据库 |
| 6379 | Redis | 缓存 |
| 5433 | TimescaleDB | 计费事件 of record(TFRM-120) |
| 4318 | OTLP Receiver | 计费事件 OTLP/HTTP 入口(Nginx 反代,TFRM-119) |
| 8443 | Infisical | 密钥管理 |
1. 上传 SSL 证书¶
证书存放在项目仓库 certs/{env}/ 目录下(已 gitignore),按域名子目录组织。
# 本地打包
tar czf /tmp/certs.tar.gz -C certs/{env} .
# 通过 SSH MCP 上传到服务器
# upload /tmp/certs.tar.gz → /tmp/certs.tar.gz
# 服务器上解压
mkdir -p /etc/nginx/ssl
tar xzf /tmp/certs.tar.gz -C /etc/nginx/ssl/
rm -f /tmp/certs.tar.gz
证书目录结构(以 staging 为例):
/etc/nginx/ssl/
├── user-staging.turingfocus.cn_other/
│ ├── user-staging.turingfocus.cn_bundle.crt
│ └── user-staging.turingfocus.cn.key
├── admin-staging.turingfocus.cn_other/
├── secret-staging.turingfocus.cn_other/
├── metrics-staging.turingfocus.cn_other/
├── grafana.staging.turingfocus.cn_other/
└── jaeger.staging.turingfocus.cn_other/
2. Nginx 配置¶
每个域名一个独立配置文件,放在 /etc/nginx/conf.d/ 下。
⚠️ 强制约束(维护性要求):
- 一域名一文件:所有业务
server {}块必须独立成文件,命名为/etc/nginx/conf.d/{domain}.conf(例如user-staging.turingfocus.cn.conf)。- 禁止写入
default.conf/nginx.conf:主配置中不得出现业务server_name(如*.turingfocus.cn)或业务upstream。/etc/nginx/nginx.conf只保留http { include /etc/nginx/conf.d/*.conf; }及全局指令。- 原因:环境清理(
cleanup-envSkill)和单域名回滚都依赖这种"一文件对应一业务单元"的结构,一旦耦合到主配置就必须手动剥离才能继续。- 耦合检查命令(部署前后都应执行):
2.1 UserPortal¶
# {domain} → UserPortal (Next.js :3000)
server {
listen 80;
server_name {domain};
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name {domain};
ssl_certificate /etc/nginx/ssl/{cert_dir}/{domain}_bundle.crt;
ssl_certificate_key /etc/nginx/ssl/{cert_dir}/{domain}.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
# Streaming API:数字员工部署进度(长连接,禁用缓冲)
location ~ ^/api/v1/digital-employees/\d+/deploy-progress$ {
proxy_pass http://127.0.0.1:3000;
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;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
location / {
proxy_pass http://127.0.0.1:3000;
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;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
2.2 AdminPortal¶
与 UserPortal 相同结构,额外包含:
- 密钥分发 API:
/api/v1/secret/直接代理到 admin-service (:8081),绕过 Next.js BFF - Streaming API:集群初始化进度
/api/v1/admin/clusters/\d+/init-progress - 管理域 OIDC issuer(TFRM-143):
/oidc/直接代理到 admin-service (:8081),绕过 BFF
OIDC
/oidc/为何必须绕过 BFF: OIDC token 交换是 server-to-server(集群内 oauth2-proxy 直连 issuer 的/oidc/oauth/token、/oidc/keys、/oidc/.well-known/openid-configuration),而 AdminPortal BFF 会重写/丢弃Authorization头且按路由手工挂载(见 §域名规划下的背景说明),故 OIDC 链路不能经 BFF,须如/api/v1/secret/一样在 admin 域名 server 块内直连 admin-service。OIDC_ENABLED=false(默认)时该 location 仍可保留(admin-service 不挂载/oidc时返回 404,无副作用)。
在 admin 域名的 server { listen 443 ... } 块内,于 BFF 反代(location / → AdminPortal :3001)之前加入:
# —— http{} 上下文:登录端点限流区(防爆破,每 IP 5 req/s + 突发 10)——
# limit_req_zone $binary_remote_addr zone=oidc_login:10m rate=5r/s;
# admin 域名 server{} 内:
# 管理域 OIDC Provider(TFRM-143):直连 admin-service,绕过 BFF
# 覆盖 discovery / authorize / token / keys / userinfo / callback 与内置登录页(均在 /oidc/ 前缀下)
# 登录端点单独限流(公网认证面防暴力破解,与应用层 lockout 互补)
location = /oidc/login {
limit_req zone=oidc_login burst=10 nodelay;
proxy_pass http://127.0.0.1:8081;
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;
}
location /oidc/ {
proxy_pass http://127.0.0.1:8081;
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;
proxy_http_version 1.1;
proxy_connect_timeout 60s;
proxy_read_timeout 60s;
}
防爆破:
/oidc/login为公网认证面,须limit_req限速(上方zone=oidc_login,需在http{}声明)。应用侧 admin-service 对 OIDC 在途授权请求做惰性 TTL 清扫(10min 驱逐未完成请求),内存不会被构造的/authorize撑爆;登录失败计数沿用adminAuthService既有审计。X-Real-IP/X-Forwarded-For由此传入,admin-service 将真实源 IP 写入登录审计。可达性(须 Operator 侧确认): 被纳管 K8s 集群内的 oauth2-proxy 需能出公网访问
https://admin{,-staging}.turingfocus.cn/oidc/...(方向与 Manager→集群 K8s API 相反)。凭据与签名密钥:
OIDC_CLIENT_ID/OIDC_CLIENT_SECRET/OIDC_SIGNING_KEY(base64 编码 RSA 私钥 PEM)/OIDC_REDIRECT_URIS等经 Infisical 注入,详见 07-security.md。
2.3 API 直通子域(机器客户端入口)¶
为非浏览器客户端(Tauri / 第三方 / Webhook)提供 JWT Bearer 直通入口,完全 bypass BFF,请求直达 user-service:8080。每个环境一个独立配置文件 /etc/nginx/conf.d/api{,-staging,-beta}.turingfocus.cn.conf:
# {api_domain} → TFRSManager user-service (:8080) 直通
server {
listen 80;
server_name {api_domain};
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name {api_domain};
ssl_certificate /etc/nginx/ssl/{api_domain}_other/{api_domain}_bundle.crt;
ssl_certificate_key /etc/nginx/ssl/{api_domain}_other/{api_domain}.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CHACHA20-POLY1305;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options DENY always;
add_header X-Content-Type-Options nosniff always;
# Streaming API:数字员工部署进度(SSE,禁用缓冲)
location ~ ^/api/v1/digital-employees/\d+/deploy-progress$ {
proxy_pass http://127.0.0.1:8080;
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;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
location / {
proxy_pass http://127.0.0.1:8080;
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;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
DNS: 在 DNSPod 添加 A 记录
api{,-staging,-beta}.turingfocus.cn→ 对应环境服务器公网 IP。新增后端接口无需改 Nginx:
location /是全量代理到:8080,user-service 路由表是事实上的契约面,新接口自动可用。不暴露 admin-service: api 子域只直通 user-service。admin-service 仍只通过 admin-* 域名(且大部分走 BFF),避免管理面误暴露给外部客户端。
2.4 Infisical¶
简单反向代理到 :8443,支持 WebSocket。
2.5 OTLP Receiver(计费事件入口 · TFRM-119)¶
计费链路 of-record 入口:K8s 集群内 ns 共享 StatefulSet OTel Collector 经公网
OTLP/HTTP 推送计费事件日志(单一 /v1/logs,Bearer 鉴权)到管理平面 OTLP Receiver。
Nginx 反向代理 metrics{,-staging,-beta}.turingfocus.cn → 127.0.0.1:4318。
Bearer token 由 Manager 经 Infisical
/otlp/{cluster_id}/PRIMARY_TOKEN下发,Receiver 侧做 sha256 校验 +cluster_id强制重写;契约详见 TFRM-119 与docs/specs/otlp-receiver-contract.md。后端未就绪时同样可将 conf 改.disabled后缀。
# 禁用(后端未就绪时)
mv /etc/nginx/conf.d/metrics-*.conf /etc/nginx/conf.d/metrics-*.conf.disabled
# 启用(后端部署后)
mv /etc/nginx/conf.d/metrics-*.conf.disabled /etc/nginx/conf.d/metrics-*.conf
nginx -t && nginx -s reload
2.7 管理域 Jaeger OIDC 鉴权端到端落地清单(TFRO-57 / TFRO-61 / TFRM-143)¶
管理域 Jaeger 无自身鉴权,必须经 oauth2-proxy + TFRM OIDC 网关鉴权(方案 B)。 仅"代码已合并/镜像已部署"不等于鉴权生效——以下 7 步缺一不可,漏任一步要么登录走不通、要么 Jaeger 公网裸奔泄露全租户 trace。Grafana 走自身原生登录,不在本清单(仅需消除 auth.proxy header 注入 + 停用弱口令,见 TFRO-57)。
按顺序落地(管理平面 VM + 被纳管 K8s 集群 + 腾讯云 CLB 三处都要动):
- OIDC 凭据生成(一次性):RSA 签名私钥
openssl genrsa 2048 | base64 -w0+ client_secretopenssl rand -hex 32。写入 Infisical 的OIDC_*全套(OIDC_ENABLED/ISSUER_URL/CLIENT_ID/CLIENT_SECRET/REDIRECT_URIS/SIGNING_KEY/SIGNING_KEY_ID),详见 07-security.md §四。OIDC_REDIRECT_URIS填https://auth.<env-domain>/oauth2/callback。 - Nginx 暴露
/oidc/:admin 域名 server 块加location /oidc/直连 admin-service,绕过 BFF(见 §2.2)。验证curl https://admin{,-staging}.turingfocus.cn/oidc/.well-known/openid-configuration返回 200 JSON(不是 307→/login)。 - 重启 admin-service:CNB 重新部署使
OIDC_*env 注入容器;启动日志应有「管理域 OIDC Provider 初始化成功 / 已挂载 loginPath=/oidc/login」。 - K8s 建 client_secret Secret:
kubectl -n tfr-mgmt-system create secret generic oauth2-proxy-client-secret --from-literal=client-secret=<与 OIDC_CLIENT_SECRET 同值>。Operator 经clientSecretRef读取。 - TFRCluster CR 填 OIDC 字段:
spec.management补oidc{issuerURL,clientID,clientSecretRef{name,key},scopes:[openid,profile,groups]}+hosts.authHost+cookieDomain(如.enaic.turingfocus.cn)。 - ⚠️ AdminPortal 的 management-config 编辑目前不支持 oidc/authHost/cookieDomain 字段(TFRM-142 遗漏),须
kubectl edit tfrcluster <name>直接改 CR。补齐后 Operator 自动渲染 oauth2-proxy Deployment + 改 EnvoyFilter Lua 为 httpCall 鉴权。 - CLB 暴露 auth 子域(腾讯云控制台,与 Grafana/Jaeger 同 CLB 同后端):① DNS A 记录
auth.<env-domain>→ istio-ingress LB IP;② 申请覆盖该子域的 TLS 证书(单域名或*.<env-domain>通配),托管到腾讯云 SSL;③ CLB HTTPS:443 监听器新增域名auth.<env-domain>,转发规则/→ 与 Jaeger 相同的后端节点组(istio-ingress nodePort,如:31080),选用该证书。 - Operator 最低版本
0.5.1-dev10:低于此版本 oauth2-proxy 网关鉴权有已知阻断缺陷—— < dev9:oauth2-proxy callback 500(buildArgs 缺--oidc-email-claim,TFRM issuer 无 email claim,TFRO-63)。< dev10:登录后无限跳转登录页(Lua/oauth2/auth成功码判定==200但 oauth2-proxy 返回 202;回跳 rd 用:scheme=http 致 Secure cookie 不发,TFRO-64)。
端到端验收(全绿才算落地):
# 未登录访问 Jaeger → 302 到 auth host(不再 200 吐全租户 trace)
curl -s -o /dev/null -w '%{http_code}\n' https://jaeger.<env-domain>/api/services # 期望 302
# 旧裸域名(若仍有解析)→ 401/404,不泄露
curl -k -s -o /dev/null -w '%{http_code}\n' https://jaeger.turingfocus.cn/api/services # 期望 401
# 浏览器:https://jaeger.<env-domain>/search → OIDC 登录 → 回跳后正常进入 Jaeger UI(无循环)
即时止血(OIDC 链未就绪却已暴露 Jaeger 时):
kubectl patch tfrcluster <name> --type merge -p '{"spec":{"management":{"enabled":false}}}'关掉整个管理域,直到 7 步全部就绪。
3. 验证¶
# 检查配置语法
nginx -t
# 重载配置
nginx -s reload
# 验证 HTTPS 证书
curl -sv https://{domain}/ 2>&1 | grep "SSL certificate verify"
# 验证各域名(服务未启动时返回 502 是正常的,说明 Nginx 配置正确)
for domain in user-staging admin-staging secret-staging api-staging; do
code=$(curl -sk -o /dev/null -w "%{http_code}" https://${domain}.turingfocus.cn/)
echo "${domain}: ${code}"
done
# API 直通子域专项验证(user-service 已启动后)
# 预期:swagger 200(证书 OK)+ 业务接口 401(JWT 中间件生效,链路打通)
curl -s -o /dev/null -w "swagger=%{http_code}\n" https://api-staging.turingfocus.cn/swagger/index.html
curl -s -o /dev/null -w "auth_probe=%{http_code}\n" https://api-staging.turingfocus.cn/api/v1/digital-employees
完成后进入下一步:基础设施部署。