Gitea 自托管仓库治理
Gitea 的价值不只是“在自己服务器上放一个 Git 仓库”。真正进入团队后,它同时承担身份入口、仓库授权、Pull Request、服务端 Git Hook、状态检查和审计入口。只把页面启动起来,却没有保护规则和权限模型,得到的仍是一台任何写成员都可能直推主分支的 Git 服务器。
下面从一套隔离实例开始,先跑通仓库协作,再把规则收紧到可供团队长期维护的程度。示例使用虚构组织 your-org、仓库 governance-lab 和地址 gitea.example.com;口令、令牌、私钥和内部域名都不能照搬到真实环境。
从隔离实例识别三种身份凭据
动手前确认开发机具备以下入口:
本机已有 Git 2.x,并能执行 git --version。若跟做本地实例,准备 Docker Engine 与 Compose V2,确认 docker version 和 docker compose version 均成功。准备两个普通测试身份:maintainer 用于配置规则,developer 用于验证规则。不要用站点管理员同时扮演所有角色。
准备一把仅用于试验的 SSH 公钥,或使用 HTTPS 凭证;私钥不得上传到 Gitea。预留一个空目录和未占用端口。正文使用 3000 提供 HTTP、2222 映射容器 SSH。
三种证据并不等价:SSH Key 证明“谁在连接 Git 服务”,提交签名证明“谁签署了这个 Git 对象”,Pull Request 审批证明“谁同意这次变更进入目标分支”。团队规则通常需要组合它们,而不是三选一。
实例页脚、gitea --version 或管理员页面给出的版本号,是选择配置文档的起点。稳定版、next 与 Enterprise 使用不同的能力基线:next 可能包含尚未发布的行为,Enterprise 则包含商业能力,例如组织级可继承保护规则。社区版实例不能因为 Enterprise 文档存在某个页面,就假定自己拥有相同控制面。
用 Compose 跑一套治理实验室
下面的 SQLite 单节点用于学习规则,不承载生产仓库。镜像固定为稳定版 1.26.4,避免一次重启悄悄跨版本;换用其他发行线时,应同时切换到对应版本的Docker 安装说明和配置参考:
services:
gitea:
image: docker.gitea.com/gitea:1.26.4
restart: unless-stopped
environment:
USER_UID: "1000"
USER_GID: "1000"
GITEA__server__ROOT_URL: "http://localhost:3000/"
GITEA__server__SSH_PORT: "2222"
GITEA__service__DISABLE_REGISTRATION: "true"
ports:
- "3000:3000"
- "2222:22"
volumes:
- gitea-data:/data
volumes:
gitea-data:启动并观察:
docker compose up -d
docker compose ps
docker compose logs --tail=100 gitea浏览器打开 http://localhost:3000 完成首次安装。SQLite 数据库路径保持在持久卷中即可;创建第一个管理员后,确认安装页不再开放。若团队已有受控实例,跳过这一段,从组织与仓库初始化开始。
选择入口时的架构取舍
| 入口 | 适用场景 | 关键边界 |
|---|---|---|
| Docker + SQLite | 个人学习、功能回归、升级预演 | 单节点、并发和备份能力有限 |
| Docker + PostgreSQL/MySQL | 小团队共享、接近生产的验证环境 | 数据库、附件与仓库目录必须一起纳入恢复设计 |
| 二进制或系统服务 | 已有 Linux 服务管理体系 | 配置、Git 用户、目录权限和升级由平台团队承担 |
| Kubernetes/Helm | 已有成熟集群治理的团队 | 不是“天然高可用”,仍需处理数据库、共享存储、SSH 与备份 |
| Gitea Enterprise | 需要商业支持或企业专属能力 | 许可、版本号和功能文档与社区版分开评估 |
自托管的核心取舍是控制权换责任:身份数据、仓库对象、LFS、附件、Actions 密钥和审计都由团队自己保管。没有明确平台 owner、恢复目标和升级窗口时,不应仅因“界面轻量”就替换现有托管平台。
先把外部地址配置正确
反向代理后的 ROOT_URL、DOMAIN、SSH_DOMAIN、SSH_PORT 决定页面回调和克隆地址。配置错误的典型表现不是服务起不来,而是页面能打开、克隆 URL 却指向容器内部端口,OAuth 回调、邮件链接或 Webhook 也跳到错误地址。
生产配置应由平台团队写入 app.ini 或 GITEA__section__KEY 环境变量。官方配置参考说明普通安装修改 app.ini 后需要完整重启,官方 Docker 镜像会在启动时应用 GITEA__<section>__<KEY> 形式的环境变量。变更后应同时验证页面链接、HTTPS clone、SSH clone、Webhook 回调和登录跳转,不能只检查首页返回 200。
用组织和团队承载权限
在页面中创建组织 your-org,再建立至少三个团队:
| 团队 | 建议权限 | 说明 |
|---|---|---|
owners | 组织 owner,人数极少 | 管团队、仓库与组织级设置,不参与日常开发授权 |
maintainers | Code / PR / Issues 写,Settings 按需管理 | 维护仓库与合并,不默认拥有站点管理权 |
developers | Code / PR / Issues 写,Settings 无权限 | 推功能分支、提交 PR、参与评审 |
auditors | 只读 | 查看代码、PR 与治理证据,不可改规则 |
Gitea 的权限模型按 Code、Issues、Pull Requests、Releases、Packages、Actions、Settings 等 Unit 分开授权。不要为了让成员重跑一次任务就把整个团队升成仓库 Admin。个人仓库适合试验;正式团队仓库放入组织,才能让入离职通过团队成员关系回收。
建立基线保护规则
进入仓库 Settings -> Branches,按保护分支规则为 main 添加保护。服务端会把规则应用到 HTTP(S)、SSH、Web 编辑器、API 和自动合并等写入入口,因此验证不能只覆盖一种协议:
禁止普通成员直接 push,关闭 force push。合并只允许 maintainers,要求至少 1 个有效审批。开启新提交后撤销旧审批,或至少不再计入旧提交上的审批。
阻止存在“请求更改”和未完成正式评审请求的 PR。开启“分支过期时禁止合并”,让 PR head 明确基于当前 main。有可靠检查上报后,再开启 status check;按实际 context 名称配置 glob。
对管理员也启用保护规则,消除日常 Force merge 绕过。
规则模式匹配整个分支名。不要想当然把正则表达式写进 glob,也不要同时放一个宽泛 * 和多个例外后不做角色验收。保护文件模式适合约束部署清单、所有权文件等少数高风险路径,但它不能替代代码评审和 CI 扫描。
签名和版本策略
“Require signed commits”会在服务端 Hook 中拒绝包含未签名或不可验证提交的 push。启用前先完成三件事:登记团队公钥;用真实合并策略验证 Gitea 生成的 merge commit;明确密钥轮换、离职和吊销如何同步到实例。
根据 Gitea 的提交签名说明,服务端不会检查外部 keyserver 中的密钥过期或吊销状态。因此“Verified”不能单独作为在职身份凭证。组织必须维护自己的允许密钥清单,并在事件响应中删除失陷公钥、撤销相关 Token,再检查受影响时间窗内的提交。
使用 maintainer 创建私有仓库 your-org/governance-lab,不要自动生成 README。随后在本地初始化:
mkdir governance-lab
cd governance-lab
git init -b main
git config user.name "Lab Developer"
git config user.email "developer@example.com"
printf "# governance-lab\n" > README.md
git add README.md
git commit -m "初始化治理测试仓库"
git remote add origin http://localhost:3000/your-org/governance-lab.git
git push -u origin main首次提交完成后再启用 main 保护规则,否则空仓库尚无可匹配分支。接着切换到 developer 凭证并验证四条路径。
直推必须失败:
printf "direct push should fail\n" >> README.md
git add README.md
git commit -m "验证主分支直推阻断"
git push origin main预期远端拒绝更新保护分支。把本地提交移到功能分支,不要丢弃它:
git switch -c docs/governance-check
git push -u origin docs/governance-checkPR 必须经过审批:在页面创建指向 main 的 Pull Request。未审批时合并按钮应被规则阻止;由具备计票资格的维护者批准后才可合并。再向源分支补一个改变 diff 的提交,确认旧审批按策略失效。
协议必须一致:若配置了 SSH,将远端换为页面显示的 SSH URL,再尝试向 main 推送。结果仍应被同一条保护规则拒绝。不要把“HTTPS 被拒绝、SSH 成功”解释为凭证差异,这通常意味着规则、仓库或测试身份并不相同。
API 必须服从权限:创建仅含必要 read:repository 的一次性 Token,用它读取仓库信息:
curl --fail-with-body \
-H "Authorization: token <gitea-api-token>" \
-H "Accept: application/json" \
"http://localhost:3000/api/v1/repos/your-org/governance-lab"预期返回仓库 JSON;用只读 Token 发起写请求应得到拒绝。不要把 Token 放入 URL 查询参数、Shell 历史或 Git remote。
验证完成后删除测试 Token、测试仓库和两个测试账号;若整套 Compose 仅为一次性实验:
docker compose down
# 确认数据不再需要后,才执行:
docker compose down -vdown -v 会删除持久卷和其中的仓库、账号、配置,不可用于共享实例。
真实项目接入不是把旧 remote 改个地址就结束。建议按以下顺序迁移:
在组织中建立目标仓库和团队授权,先不开放全员 Admin。镜像或推送完整 refs 后,核对默认分支、tag、LFS、submodule URL 和提交签名显示。建立保护分支、保护 tag、审批和状态检查;先用测试 PR 验证,再宣布切换。
更新开发者 HTTPS/SSH remote、机器人 Token、Webhook、外部 CI 回调和依赖仓库凭证。将旧仓库设为只读并保留迁移窗口,避免双边继续产生提交。
可用下面的只读命令核对接入结果:
git remote -v
git ls-remote --heads origin
git ls-remote --tags origin
git fetch --prune --tags origin
git status --short --branch项目模板应记录仓库 owner、默认分支、规则目标、状态检查 context、审批人来源和例外流程。不要在模板里保存管理员 Token,也不要让所有仓库长期复制一份无人维护的规则快照。
日常 PR 闭环
git switch main
git pull --ff-only
git switch -c feat/example-change
# 修改并本地验证
git push -u origin feat/example-change随后创建 PR,检查目标分支、变更范围、审批、状态检查和提交签名。合并后删除远端功能分支,本地执行 git fetch --prune。是否使用 merge、squash 或 rebase merge,应由仓库基线统一,而不是由每个维护者临场选择。
只读盘点 API Token
Gitea 的API 使用说明指出 Token 在创建后只显示一次,并支持 read:* 与 write:* 细粒度 scope。为盘点脚本创建只读 scope,不要使用 all:
curl --fail-with-body \
-H "Authorization: token <gitea-api-token>" \
"https://gitea.example.com/api/v1/user/repos?limit=50&page=1"分页不能忽略;实例默认和最大响应项由管理员配置控制。自动化需要写仓库时,单独创建 write:repository Token,绑定专用机器人身份,并记录 owner、用途、到期日和撤销演练。
规则变更前后做双身份验证
每次规则变更至少用普通写成员和仓库管理员分别验证:普通成员不能直推;管理员在“必须遵守保护规则”开启后也不能随意绕过;合规 PR 可以正常合并。只看管理员页面中开关状态,无法证明服务端 Hook 真正执行了预期策略。
页面可访问,克隆地址或回调却错误
页面打开正常,但 clone URL 指向内网主机、错误端口或 HTTP;Webhook/OAuth 回调跳错地址。对比页面生成 URL、ROOT_URL、SSH_DOMAIN、SSH_PORT 和反向代理转发头。只改了代理,没有同步 Gitea 外部地址;容器端口和外部端口混用。
统一外部 URL 配置,重启后重新生成和验证链接;不要靠开发者逐个手改 URL 掩盖配置错误。分别执行页面登录、HTTPS clone、SSH clone 和一次 Webhook 测试投递。
容器不断重启或无法写仓库
日志出现 permission denied,安装页反复出现,或重启后仓库消失。检查 docker compose ps、容器日志、/data 是否真实挂载,以及宿主目录 owner 是否与 USER_UID/USER_GID 一致。绑定目录权限错误,或数据写在容器可写层而未持久化。
停机后修正目录归属或改用 named volume;先备份再移动已有数据。创建测试仓库、重启容器,再次 clone 并确认对象仍在。
保护分支开启后仍有人能直推
普通验证通过,但管理员、部署密钥或某协议仍可写 main。确认命中的规则模式、push allowlist、deploy key、管理员绕过开关和实际测试账号;用 HTTP、SSH、API 分别复测。规则 glob 未匹配完整分支名;管理员可 Force merge;某个 deploy key 被加入允许列表;测试用的是另一仓库。
收紧 allowlist,开启管理员遵守规则,删除不必要的写部署密钥,并留下限时例外记录。普通成员、管理员和机器人都直推一次,再通过合规 PR 完成一次成功合并。
状态检查一直等待或错误放行
PR 明明完成测试仍显示等待,或错误的 job 成功后就可合并。查看 PR head commit 上报的 context 名称,并与规则 glob 逐字比较;确认报告来自最新提交。复制了另一仓库的 context;大小写或前缀变化;宽泛 * 接受了无关成功状态;CI 只给旧 SHA 上报。
让真实检查先上报一次,再从最近 context 中选取精确模式;变更 job 名时同步规则。对最新提交分别上报失败与成功,确认前者阻断、后者放行。
签名看似有效,身份却已经失效
离职成员或已过期密钥签出的提交仍显示可验证。对照 Gitea 登记公钥、内部密钥资产表和人员状态,不只看绿色标记。Gitea 验证对象和登记公钥的匹配,但不会查询外部吊销与过期状态。
删除失效公钥与 Token,冻结账号,审计密钥有效期内的相关提交;必要时重做合并。旧密钥新签的测试提交必须被治理流程拒绝,新密钥完成登记后才可进入保护分支。
API 返回 401、403 或 404
Token 可以登录某些接口,却无法读写目标仓库。401 查认证格式和 Token;403 查 scope 与账号仓库权限;私有仓库的 404 也可能是为避免泄露而隐藏资源。使用 Bearer 代替 Gitea PAT 的历史 token 头;缺少 read:repository / write:repository;账号不在团队;Token 已撤销。
按实例版本 Swagger 核对认证方式,重建最小 scope Token,修复团队授权而非直接给 Admin。先 GET 当前用户和仓库,再执行一项被授权的最小写操作,最后撤销 Token 并确认请求失败。
开发者访问 Gitea 可能经过三层网络:浏览器/HTTPS Git 的企业代理、SSH 的跳板或 ProxyCommand、Gitea 服务端访问外部 OAuth/Webhook/镜像源的出口代理。三者配置位置不同。浏览器能打开页面,不代表 Git HTTPS、SSH 或服务端 Webhook 可达。
凭证按用途拆分:
| 用途 | 推荐凭证 | 禁止做法 |
|---|---|---|
| 人工 Git 操作 | SSH Key 或受系统凭据库保护的 HTTPS 凭证 | 多人共享私钥、把 Token 写入 remote URL |
| 只读盘点 | read:repository Token | 使用站点管理员全权 Token |
| 仓库自动化 | 专用机器人 + 最小写 scope | 复用员工个人 Token |
| 部署拉取 | 只读 deploy key 或受限机器人 | 给部署环境可写主分支的凭证 |
| 签名 | 个人 GPG/SSH 签名密钥 | 用服务器登录 SSH Key 代替签名治理 |
HTTPS 代理若进行 TLS 检查,应向受管信任库分发企业 CA。不要长期设置 http.sslVerify=false。SSH 首次连接要通过可信渠道核对主机指纹,不把跳过 known_hosts 校验写进团队脚本。
仓库治理至少需要四类 owner:平台 owner 负责实例版本与身份源;组织 owner 负责团队和仓库归属;仓库 owner 负责保护规则与例外;检查 owner 负责状态 context 的稳定命名和结果真实性。一个人可以兼任,但责任不能空缺。
建议把下列基线做成季度复查,而不是一次性配置:
组织 owner、站点管理员、仓库 Admin 和可写 deploy key 清单。main、长期维护分支和发布 tag 的保护规则及绕过者。机器人 Token 的 scope、owner、最后使用时间、轮换与撤销结果。
状态检查 context 是否仍有任务上报,审批失效策略是否与团队约定一致。登记签名密钥与在职人员、有效期和吊销记录是否一致。当前实例版本对应的 stable 文档,以及 next/Enterprise 能力是否被误写入基线。
紧急绕过必须是“限时、双人批准、可追溯、事后复盘”的例外。不要为了处理一次故障永久关闭管理员约束。若规则变更造成全员无法合并,回滚应恢复上一份已验证规则,而不是删除全部保护。
架构取舍
什么时候适合 Gitea
Gitea 适合需要数据驻留、自托管 Git、轻量资源占用、可控升级节奏,且已有 Linux、数据库、存储、身份和备份能力的团队。它也适合作为隔离网络内的代码协作入口,或作为上游平台的镜像与灾备组件,但双向镜像的冲突与权限边界必须另行设计。
不适合的信号包括:团队没有平台 owner;无法承诺恢复演练;依赖大量托管生态集成;审计、合规或全组织策略需要社区版未提供的集中能力;希望“装一个容器”就获得完整企业代码平台。此时托管平台或商业支持可能降低总拥有成本。
社区版、next 与 Enterprise
稳定社区版文档是当前实例配置的第一依据;next 只用于提前评估兼容风险;Enterprise 能力要结合许可证和对应版本验证。例如组织级可继承保护规则属于 Enterprise 能力,社区版团队不能在方案里假定规则会自动下发。社区版若采用逐仓库规则,就必须补模板、盘点 API 和漂移审计。
Gitea Enterprise 审计日志能够记录登录、clone、push、保护规则、Webhook、Token、PR 审查与合并等多类事件,但这是 Enterprise 能力,不能从社区版应用日志反推等价的合规审计。选型时要用事件矩阵逐项验证“谁、在什么时间、对哪个对象、做了什么、结果如何”,并确认查询权限、导出、留存与防篡改方案。缺少关键事件时,应由身份源、反向代理、Git 服务日志和变更系统补偿,同时承认跨系统关联与长期存储的成本。
规则漂移比“没有规则”更难发现
几十个仓库手工配置后,最常见的事故不是全部未保护,而是少数仓库缺一个审批、允许管理员绕过,或状态 context 已改名。页面抽查无法证明全量一致。应通过只读 API 定期导出规则,和版本化基线比较;新增仓库必须进入同一盘点。无法集中继承时,要把漂移检测成本计入自托管选型。
检查名称是仓库规则与 CI 的隐式契约
保护规则消费的是 PR 最新提交上的 context 字符串。CI 重命名 job、拆矩阵或更换供应商,都可能让门禁永远等待或被宽泛模式错误满足。状态 context 应像 API 一样有 owner 和变更评审;迁移时可短期同时上报新旧 context,验证后再切规则。
签名不等于人员生命周期治理
对象签名长期存在,而账号、雇佣关系和密钥有效性会变化。仅要求签名,无法回答“签署时是否在职、密钥是否已泄露、批准者是否独立”。团队需要把签名、账号冻结、密钥资产、审批与审计时间线关联起来。
自托管把恢复责任留在组织内部
仓库 Git 对象并不是全部状态。数据库保存账号、团队、PR、审批和配置;文件存储还可能包含 LFS、附件、包和 Actions 数据。只备份 repositories 目录,恢复后会得到“代码还在、治理证据消失”的残缺系统。
Gitea 的备份与恢复说明要求一致性备份时停止实例,再由运行 Gitea 的 OS 用户执行 dump。MySQL 或 PostgreSQL 场景应同时评估原生数据库工具,因为官方说明 XORM 生成的 SQL dump 仍有恢复风险:
# 进入维护窗口并停止写入后执行
sudo -u git /usr/local/bin/gitea dump -c /etc/gitea/conf/app.ini
# PostgreSQL 示例,密码从受控凭证入口读取
pg_dump -U gitea gitea > gitea-db.sql恢复不是一条自动命令。应在隔离环境手工还原 app.ini、数据库、repositories、LFS、附件及其他应用数据,修正文件 owner;安装方式或路径变化后,再重新生成仓库 Hook:
sudo -u git /usr/local/bin/gitea \
-c /etc/gitea/conf/app.ini admin regenerate hooks恢复成功的判断不能停在“首页能打开”。至少抽取一个私有仓库验证 HTTPS/SSH clone、保护分支拒绝直推、PR 与审批记录、LFS 对象、Webhook 配置和最小权限 Token。备份 owner 负责产出可恢复副本,平台 owner 负责实例与密钥,数据库 owner 负责一致性和原生恢复,仓库 owner 负责业务级抽样;RPO、RTO、保留周期和销毁规则必须有明确责任人。
管理员绕过会形成不可见的第二套流程
管理员为救急而 Force merge,短期看似高效,长期会让审批和检查数据失真。开启管理员遵守保护规则后,真正的紧急通道应是临时授权或限时规则变更,并记录请求人、批准人、时间窗、提交 SHA、恢复动作和复盘链接。
升级影响的是治理语义,不只是页面
保护规则匹配、签名校验、Token scope、API 字段和权限行为都可能随版本变化。升级预演应复制脱敏配置,在测试实例执行“直推失败、PR 通过、旧审批失效、检查阻断、Token 最小权限、签名显示”整套契约测试。只验证首页和仓库 clone,不足以证明治理没有回退。
已确认实例实际版本,并只使用对应 stable 文档配置能力。已区分社区版、next 和 Enterprise 文档,不把未来或商业能力写入现状。组织仓库通过团队授权,没有用个人逐仓库授权替代长期治理。
main 禁止普通成员直推和强推,管理员绕过策略明确。PR 审批、新提交后的旧审批、请求更改和过期分支策略均已验证。状态检查使用真实上报的 context,并验证失败阻断、成功放行。
HTTP(S)、SSH、Web/API 路径都服从同一规则。签名密钥有登记、轮换、吊销和离职回收流程,不只依赖 Verified 标记。人工、机器人、部署和盘点凭证已分离,Token scope 满足最小权限。
未在 remote、脚本、日志或文档中保存真实 Token、私钥和代理口令。规则、团队、管理员、deploy key 和 Token 有 owner 与定期盘点。升级前后会执行治理契约测试,并有恢复上一版本与上一份规则的入口。
测试仓库、测试账号、临时 Token 和本地容器数据已按用途清理。
