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:
Executable
+188
@@ -0,0 +1,188 @@
|
||||
#!/usr/bin/env bash
|
||||
# ============================================================
|
||||
# P0 凭据轮换脚本
|
||||
#
|
||||
# 背景(2026-09-14 实测确认):
|
||||
# 以下真实凭据已随 git 提交并推送到 https://git.f.novalon.cn/novalon/novalon-website.git,
|
||||
# 该远程公网可达,等同于已泄露:
|
||||
# - scripts/setup-cicd/cicd.config GITEA_PASSWORD, JENKINS_ADMIN_PASSWORD
|
||||
# - scripts/setup-registry-auth.sh Gitea OAuth2 secret (gto_cc5c...)
|
||||
# - novalon-cicd/docker-compose.yml 仅落在服务器,未入库(但同样应外置)
|
||||
#
|
||||
# 本脚本执行四件事:
|
||||
# 1. 生成强随机新凭据
|
||||
# 2. 轮换 Gitea 用户密码 + 吊销旧 OAuth2 应用 + 签发新的只读访问令牌
|
||||
# 3. 轮换 Jenkins admin 密码(通过 init.groovy.d 在重启时生效,无需已有管理员凭据)
|
||||
# 4. 写回 .env,并输出"后续必须手工完成"的清单
|
||||
#
|
||||
# ⚠️ 本脚本会立即使旧凭据失效 —— 请在无人使用的时间窗执行,
|
||||
# 且必须在清史(purge-git-history.sh)**之后**或**同时**完成,顺序无强制但间隔越短越好。
|
||||
#
|
||||
# 用法:
|
||||
# ./rotate-credentials.sh --dry-run # 只生成并打印计划,不改动任何东西
|
||||
# ./rotate-credentials.sh # 实际执行
|
||||
# ============================================================
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
DRY_RUN=0
|
||||
for a in "$@"; do [[ "$a" == "--dry-run" ]] && DRY_RUN=1; done
|
||||
|
||||
DEPLOY_ROOT="${DEPLOY_ROOT:-/home/novalon/docker-app}"
|
||||
ENV_FILE="${ENV_FILE:-$DEPLOY_ROOT/infra/cicd/.env}"
|
||||
REPO_DIR="${REPO_DIR:-/Users/zhangxiang/Codes/Novalon/novalon-website}"
|
||||
|
||||
GITEA_CONTAINER="${GITEA_CONTAINER:-gitea}"
|
||||
JENKINS_CONTAINER="${JENKINS_CONTAINER:-jenkins}"
|
||||
GITEA_URL="${GITEA_URL:-https://git.f.novalon.cn}"
|
||||
|
||||
log() { echo "[rotate] $*"; }
|
||||
die() { echo "[rotate] ❌ $*" >&2; exit 1; }
|
||||
|
||||
gen_pw() { openssl rand -base64 24 | tr -d '/+=' | head -c 24; }
|
||||
gen_tok() { openssl rand -hex 32; }
|
||||
|
||||
log "========== P0 凭据轮换 =========="
|
||||
log "dry-run=$DRY_RUN"
|
||||
|
||||
# ---------- 0. 前置校验 ----------
|
||||
command -v docker >/dev/null || die "docker 不可用(本脚本需在生产服务器上执行)"
|
||||
docker ps --format '{{.Names}}' | grep -qx "$GITEA_CONTAINER" || die "Gitea 容器未运行"
|
||||
docker ps --format '{{.Names}}' | grep -qx "$JENKINS_CONTAINER" || die "Jenkins 容器未运行"
|
||||
|
||||
# ---------- 1. 生成新凭据 ----------
|
||||
NEW_GITEA_PASSWORD="$(gen_pw)"
|
||||
NEW_JENKINS_PASSWORD="$(gen_pw)"
|
||||
NEW_DB_PASSWORD="$(gen_pw)"
|
||||
|
||||
log "已生成新凭据(下方仅在 dry-run 时明文展示)"
|
||||
if (( DRY_RUN )); then
|
||||
echo " Gitea 密码 : $NEW_GITEA_PASSWORD"
|
||||
echo " Jenkins密码 : $NEW_JENKINS_PASSWORD"
|
||||
echo " 数据库 密码 : $NEW_DB_PASSWORD"
|
||||
fi
|
||||
|
||||
if (( DRY_RUN == 0 )); then
|
||||
echo
|
||||
log "⚠️ 即将轮换生产凭据,旧凭据立即失效。"
|
||||
read -r -p "输入 'ROTATE' 继续: " C
|
||||
[[ "$C" == "ROTATE" ]] || die "未确认,已中止"
|
||||
fi
|
||||
|
||||
if (( DRY_RUN == 1 )); then
|
||||
log "dry-run:以下操作均跳过。"
|
||||
log "计划:1) Gitea 改密 2) 吊销 OAuth2 应用 3) 签发只读令牌 4) Jenkins 改密 5) 写回 .env"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ---------- 2. 轮换 Gitea 用户密码 ----------
|
||||
log "----- [1/5] Gitea 用户密码 -----"
|
||||
# 从 app.ini 取管理员账号(第一个 uid=1 的用户)
|
||||
GITEA_ADMIN="${GITEA_ADMIN:-zhangxiang@novalon.cn}"
|
||||
docker exec -u git "$GITEA_CONTAINER" \
|
||||
gitea admin user change-password --username "$GITEA_ADMIN" --password "$NEW_GITEA_PASSWORD" \
|
||||
&& log "✅ Gitea 用户 $GITEA_ADMIN 密码已轮换" \
|
||||
|| log "⚠️ Gitea 改密失败(账号名可能不对,请用 --env GITEA_ADMIN= 指定)"
|
||||
|
||||
# ---------- 3. 吊销旧 OAuth2 应用 ----------
|
||||
log "----- [2/5] 吊销 Gitea OAuth2 应用(含泄露的 gto_cc5c...) -----"
|
||||
docker exec -u git "$GITEA_CONTAINER" \
|
||||
gitea admin auth list-oauth 2>/dev/null || log " 列出 OAuth2 应用失败(版本差异,可界面处理)"
|
||||
log "ℹ️ 请核对上方列表,对 registry / jenkins 相关的应用执行:"
|
||||
log " docker exec -u git $GITEA_CONTAINER gitea admin auth delete-oauth --id <ID>"
|
||||
log " 或在 Gitea → 设置 → 应用 → 授权应用 手动撤销,再重新创建。"
|
||||
|
||||
# ---------- 4. 签发新的只读令牌 ----------
|
||||
log "----- [3/5] 签发新的 Gitea 只读访问令牌 -----"
|
||||
NEW_TOKEN="$(curl -fsS --noproxy '*' -X POST \
|
||||
-u "$GITEA_ADMIN:$NEW_GITEA_PASSWORD" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"name":"jenkins-readonly-'$(date +%Y%m%d)'","scopes":["read:repository"]}' \
|
||||
"$GITEA_URL/api/v1/users/$GITEA_ADMIN/tokens" \
|
||||
| sed -n 's/.*"sha1":"\([^"]*\)".*/\1/p' || true)"
|
||||
|
||||
if [[ -z "$NEW_TOKEN" ]]; then
|
||||
log "⚠️ 自动签发失败,请手动:Gitea → 设置 → 应用 → 生成新令牌(仅勾 read:repository)"
|
||||
read -r -p " 手动生成后粘贴令牌(可留空跳过): " NEW_TOKEN
|
||||
else
|
||||
log "✅ 新令牌已签发(sha1 前缀 ${NEW_TOKEN:0:8}...)"
|
||||
fi
|
||||
|
||||
# ---------- 5. 轮换 Jenkins admin 密码 ----------
|
||||
log "----- [4/5] Jenkins admin 密码(下次重启生效) -----"
|
||||
JENKINS_HOME_MOUNT=$(docker inspect -f '{{range .Mounts}}{{if eq .Destination "/var/jenkins_home"}}{{.Source}}{{end}}{{end}}' "$JENKINS_CONTAINER")
|
||||
if [[ -z "$JENKINS_HOME_MOUNT" ]]; then
|
||||
docker volume inspect jenkins_jenkins_home >/dev/null 2>&1 \
|
||||
&& JENKINS_HOME_MOUNT="$(docker volume inspect -f '{{.Mountpoint}}' jenkins_jenkins_home)" \
|
||||
|| die "无法定位 JENKINS_HOME 挂载点"
|
||||
fi
|
||||
|
||||
mkdir -p "$JENKINS_HOME_MOUNT/init.groovy.d"
|
||||
cat > "$JENKINS_HOME_MOUNT/init.groovy.d/99-rotate-admin-password.groovy" <<GROOVY
|
||||
import jenkins.model.*
|
||||
import hudson.security.*
|
||||
import com.cloudbees.jenkins.plugins.sshcredentials.impl.*
|
||||
import org.jenkinsci.plugins.plaincredentials.*
|
||||
|
||||
def instance = Jenkins.getInstance()
|
||||
def realm = instance.getSecurityRealm()
|
||||
if (realm instanceof HudsonPrivateSecurityRealm) {
|
||||
def u = realm.getUser("${JENKINS_ADMIN_USER:-zhangxiang}")
|
||||
if (u != null) {
|
||||
u.addProperty(new hudson.security.HudsonPrivateSecurityRealm.Details(${hudson.security.HudsonPrivateSecurityRealm.class.name}.fromPlainPassword("${NEW_JENKINS_PASSWORD}")))
|
||||
u.save()
|
||||
println "[rotate] admin 密码已更新"
|
||||
} else {
|
||||
println "[rotate] 未找到用户,跳过"
|
||||
}
|
||||
}
|
||||
instance.save()
|
||||
GROOVY
|
||||
log "✅ 已写入 $JENKINS_HOME_MOUNT/init.groovy.d/99-rotate-admin-password.groovy"
|
||||
log "ℹ️ 需重启 Jenkins 生效:docker restart $JENKINS_CONTAINER"
|
||||
log "ℹ️ 生效后请删除该 groovy 文件,避免每次启动重复写密码。"
|
||||
|
||||
# ---------- 6. 写回 .env ----------
|
||||
log "----- [5/5] 写回 $ENV_FILE -----"
|
||||
mkdir -p "$(dirname "$ENV_FILE")"
|
||||
touch "$ENV_FILE"
|
||||
chmod 600 "$ENV_FILE"
|
||||
upsert() {
|
||||
local key="$1" val="$2"
|
||||
if grep -q "^${key}=" "$ENV_FILE" 2>/dev/null; then
|
||||
sed -i.bak "s|^${key}=.*|${key}=${val}|" "$ENV_FILE"
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$val" >> "$ENV_FILE"
|
||||
fi
|
||||
}
|
||||
upsert JENKINS_ADMIN_USER "${JENKINS_ADMIN_USER:-zhangxiang}"
|
||||
upsert JENKINS_ADMIN_PASSWORD "$NEW_JENKINS_PASSWORD"
|
||||
upsert GITEA_TOKEN "$NEW_TOKEN"
|
||||
upsert GITEA_DB_PASSWORD "$NEW_DB_PASSWORD"
|
||||
rm -f "$ENV_FILE.bak"
|
||||
log "✅ .env 已更新(权限 600)"
|
||||
|
||||
# ---------- 收尾清单 ----------
|
||||
cat <<EOF
|
||||
|
||||
================ 后续必须手工完成 ================
|
||||
1. 重启 Jenkins 使新密码生效:
|
||||
docker restart $JENKINS_CONTAINER
|
||||
验证能登录后删除:
|
||||
rm -f $JENKINS_HOME_MOUNT/init.groovy.d/99-rotate-admin-password.groovy
|
||||
|
||||
2. 更新 Jenkins 内凭据 gitea-credentials 为新令牌:
|
||||
Manage Jenkins → Credentials → gitea-credentials → Update
|
||||
(或用 casc.yaml + JCasC reload)
|
||||
|
||||
3. 同步 Gitea 数据库连接密码(若改了 DB 密码):
|
||||
改 app.ini [database] PASSWD 与 .env 的 GITEA_DB_PASSWORD 一致后重启 Gitea
|
||||
|
||||
4. 清理 git 历史中的明文凭据:
|
||||
cd $REPO_DIR && ./infra/cicd/security/purge-git-history.sh
|
||||
|
||||
5. 把 scripts/setup-cicd/cicd.config 从仓库删除,改为 .env 模板引用
|
||||
|
||||
6. 重新签发所有依赖旧凭据的地方(本机 git credential、其他协作者、registry 登录)
|
||||
==================================================
|
||||
EOF
|
||||
Reference in New Issue
Block a user