# Gitea 入口链路 —— product-demo.tvustream.com

> 勘察日期：2026-08-11。所有结论均来自当天在 `ux_server` 上的实测，非推断。
> 起因：`http://product-demo.tvustream.com:3001/gitea/` 输入账号密码后被弹回登录页，
> 而 `https://product-demo.tvustream.com/gitea/` 正常。
>
> **状态：已于同日按[路径 3](#已实施路径-32026-08-11) 修复，`:3001` 登录恢复（浏览器实测确认）。**
> 下面「为什么登不上」「时间线」两节描述的是修复前的状态，保留作为根因记录。

## TL;DR

`:3001` 曾经**能打开、能浏览，但登不上**。原因不是端口被关，而是 2026-07-30 的 INFRA-F74
把 `ROOT_URL` 改成 https 后，Gitea 自动打开了 `COOKIE_SECURE`，浏览器拒绝在明文 http
来源上保存带 `Secure` 的 session cookie。F89 修好了页面 404，但没处理这条。

2026-08-11 显式设 `COOKIE_SECURE=false` 后两个入口都能登录。**但 `https://product-demo.tvustream.com/gitea/`
仍是推荐入口** —— 它是唯一走 TLS 的那个，`:3001` 上的一切（含 session cookie）都是明文过网。

## 当前链路（实测）

```
Internet
 └─ nginx :443                    root 拥有，持真 Let's Encrypt 证书，nancy_zeng 无权改
     └─ caddy :80（宿主 8080）     容器 caddy，handle_path /gitea/* 剥掉前缀
         └─ gitea-proxy :3001      容器 gitea-proxy（caddy:2-alpine），F89 建，再剥一次前缀
             └─ gitea :3000        容器 gitea 1.22，宿主只暴露 127.0.0.1:3003
```

对应证据：https 响应头里有 `Server: nginx` + **两个** `Via: 1.1 Caddy`（两跳 Caddy）。

### 关键陷阱

**`:3001` 不是一个并列的备用入口，它是 443 链路上的中间一跳。**
主 Caddy 的上游写死为明文 `reverse_proxy 172.239.57.41:3001`。

> ⚠️ 因此**不能**给 `:3001` 加 TLS —— 主 Caddy 会拿明文去敲 TLS 端口，443 入口当场断。

## 为什么 :3001 登不上

Gitea 的 `COOKIE_SECURE` 默认值跟随 `ROOT_URL` 协议（[官方 config cheat sheet][cs]：
「If not set, it defaults to `true` if the ROOT_URL is an HTTPS URL」）。

当前 `GITEA__server__ROOT_URL=https://product-demo.tvustream.com/gitea/`，于是：

```
Set-Cookie: i_like_gitea=…; Path=/gitea; HttpOnly; Secure; SameSite=Lax
Set-Cookie: _csrf=…;        Path=/gitea; HttpOnly; Secure; SameSite=Lax
```

浏览器规则：从非安全来源（`http://`，`localhost` 除外）收到带 `Secure` 的 Set-Cookie
一律静默丢弃。所以在 `:3001` 上登录 → 服务端其实认了账号密码 → cookie 被浏览器扔掉
→ 下一个请求仍是匿名 → 弹回登录页，且不报错。

实测确认响应头里**没有** `Strict-Transport-Security`，所以这不是 HSTS 强制升级导致的。

### 只有 Web UI 登录坏了，git 操作一直是好的

`git clone/fetch/push` 走 HTTP Basic auth（用户名 + 密码或 token），**不依赖 session cookie**，
所以 `remote = http://product-demo.tvustream.com:3001/ux-team/....git` 全程正常。
这也是为什么这个问题只在有人想开 Web 页面时才被注意到 —— 日常 push 从没报错。

[cs]: https://docs.gitea.com/administration/config-cheat-sheet

## 时间线：端口是怎么一步步变成现在这样的

带时间戳的备份文件保留在 `/data/nancy_zeng/gitea/` 和 `/data/nancy_zeng/web/`。

### 起点（2026-05-19 起，团队最早一直用的）

```yaml
GITEA__server__DOMAIN=172.239.57.41
GITEA__server__ROOT_URL=http://172.239.57.41:3001/     # ← http，IP，无 /gitea 前缀
ports:
  - '3001:3000'                                        # ← Gitea 自己占 3001
```

ROOT_URL 是明文 http → `COOKIE_SECURE` 自动为 false → cookie 不带 `Secure`
→ **`:3001` 登录完全正常**。这就是团队记忆里"能用"的那个版本。

### 第一刀：INFRA-F74，2026-07-30 04:10:17

只改了两行环境变量：

```diff
- GITEA__server__DOMAIN=172.239.57.41
+ GITEA__server__DOMAIN=product-demo.tvustream.com
- GITEA__server__ROOT_URL=http://172.239.57.41:3001/
+ GITEA__server__ROOT_URL=https://product-demo.tvustream.com/gitea/
```

同日主 Caddy 从纯静态站加上了 `/gitea` 反代段。这一改产生**两个**后果，当时只发现了一个：

1. **发现了的**：Gitea 按 `/gitea/` 前缀生成所有 asset 与导航链接，从 `:3001` 直连打开全部 404。
2. **没发现的**：ROOT_URL 变 https → `COOKIE_SECURE` 自动打开 → **明文 `:3001` 从此登不上**。

**团队体感上"3001 不能用了"的时间点就是这一刻**，而不是端口被移除的时刻。

### 第二刀：INFRA-F89，2026-07-31 08:45–08:48

```diff
  ports:
-   - '3001:3000'
+   - '127.0.0.1:3003:3000'
```

Gitea 本体从 3001 撤到本地 3003，新建 `gitea-proxy` 容器占住 3001，负责剥掉 `/gitea`
前缀再转给 `gitea:3000`。F89 的自述目标是「让 :3001 这个老入口重新可用」——它确实修好了
上面的第 1 条（404），**但第 2 条（Secure cookie）没被识别**，所以 :3001 至今停在
「能打开、能浏览、就是登不上」的半可用状态。

### F89 回滚说明的一个缺陷

F89 在 Caddyfile 注释里写的回滚方式是「把 gitea 的 ports 改回 `3001:3000`」。
但那样只会退回到 ROOT_URL 与端口不匹配的状态（ROOT_URL 仍是 https 带 `/gitea` 前缀），
**并不能恢复 :3001 的登录能力**。真要恢复得连 ROOT_URL 一起回退，而那会打断 F74 建的
https 入口。

这就是这两个工单埋下的结构性矛盾：**`ROOT_URL` 只有一个值，而两个入口协议不同。**
Gitea 不支持多 ROOT_URL / 多域名（官方 cheat sheet 已确认为单值）。

## 如果要让 :3001 也能登录

按可行性排序。**注意 `:3001` 加 TLS 这条路是死的**（见上文陷阱）。

| | 做法 | 权限 | 代价 |
|---|---|---|---|
| 1 | **SSH 隧道**：`ssh -N -L 3001:localhost:3001 ux_server`，开 `http://localhost:3001/gitea/` | 现有权限，零改动 | 每次要起隧道；只对自己有效 |
| 2 | 让有 root 的人在 **nginx 上加一个 TLS 端口**（如 `listen 3443 ssl`，证书现成），proxy 到宿主 8080 | 需要 root | 要等运维；但最干净，不动现有链路 |
| 3 | Gitea compose 加 `GITEA__session__COOKIE_SECURE=false` 并重启 | 现有权限 | 443 入口的 cookie 也失去 `Secure` 保护 |

选项 1 之所以成立：浏览器把 `http://localhost` 视为安全上下文，带 `Secure` 的 cookie
在 localhost 来源下允许保存。已实测隧道能通、页面正常渲染
（`<title>Sign In - TVU UX Code</title>`），但"登录能保持"这一步需在浏览器里实点确认，
curl 不实现浏览器的这条 localhost 例外规则。

另注：cookie 作用域**不区分端口**。所以一旦 :3001 走上 https，443 上已有的会话会直接生效。

## 已实施：路径 3（2026-08-11）

`/data/nancy_zeng/gitea/docker-compose.yml` 的 `environment` 加一行（连同解释与回滚注释）：

```yaml
- GITEA__session__COOKIE_SECURE=false
```

`docker compose up -d` 重建 `gitea` 容器。备份：`docker-compose.yml.pre-cookie-secure-20260811-093414`。

### 验证证据

| 检查 | 改前 | 改后 |
|---|---|---|
| `Set-Cookie` 含 `Secure` 的条数（443） | 4 | **0** |
| `Set-Cookie` 含 `Secure` 的条数（:3001） | 4 | **0** |
| session cookie 在明文 http 上往返 | — | **成立**（带 jar 二次请求，服务端复用同一 `i_like_gitea`，未重新下发） |
| 443 入口状态码 | 200 | **200**（两跳 `Via: Caddy` 不变） |

curl 与浏览器一样拒绝把 `Secure` cookie 发往 `http://`，所以「明文往返成立」就是原先断掉的那一环
已经接上。

**浏览器侧已确认**（同日 10:01 UTC，Gitea router 日志）：给 :3001 的 URL 加上唯一标记
`?probe=A` 后，`GET /user/login?probe=A` 返回 **303**（Gitea 把已登录用户踢离登录页），
紧随其后是 `GET /repo/search?...&uid=1`（dashboard 渲染）。浏览器永不把 `Secure` cookie
发往 `http://`，所以这个请求能带着有效 session 到达，就证明用的是修复后不带 `Secure` 的
cookie，且在明文链路上往返成功；修复前同一请求只会返回 200 登录页。

> 排查这类"到底是哪个入口"的问题时，两条链路在 Gitea 日志里长得完全一样（两个 Caddy 都没开
> access log），**给 URL 加一个唯一 query 标记是最省事的判别法** —— 它会原样进 router 日志。

### 两个容易踩的副作用

1. **回滚不能只删 env 那行。** Gitea 启动时会把 `GITEA__*` 写进 `/data/gitea/conf/app.ini`
   （实测：app.ini mtime 恰为容器重建时刻，`[session] COOKIE_SECURE = false` 已落盘）。
   删掉 compose 里的 env 不会清掉 app.ini 里已写入的值 → **回滚要显式改成 `=true` 再重启**。
2. **这次重启没有把人踢下线。** `[session] PROVIDER = file`、`PROVIDER_CONFIG = /data/gitea/sessions`
   在持久化卷上，所以会话跨容器重建存活。（若哪天 provider 变回默认的 `memory`，重启即全员登出。）

### 代价（明确记录）

443 入口的 session cookie 也一并失去 `Secure`：浏览器此后会在**任何**同域明文 http 请求里带上它，
`:3001` 上的会话等于明文过网。这台机器上的内容是设计系统代码而非客户数据，故判定可接受；
但如果哪天挂上敏感仓库，应改走[路径 2](#如果要让-3001-也能登录)（root 在 nginx 上加 TLS 端口），
把这条 `COOKIE_SECURE=false` 撤掉。

## 权限现状（勘察时实测）

```
sudo                            → a password is required
/etc/letsencrypt/live/          → Permission denied
find 可读的 product-demo 证书    → 0 个
主 caddy 全局块                  → local_certs（只有自签 localhost / 172.239.57.41）
docker                          → 免 sudo 可用
```

配置文件都在 `nancy_zeng` 自己名下，可直接改：

- `/data/nancy_zeng/gitea/docker-compose.yml`
- `/data/nancy_zeng/gitea-proxy/{Caddyfile,docker-compose.yml}`
- `/data/nancy_zeng/web/Caddyfile`
