Commit 0089d522 authored by xuwang's avatar xuwang
Browse files

feat(renovate): renovate-bot-rotate — atomic token rotation



Add `make renovate-bot-rotate` (renovate-bot.sh rotate) using GitLab's rotate API:
revokes the old token and issues a new one with the same name/scopes + fresh
RENOVATE_BOT_EXPIRES, then rewrites RENOVATE_BOT_TOKEN_FILE and the Vault path.
- group/project token: rotate by id with the creator token
- service account: self-rotate with the bot token itself (no admin)
confirm.sh-guarded (cron-friendly via NONINTERACTIVE). Replaces the old
re-create-then-revoke dance (which left duplicate same-named tokens).

Changelog: added
Co-Authored-By: default avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 21f751c1
Loading
Loading
Loading
Loading
+16 −3
Original line number Diff line number Diff line
@@ -223,6 +223,7 @@ Created with your admin token (`GITLAB_TOKEN` / `~/.gitlab-token`, Owner/Maintai
| --- | --- |
| `renovate-bot-create` | Create the bot token — a **group** token if `RENOVATE_BOT_GROUP` is set (one identity for the whole multi-repo scan), else a **project** token (one repo's CI). Stored at `RENOVATE_BOT_TOKEN_FILE`. |
| `renovate-bot-list` | List `renovate-bot` tokens on the target group/project. |
| `renovate-bot-rotate` | Atomically rotate the token (GitLab revokes the old, issues a new one with the same name/scopes) and rewrite the file + Vault (`confirm.sh`-guarded). |
| `renovate-bot-revoke` | Revoke the token and remove the local file (`confirm.sh`-guarded). |
| `renovate-bot-ci-set` | Set the masked `RENOVATE_TOKEN` CI/CD variable on a project (`REPO=ns/proj`) from the stored token. |

@@ -232,7 +233,7 @@ Created with your admin token (`GITLAB_TOKEN` / `~/.gitlab-token`, Owner/Maintai
| `RENOVATE_BOT_REPO` | origin | Project for project-token / `ci-set` |
| `RENOVATE_BOT_TOKEN_FILE` | `~/.otica-tokens/renovate.token` | Where the token is stored |
| `RENOVATE_BOT_ACCESS_LEVEL` | `30` (Developer) | `40` (Maintainer) if the bot must automerge protected branches |
| `RENOVATE_BOT_EXPIRES` | +365 days | Token expiry (`YYYY-MM-DD`); re-run create to rotate |
| `RENOVATE_BOT_EXPIRES` | +365 days | Token expiry (`YYYY-MM-DD`); used for new and rotated tokens |

**Use it for the scan** — point Renovate at the bot token (it outranks the ambient
`GITLAB_TOKEN`):
@@ -252,8 +253,20 @@ make renovate-bot-create # project token (origin)
make renovate-bot-ci-set        # sets masked RENOVATE_TOKEN on the project
```

Rotate by re-running `renovate-bot-create` before `RENOVATE_BOT_EXPIRES`; tear down
with `renovate-bot-revoke`.
### Rotation

`make renovate-bot-rotate` rotates **atomically** via GitLab's rotate API — it
revokes the old token and issues a new one with the same name/scopes and a fresh
`RENOVATE_BOT_EXPIRES`, then rewrites `RENOVATE_BOT_TOKEN_FILE` and the Vault path.
No duplicate tokens, no manual revoke.

- group/project token → rotated by id with your creator token;
- service-account token → self-rotated using the bot token itself (no admin).

It's `confirm.sh`-guarded, so it's cron-friendly with `NONINTERACTIVE=1` — schedule
it ahead of `RENOVATE_BOT_EXPIRES`. If the token feeds a CI variable, re-run
`renovate-bot-ci-set` after rotating (or read it from Vault in CI so there's
nothing to update). Tear down entirely with `renovate-bot-revoke`.

### Cross-group bot (sub-repos in several top-level groups)

+4 −0
Original line number Diff line number Diff line
@@ -64,6 +64,10 @@ renovate-bot-create: ## create a dedicated Renovate bot token (group if RENOVATE
renovate-bot-list: ## list renovate-bot tokens on the target group/project
	@renovate-bot.sh list

.PHONY: renovate-bot-rotate
renovate-bot-rotate: ## rotate the renovate-bot token (revoke old + issue new); updates file/Vault
	@if confirm.sh "Rotate the renovate-bot token (the old token will be revoked)?"; then renovate-bot.sh rotate ; fi

.PHONY: renovate-bot-revoke
renovate-bot-revoke: ## revoke the renovate-bot token and remove the local file
	@if confirm.sh "Revoke the renovate-bot token?"; then renovate-bot.sh revoke ; fi
+53 −2
Original line number Diff line number Diff line
@@ -17,6 +17,7 @@
# Usage:
#   renovate-bot.sh create            # create the bot token, store it locally
#   renovate-bot.sh list              # list renovate-bot tokens on the target
#   renovate-bot.sh rotate            # atomically revoke-old + issue-new, update file/Vault
#   renovate-bot.sh revoke            # revoke it + remove the local file
#   renovate-bot.sh ci-set [repo]     # set masked RENOVATE_TOKEN CI var on a
#                                      # project from the stored bot token
@@ -187,15 +188,65 @@ cmd_add_groups() {
    done
}

# ── Rotation ────────────────────────────────────────────────────────────────────
# Store a token to the local file and (if configured) Vault — shared by rotate.
_store_token() {   # _store_token <token> [vault_default_user]
    local tok="$1" vuser="${2:-}" vpath
    mkdir -p "$(dirname "${TOKEN_FILE}")"; chmod 700 "$(dirname "${TOKEN_FILE}")" 2>/dev/null || true
    printf '%s' "${tok}" > "${TOKEN_FILE}"; chmod 600 "${TOKEN_FILE}"
    echo "  token stored: ${TOKEN_FILE}  (expires ${EXPIRES})"
    vpath="${RENOVATE_TOKEN_VAULT_PATH:-}"
    [ -z "${vpath}" ] && [ -n "${SEC_PATH:-}" ] && [ -n "${vuser}" ] && vpath="${SEC_PATH}/gitlab/renovate_bot/${vuser}/token"
    if [ -n "${vpath}" ]; then
        "${THIS_DIR}/vault-write.sh" "${vpath}" "${tok}" >/dev/null \
            && echo "  vault:   ${vpath}" || echo "  vault:   FAILED to write ${vpath}" >&2
    fi
}

# Read the current bot token (for SA self-rotate): file, else Vault.
_current_token() {
    [ -f "${TOKEN_FILE}" ] && { cat "${TOKEN_FILE}"; return; }
    [ -n "${RENOVATE_TOKEN_VAULT_PATH:-}" ] && "${THIS_DIR}/vault-read.sh" "${RENOVATE_TOKEN_VAULT_PATH}" 2>/dev/null && return
    return 1
}

# Rotate atomically (GitLab revokes the old token, issues a new one with the same
# name/scopes). Group/project: rotate by token id with the creator token. Service
# account (RENOVATE_BOT_USER set, no group): self-rotate with the bot token itself.
cmd_rotate() {
    local resp new id cur
    if [ -z "${RENOVATE_BOT_GROUP:-}" ] && [ -n "${RENOVATE_BOT_USER:-}" ]; then
        cur="$(_current_token || true)"
        [ -n "${cur}" ] || err "need the current SA token (RENOVATE_BOT_TOKEN_FILE or RENOVATE_TOKEN_VAULT_PATH)."
        resp=$(curl -s --header "PRIVATE-TOKEN: ${cur}" -X POST \
            "${GITLAB_API}/personal_access_tokens/self/rotate" --data "expires_at=${EXPIRES}")
        new=$(echo "${resp}" | jq -r '.token // empty')
        [ -n "${new}" ] || err "SA self-rotate failed: $(echo "${resp}" | jq -r '.message // .error // .')"
        echo "Rotated service-account token for ${RENOVATE_BOT_USER} (old revoked)."
        _store_token "${new}" "${RENOVATE_BOT_USER}"
    else
        id=$(curl -s "${hdr[@]}" "${api}" | jq -r --arg n "${NAME}" \
            '[.[]? | select(.name==$n and .active==true)] | sort_by(.expires_at) | last | .id // empty')
        [ -n "${id}" ] || err "no active ${NAME} token on ${KIND} ${TARGET} to rotate (create one first)."
        resp=$(curl -s "${hdr[@]}" -X POST "${api}/${id}/rotate" --data "expires_at=${EXPIRES}")
        new=$(echo "${resp}" | jq -r '.token // empty')
        [ -n "${new}" ] || err "rotate failed on ${KIND} ${TARGET}: $(echo "${resp}" | jq -r '.message // .error // .')"
        echo "Rotated ${NAME} on ${KIND} ${TARGET} (old revoked)."
        _store_token "${new}"
    fi
    echo "  if you push the token to CI vars, re-run: renovate-bot.sh ci-set [repo]"
}

cmd="${1:-help}"; shift || true
case "${cmd}" in
    create)     cmd_create "$@";;
    list)       cmd_list "$@";;
    revoke)     cmd_revoke "$@";;
    rotate)     cmd_rotate "$@";;
    ci-set)     cmd_ci_set "$@";;
    sa-create)  cmd_sa_create "$@";;
    add-groups) cmd_add_groups "$@";;
    *) echo "usage: renovate-bot.sh <create | list | revoke | ci-set [repo] | sa-create | add-groups>";
       echo "  single group/project token: renovate-bot.sh create";
    *) echo "usage: renovate-bot.sh <create | list | revoke | rotate | ci-set [repo] | sa-create | add-groups>";
       echo "  single group/project token: renovate-bot.sh create   (rotate: renovate-bot.sh rotate)";
       echo "  cross-group service account: sa-create (admin) then add-groups (group owner)";;
esac