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:
2026-09-20 10:37:57 +08:00
parent 7df2dde438
commit 4ab2f3cd8e
15 changed files with 2092 additions and 0 deletions
+164
View File
@@ -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 1cron 可直接接告警
./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 / 缓存 / 搜索引擎抓取,清除是**尽力而为**。
密码一旦进过公网仓库,唯一可靠的补救是**让它失效**。
所以顺序是:先轮换(让泄露的密码立即变成废纸),再清史(降低持续暴露面)。
+86
View File
@@ -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-websiteNext.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 用 8590% | `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 单份日志 849MBcompose 配置在,运行容器 `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'
```