<!-- last-reviewed: 2026-06-02 · owner: Security & Privacy (Security Director) -->
SOP-102 — Slack credential rotation (signing secret + OAuth token)
SOP / runbook · owner: Security & Privacy (Security Director) (content + policy); Operations (Process Engineer) (Vercel/CI wiring); Principal (executes the Slack admin + Vercel steps). Layer: SOP/Runbook (
docs/canon.md). The first-response runbook for rotatingSLACK_SIGNING_SECRETandSLACK_OAUTH_TOKENafter exposure. Frame: SOP-083 (incident runbook), SOP-004 (docs/sop/SOP-004-secrets-management.md),hr/policies/security.md.
Trigger context (the P0 this closes)
On 2026-06-02 EOD (UTC canonical; ET shown), SLACK_OAUTH_TOKEN and
SLACK_SIGNING_SECRET were exposed in a transcript. Both are managed-store secrets
({{project-slug}}-api/api/slack/events.ts requires them). Per SOP-083 this is SEV-2 (credential of
known exposure on a live external surface) — and because SLACK_SIGNING_SECRET is the
only authenticator on the no-platform-auth {{project-slug}}-api Slack endpoint, an attacker holding
it can forge HMAC-signed events; SLACK_OAUTH_TOKEN lets them call the Slack Web API as the
app. Contain first: rotate now, do not wait for a report cycle.
Interface Contract
- Inputs: exposed-Slack-credential signal (P0, 2026-06-02 EOD) ← from <any agent / Security Analyst / monitoring> · trigger:
review requested - Outputs: env-update requirement (rotated values into Vercel, both projects) → to Engineering Director · trigger:
requirements issued - Outputs: rotation incident report + secret-register date update → to Archivist · trigger:
artifact approved - Outputs: runbook / posture change → to Documentarian · trigger:
document changed - Outputs: escalation (active credential exposure) → to Principal · trigger: (exempt — Principal edge,
interface-contract-spec.md§3.1 R2) - Handoff trigger (in): a Slack credential is reported or detected exposed
- Handoff trigger (out): both credentials revoked + rotated, Vercel updated on both projects, webhook re-verified, repo confirmed secret-free, report archived
- Definition of Done before handoff:
- [ ] SEV-2 confirmed and Principal notified immediately (SOP-083)
- [ ] Both credentials revoked at Slack first, then rotated (SOP-004 — revoke before replace)
- [ ] Rotated values set in Vercel for both projects (intranet + {{project-slug}}-api); old values purged
- [ ] Webhook re-verified (Slack URL-verification challenge → 200; a signed test event ingests)
- [ ] Repo confirmed free of any committed Slack secret value (grep evidence below)
- [ ] Secret-register date updated (SOP-004 §register); dated report handed to Archivist (
artifact approved) - [ ] Learning captured to
docs/knowledge/security/
Triggers are drawn verbatim from
docs/handoff-triggers.md(review requested,requirements issued,artifact approved,document changed). The in-edge reusesreview requestedper the registry’s explicit disposition ofincident detected(“reuse, do not add”;docs/handoff-triggers.md§“Not registered (no live edge)”). No new trigger is required for this SOP. The Principal escalation edge is exempt (§3.1 R2).
Procedure (the Principal executes; Security commands the incident)
1. Contain — revoke at Slack first (do not just edit env)
- Open the Slack app config: api.slack.com/apps → [the {{PROJECT_NAME}} app] → Basic Information.
- Signing Secret: Basic Information → App Credentials → Signing Secret → Regenerate. Regenerating invalidates the exposed secret immediately. Copy the new value.
- OAuth token: OAuth & Permissions → revoke/rotate the bot token. Reinstall the app
to the workspace if revocation requires re-auth; copy the new
xoxb-…bot token. (Revoking invalidates the exposedSLACK_OAUTH_TOKEN— the goal of SOP-004 “revoke first.”) - Do not paste either new value into a chat, PR, issue, doc, or log (charter rule 3;
hr/policies/security.md). They go straight into the managed store (step 2).
2. Update the managed store — Vercel env, BOTH projects
The Slack secrets are consumed by code in two Vercel projects; update both or the webhook breaks / stays compromised:
- {{project-slug}}-api project (
{{project-slug}}-api/api/slack/events.ts— the live webhook): Vercel → [{{project-slug}}-api project] → Settings → Environment Variables. SetSLACK_SIGNING_SECRETandSLACK_OAUTH_TOKENto the new values (Production + Preview as scoped). Delete the old entries (do not leave a stale value). - intranet project (
intranet/src/lib/slack.tsusesSLACK_OAUTH_TOKEN): Vercel → [intranet project] → Settings → Environment Variables. UpdateSLACK_OAUTH_TOKENto the new bot token. (The intranet does not verify inbound HMAC, so it does not hold the signing secret unless present — update it only where set.) - Redeploy each project so the running functions pick up the new env (env changes apply on next deploy). Respect GR-7 — this is a deliberate security checkpoint, batching is fine.
3. Re-verify the webhook
- In Slack app config → Event Subscriptions, click Retry / re-verify the Request
URL (
…/api/slack/events). Expect the URL-verification challenge to return 200 with the echoedchallenge(handler lines 159–161). - Send a benign test message in a non-blocklisted channel; confirm it appears in the intranet chatter feed (HMAC now validates against the new signing secret — old-secret forgeries now fail 403, which is the point).
- If verification fails: the new signing secret in Vercel does not match Slack — re-copy and redeploy. Do not roll back to the exposed secret.
4. Confirm no Slack secret value is committed in the repo
Run (read-only; evidence for the incident report):
grep -rnIE "(xox[bpas]-[0-9A-Za-z-]{10,}|SLACK_[A-Z_]+\s*[:=]\s*['\"][0-9a-fA-F]{16,})" . \
--include="*.ts" --include="*.js" --include="*.json" --include="*.md" \
--include="*.env*" --include="*.yml" --include="*.yaml" | grep -v node_modules
Expect no matches for any literal token/secret value — only process.env.SLACK_*
references (env-var names) are acceptable. Confirm .env / .env.* are gitignored and no
.env file is tracked (git ls-files | grep '\.env' should show only .env.example, which
must hold empty placeholders, never a value). See the 2026-06-02 scan result recorded in
the rotation report.
5. Document, register, archive
- Update the secret register (SOP-004 §register) with the rotation date — never the value.
- Issue the env-update requirement to Engineering if any code/config change is needed
(
requirements issued). - File a dated rotation report (timeline · what was exposed · containment · re-verify result
· residual) to the Archivist (
artifact approved); capture the learning todocs/knowledge/security/.
Roles / approvers
Security Director = incident commander. Principal executes the Slack-admin + Vercel steps (the credentials are the Principal’s to rotate) and is notified immediately (SOP-083, SEV-2). Engineering / Process Engineer handle any env/CI wiring. Security Analyst verifies the repo-clean scan (SOP-084). Archivist preserves the report.
Registry note (for Operations — do NOT self-edit the ledger)
This SOP needs a row in docs/sop/registry.md. Proposed row for Operations to add:
| SOP-102 | Slack credential rotation (signing secret + OAuth token) | Security & Privacy (Security Director) | docs/sop/SOP-102-slack-credential-rotation.md |
102 is in the Security band’s natural next slot beyond 081–084; Operations is the
numbering authority and assigns/confirms the ID. No new handoff trigger is required — the
Interface Contract above reuses registered phrases only.
Guardrails
- Revoke before replace (SOP-004) — regenerating at Slack invalidates the exposed value; do not merely overwrite Vercel and leave the old secret valid.
- Never paste a secret into a chat/PR/doc/log — straight to the managed store
(
hr/policies/security.md, charter rule 3). - Both projects — a half-rotation ({{project-slug}}-api updated, intranet stale, or vice-versa) leaves an exposed credential live.
- Verify before done — the webhook must re-verify and the repo must scan clean before the incident is closed.
See also: SOP-083 (incident runbook) · SOP-004 (secrets management) ·
docs/security/threat-models/slack-neon.md (T5) · hr/policies/security.md ·
{{project-slug}}-api/api/slack/events.ts · intranet/src/lib/slack.ts · docs/handoff-triggers.md.