Rotating CONTENT_API_KEY — runbook
The CONTENT_API_KEY bearer token gates POST /content/publish and POST /content/rollback on the CAP srv. It's the only credential that can replace tutorial HTML in HANA, so any suspected exposure requires immediate rotation.
Filed as #887. This runbook is the rotation half; the code fix (placeholder in deploy/dev.mtaext, docs scrub, .gitleaks.toml guard) is already merged in the same PR.
When to rotate
- Anyone unauthorized has had shell access to a CF app or a developer workstation with the key exported.
- The key has been printed to any log, ticket, chat, or email that isn't access-controlled.
- Time-based: every 90 days as routine hygiene.
- Immediately if a secret-scanning alert fires on the repo.
Prerequisites
- CF CLI logged in against the target CF space (see
cf target). - Access to the BTP credstore admin UI at
https://<subaccount>.credential-store.cfapps.<region>.hana.ondemand.com/— or the ability to runcf credstore-writevia the credstore CLI plugin. - The GitHub Actions secret
CONTENT_API_KEYvisible/editable at https://github.com/sap-tutorials/tutorials-ims/settings/secrets/actions.
The DEV env has three consumers of the key that must all move together:
- CAP srv — reads via
srv/lib/secret-resolver.js(credstore-first, env fallback). - AppRouter — reads via
approuter/lib/credstore-secret.jsfor/admin/rebuild(usesREBUILD_API_KEY, not this key — separate rotation). - CI publish workflows —
.github/workflows/rebuild-content.ymlreads from theCONTENT_API_KEYrepo secret.
Steps
1. Generate a fresh value
# 40-byte URL-safe random string. Do NOT reuse a value that has ever been
# committed to git or shown in a doc.
openssl rand -base64 40 | tr -d '\n=/' | head -c 48Copy the output to a scratchpad — you'll need it in three places.
2. Update the credstore
Preferred: BTP credstore admin UI → Values tab of the tutorials-credstore instance → find CONTENT_API_KEY → Edit → paste the new value → Save.
CLI alternative (requires the cf credstore plugin):
cf credstore-write CONTENT_API_KEY '<new-value>' --namespace tutorials-srv3. Update the GitHub Actions secret
https://github.com/sap-tutorials/tutorials-ims/settings/secrets/actions → CONTENT_API_KEY → Update secret → paste the new value → Update.
4. Restart the srv to pick up the new value
The credstore cache in srv/lib/secret-resolver.js has a 5-minute TTL, so a restart is not strictly required — but it's the safest way to ensure the next publish uses the new value without a partial-cache window.
cf restart tutorials-srvIf PROD or QA is being rotated, do the same for tutorials-srv-prod / tutorials-srv-qa.
5. Verify
# The publish workflow accepts empty diffs as a no-op; use --dry-run to
# confirm auth succeeds without actually replacing content.
gh workflow run rebuild-content.yml --repo sap-tutorials/tutorials-ims \
--ref main -f mode=slug-targeted -f slug=tutorial-platform-feature-cookbookWatch the workflow log for HTTP 200 on the /content/hashes fetch — that's the auth check. If it 401s, the credstore write and the workflow secret don't agree yet.
6. Revoke the old value
Nothing to do — bearer tokens are single-value in the credstore. Overwriting is revocation.
Post-rotation checklist
- [ ] Old value scrubbed from any incident tickets, chat threads, or shared docs.
- [ ] Rotation date recorded in the operations log (
docs/developers/operations/rotation-log.md— start it if it doesn't exist). - [ ] Next rotation scheduled 90 days out.
Why the docs no longer contain the value
Prior to #887 the DEV value was a well-known ASCII string embedded in CLAUDE.md, the deploy/dev.mtaext, several developer docs, every superpowers plan that mentioned publish testing, and — worst — the built VitePress site under docs/.vitepress/dist/ which is deployed to GitHub Pages at https://sap-tutorials.github.io/tutorials-ims/.
The fix:
deploy/dev.mtaextnow uses${CONTENT_API_KEY}placeholder (likeprod.mtaext/qa.mtaext), resolved byenvsubstin the deploy workflow.- All doc references have been replaced with
$CONTENT_API_KEY(shell variable) or<DEV-content-api-key — fetch from BTP credstore>placeholders. docs/.vitepress/dist/is regenerated at everynpm run docs:build, so the scrub takes effect on the next docs deploy.- The srv-lib admin-docs index (
srv/data/admin-docs-index.json) is generated at build time byscripts/build-admin-docs-index.ts, so it picks up the scrubbed source automatically.
If any future documentation change ever needs to reference the value, use $CONTENT_API_KEY as an unresolved shell variable and point at this runbook.