Add daily Coder token auto-rotation workflow

This Coder deployment caps API token lifetime at 168h (7 days), so
rather than raising that cap deployment-wide, add
.gitea/workflows/rotate-coder-token.yml: runs daily, mints a new 168h
coder token, PUTs it into this repo's CODER_SESSION_TOKEN secret via
Gitea's actions/secrets API (confirmed against the live instance's
swagger.v1.json - PUT /repos/{owner}/{repo}/actions/secrets/{name} with
{"data": "..."}), then deletes the token(s) it replaced. Old token isn't
touched until the new one is confirmed live, so a failed run fails safe.

Needs a one-time GITEA_ROTATION_TOKEN secret (a Gitea PAT with
write:repository scope, no expiration) so the workflow can write to its
own repo's secrets going forward - documented in README. After that,
CODER_SESSION_TOKEN (used by coder-templates.yml) never needs manual
attention again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 12:02:22 +02:00
parent a9583535f0
commit a5a760b24a
2 changed files with 112 additions and 4 deletions
+83
View File
@@ -0,0 +1,83 @@
name: Rotate Coder API Token
# Keeps the CODER_SESSION_TOKEN secret (used by coder-templates.yml) alive
# forever without anyone needing to remember to refresh it. This Coder
# deployment caps token lifetime at 168h (7 days), so this runs daily,
# mints a fresh 168h token, writes it back into this repo's
# CODER_SESSION_TOKEN secret via the Gitea API, then deletes the token(s)
# it replaced.
#
# One-time bootstrap (see README "Auto-provisioning" section): a
# GITEA_ROTATION_TOKEN secret holding a Gitea personal access token
# (write:repository scope, no expiration) with permission to write this
# repo's Actions secrets. Nothing else needs to touch this ever again.
on:
schedule:
- cron: "0 3 * * *"
workflow_dispatch: {}
jobs:
rotate:
runs-on: ubuntu-latest
env:
CODER_URL: ${{ secrets.CODER_URL }}
CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }}
GITEA_ROTATION_TOKEN: ${{ secrets.GITEA_ROTATION_TOKEN }}
GITEA_API_URL: ${{ github.server_url }}/api/v1
GITEA_REPO_PATH: ${{ github.repository }}
steps:
- name: Install coder CLI and jq
run: |
set -e
curl -fsSL https://coder.com/install.sh | sh
coder version
command -v jq >/dev/null 2>&1 || (apt-get update -qq && apt-get install -y -qq jq)
- name: Create a new token
id: new_token
run: |
set -e
NAME="gitea-ci-$(date -u +%Y%m%dT%H%M%SZ)"
TOKEN="$(coder tokens create --name "$NAME" --lifetime 168h)"
if [ -z "$TOKEN" ]; then
echo "::error::coder tokens create returned no token"
exit 1
fi
echo "::add-mask::$TOKEN"
echo "name=$NAME" >> "$GITHUB_OUTPUT"
echo "token=$TOKEN" >> "$GITHUB_OUTPUT"
- name: Publish new token to CODER_SESSION_TOKEN secret
env:
NEW_TOKEN: ${{ steps.new_token.outputs.token }}
run: |
set -e
BODY="$(jq -n --arg data "$NEW_TOKEN" '{data:$data}')"
HTTP_STATUS="$(curl -s -o /tmp/put-secret.out -w '%{http_code}' \
-X PUT \
-H "Authorization: token ${GITEA_ROTATION_TOKEN}" \
-H "Content-Type: application/json" \
-d "$BODY" \
"${GITEA_API_URL}/repos/${GITEA_REPO_PATH}/actions/secrets/CODER_SESSION_TOKEN")"
if [ "$HTTP_STATUS" != "201" ] && [ "$HTTP_STATUS" != "204" ]; then
echo "::error::Failed to update CODER_SESSION_TOKEN secret (HTTP $HTTP_STATUS)"
cat /tmp/put-secret.out
exit 1
fi
echo "CODER_SESSION_TOKEN secret updated (HTTP $HTTP_STATUS)."
- name: Delete the token(s) this replaced
env:
KEEP_NAME: ${{ steps.new_token.outputs.name }}
run: |
set -e
coder tokens list -o json \
| jq -r --arg keep "$KEEP_NAME" \
'.[] | select(.token_name | startswith("gitea-ci-")) | select(.token_name != $keep) | .id' \
| while read -r id; do
[ -z "$id" ] && continue
echo "Removing superseded token $id"
coder tokens delete "$id" --delete \
|| echo "::warning::Failed to delete superseded token $id"
done
+29 -4
View File
@@ -134,19 +134,44 @@ It runs on the `ubuntu-latest` self-hosted runner already registered on this
Gitea instance and installs the `coder` CLI itself via `coder.com/install.sh`.
**One-time setup required** (not something this workflow can do for itself -
needs a human with Coder access):
needs a human with Coder access). This deployment caps API token lifetime at
168h (7 days), so rather than raising that cap deployment-wide,
`.gitea/workflows/rotate-coder-token.yml` (see below) keeps a fresh token
flowing into the secret automatically:
1. Create a Coder API token - ideally under a dedicated service account
rather than a personal login, since this token can create/delete
templates:
```sh
coder login https://code.octoturge.com
coder tokens create --name gitea-ci --lifetime 8760h
coder tokens create --name gitea-ci --lifetime 168h
```
2. In Gitea, go to this repo's **Settings -> Actions -> Secrets** (or the
org-level equivalent to share across repos) and add:
- `CODER_URL` = `https://code.octoturge.com`
- `CODER_SESSION_TOKEN` = the token printed by step 1
Until those secrets exist, the workflow will run and fail cleanly at the
`coder templates push` step rather than doing anything destructive.
Until those secrets exist, `coder-templates.yml` will run and fail cleanly
at the `coder templates push` step rather than doing anything destructive.
### Token rotation (Gitea Actions)
`.gitea/workflows/rotate-coder-token.yml` runs daily and keeps
`CODER_SESSION_TOKEN` alive forever without anyone needing to remember to
refresh it: it mints a new 168h Coder token, writes it into the
`CODER_SESSION_TOKEN` secret via the Gitea API, then deletes the token(s) it
just replaced. If a run ever fails, the previous token is still untouched
and still valid (nothing gets deleted until the new one is confirmed live),
so it fails safe rather than locking you out.
**One-time bootstrap** (also needs a human - this is what lets the rotation
workflow write to its own repo's secrets):
1. Create a Gitea personal access token with **write:repository** scope and
**no expiration** (Settings -> Applications -> Generate New Token). This
one doesn't rotate itself, so give it a long life up front.
2. Add it as a repo/org Actions secret named `GITEA_ROTATION_TOKEN`.
After that, `CODER_SESSION_TOKEN` never needs manual attention again - you
can also trigger a rotation on demand from Gitea's Actions tab
(`workflow_dispatch`) instead of waiting for the daily schedule.