|
|
5562d584d3
|
fix(gateway): https 上游不再无条件关闭证书校验
审计第 2 条:writeProxyLocation(主 location / bypass 段)与 writeRouteServer
的 favicon 段原本都是
if r.InsecureTLS || strings.HasPrefix(strings.ToLower(upstream), "https://") {
proxy_ssl_verify off;
}
即「只要上游是 https 就关掉上游证书校验」,对所有 https 上游一律不验签 ——
中间人可冒充任意 https 后端,用户提交的凭据与 cookie 全部可截。
现在条件收敛为 `r.InsecureTLS && isHTTPSUpstream(upstream)`:
「上游是 https」不再是关闭校验的理由,只有**显式勾选 insecureTls** 的路由
才会输出 proxy_ssl_verify off。两处(writeProxyLocation 用 r.Upstream、
favicon 段用局部变量 up)都改到了,并抽出 isHTTPSUpstream 消除重复判断。
⚠️ 运维需知的行为变化(有意为之,不要偷偷改回去):
- 普通 https 上游现在走 nginx 默认:**校验上游证书**,使用系统 CA 库。
公共 CA(Let's Encrypt 等)签发的上游不受影响,无需任何配置。
- 仓库内没有 proxy_ssl_trusted_certificate 的配置,因此**自签证书的上游现在会 502**。
这是预期的 fail-closed 结果:要接自签上游,请在该路由上显式勾选 insecureTls
(等价于确认「我知道这条链路不验签」)。
- http 上游行为不变(本来就不会有 TLS 握手);本次也顺带保证不会给它写这行噪声配置。
测试(admin-server/gateway_tls_verify_test.go,沿用 gateway_test.go 的
RenderGatewayConf + section 取值风格):
1. https 上游 + InsecureTLS=false → 主 location 不含 proxy_ssl_verify off;
2. https 上游 + InsecureTLS=true → 含;
3. http 上游(两种情况)→ 都不含;
4. favicon 段(location = /favicon.ico 与 /favicon.svg)逐条断言 1/2 两种行为,
防止只改主 location 漏掉 favicon 段;
5. 计数断言:整份配置里 proxy_ssl_verify off 的出现次数 == 勾了 insecureTls 的
https 路由贡献的 location 数(主/bypass + 2 个 favicon),兜住别处同类短路;
6. 源码护栏:不得再出现两种旧写法,且注释必须保留「自签默认 502 是预期」的说明。
反向验证:把两处判断改回 `|| https://` 后,1/3/4/5/6 全部报红(详见
`go test -run 'TestHTTPSUpstream|TestHTTPUpstream|TestFaviconPassthrough|TestSSLVerifyOff|TestInsecureTLSBehaviour'`),
计数用例给出 13 次 vs 期望 7 次;恢复后全绿。
|
2026-10-22 23:14:32 +08:00 |
|
|
|
dea722f51c
|
fix(security): 管理 API 默认拒绝 + 堵住渗透测试报告的 5 个攻击面
2026-10-22 渗透测试报告(report.md)实测确认:统一入口的管理 API 在公网
完全未鉴权,可匿名改路由表、拆登录墙、改门户口令、写门户 HTML、读全部
浏览历史、做内网 SSRF 探测。根因有两层,本次一并修复。
## 根因 1:requireAdminToken 默认放行(代码层)
if !requireTokenForWrite { return true }
线上没有 NAVI_ADMIN_REQUIRE_TOKEN,于是所有"调了鉴权"的 handler 实际全部
放行——形同虚设。现在改为 fail-closed:
- 配了 NAVI_ADMIN_TOKEN → 校验 Bearer token
- 未配且显式 NAVI_ADMIN_ALLOW_UNAUTHENTICATED=1 → 放行(仅本机调试)
- 其它 → 401 拒绝
## 根因 2:login 域没有任何 auth_request(部署层)
kind=auth 的 server 块全站 requireLogin=false(它本身就是登录服务,这点
合理),但 location / 把 /api/、/admin/ 原样反代到 127.0.0.1:3005。两层
都没管 → 管理 API 裸奔。
新增 Route.AdminGuard:开启后 server 级挂 auth_request,只白名单放行登录
流程(/_portal/login|logout|verify、/login、/portal-page、favicon),
其余 /api/、/admin/ 一律要求已登录。
## 同时修掉的 4 个具体问题
1. /api/sync-info 明文回显 syncToken → 不再回显,且要求鉴权。
泄露该 token 等于浏览器历史的全读写权。
2. /api/history GET 无鉴权 → 与 DELETE 分支一致,要求管理员令牌。
3. /api/frameable 未鉴权 SSRF → 加鉴权 + 内网/回环黑名单。除 IP 字面量外
还对域名解析结果做校验,防止 localtest.me 之类指向 127.0.0.1 的绕过。
4. dashy 域 bypassPaths 里的 "/api/" 过宽 → 收窄到 "/api/history/"
(本意只放行浏览记录上报,实际把整个管理 API 暴露了)。
## 回归护栏
新增 admin-server/security_test.go(10 条)与 main_test_setup_test.go。
每条断言都是"改回旧行为就会失败",已用 3 次变异测试验证:
- 恢复默认放行 → TestAdminAPIFailsClosedByDefault FAIL
- sync-info 回显 token → TestSyncInfoDoesNotLeakToken FAIL
- 移除内网黑名单 → TestFrameableBlocksInternalTargets FAIL
- /api/ 豁免认证 → TestAuthKindWithAdminGuardProtectsAPI FAIL
- 不给 server 级 auth → 同上 FAIL
测试 56 项全绿。
## 顺带
searxng-settings.yml:停用 360search / ddgw / wikidata / wikipedia
(各自的实测理由见文件内注释),request_timeout 8.0→4.0。
Dockerfile:给 duckduckgo_web 引擎硬编码的 timeout=2 打补丁(经代理必超时)。
|
2026-10-22 14:08:06 +08:00 |
|
|
|
ca80ba2510
|
fix(gateway): auth 类路由重复写 /favicon.ico 导致 nginx -t 失败(v96 部署被回滚的真因)
v96 部署时脚本报"网关配置重新生成失败 → 回滚到 ui-v95"。根因:auth 类路由的 Upstream
是在 writeErrorPages 之后才赋值的,所以它先按"门户自己渲染"拿到了门户 favicon,
随后新的"favicon 直通上游"块又写了同一个 location → nginx 拒绝重复 location。
修法:直通块对 auth 类跳过(门户页/登录页继续用门户自己的图标)。
另加回归测试 TestNoDuplicateExactLocations:解析渲染结果,断言任一 server 块内
不得出现重复的 `location = X`;已用"临时移除守卫"做过反向对照,确认该测试真能抓到。
|
2026-09-13 08:58:41 +08:00 |
|
|
|
9c0d9f2bb5
|
fix(gateway): 被代理站点的 favicon 免登录直通上游(而非门户静态文件/登录墙)
承接上一提交:把门户 favicon 从代理块里去掉后暴露两个问题:
- requireLogin 的站点 /favicon.ico 变成 302(浏览器标签与图标抓取都取不到);
- 而 favicon 本来就不该要求登录。
改为:代理块显式写
location = /favicon.ico { auth_request off; proxy_pass <上游>; ... }
location = /favicon.svg { auth_request off; proxy_pass <上游>; ... }
(https/自签上游附带 proxy_ssl_verify off)。
效果:浏览器标签显示各站点自己的图标、无需登录;抓取端也因此能在未登录时取到真实图标。
门户页/登录页/默认 404 块仍提供门户自己的 favicon。测试断言同步改为"直通上游"语义。
|
2026-09-13 08:52:41 +08:00 |
|
|
|
3d1ed788f3
|
fix(gateway/favicon): 被代理域名的 /favicon.ico 被门户图标顶掉(图标全错的真因)
用户问"同时被路由的网站为什么图标不正确"。实测三处证据:
① gateway.conf 里**每个** server 块都有
location = /favicon.ico { auth_request off; try_files $uri =404; }
而 try_files 在门户自己的 root(/app/portal) 里找文件;
② 于是 https://pi|ittools|vikunja.jianfgit.xyz/favicon.ico 三者返回**同一个**
门户图标(md5 80498a88…),而它们内部上游的真实图标各不相同
(ittools dc36588f… / vikunja 6d9507b6… / pi cf3332ac…);
③ 导航图标抓取优先抓 /favicon.ico → pi/ittools/vikunja 存下了同一张 1091B 门户图标。
修法两处:
- admin-server/gateway.go:门户 favicon 的两个 location 只在"门户自己渲染"的
server 块(kind=portal/auth、默认 404 块)里输出;代理到上游的块不再输出,
让 /favicon.ico 正常走 location / 落到上游。新增断言测试。
- history-server/favicon.go:抓取改为**优先走内部上游**(读 gateway.yml 把对外域名
映射到内部地址,带 Host 头、容忍自签证书),失败才回落对外地址 —— 这样受统一登录
保护的站点也能拿到真实图标,且不再依赖门户的 favicon 行为。
|
2026-09-13 08:48:42 +08:00 |
|
|
|
b99319b38b
|
chore(sanitize): 备案号与域名一律可配置,仓库不再预置真实域名/备案号/内网地址
用户要求:备案号与域名都应当可配置,仓库内容要脱敏。
功能(不只是替换字面量):
- 被代理页面注入的备案号改为读后台「站点信息」的 icp/gongan,**未配置就不注入**;
原先写死具体备案号,等于每个部署者都会把别人的备案信息印到自己页面上
- 注入内容做 nginx 安全处理(去引号/反斜杠/换行/美元符),并有测试覆盖
- auth.go:PORTAL_COOKIE_DOMAIN / PORTAL_HOME_URL 不再预置域名,
cookie 默认 host-only、首页默认相对路径 "/"
- gateway.go:默认 loginUrl 不再预置域名(受保护路由缺配置会明确报错,不静默生成跳不出去的登录页)
- 首装脚本在给了 NAVI_DOMAIN 时自动注入 PORTAL_COOKIE_DOMAIN/PORTAL_HOME_URL,
避免"去掉写死默认值"导致跨子域单点登录失效
脱敏:内网 IP(10.0.0.10)与真实备案号清零;真实域名仅剩门户页内容 8 处(待定);
测试夹具/示例/错误页/脚本/后台占位提示全部通用化。
README:新增「跨子域单点登录需要的两个环境变量」;环境变量表改为 NAVI_HOST 必填。
测试:新增备案号注入三条(用配置、未配置不注入、危险字符清理)。
|
2026-09-12 10:10:52 +08:00 |
|
|
|
b01e49de49
|
fix(gateway): http→https 跳转是否带端口改为显式开关(避免生产回归)
练兵发现:跳转写成 return 302 https://$host$request_uri(不带端口),
非 443 端口的新装会跳到 443 打不开。
但直接用'端口 != 443 就带端口'推导会把生产弄坏:生产容器监听 3080,
对外经 frp 映射为 443,带端口会把公网用户导向 https://host:3080。
因此新增 gateway.yml 字段 redirectWithPort(默认 false,行为不变):
- 默认:跳 https://host(前置 frp/443 的部署,含现生产)
- 全新安装且对外端口=入口端口:install-host.sh 自动置 true
测试补两条:开关为真时带端口、默认(生产形态)不带端口。
|
2026-09-12 09:52:20 +08:00 |
|
|
|
b4241741d6
|
fix(gateway): 门户段重复 root 导致 nginx -t 失败
v52 部署被 apply 的校验拦下("root directive is duplicate"):writeErrorPages 已写过
root /app/portal;,我又在 portal 分支写了一次。移除重复行,并加单测断言
该指令在生成结果里恰好出现一次。回滚机制按预期工作,服务未中断。
|
2026-09-12 09:20:18 +08:00 |
|
|
|
52164b6abe
|
fix(gateway): 门户首页走 admin-server 渲染(上次改动未落盘)
上一轮声称改过 kind=portal 的生成逻辑,但 git log 显示 gateway.go 从未提交该改动
(我的修改没有真正落盘),导致:线上仍用内置模板,而新镜像的模板带 HTML 占位符
→ 门户页占位符裸露、标题为空、文案不可配置。
本次:portal 分支改为 location = / 反代到 :3005/portal-page(静态资源仍 root
/app/portal),并新增单测断言该引用存在——今后若再被改掉,测试会失败。
|
2026-09-12 09:17:34 +08:00 |
|
|
|
9798b68d07
|
feat(status): 按用户要求彻底关闭可达性状态显示
用户决定不显示服务状态,避免不准确的信号误导。因此:
- 移除导航同步里写入的 statusCheck / statusCheckUrl
- 移除网关为每条路由生成的 /.navi-ping(不再需要,且不应保留一个
免认证旁路路径)
- 删除对应单测
生产 conf.yml 的 statusCheck 在同一次部署中关闭(appConfig 置 false、
条目级该行删除),并重新 apply 生成网关配置以清掉 /.navi-ping。
|
2026-09-12 08:32:03 +08:00 |
|
|
|
da7a56e7ec
|
fix(status): 修掉 Dashy 可达性检查"全黄"——改用免认证探测路径
用户反馈可达性全黄。实测:appConfig.statusCheck 已开启,但等 6 秒仍无状态
节点渲染;上游直连全部 200。机制:Dashy 用 no-cors 探测条目 URL,而受保护域名
会 302 跳到另一个域(登录页),no-cors 下跨域重定向直接报错 → 检查永远停在
"进行中" → 全黄。
修法:
- gateway 为每条路由生成 location = /.navi-ping:auth_request off + 反代到
真实上游(上游挂了返回 5xx → 红;正常 2xx → 绿),portal 类型直接 204
- 导航同步时设置 NavItem.StatusCheck = true 与 statusCheckUrl 指向该路径
- 注意:statusCheck 是 NavItem 的结构体字段,不能塞进 inline Extra(yaml 会
panic: conflicts with struct field)——已改为设置字段本身
测试:新增断言——探测路径存在、免认证、反代到真实上游、portal 返回 204。
|
2026-09-12 08:23:33 +08:00 |
|
|
|
b941d8e8e3
|
fix(gateway): 错误页必须跳过 auth_request,否则上游故障会变成 401
实测:上游连接被拒(502)时 nginx 内部跳转 /500.html,而服务端的
auth_request 被这个内部请求继承 → 用户收到 nginx 默认 401 页,
自定义 404/500 永远显示不出来。
错误页及其静态资源一律 auth_request off,并加单测锁住。
|
2026-09-11 22:33:45 +08:00 |
|
|
|
1844586feb
|
fix(gateway): TLS 段的 http2 必须写成 listen 参数
容器的 nginx 是 1.22,独立的 `http2 on;` 会报 unknown directive
(该指令 1.25.1 才引入;原 portal-nginx 用 nginx:latest 所以没暴露)。
改为 `listen PORT ssl http2;`,1.22 与 1.25+ 都可用,并加单测锁住。
本轮由旁路端口 TLS 验证发现;apply 的校验-保留机制正确拦下了这次坏配置。
|
2026-09-11 22:09:57 +08:00 |
|
|
|
53beff646a
|
feat(admin): 现有配置导入 + 添加表单补齐认证/导航开关 + 统一登录账号设置 + 设置分组
针对反馈的五个问题:
1. 后台"没有映射":新增 /admin/api/gateway/import 与界面「从现有 nginx 配置导入」,
把老统一入口的 nginx 配置贴进来即可解析出全部「域名 → 后端」初稿
(含 requireLogin / 免认证路径 / SSE 流式 / 备案注入 / 自签证书 / auth 与 portal 类型)。
解析只读不落盘,确认后才保存;导入默认不接管导航,避免一导入就改掉手工导航。
2. 添加转发时没有导航选项:添加表单补齐「同时加入 Dashy 导航 / 分组 / 名称 / 描述 /
图标 / 自动抓取」。
3. 添加转发时没有 portal 保护选项:添加表单补齐「启用 portal 统一登录保护」,
并补上 WebSocket 透传与上传上限。
4. 设置里无法配置 portal 账号:新增统一登录账号卡片,账号存数据卷
(portal-auth.json,只存加盐哈希),优先级 数据卷 > 环境变量,
改完立即生效、无需重新部署;会话校验按当前用户名,改账号后旧会话失效。
5. 设置项太乱:按用途分组(站点与外观 / 导航内容 / 历史记录 / 转发与统一登录 / 高级),
外观卡片并入站点组。
另修两个本轮引入的缺陷:
- 容器假 unhealthy:内部端口改 18080 后,基础镜像的 HEALTHCHECK 仍指向 8080;
部署脚本生成的 Dockerfile 现在显式覆盖为 :18080/healthz。
- kind=auth 不依赖 portal-auth 容器,指向内置登录服务;避免生成空 proxy_pass。
测试:nginx 配置解析(含兜底段忽略、多域名段、类型判定、默认不接管导航)、
垃圾输入报错、账号存储(不明文、可校验、改名后旧口令失效)、auth 类型生成。
|
2026-09-11 22:00:09 +08:00 |
|
|
|
994d93cc98
|
feat(gateway): 一体化统一入口——路由与 Dashy 导航同一份数据、内置登录、原自定义错误页
目标:一个 HomeDash 容器取代 navi + portal-nginx + portal-auth。
- Route 扩展为"反代 + 导航"一条记录:kind(proxy/portal/auth)、streaming、
insecureTls、bypassPaths、injectBeian,以及 navEnabled/navSection/navName/
navDescription/navUrl/navIcon/navAutoIcon
- 保存路由时**同时**写 gateway.yml 与 conf.yml 的导航项(gateway_nav.go):
同 host 接管而不是重复、未勾选导航的会被清理、无关手工项与系统「历史访问」分组
一律不动;两步任一步失败都回滚(避免后台显示与导航页各说各话)
- 未知域名与上游故障改用**内置自定义 404/500 页面**(portal/ 目录),
不再是一行纯文本;HTTP→HTTPS 跳转段用 RedirectPort 复刻原 3081 设计
- 内置统一登录(auth.go):HMAC 签名 cookie、恒定时间比较、登录跳转白名单校验,
portal-auth 容器可以退场
- 自动抓图标对托管导航项同样生效(history-server 识别 naviGatewayAutoIcon)
- 后台卡片补齐"加入导航/分组/名称/描述/图标/自动抓图标/服务类型"字段
- 内部入口端口 8080 → 18080:为一体化 host 模式让开宿主机 blog(:8080)
- 修掉上一轮编辑写坏的 deploy/zd.sh 一行(会让脚本直接失败)
测试:新增自定义错误页、portal 类型、导航字段校验、导航同步(接管/清理/保留)、
分组不存在必须报错等用例;admin-server 与 history-server 全绿。
|
2026-09-11 21:27:40 +08:00 |
|
|
|
5f6fca5c59
|
fix(gateway): 未知域名必须被兜底拒绝,不能落进某条路由的后端
实测发现:入口端口上若没有匹配 server_name 的请求,nginx 会交给该端口排序
第一的 server 块,于是一个打错的域名会访问到别的服务(若第一条恰好是开放
路由,等于把该服务暴露在错误域名下)。现在为 HTTP/HTTPS 各生成一个
default_server 返回 404,并在单测中断言其存在。
|
2026-09-11 20:34:20 +08:00 |
|
|
|
19d082e70a
|
feat(gateway): 统一入口路由表(后端基础层)——"哪些服务走统一认证"变成一条数据
目标:不再手工维护外部 nginx 配置,把"域名→内网后端、是否走统一登录认证"
变成后台可编辑的数据,由本进程生成 nginx 配置并热加载。
本提交只做基础层(纯 Go、可测、零行为变更):
- gateway.go:Route/H gatewayConfig 数据模型 + ValidateRoute + RenderGatewayConf
· 认证**按路由**决定:RequireLogin 为真才生成 auth_request 与登录跳转,
也就是"哪些服务受统一认证保护"完全由路由表决定
· 支持 websocket 透传、单域名上传上限、HTTPS 段(可同时生成旁路端口)
· 路由表单独存 gateway.yml(不混进 Dashy 的 conf.yml),写坏了也不影响导航页
- API:GET/POST /admin/api/gateway/routes(保存)、POST /admin/api/gateway/apply
· apply 的顺序是 生成 → nginx -t 校验 → 通过才 reload;**校验失败把上一份可用
配置写回去并把 nginx 原始报错回给界面**,避免"点了应用整个入口挂掉"
- nginx.conf 加 include /app/data/gateway/*.conf(生成物在容器自己的卷里,
不碰宿主机文件,天然避开"单文件 bind mount 不能用 sed -i"的坑)
- 生成物默认监听旁路端口(8081/8443):可以先与正式入口并存验证,再切 80/443
测试:受保护域名必须有 auth_request、未受保护域名必须没有;websocket/上传上限;
空域名/带协议域名/非 http 后端/域名重复/需认证却无认证地址/启用 HTTPS 却无证书
六种非法输入必须报错;缺文件时回落默认配置、保存后回读一致。
|
2026-09-11 20:17:57 +08:00 |
|