chore(infra): 新增 Gitea+Jenkins CI/CD 部署与凭据整改
- infra/cicd:docker-compose(gitea/jenkins)、JCasC(凭据统一 ${ENV} 注入,无硬编码)、
备份/恢复、健康检查、凭据轮换与 git 历史清除脚本
- docs/deployment/cicd:安装、高可用备份监控、凭据事故复盘
- .env.example 仅为占位模板;真实 .env 由 .gitignore 排除
This commit was merged in pull request #27.
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
# 01 · 安装部署 / 安全配置 / Gitea↔Jenkins 集成
|
||||
|
||||
配置源码:[`infra/cicd/`](../../../infra/cicd/)。本文描述**目标态**与从现状迁移的步骤。
|
||||
|
||||
## 1. 安装部署
|
||||
|
||||
### 1.1 前置条件
|
||||
|
||||
```bash
|
||||
# Docker 20.10+,docker compose v2
|
||||
docker --version && docker compose version
|
||||
# 外部网络(nginx 反代依赖同一网络做服务名解析)
|
||||
docker network create novalon-network 2>/dev/null || true
|
||||
```
|
||||
|
||||
### 1.2 部署 Gitea
|
||||
|
||||
```bash
|
||||
cd /home/novalon/docker-app/infra/cicd
|
||||
cp .env.example .env && vim .env # 填 GITEA_DB_PASSWORD 等,.env 绝不入库
|
||||
docker compose -f docker-compose.gitea.yml up -d
|
||||
docker compose -f docker-compose.gitea.yml ps # 等 health=healthy
|
||||
```
|
||||
|
||||
要点:
|
||||
- **资源限制用 `cpus`/`mem_limit` 顶层键,不要用 `deploy.resources`**。
|
||||
现状 compose 写的是 `deploy:`,非 swarm 下被 `docker compose` **静默忽略**,
|
||||
等于"看起来限制了实际没有"。这是本次审计确认的实锤。
|
||||
- SSH 映射 `2222:22`,HTTP 仅 `127.0.0.1:3001`,由 nginx 终止 TLS。
|
||||
|
||||
### 1.3 部署 Jenkins
|
||||
|
||||
```bash
|
||||
# ⚠️ 现状卷属主是 root(因历史 user: root),切非 root 前必须先改属主
|
||||
docker stop jenkins
|
||||
docker run --rm -v jenkins_jenkins_home:/v alpine chown -R 1000:1000 /v
|
||||
cd /home/novalon/docker-app/infra/cicd
|
||||
docker compose -f docker-compose.jenkins.yml up -d
|
||||
```
|
||||
|
||||
### 1.4 迁移时的破坏性变更(需维护窗口)
|
||||
|
||||
| 变更 | 为什么 | 影响 |
|
||||
|------|--------|------|
|
||||
| Jenkins `user: root` → `1000:1000` | root 容器 = 宿主 root | 需先 `chown` 卷,服务中断 ~2 分钟 |
|
||||
| 移除 `/var/run/docker.sock` | 作业 config.xml 实测 **0 处** docker 调用 | 若未来作业要用 docker,需改用 dind/socket-proxy |
|
||||
| 移除 `/home/novalon/docker-app` 挂载 | `deploy.sh` 是 `rsync` 到远端,不读本地该目录 | 无 |
|
||||
| `/root/.ssh` → `./jenkins-ssh` | 不再暴露宿主 root 私钥 | 需生成专用部署密钥并放服务器 `authorized_keys` |
|
||||
|
||||
## 2. 基础安全配置
|
||||
|
||||
### 2.1 HTTPS
|
||||
|
||||
由 `nginx-static-production.conf` + `conf.d/*.conf` 终止,无需应用自配。
|
||||
|
||||
```nginx
|
||||
# 已有配置(conf.d/git.f.novalon.cn.conf)
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
|
||||
location /.well-known/acme-challenge/ { root /var/www/certbot; }
|
||||
location / { return 301 https://$host$request_uri; }
|
||||
```
|
||||
|
||||
**已知小瑕疵(P3)**:nginx `add_header` 与 Gitea 自身都注入 `x-frame-options`/`x-content-type-options`,
|
||||
响应里出现两次。不影响安全(重复值同语义),如要清理:nginx 侧改用 `proxy_hide_header` 后再 `add_header`。
|
||||
|
||||
### 2.2 访问权限
|
||||
|
||||
| 层 | 配置 | 实测状态 |
|
||||
|----|------|----------|
|
||||
| Gitea 注册 | `DISABLE_REGISTRATION=true` + `REQUIRE_SIGNIN_VIEW=true` | 环境变量已设;**注意**:env 只在首次安装写库,需核对 `app.ini [service]` 是否同步 |
|
||||
| Gitea 匿名读 | `REQUIRE_SIGNIN_VIEW` | 未登录访问 `/` 返回 303 → `/explore` |
|
||||
| Jenkins 匿名读 | `allowAnonymousRead: false`(casc.yaml) | 待 JCasC 生效后验证 |
|
||||
| Jenkins 自助注册 | `allowsSignup: false` | 同上 |
|
||||
| agent 协议 | 仅 `Ping`,关 JNLP 入站 | 无 agent,可整段删 `50000` 映射 |
|
||||
|
||||
### 2.3 凭据管理(重点)
|
||||
|
||||
**原则:任何密码/令牌只允许出现在两个地方 —— 服务器 `.env`(600 权限)或 Jenkins Credentials。**
|
||||
|
||||
```
|
||||
infra/cicd/.env ← 服务器本地,600,不入库
|
||||
infra/cicd/.env.example ← 模板,只有占位符,可入库
|
||||
jenkins/casc.yaml ← 凭据用 ${VAR} 引用环境变量,不写明文
|
||||
```
|
||||
|
||||
**禁止**再出现 `cicd.config` 那种明文密码入库。相关 P0 事故见 [03](03-credential-incident.md)。
|
||||
|
||||
Gitea 令牌最小权限:Jenkins 拉代码只需 `read:repository`,不要给 `write:administration`。
|
||||
|
||||
## 3. Gitea ↔ Jenkins 集成
|
||||
|
||||
### 3.1 数据流
|
||||
|
||||
```
|
||||
git push (main/develop)
|
||||
└─► Gitea webhook → https://ci.f.novalon.cn/generic-webhook-trigger/invoke?token=***
|
||||
└─► GenericTrigger 解析 $.ref / $.repository.full_name / $.after
|
||||
└─► regexpFilterExpression ^refs/heads/(main|develop)$ 通过 → 触发构建
|
||||
└─► Jenkinsfile 各 stage
|
||||
```
|
||||
|
||||
### 3.2 作业定义(现状)
|
||||
|
||||
```xml
|
||||
<!-- /var/jenkins_home/jobs/novalon-website-ci-cd/config.xml -->
|
||||
<definition class="...CpsScmFlowDefinition">
|
||||
<scm class="hudson.plugins.git.GitSCM">
|
||||
<url>https://git.f.novalon.cn/novalon/novalon-website.git</url>
|
||||
<credentialsId>gitea-credentials</credentialsId>
|
||||
<branches><name>*/main</name></branches>
|
||||
</scm>
|
||||
<scriptPath>Jenkinsfile</scriptPath>
|
||||
<lightweight>true</lightweight>
|
||||
</definition>
|
||||
<triggers/>
|
||||
```
|
||||
|
||||
### 3.3 ⚠️ 排障:推送不触发构建
|
||||
|
||||
这是本次排查中最容易踩的坑,按顺序核对:
|
||||
|
||||
1. **`<triggers/>` 为空是正常的吗?** 是 —— `GenericTrigger` 声明在 Jenkinsfile 的
|
||||
`triggers {}` 块里,**第一次成功构建之后**才会在作业上注册 trigger。
|
||||
所以:全新作业 push 后不触发,**先手动 Build Once 一次**。
|
||||
2. **Token 是否一致**:Jenkinsfile 里 `token: '${PROJECT_NAME}-ci-token'`
|
||||
是 **Groovy 字符串插值**,在双引号字符串里才会展开。
|
||||
写在 `token: '...'`(单引号)里就是字面量 `${PROJECT_NAME}-ci-token`。
|
||||
Gitea webhook 里填的 token 必须与 Jenkins 实际生效值一致(去作业配置页看真实 token)。
|
||||
3. **Webhook 状态**:Gitea → 仓库 → 设置 → Web 钩子 → 最近推送记录,
|
||||
看 HTTP 状态码。`403` = token 错;`404` = 插件端点错;`200/204` = 已接受。
|
||||
4. **过滤表达式**:`regexpFilterExpression: '^(refs/heads/main|refs/heads/develop)$'`
|
||||
—— 只在这两个分支触发,其它分支 push 静默忽略属预期。
|
||||
|
||||
### 3.4 流水线阶段(Jenkinsfile 现状)
|
||||
|
||||
| Stage | 内容 | 分支条件 |
|
||||
|-------|------|----------|
|
||||
| 🔧 环境检测 | 工具版本 + SSH 连通性 + registry | 全部 |
|
||||
| 📥 安装依赖 | `npm ci`(失败回退 `npm install --legacy-peer-deps`) | 全部 |
|
||||
| 🔍 质量检查 | ESLint ∥ TypeScript | 全部 |
|
||||
| 🧪 单元测试 | `test:coverage:check` + Coverage Report | 全部 |
|
||||
| 🌐 E2E | build + `@smoke\|@critical` + `@journey` | main |
|
||||
| 👁️ 视觉回归 | `visual-regression.spec.ts` | main |
|
||||
| 🔒 安全扫描 | `npm audit` + 安全响应头 | main |
|
||||
| 🏗️ 构建 | `build:clean` → `dist/`,archiveArtifacts | 全部 |
|
||||
| 🚀 部署 | `deploy.sh deploy --skip-build`,失败自动 rollback | main + 参数开关 |
|
||||
|
||||
> 注意:E2E/视觉回归 stage 里 `|| echo "⚠️ ..."` 会**吞掉失败退出码**,
|
||||
> 测试挂了流水线仍然绿。若要严格门禁需改为 `exit 1`。
|
||||
|
||||
### 3.5 集成自检命令
|
||||
|
||||
```bash
|
||||
# 1. Gitea 可达
|
||||
curl -s -o /dev/null -w '%{http_code}\n' https://git.f.novalon.cn/api/v1/version
|
||||
|
||||
# 2. Jenkins 通用 webhook 端点存在(错误 token 应 403 而非 404)
|
||||
curl -s -o /dev/null -w '%{http_code}\n' \
|
||||
'https://ci.f.novalon.cn/generic-webhook-trigger/invoke?token=wrong'
|
||||
|
||||
# 3. Gitea webhook 配置(需管理员或有仓库权限的令牌)
|
||||
# Gitea UI → 仓库 → 设置 → Web 钩子
|
||||
```
|
||||
@@ -0,0 +1,176 @@
|
||||
# 02 · 高可用 / 数据备份 / 日志监控
|
||||
|
||||
脚本源码:[`infra/cicd/backup/`](../../../infra/cicd/backup/)、[`infra/cicd/monitoring/`](../../../infra/cicd/monitoring/)。
|
||||
本文所有数字均为 2026-09-14 在生产实测所得。
|
||||
|
||||
## 1. 高可用:先说清现状边界
|
||||
|
||||
**当前是单机部署,没有真正的 HA。** 不要被 compose 里的 `deploy.replicas` 迷惑 ——
|
||||
那是 Swarm 语法,`docker compose` 非 swarm 模式下**完全忽略**,replicas 实际恒为 1。
|
||||
|
||||
| 组件 | 现状 | 单点风险 | 建议(按性价比排序) |
|
||||
|------|------|----------|---------------------|
|
||||
| Jenkins | 单实例 | 构建历史丢失(可重建) | 接受单点,**靠备份**,不值得上 HA |
|
||||
| Gitea | 单实例 | 代码托管中断(clone 有本地副本) | 接受单点 + 异地备份;量大了再考虑主从 |
|
||||
| PostgreSQL | 单实例 | **用户/权限/PR/Issue/Webhook 唯一副本** | 最高优先级,每日备份必保 |
|
||||
| nginx | 单实例 | 全站入口 | 20G/2C 小机,先做日志轮转与监控,不建议硬上 keepalived |
|
||||
| 宿主机 | 单机 | 全部 | **异地备份是唯一真正有效的容灾** |
|
||||
|
||||
结论:这台 2C/20G 机器的 HA 策略 = **快速恢复能力**(备份 + 恢复演练 + 运维手册),
|
||||
而不是冗余部署。冗余的成本(至少 3 台机器 + 运维复杂度)远超当前业务规模所需。
|
||||
|
||||
## 2. 数据备份
|
||||
|
||||
### 2.1 备份什么(以及为什么这样分)
|
||||
|
||||
| 数据 | 体积(实测) | 丢失后果 | 策略 |
|
||||
|------|-----------|----------|------|
|
||||
| Gitea PostgreSQL | 1.7MB | **不可重建**:用户、权限、PR、Issue、webhook | **每日必备** |
|
||||
| Jenkins 配置 | 18KB | 作业定义、凭据、用户 | **每日必备** |
|
||||
| Jenkins plugins | 265M | 可从清单重装 | **不备份**,导出清单 |
|
||||
| Gitea 裸库 | 939M | Git 分布式,每份 clone 都是完整历史 | **每周全量** + 异地 |
|
||||
| jenkins_home/war | 111M | 镜像自带 | 不备份 |
|
||||
|
||||
> 关键取舍:Jenkins 薄备份原本 222MB,**99% 是 `plugins/`**。
|
||||
> 排除后降到 18KB,磁盘压力从"不可持续"变为"可忽略"。
|
||||
> 恢复时用 `jenkins-plugins-<ts>.txt` + `jenkins-plugin-cli` 重装即可。
|
||||
|
||||
### 2.2 备份脚本
|
||||
|
||||
[`backup-cicd.sh`](../../../infra/cicd/backup/backup-cicd.sh) 的关键设计:
|
||||
|
||||
```bash
|
||||
# 每份产物落盘后立刻校验:非空 + 归档可列出,坏备份当场失败
|
||||
verify_archive "$f" tar
|
||||
|
||||
# 磁盘预检:剩余 < MIN_FREE_GB 直接中止(防写满拖垮 nginx)
|
||||
if (( FREE_GB < MIN_FREE_GB )); then fail "..."; fi
|
||||
|
||||
# Jenkins 归档容忍退出码 1("读取时文件变化"),>=2 才算失败
|
||||
if (( TAR_RC >= 2 )); then fail "..."; fi
|
||||
```
|
||||
|
||||
实测输出(首份全量备份):
|
||||
|
||||
```
|
||||
✅ gitea-db-20260914_174059.sql.gz 1.7MB
|
||||
✅ gitea-repos-20260914_174059.tar.gz 939MB (3177 条目)
|
||||
✅ jenkins-config-20260914_174059.tar.gz 222MB (2829 条目)
|
||||
✅ 凭据解密所需的 master key 与 hudson.util.Secret 均已包含
|
||||
```
|
||||
|
||||
### 2.3 调度(已安装到生产 crontab)
|
||||
|
||||
```
|
||||
30 3 * * 1-6 backup-cicd.sh --skip-repos # 周一至周六:薄(~2MB)
|
||||
30 3 * * 0 backup-cicd.sh # 周日:全量(~940MB)
|
||||
```
|
||||
|
||||
保留策略**按类型分开计数**(`BACKUP_FULL_RETENTION=1`、`BACKUP_THIN_RETENTION=3`):
|
||||
|
||||
> 踩坑记录:若统一按「最近 N 份」裁剪,周中的薄备份会把周日全量挤出窗口,
|
||||
> 仓库实际只受保护 3 天。分层计数后才真正保住全量。
|
||||
|
||||
### 2.4 恢复
|
||||
|
||||
[`restore-cicd.sh`](../../../infra/cicd/backup/restore-cicd.sh) 内置三道防线:
|
||||
|
||||
1. 归档完整性校验(tar/gzip 可列出)
|
||||
2. **`secrets/master.key` 存在性校验** —— 缺失则直接拒绝恢复
|
||||
(否则恢复完所有 Jenkins 凭据都解不开,等于白做)
|
||||
3. 恢复前先对当前库做一份快照
|
||||
|
||||
```bash
|
||||
./restore-cicd.sh --from /home/novalon/backups/cicd/<ts> --component jenkins --dry-run
|
||||
./restore-cicd.sh --from /home/novalon/backups/cicd/<ts> --component jenkins
|
||||
```
|
||||
|
||||
**铁律:未做过恢复演练的备份等于没有备份。** 每季度至少跑一次 `--dry-run`。
|
||||
|
||||
### 2.5 ⚠️ 当前最大缺口:仅本地备份
|
||||
|
||||
同盘备份不抗主机故障(盘坏/被删/勒索 = 全没)。脚本会持续警告:
|
||||
|
||||
```
|
||||
⚠️ 未配置 RCLONE_REMOTE —— 备份仅存本地。同盘备份不抗主机故障。
|
||||
```
|
||||
|
||||
**待办(需要决策)**:
|
||||
- 配置对象存储(COS/OSS/S3 任一),`rclone` 安装后在 `.env` 填 `RCLONE_REMOTE`
|
||||
- 或扩容磁盘后仍建议至少一份异地
|
||||
|
||||
## 3. 日志
|
||||
|
||||
### 3.1 已知事故:849MB 单份日志
|
||||
|
||||
2026-09-14 实测:`novalon-cicd/docker-compose.yml` 里明确配了
|
||||
`logging: { max-size: 10m, max-file: 3 }`,但运行中容器的
|
||||
`HostConfig.LogConfig` 实际是 `{"Type":"json-file","Config":{}}` —— **空配置**。
|
||||
|
||||
根因:**logging 参数在容器创建时固化**,事后改 compose 不重建就不生效。
|
||||
Gitea 日志因此涨到 849MB,占整盘 4%。
|
||||
|
||||
已做的两件事:
|
||||
1. 立即 `truncate -s 0` 释放(磁盘 85% → 81%)
|
||||
2. 兜底 cron 每日 05:10 跑 [`truncate-container-logs.sh`](../../../infra/cicd/monitoring/truncate-container-logs.sh)
|
||||
|
||||
**终态修复**(待维护窗口):用 `infra/cicd/docker-compose.gitea.yml` 重建容器,
|
||||
使 logging 配置真正生效,之后兜底脚本可退化为纯监控。
|
||||
|
||||
### 3.2 日志留存
|
||||
|
||||
| 来源 | 位置 | 轮转 |
|
||||
|------|------|------|
|
||||
| 容器 stdout | `/var/lib/docker/containers/<id>/*-json.log` | 重建后 10m×3;兜底 cron 每日截断 >100MB |
|
||||
| nginx access/error | `/var/log/nginx/*.log` | docker-compose-nginx.yml 挂载宿主 |
|
||||
| 备份 | `/var/log/cicd-backup.log` | 无(体积小,建议加 logrotate) |
|
||||
| 健康检查 | `/var/log/cicd-healthcheck.log` | 无(同上) |
|
||||
|
||||
## 4. 监控
|
||||
|
||||
### 4.1 healthcheck-cicd.sh(每 10 分钟)
|
||||
|
||||
6 个维度,全部实测可跑:
|
||||
|
||||
```
|
||||
[1] 容器状态 gitea/jenkins/postgresql/nginx 的 healthy
|
||||
[2] HTTPS 端点 三个域名 200/303
|
||||
[3] TLS 证书 剩余天数(<21 天 WARN,<7 天 FAIL)
|
||||
[4] 磁盘与卷 使用率(>=80 WARN,>=90 FAIL)+ jenkins_home 体积
|
||||
[5] 备份新鲜度 最新备份距今 <36h + 产物数 >=2
|
||||
[6] Jenkins 队列 积压 >10 任务 FAIL
|
||||
```
|
||||
|
||||
实测首跑即抓到 2 个真问题(容器 healthcheck 判定 bug 已修 + 磁盘 90%)。
|
||||
|
||||
```bash
|
||||
# FAIL 时 exit 1,cron 可直接接告警
|
||||
./healthcheck-cicd.sh # 全量输出
|
||||
./healthcheck-cicd.sh --quiet # 只在 FAIL/WARN 时输出
|
||||
./healthcheck-cicd.sh --json # 供采集器
|
||||
```
|
||||
|
||||
### 4.2 告警
|
||||
|
||||
`.env` 配 `ALERT_WEBHOOK_URL`(企业微信/钉钉 webhook)后,
|
||||
健康检查 FAIL 和备份失败都会推送。
|
||||
|
||||
### 4.3 现有监控资产
|
||||
|
||||
`scripts/monitoring/` 已有轻量方案(`setup-lightweight-monitoring.sh` 等),
|
||||
本次未改动。若要上 Prometheus,注意这台机器磁盘余量,指标留存周期要设短。
|
||||
|
||||
## 5. 已知环境陷阱(排障速查)
|
||||
|
||||
| 症状 | 真因 | 处理 |
|
||||
|------|------|------|
|
||||
| compose 配了资源限制但没生效 | `deploy:` 段非 swarm 下被忽略 | 用 `cpus`/`mem_limit` |
|
||||
| compose 配了日志轮转但没生效 | logging 参数创建时固化 | 重建容器 |
|
||||
| `grep -q` 在 `set -o pipefail` 下恒假 | `-q` 提前退出 → 上游 SIGPIPE(141) | 用 herestring 或 `grep -c` |
|
||||
| 函数做布尔用却返回非 0 | `f() { [[ cond ]] && echo; }` 在 cond 为假时返回 1,`cond2 && f \|\| g` 会跌进 g | 函数末尾显式 `return 0` |
|
||||
| 健康检查 FAIL 数随 `--quiet`/`--json` 抖动 | 同上:输出被抑制 → 助手函数返回 1 | 同上 |
|
||||
| `curl -w '%{http_code}' \|\| echo 000` 得到 `200000` | curl 已输出 200 但退出码非 0,`echo` 追加拼接 | 用 `|| true` + 正则校验三位数字 |
|
||||
| tar 报错但文件都存在 | 归档列表里写了不存在的路径(如 `nodes/`) | 归档 `.` + `--exclude` |
|
||||
| `for x in "$@"` 内 `shift` 取错值 | for 迭代的是展开后的固定列表,shift 不影响序列 | 用 `while [[ $# -gt 0 ]]` 解析 |
|
||||
| SSH 备份脚本中途死掉 | 断连触发 SIGHUP | cron 场景无此问题;手动跑用 `setsid` |
|
||||
| `/root` 占 2.7G | `/root/.trae-cn-server`(bin 2.1G + manager-logs 522M) | 属 AI 工具数据,清理需 owner 确认 |
|
||||
@@ -0,0 +1,99 @@
|
||||
# 03 · P0 凭据入库事故(待处置)
|
||||
|
||||
> **状态:待执行。** 本文档是处置手册,不是已完成的整改记录。
|
||||
> 2026-09-14 由会话内审计发现,张翔已确认「出方案、由他执行」。
|
||||
|
||||
## 1. 事故定性
|
||||
|
||||
**真实生产凭据已提交进 git 并推送到公网可达的远程仓库。**
|
||||
|
||||
- 远程:`https://git.f.novalon.cn/novalon/novalon-website.git`(公网可达,实测 HTTP 200)
|
||||
- 泄露物:Gitea 登录密码、Jenkins admin 密码、Gitea OAuth2 应用 secret、CI webhook token
|
||||
|
||||
**任何能访问该仓库的人都可以直接登录生产 Gitea 与 Jenkins。**
|
||||
即使是私有仓库,也等效于把密码写进了版本历史 —— 无法撤销,只能轮换。
|
||||
|
||||
## 2. 泄露位置(实测)
|
||||
|
||||
| 文件 | 泄露内容 | 首次入库 |
|
||||
|------|----------|----------|
|
||||
| `scripts/setup-cicd/cicd.config` | `GITEA_PASSWORD`、`JENKINS_ADMIN_PASSWORD`、`WEBHOOK_TOKEN` | `b8ab1fd` |
|
||||
| `scripts/setup-registry-auth.sh` | Gitea OAuth2 应用 secret(`gto_…`,共 48 字符) | 同期 |
|
||||
| `novalon-cicd/docker-compose.yml` | PostgreSQL 密码(仅服务器,未入库) | — |
|
||||
|
||||
确认命令(可复现):
|
||||
|
||||
```bash
|
||||
git ls-files --error-unmatch scripts/setup-cicd/cicd.config # → 已跟踪
|
||||
git log --oneline -- scripts/setup-cicd/cicd.config # → af57504, b8ab1fd
|
||||
grep -rn "PASSWORD\s*=" scripts/setup-cicd/ scripts/setup-registry-auth.sh
|
||||
```
|
||||
|
||||
## 3. 处置步骤(按序执行)
|
||||
|
||||
工具已备好:[`infra/cicd/security/`](../../../infra/cicd/security/)
|
||||
|
||||
### Step 1 — 轮换凭据(先做,别等清史)
|
||||
|
||||
```bash
|
||||
ssh root@139.155.109.62 \
|
||||
'cd /home/novalon/docker-app/infra/cicd/security && ./rotate-credentials.sh --dry-run'
|
||||
# 确认无误后去掉 --dry-run 实际执行
|
||||
```
|
||||
|
||||
脚本会:
|
||||
1. 生成强随机新凭据(`openssl rand`)
|
||||
2. 轮换 Gitea 用户密码(`gitea admin user change-password`)
|
||||
3. 吊销泄露的 OAuth2 应用
|
||||
4. 签发新的**只读**(`read:repository`)Gitea 令牌
|
||||
5. Jenkins admin 密码经 `init.groovy.d` 在下次重启生效(无需已有管理员凭据)
|
||||
6. 写回服务器 `.env`(600 权限)
|
||||
|
||||
**⚠️ 执行后旧凭据立即失效**,请在无人使用 CI/CD 的时间窗操作。
|
||||
|
||||
### Step 2 — 清除 git 历史
|
||||
|
||||
```bash
|
||||
pip install git-filter-repo # 或 brew install git-filter-repo
|
||||
cd /path/to/novalon-website
|
||||
./infra/cicd/security/purge-git-history.sh --dry-run # 先扫描确认命中数
|
||||
./infra/cicd/security/purge-git-history.sh # 会先做 mirror 备份
|
||||
```
|
||||
|
||||
脚本做了三重保护:
|
||||
- 改写前自动 `git clone --mirror` 备份
|
||||
- 二次确认(输入 `PURGE`)
|
||||
- 清史后重新扫描,仍有残留则**拒绝推送**并中止
|
||||
|
||||
**强制推送的连带影响(必须提前通知)**:
|
||||
- 所有协作者必须 `rm -rf` 本地仓库后重新 clone —— 旧仓库 push 会把泄露内容带回
|
||||
- 未合入的 PR / fork 可能失效
|
||||
|
||||
### Step 3 — 删除远程残留对象
|
||||
|
||||
清史后远程的旧 blob 仍可通过 commit sha 直接访问:
|
||||
|
||||
```
|
||||
Gitea → 仓库 → 设置 → 维护 → 运行垃圾回收
|
||||
```
|
||||
|
||||
### Step 4 — 防复发
|
||||
|
||||
- 删除 `scripts/setup-cicd/cicd.config`,改用 `.env` + `.env.example` 模式
|
||||
- 装秘密扫描钩子(gitleaks / trufflehog),`pre-commit` + CI 双层
|
||||
- 把「凭据只进 `.env`(600)或 Jenkins Credentials」写进 code review 检查项
|
||||
|
||||
## 4. 验收标准(执行完必须逐条核对)
|
||||
|
||||
- [ ] `git grep` 全历史无明文密码(`git log -p | grep -c` 为 0)
|
||||
- [ ] 旧密码登录 Gitea/Jenkins 均失败
|
||||
- [ ] 新令牌权限为 `read:repository`,Jenkins 能正常拉代码
|
||||
- [ ] 一次 push 能触发流水线(验证凭据替换没破坏链路)
|
||||
- [ ] Gitea OAuth2 应用列表中无泄露的 `gto_…` 应用
|
||||
- [ ] 服务器 `.env` 权限为 600,且已加入 `.gitignore`
|
||||
|
||||
## 5. 为什么「清史」不能替代「轮换」
|
||||
|
||||
历史可能已被 clone / fork / 缓存 / 搜索引擎抓取,清除是**尽力而为**。
|
||||
密码一旦进过公网仓库,唯一可靠的补救是**让它失效**。
|
||||
所以顺序是:先轮换(让泄露的密码立即变成废纸),再清史(降低持续暴露面)。
|
||||
@@ -0,0 +1,86 @@
|
||||
# CI/CD 生产环境:Gitea + Jenkins 运维文档
|
||||
|
||||
> 基准日期:2026-09-14。本文所有数据来自对生产服务器 `139.155.109.62` 的实测(SSH 探测 + HTTP 探测),
|
||||
> 非泛泛的最佳实践摘抄。配置源码在 [`infra/cicd/`](../../../infra/cicd/)。
|
||||
|
||||
## 文档索引
|
||||
|
||||
| 文档 | 内容 |
|
||||
|------|------|
|
||||
| [01-installation.md](01-installation.md) | 安装部署、安全加固、Gitea↔Jenkins 集成、webhook 排障 |
|
||||
| [02-ha-backup-monitoring.md](02-ha-backup-monitoring.md) | 高可用、备份与恢复、日志与监控 |
|
||||
| [03-credential-incident.md](03-credential-incident.md) | **P0**:凭据入库事故与处置清单(待执行) |
|
||||
|
||||
## 当前架构(实测)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ 139.155.109.62 (VM-0-15-debian) │
|
||||
│ Debian · Docker 20.10.24 │
|
||||
│ │
|
||||
推送 git push ────────┐│ novalon-nginx-secure (nginx:alpine)│
|
||||
│ :80/:443 终止 TLS + HSTS │
|
||||
│ └──────┬──────────────┬──────────────┘
|
||||
│ │ │
|
||||
│ git.f.novalon.cn ci.f.novalon.cn
|
||||
│ │ │
|
||||
│ upstream gitea_app upstream jenkins_app
|
||||
│ │ │
|
||||
│ ┌───────▼─────┐ ┌─────▼──────┐
|
||||
│ │ gitea │ │ jenkins │
|
||||
└─────────►│ 1.27.2 │ │ 2.568.1 │
|
||||
│ :3001(本机) │ │ :8080(本机) │
|
||||
│ :2222 SSH │ └─────┬──────┘
|
||||
└───────┬─────┘ │
|
||||
│ │ rsync+ssh
|
||||
┌───────▼─────┐ │
|
||||
│ postgresql │ ▼
|
||||
│ 17-alpine │ /home/novalon/docker-app/
|
||||
└─────────────┘ novalon-website(Next.js)
|
||||
```
|
||||
|
||||
- 代码仓库:`git.f.novalon.cn/novalon/novalon-website`(本仓库 `origin` 即指向此处)
|
||||
- 流水线:`Jenkinsfile`(仓库根目录),作业名 `novalon-website-ci-cd`
|
||||
- 触发:Gitea push → webhook → Jenkins `GenericTrigger` → 构建/测试 → `scripts/deploy.sh` rsync 发布
|
||||
- 反代与 TLS:`nginx-static-production.conf` 定义 upstream,`conf.d/*.conf` 定义 server 块
|
||||
|
||||
## 2026-09-14 审计结论
|
||||
|
||||
### 已就绪(无需改动)
|
||||
|
||||
| 项 | 实测证据 |
|
||||
|----|----------|
|
||||
| HTTPS + HSTS | 两域名均 `strict-transport-security: max-age=63072000` |
|
||||
| Gitea Cookie 安全 | `i_like_gitea` 带 `HttpOnly; Secure; SameSite=Lax` |
|
||||
| 作业与插件齐备 | `gitea`、`generic-webhook-trigger`、`credentials-binding` 已装 |
|
||||
| 反向代理信任链 | `REVERSE_PROXY_TRUSTED_PROXIES=172.20.0.0/16,127.0.0.1` |
|
||||
| 服务端口不外暴 | Gitea/Jenkins HTTP 均仅 `127.0.0.1`,只有 nginx 占 80/443 |
|
||||
|
||||
### 发现的问题(按严重度)
|
||||
|
||||
| 级别 | 问题 | 证据 | 状态 |
|
||||
|------|------|------|------|
|
||||
| **P0** | 真实凭据已提交 git 并推送公网 | `cicd.config` Gitea/Jenkins 密码、OAuth2 secret | 待处置([03](03-credential-incident.md)) |
|
||||
| **P0** | Jenkins 从未备份 | `backup.sh` 存在但 crontab 无此项,`/home/novalon/backups/` 不存在 | ✅ 已修复 |
|
||||
| **P1** | 磁盘 20G 用 85–90% | `df -h /` → 88% used, 2.4G avail | ⚠️ 需扩容/异地 |
|
||||
| **P1** | Jenkins 以 root 运行 + 挂 docker.sock + /root/.ssh | compose `user: root`;作业实测 0 处 docker 调用 | 配置已备好,待维护窗口 |
|
||||
| **P1** | Gitea SSH 克隆地址不可用 | `app.ini SSH_PORT=22`,实际暴露 `0.0.0.0:2222` | 配置已备好 |
|
||||
| **P2** | 容器日志轮转未生效 | Gitea 单份日志 849MB(compose 配置在,运行容器 `LogConfig={}`) | ✅ 已截断+兜底 cron |
|
||||
| **P2** | Gitea 数据库零备份 | `postgresql_backup/` 为空 | ✅ 已修复 |
|
||||
| **P3** | 安全头重复注入 | Gitea 响应 `x-frame-options` 出现两次(nginx + 应用) | 低优先 |
|
||||
|
||||
## 快速操作
|
||||
|
||||
```bash
|
||||
# 备份(薄:仅 DB + Jenkins 配置,约 2MB)
|
||||
ssh root@139.155.109.62 '/home/novalon/docker-app/infra/cicd/backup/backup-cicd.sh --skip-repos'
|
||||
|
||||
# 备份(全量:含 Gitea 裸库,约 940MB)
|
||||
ssh root@139.155.109.62 '/home/novalon/docker-app/infra/cicd/backup/backup-cicd.sh'
|
||||
|
||||
# 健康检查(6 维度,FAIL 时 exit 1)
|
||||
ssh root@139.155.109.62 '/home/novalon/docker-app/infra/cicd/monitoring/healthcheck-cicd.sh'
|
||||
|
||||
# 已装的定时任务
|
||||
ssh root@139.155.109.62 'crontab -l | grep infra/cicd'
|
||||
```
|
||||
Reference in New Issue
Block a user