跳转至

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.tsTFRobotAdminPortal/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/ 下。

⚠️ 强制约束(维护性要求):

  1. 一域名一文件:所有业务 server {} 块必须独立成文件,命名为 /etc/nginx/conf.d/{domain}.conf(例如 user-staging.turingfocus.cn.conf)。
  2. 禁止写入 default.conf / nginx.conf:主配置中不得出现业务 server_name(如 *.turingfocus.cn)或业务 upstream/etc/nginx/nginx.conf 只保留 http { include /etc/nginx/conf.d/*.conf; } 及全局指令。
  3. 原因:环境清理(cleanup-env Skill)和单域名回滚都依赖这种"一文件对应一业务单元"的结构,一旦耦合到主配置就必须手动剥离才能继续。
  4. 耦合检查命令(部署前后都应执行):
    grep -iE 'server_name.*turingfocus' /etc/nginx/conf.d/default.conf /etc/nginx/nginx.conf 2>/dev/null
    # 预期无输出;若有输出说明违反约束,需立刻剥离到独立 conf 文件
    

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.cn127.0.0.1:4318

Bearer token 由 Manager 经 Infisical /otlp/{cluster_id}/PRIMARY_TOKEN 下发,Receiver 侧做 sha256 校验 + cluster_id 强制重写;契约详见 TFRM-119docs/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 三处都要动):

  1. OIDC 凭据生成(一次性):RSA 签名私钥 openssl genrsa 2048 | base64 -w0 + client_secret openssl 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_URIShttps://auth.<env-domain>/oauth2/callback
  2. 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)。
  3. 重启 admin-service:CNB 重新部署使 OIDC_* env 注入容器;启动日志应有「管理域 OIDC Provider 初始化成功 / 已挂载 loginPath=/oidc/login」。
  4. K8s 建 client_secret Secretkubectl -n tfr-mgmt-system create secret generic oauth2-proxy-client-secret --from-literal=client-secret=<与 OIDC_CLIENT_SECRET 同值>。Operator 经 clientSecretRef 读取。
  5. TFRCluster CR 填 OIDC 字段spec.managementoidc{issuerURL,clientID,clientSecretRef{name,key},scopes:[openid,profile,groups]} + hosts.authHost + cookieDomain(如 .enaic.turingfocus.cn)。
  6. ⚠️ AdminPortal 的 management-config 编辑目前不支持 oidc/authHost/cookieDomain 字段(TFRM-142 遗漏),须 kubectl edit tfrcluster <name> 直接改 CR。补齐后 Operator 自动渲染 oauth2-proxy Deployment + 改 EnvoyFilter Lua 为 httpCall 鉴权。
  7. 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),选用该证书。
  8. Operator 最低版本 0.5.1-dev10:低于此版本 oauth2-proxy 网关鉴权有已知阻断缺陷——
  9. < dev9:oauth2-proxy callback 500(buildArgs 缺 --oidc-email-claim,TFRM issuer 无 email claim,TFRO-63)。
  10. < 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

完成后进入下一步:基础设施部署