Skip to content

Commit 905b962

Browse files
committed
security(server): nginx 安全头片段化与 compose 强制配置校验
1 parent b66efe9 commit 905b962

5 files changed

Lines changed: 179 additions & 30 deletions

File tree

‎server/.env.production.example‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# ===========================================
2+
# entropy-decrease 生产环境配置模板
3+
# ===========================================
4+
# 使用方法:复制为 .env.production 并填入真实值
5+
# cp .env.production.example .env.production
6+
# 所有 CHANGE_ME 占位符必须在部署前替换为真实值(deploy.sh 会硬校验)。
7+
# .env.production 含敏感信息,已被 .gitignore 排除,切勿提交到 Git。
8+
9+
# --- 应用环境 ---
10+
APP_ENV=production
11+
12+
# --- 数据库配置 ---
13+
# DB_USER: PostgreSQL 用户名
14+
DB_USER=CHANGE_ME
15+
# DB_PASSWORD: PostgreSQL 密码(使用强随机密码,如 openssl rand -base64 32)
16+
DB_PASSWORD=CHANGE_ME
17+
# DB_NAME: 数据库名称
18+
DB_NAME=CHANGE_ME
19+
20+
# --- Redis 配置 ---
21+
# REDIS_PASSWORD: Redis 认证密码(使用强随机密码;docker-compose.prod.yml
22+
# 使用 :? 语法强制要求显式配置,缺省即拒绝启动)
23+
REDIS_PASSWORD=CHANGE_ME
24+
25+
# --- JWT 安全密钥(生成方法: openssl rand -hex 32)---
26+
JWT_SECRET=CHANGE_ME
27+
28+
# --- AI 模型 API Key ---
29+
# 通义千问:https://dashscope.aliyun.com
30+
QWEN_API_KEY=CHANGE_ME
31+
32+
# DeepSeek:https://platform.deepseek.com
33+
DEEPSEEK_API_KEY=CHANGE_ME
34+
35+
# 智谱 GLM:https://open.bigmodel.cn
36+
GLM_API_KEY=CHANGE_ME
37+
38+
# Gemini(用于视频分析功能):https://aistudio.google.com/app/apikey
39+
GEMINI_API_KEY=CHANGE_ME
40+
41+
# --- Supabase(如使用)---
42+
# 从 Supabase Dashboard > Settings > API > JWT Settings 获取 JWT Signing Secret(非 anon key)
43+
SUPABASE_JWT_SECRET=CHANGE_ME
44+
# 项目 URL:从 Supabase Dashboard > Settings > API 获取
45+
SUPABASE_URL=CHANGE_ME
46+
# JWKS URL:通常为 <SUPABASE_URL>/auth/v1/.well-known/jwks.json
47+
SUPABASE_JWKS_URL=CHANGE_ME
48+
49+
# --- CORS 允许的域名(多个用逗号分隔)---
50+
# 生产环境仅保留正式域名,禁止使用通配符 *(compose 强制要求显式配置)
51+
# https://entropydecrease.com —— 官网主域名(HTTPS),用于网站前端请求 AI 网关与同步服务
52+
# http://entropydecrease.com —— HTTP 回退域名,兼容未启用 HTTPS 的过渡期流量
53+
CORS_ORIGINS=https://entropydecrease.com,http://entropydecrease.com
54+
55+
# --- AI 网关开发者白名单(最高权限身份,切勿填普通用户)---
56+
DEV_USER_IDS=dev@example.com

‎server/deploy.sh‎

Lines changed: 59 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
# =============================================================================
33
# entropy-decrease 后端服务一键部署脚本
44
# 用法:chmod +x deploy.sh && ./deploy.sh
5+
#
6+
# 重部署前置条件:必须在 .env.production 中补齐 REDIS_PASSWORD 与 CORS_ORIGINS——
7+
# docker-compose.prod.yml 对二者使用 ${VAR:?...} 强制插值,缺失会在 compose
8+
# 阶段报难读错误;本脚本在第 2 节提前做可读校验(三维复审 #10 护栏)。
59
# =============================================================================
610

7-
set -e
11+
# 严格模式:未定义变量/命令失败/管道失败均立即退出,避免静默错误继续部署
12+
set -euo pipefail
813

914
RED='\033[0;31m'
1015
GREEN='\033[0;32m'
@@ -53,15 +58,33 @@ if [ ! -f "$ENV_FILE" ]; then
5358
exit 1
5459
fi
5560

56-
# 检查关键配置是否已修改
61+
# 检查关键配置是否已修改(硬失败:生产环境禁止带占位符部署,不提供跳过选项)
5762
if grep -q "CHANGE_ME" "$ENV_FILE"; then
58-
log_warn "检测到 $ENV_FILE 中仍有 CHANGE_ME 占位符"
59-
echo " 请编辑 $ENV_FILE,替换所有 CHANGE_ME 为真实值"
60-
read -p " 是否继续?(y/N) " -n 1 -r
61-
echo
62-
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
63-
exit 1
64-
fi
63+
log_error "检测到 $ENV_FILE 中仍有 CHANGE_ME 占位符,部署已中止"
64+
echo " 请编辑 $ENV_FILE,替换所有 CHANGE_ME 为真实值后重新运行"
65+
exit 1
66+
fi
67+
68+
# compose 强制项迁移护栏(三维复审 #10):REDIS_PASSWORD / CORS_ORIGINS 在
69+
# docker-compose.prod.yml 中以 ${VAR:?...} 强制插值——变量缺失时 compose 阶段
70+
# 才报错且信息难读;此处提前 source 校验,转为可读提示快速失败。
71+
# 临时放宽 -u:env 文件中可能存在 ${VAR} 自引用占位行,source 时不应因
72+
# 未定义变量中止(与第 7 节 Redis 健康检查同口径)
73+
set +u
74+
set -a
75+
# shellcheck source=/dev/null
76+
source "$ENV_FILE"
77+
set +a
78+
set -u
79+
80+
MISSING_VARS=""
81+
if [ -z "${REDIS_PASSWORD:-}" ]; then MISSING_VARS="$MISSING_VARS REDIS_PASSWORD"; fi
82+
if [ -z "${CORS_ORIGINS:-}" ]; then MISSING_VARS="$MISSING_VARS CORS_ORIGINS"; fi
83+
if [ -n "$MISSING_VARS" ]; then
84+
log_error "$ENV_FILE 缺少 compose 强制配置:$MISSING_VARS"
85+
echo " 请编辑 $ENV_FILE 补齐上述变量后重新部署"
86+
echo " (docker-compose.prod.yml 以 \${VAR:?} 强制要求,缺失将无法启动)"
87+
exit 1
6588
fi
6689

6790
log_info "$ENV_FILE 已就绪 ✓"
@@ -143,12 +166,34 @@ else
143166
ERRORS=$((ERRORS + 1))
144167
fi
145168

146-
# Redis
147-
if docker exec entropy-decrease-redis redis-cli -a "$(grep REDIS_PASSWORD "$ENV_FILE" | cut -d= -f2)" ping > /dev/null 2>&1; then
148-
log_info "Redis ✓ 运行正常"
149-
else
150-
log_error "Redis ✗ 未就绪"
169+
# Redis:密码经 REDISCLI_AUTH 环境变量注入容器(redis-cli 官方支持的认证方式),
170+
# 完全不在宿主机/容器命令行出现密码参数(-a 方式在进程列表可见;
171+
# --env VAR 无值形式从当前进程环境透传,同样不落命令行)
172+
# 临时放宽 -u:env 文件中可能存在 ${VAR} 自引用占位行,source 时不应因
173+
# 未定义变量中止(本处仅需 REDIS_PASSWORD)
174+
set +u
175+
set -a
176+
# shellcheck source=/dev/null
177+
source "$ENV_FILE"
178+
set +a
179+
set -u
180+
181+
if [ -z "${REDIS_PASSWORD:-}" ]; then
182+
log_error "Redis ✗ REDIS_PASSWORD 未在 $ENV_FILE 中配置"
151183
ERRORS=$((ERRORS + 1))
184+
else
185+
# 三维复审 #1 修复:--env REDISCLI_AUTH(无值形式)仅从**当前进程环境**透传,
186+
# 而 source 出的变量名为 REDIS_PASSWORD——此前 REDISCLI_AUTH 在环境中不存在,
187+
# docker 静默丢弃该 --env,redis-cli 无认证必然 NOAUTH 失败。
188+
# 显式 export 把密码映射到 REDISCLI_AUTH 后,现有 --env 透传写法即生效。
189+
export REDISCLI_AUTH="$REDIS_PASSWORD"
190+
if docker exec --env REDISCLI_AUTH entropy-decrease-redis \
191+
redis-cli ping > /dev/null 2>&1; then
192+
log_info "Redis ✓ 运行正常"
193+
else
194+
log_error "Redis ✗ 未就绪"
195+
ERRORS=$((ERRORS + 1))
196+
fi
152197
fi
153198

154199
# sync-service

‎server/docker-compose.prod.yml‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ services:
4242
restart: unless-stopped
4343
ports:
4444
- "127.0.0.1:6379:6379"
45-
command: redis-server --requirepass ${REDIS_PASSWORD:-entropy-decrease_redis_prod}
45+
# 生产环境禁止默认密码回退:REDIS_PASSWORD 必须在 .env.production 显式配置,缺省即拒绝启动
46+
command: redis-server --requirepass ${REDIS_PASSWORD:?REDIS_PASSWORD 必须在 .env.production 显式配置}
4647
volumes:
4748
- redis_data:/data
4849
healthcheck:
49-
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-entropy-decrease_redis_prod}", "ping"]
50+
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:?REDIS_PASSWORD 必须在 .env.production 显式配置}", "ping"]
5051
interval: 10s
5152
timeout: 5s
5253
retries: 5
@@ -67,13 +68,15 @@ services:
6768
container_name: entropy-decrease-sync-service
6869
restart: unless-stopped
6970
ports:
70-
- "0.0.0.0:8080:8080"
71+
# 仅绑定宿主机回环:对外流量统一由 nginx 同宿主反代(80/443),
72+
# 避免内部服务端口直接暴露公网
73+
- "127.0.0.1:8080:8080"
7174
environment:
7275
PORT: "8080"
7376
APP_ENV: production
7477
GIN_MODE: release
7578
DATABASE_URL: postgres://${DB_USER:-entropy-decrease}:${DB_PASSWORD}@postgres:5432/${DB_NAME:-entropy-decrease}
76-
REDIS_URL: redis://:${REDIS_PASSWORD:-entropy-decrease_redis_prod}@redis:6379/0
79+
REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD 必须在 .env.production 显式配置}@redis:6379/0
7780
JWT_SECRET: ${JWT_SECRET}
7881
SUPABASE_JWT_SECRET: ${SUPABASE_JWT_SECRET:-}
7982
depends_on:
@@ -104,15 +107,17 @@ services:
104107
container_name: entropy-decrease-ai-gateway
105108
restart: unless-stopped
106109
ports:
107-
- "0.0.0.0:8000:8000"
110+
# 仅绑定宿主机回环:对外流量统一由 nginx 同宿主反代(80/443),
111+
# 避免内部服务端口直接暴露公网
112+
- "127.0.0.1:8000:8000"
108113
environment:
109114
APP_ENV: production
110115
QWEN_API_KEY: ${QWEN_API_KEY:-}
111116
DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY:-}
112117
GLM_API_KEY: ${GLM_API_KEY:-}
113118
# Gemini API 密钥:用于视频分析功能(多模态视觉理解),从 .env.production 注入
114119
GEMINI_API_KEY: ${GEMINI_API_KEY:-}
115-
REDIS_URL: redis://:${REDIS_PASSWORD:-entropy-decrease_redis_prod}@redis:6379/1
120+
REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD 必须在 .env.production 显式配置}@redis:6379/1
116121
SYNC_SERVICE_URL: http://sync-service:8080
117122
# JWT 验证密钥:网关校验请求方身份(与 sync-service 同一套签发/验证密钥)
118123
JWT_SECRET: ${JWT_SECRET}
@@ -136,7 +141,8 @@ services:
136141
GATEWAY_ALLOW_DEV_AUTH: "false"
137142
# OpenTelemetry 导出(可选)
138143
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
139-
CORS_ORIGINS: ${CORS_ORIGINS:-*}
144+
# 生产环境禁止通配符回退:CORS 白名单必须在 .env.production 显式配置
145+
CORS_ORIGINS: ${CORS_ORIGINS:?CORS_ORIGINS 必须在 .env.production 显式配置(禁止通配符 *)}
140146
depends_on:
141147
redis:
142148
condition: service_healthy
@@ -166,6 +172,9 @@ services:
166172
- "443:443"
167173
volumes:
168174
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
175+
# 安全响应头统一片段(nginx.conf 内 include conf.d/security_headers.conf,
176+
# 相对路径基于 nginx 前缀 /etc/nginx 解析,故挂载到 conf.d/ 下)
177+
- ./nginx/conf.d/security_headers.conf:/etc/nginx/conf.d/security_headers.conf:ro
169178
- ./nginx/certs:/etc/nginx/certs:ro
170179
- /opt/Entropydecrease/website:/usr/share/nginx/html:ro
171180
# 安装包下载目录:独立于 website(其部署会 rm 清空目录),由 release.yml 同步
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# =============================================================================
2+
# 安全响应头统一片段(可 include 复用)
3+
# =============================================================================
4+
# Why: nginx 的 add_header 继承规则——location 内一旦出现任何 add_header,
5+
# 就不再继承 server 级的 add_header。因此所有自带 add_header 的 location
6+
# (缓存头/CORS 头等)必须显式 include 本片段,否则安全头会在这些路径丢失。
7+
# 新增带 add_header 的 location 时,务必同步 include 本文件。
8+
9+
# HSTS:强制浏览器后续全部走 HTTPS(仅应在 HTTPS server 块内 include)
10+
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
11+
# 防点击劫持:禁止被第三方站点以 iframe 嵌入
12+
add_header X-Frame-Options "SAMEORIGIN" always;
13+
# 防 MIME 嗅探:禁止浏览器猜测 Content-Type
14+
add_header X-Content-Type-Options "nosniff" always;
15+
# 限制 Referer 泄露:跨站请求仅携带 origin
16+
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
17+
# 内容安全策略:官网为静态站(Next.js 静态导出),采用保守基线——
18+
# 默认仅允许同源;样式允许同源与内联(Next.js 静态导出含内联 style);
19+
# 图片允许同源与 data:(少量 base64 图标);脚本仅同源(无第三方脚本)。
20+
# 后续如需引入第三方资源,按需最小化放宽,勿回退为 unsafe-eval/*。
21+
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;
22+
# 注:X-XSS-Protection 已被现代浏览器废弃且可引入新攻击面,不再启用。

‎server/nginx/nginx.conf‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ http {
4747
# HTTP → HTTPS 重定向
4848
server {
4949
listen 80;
50+
# 公网 IP 保留在 server_name:用于 IP 直连访问/域名验证场景(如 CDN 归属验证、
51+
# 证书签发前的连通性测试)命中本 server 块而非默认块;删除会导致此类请求落入
52+
# 错误 server 块,故保留不删
5053
server_name entropydecrease.com www.entropydecrease.com 121.40.24.242;
5154

5255
# 域名归属验证与 ACME http-01 挑战必须在 HTTP 下直接可达:
@@ -102,12 +105,10 @@ http {
102105
ssl_session_cache shared:SSL:10m;
103106
ssl_session_timeout 10m;
104107

105-
# 安全头
106-
add_header X-Frame-Options "SAMEORIGIN" always;
107-
add_header X-Content-Type-Options "nosniff" always;
108-
add_header X-XSS-Protection "1; mode=block" always;
109-
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
110-
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
108+
# 安全头(统一片段,含 HSTS/X-Frame-Options/nosniff/Referrer-Policy/CSP)
109+
# 注意:下方任何自带 add_header 的 location 都不会继承此处,
110+
# 必须各自 include 该片段(见 conf.d/security_headers.conf 头部说明)
111+
include conf.d/security_headers.conf;
111112

112113
# AI 网关健康检查(转发到 ai-gateway,不可用时降级为 Nginx 自检)
113114
location /health {
@@ -131,7 +132,10 @@ http {
131132
# 健康检查降级 fallback(ai-gateway 不可达时由 Nginx 直接响应)
132133
# 命名 location 不继承 server 级 add_header,且不经过 FastAPI CORSMiddleware,必须自带 CORS 头
133134
location @health_fallback {
134-
add_header Access-Control-Allow-Origin "*" always;
135+
# 命名 location 内出现 add_header 即不再继承 server 级安全头,需显式 include
136+
include conf.d/security_headers.conf;
137+
# CORS 收敛为官网主域(与 CORS_ORIGINS 一致),不再允许任意来源
138+
add_header Access-Control-Allow-Origin "https://entropydecrease.com" always;
135139
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
136140
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
137141
return 200 '{"status":"degraded","service":"ai-gateway","version":"unknown","error":"gateway unreachable"}';
@@ -152,6 +156,8 @@ http {
152156
proxy_read_timeout 300s;
153157
# SSE 必需配置:禁用缓存
154158
proxy_cache off;
159+
# 本 location 自带 add_header,不再继承 server 级安全头,需显式 include
160+
include conf.d/security_headers.conf;
155161
add_header X-Accel-Buffering no;
156162
}
157163

@@ -255,10 +261,13 @@ http {
255261
limit_conn dl_total 64;
256262

257263
# 更新元数据(latest.yml / latest.json)禁止缓存,
258-
# 否则客户端会长期读到旧版本信息;允许 www 子域跨域读取
264+
# 否则客户端会长期读到旧版本信息;仅允许官网域跨域读取
259265
location ~* \.(yml|json)$ {
266+
# 本 location 自带 add_header,不再继承 server 级安全头,需显式 include
267+
include conf.d/security_headers.conf;
260268
add_header Cache-Control "no-cache" always;
261-
add_header Access-Control-Allow-Origin "*" always;
269+
# CORS 收敛为官网主域(www 已 301 至主域,无需额外放行)
270+
add_header Access-Control-Allow-Origin "https://entropydecrease.com" always;
262271
}
263272
}
264273

@@ -274,15 +283,19 @@ http {
274283
location ^~ /pwa/assets/ {
275284
root /usr/share/nginx;
276285
expires 365d;
286+
# 本 location 自带 add_header,不再继承 server 级安全头,需显式 include
287+
include conf.d/security_headers.conf;
277288
add_header Cache-Control "public, immutable";
278289
access_log off;
279290
}
280291
location = /pwa/manifest.webmanifest {
281292
root /usr/share/nginx;
293+
include conf.d/security_headers.conf;
282294
add_header Cache-Control "no-cache" always;
283295
}
284296
location = /pwa/sw.js {
285297
root /usr/share/nginx;
298+
include conf.d/security_headers.conf;
286299
add_header Cache-Control "no-cache" always;
287300
}
288301

@@ -301,6 +314,8 @@ http {
301314
location /_next/static/ {
302315
root /usr/share/nginx/html;
303316
expires 365d;
317+
# 本 location 自带 add_header,不再继承 server 级安全头,需显式 include
318+
include conf.d/security_headers.conf;
304319
add_header Cache-Control "public, immutable";
305320
access_log off;
306321
}
@@ -309,6 +324,8 @@ http {
309324
location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|woff|woff2|ttf)$ {
310325
root /usr/share/nginx/html;
311326
expires 30d;
327+
# 本 location 自带 add_header,不再继承 server 级安全头,需显式 include
328+
include conf.d/security_headers.conf;
312329
add_header Cache-Control "public";
313330
access_log off;
314331
}

0 commit comments

Comments
 (0)