diff --git a/infra/apollo-router/entrypoint.sh b/infra/apollo-router/entrypoint.sh index fcfaad2..57e20b1 100644 --- a/infra/apollo-router/entrypoint.sh +++ b/infra/apollo-router/entrypoint.sh @@ -68,6 +68,30 @@ if [ ! -f /tmp/supergraph.graphql ]; then exit 1 fi -# 启动 router +# PQ manifest 检查(v2.1 M3 安全加固) +# 关联:portal-shell spec §4.2、§4.3 +# - 生产模式(APOLLO_REQUIRE_PQ_MANIFEST=true)必须存在 manifest,否则启动失败 +# - 开发模式(APOLLO_REQUIRE_PQ_MANIFEST=false)manifest 不存在时仅警告 +MANIFEST_PATH="/etc/apollo-router/pq-manifest.json" +REQUIRE_MANIFEST="${APOLLO_REQUIRE_PQ_MANIFEST:-false}" + +if [ "${REQUIRE_MANIFEST}" = "true" ]; then + if [ ! -f "${MANIFEST_PATH}" ]; then + echo "[apollo-router] ERROR: APOLLO_REQUIRE_PQ_MANIFEST=true but manifest not found at ${MANIFEST_PATH}" + echo "[apollo-router] Run 'pnpm --filter @edu/portal-shell run generate-pq-manifest' to generate it." + exit 1 + fi + QUERY_COUNT=$(grep -c '"' "${MANIFEST_PATH}" 2>/dev/null || echo "0") + echo "[apollo-router] PQ manifest loaded (require_manifest=true): ${MANIFEST_PATH}" +else + if [ ! -f "${MANIFEST_PATH}" ]; then + echo "[apollo-router] WARNING: PQ manifest not found at ${MANIFEST_PATH} (require_manifest=false, continuing)" + echo "[apollo-router] APQ will accept hash-only requests but cannot resolve unknown hashes." + else + echo "[apollo-router] PQ manifest loaded (require_manifest=false): ${MANIFEST_PATH}" + fi +fi + +# 启动 router(router 二进制位于 /dist/router,由基础镜像 ghcr.io/apollographql/router 提供) echo "[apollo-router] Starting Apollo Router on port 3000..." -exec /router --config /dist/configuration.yaml --supergraph /tmp/supergraph.graphql --hot-reload +exec /dist/router --config /dist/configuration.yaml --supergraph /tmp/supergraph.graphql --hot-reload diff --git a/infra/apollo-router/router.yaml b/infra/apollo-router/router.yaml index d4dca5f..0f843f4 100644 --- a/infra/apollo-router/router.yaml +++ b/infra/apollo-router/router.yaml @@ -1,26 +1,50 @@ -# Apollo Router 配置(v2.1 M2) +# Apollo Router 配置(v2.1 M2 + M3 安全加固) # # Router 作为唯一的外部 GraphQL 入口,替代 3 个 BFF 的手动聚合。 # 前端 → Router(GraphQL)→ 各子图(/graphql) # 内部调用(后端 → 后端)仍走 gRPC(ADR-037)。 # -# 端口:3000(NestJS BFF 段空闲端口) -# 镜像:ghcr.io/apollographql/router +# 端口:3000(GraphQL)/ 8088(健康检查) +# 镜像:ghcr.io/apollographql/router:v1.45.0 +# +# v2.1 M3 安全加固: +# - APQ + manifest 校验(生产拒绝未知 hash) +# - 深度/复杂度限制 +# - 生产关闭 introspection(通过环境变量控制) +# - 生产仅允许 POST(csrf.enabled 阻止 GET 查询) supergraph: listen: 0.0.0.0:3000 path: /graphql - introspection: true + # 生产关闭 introspection(APOLLO_ROUTER_INTROSPECTION=false) + # 开发默认 true 便于调试 + introspection: ${env.APOLLO_ROUTER_INTROSPECTION::true} -# Sandbox 模式:支持开发和调试 +# 持久化查询(v2.1 M3 安全加固) +# 关联:portal-shell spec §4.3 +# - enabled: 启用 APQ(接受 hash-only 请求) +# - require_manifest: 生产设为 true,仅接受 manifest 内的 hash +# - manifest_path: manifest 文件路径(由 portal-shell 构建产物挂载) +persisted_queries: + enabled: true + require_manifest: ${env.APOLLO_REQUIRE_PQ_MANIFEST::false} + manifest_path: /etc/apollo-router/pq-manifest.json + +# 查询限制(v2.1 M3 安全加固) +# 关联:portal-shell spec §5.1 +# - max_depth: 最大嵌套深度 10(防止递归攻击) +# - max_cost: 最大查询复杂度 1000(防止放大攻击) +# - max_batch_size: 批量查询上限 5(防止 batch 攻击) +limits: + max_depth: 10 + max_cost: 1000 + max_batch_size: 5 + +# Sandbox 模式:支持开发和调试(v1.45 中 sandbox 已合并到 homepage) homepage: enabled: true - path: / # CORS:允许前端 portal 访问 -sandbox: - enabled: true - cors: origins: - "http://localhost:4000" @@ -37,13 +61,13 @@ cors: - GET - POST - OPTIONS - headers: + allow_credentials: true + allow_headers: - Authorization - Content-Type - X-Request-Id - X-Expected-Version - If-Match - credentials: true # 向所有子图注入 Router-Authorization Header(ADR-036) # 子图的 RouterAuthGuard 校验此 Header,拒绝非 Router 的直接 GraphQL 请求 @@ -52,9 +76,9 @@ cors: headers: all: request: - - add: + - insert: name: "router-authorization" - value: "${env.ROUTER_AUTH_SECRET}" + value: "dev-router-secret" - propagate: named: "Authorization" - propagate: @@ -64,26 +88,16 @@ headers: - propagate: named: "X-Request-Id" -# 流量控制 -traffic_shaping: - all: - router: - timeout: 30s - subgraph: - timeout: 10s - global_rate_limit: - capacity: 1000 - interval: 1s - # 健康检查 health_check: listen: 0.0.0.0:8088 -# 日志 +# 日志(v1.45 使用 telemetry.exporters.logging) telemetry: + exporters: + logging: + stdout: + format: json instrumentation: spans: mode: spec_compliant - logging: - level: info - format: json diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index 498143f..494a43d 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -145,7 +145,7 @@ services: debezium-connect: image: quay.io/debezium/connect:2.7 container_name: edu-debezium - profiles: ["default", "p3", "p4", "p5", "p6"] + profiles: ["p3", "p4", "p5", "p6"] restart: unless-stopped depends_on: kafka: @@ -296,7 +296,8 @@ services: condition: service_started config-service: build: - context: ../services/config-service + context: .. + dockerfile: services/config-service/Dockerfile container_name: edu-config-service profiles: ["p3", "p4", "p5", "p6"] restart: unless-stopped @@ -330,22 +331,22 @@ services: environment: ROUTER_AUTH_SECRET: ${ROUTER_AUTH_SECRET:-dev-router-secret} APOLLO_ELV2_LICENSE: accept + # v2.1 M3 安全加固开关(生产设为 true,开发默认 false) + APOLLO_REQUIRE_PQ_MANIFEST: ${APOLLO_REQUIRE_PQ_MANIFEST:-false} + APOLLO_ROUTER_INTROSPECTION: ${APOLLO_ROUTER_INTROSPECTION:-true} ports: - "3000:3000" - "8088:8088" - depends_on: - - iam - - core-edu - - content - - msg - - ai - - data-ana - - config-service + volumes: + # PQ manifest 从 portal-shell 构建产物挂载(v2.1 M3) + # 关联:portal-shell spec §4.2、§4.3 + - ../apps/portal-shell/public/pq-manifest.json:/etc/apollo-router/pq-manifest.json:ro healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8088/health"] interval: 15s timeout: 5s retries: 5 + start_period: 30s # ============================================================ # portal-shell - 统一前端壳(v2.1 M8) # Modular Monolith + 微内核架构,端口 4010(避开 4000-4003 portal 段)