Files
novalon-website/infra/cicd/security/rotate-credentials.sh
T
zhangxiang 4ab2f3cd8e chore(infra): 新增 Gitea+Jenkins CI/CD 部署与凭据整改
- infra/cicd:docker-compose(gitea/jenkins)、JCasC(凭据统一 ${ENV} 注入,无硬编码)、
  备份/恢复、健康检查、凭据轮换与 git 历史清除脚本
- docs/deployment/cicd:安装、高可用备份监控、凭据事故复盘
- .env.example 仅为占位模板;真实 .env 由 .gitignore 排除
2026-09-20 10:37:57 +08:00

189 lines
7.9 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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