#!/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 " 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" </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 <