Playbook: DISCORD_BOT_TOKEN Rotation
Superseded — the canonical, more complete playbook is
rotate-discord-bot-token.md(adds GH Secret rotation,/api/v10/applications/@meverification, n8n restart, and the browser-close security step). Prefer that one; this file is retained for backward-compatible links.
Service: Discord Developer Portal (https://discord.com/developers/applications)
Secret: DISCORD_BOT_TOKEN
SOPS file: secrets/monitoring.env.sops
Rotation frequency: 90 days (or immediately on suspected exposure)
Last rotated: 2026-06-27
Next due: 2026-09-27
What uses this credential
| Consumer | How | Effect if missing |
|---|---|---|
| n8n workflows on bms-4 | HTTP Request nodes posting to Discord via Bot API | Bot messages fail; webhook fallback may still work |
| Monitoring alert automation | Discord bot posts structured embeds to alert channels | Alert embeds stop; only raw webhook messages remain |
Note: Discord webhook URLs (
P24_DISCORD_INFRA_SCRIPTS_ERRORS_WEBHOOK_URL, etc.) are separate from the bot token. Webhooks do not require the bot token and are unaffected by this rotation.
Where stored
| Location | Key name | How to update |
|---|---|---|
secrets/monitoring.env.sops | DISCORD_BOT_TOKEN | SOPS write pattern |
| vps-i1 deployed env | /opt/p24-infra/monitoring/.env | Auto-synced by secrets-sync.yml on merge |
Login credentials
- URL:
https://discord.com/developers/applications - Email: radieu@gmail.com
- Password: stored in
discord_radieu_passwordkey insecrets/monitoring.env.sops- Safe extraction:
(sops --decrypt ... secrets\monitoring.env.sops | Select-String "^discord_radieu_password=").ToString().Split('=',2)[1]
- Safe extraction:
Automation status
Playwright-automatable — Discord Developer Portal bot token reset is accessible via web UI. No additional MFA required after login (2FA may be on the account; check if TOTP is needed or if the session stays active).
Rotation steps
Option A — Playwright agent (preferred)
You are rotating DISCORD_BOT_TOKEN for the Discord bot used by p24-infra automation.
IMPORTANT: Never display any secret value in your response. Reference key names only.
1. Read login credentials safely (never print values):
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$env:DISCORD_PASS = (sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops |
Select-String "^discord_radieu_password=").ToString().Split('=',2)[1].Trim()
2. Use Playwright to navigate to https://discord.com/login
- Email: radieu@gmail.com
- Password: $env:DISCORD_PASS (pass as fill value, never log it)
- If 2FA is prompted, check .env.local for DISCORD_TOTP_SECRET or ask user for OTP
- Navigate to https://discord.com/developers/applications
- Find the p24-infra bot application (look for "p24" or "infra" in app names)
- Click the application → Bot (left sidebar)
- Click "Reset Token" → confirm → copy new token into $env:NEW_TOKEN (never print)
- Close the browser
$env:DISCORD_PASS = ""
3. Update secrets/monitoring.env.sops:
$plain = sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops
$temp = "$PWD\secrets\monitoring-edit.env.sops"
$updated = $plain -replace "^DISCORD_BOT_TOKEN=.*", "DISCORD_BOT_TOKEN=$env:NEW_TOKEN"
[System.IO.File]::WriteAllText($temp, ($updated -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
$enc = sops --encrypt --input-type dotenv --output-type dotenv $temp
[System.IO.File]::WriteAllText("$PWD\secrets\monitoring.env.sops", ($enc -join "`n") + "`n", [System.Text.UTF8Encoding]::new($false))
sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops | Out-Null
if ($LASTEXITCODE -ne 0) { throw "SOPS corrupt — do NOT commit" }
[System.IO.File]::Delete($temp); $env:NEW_TOKEN = ""; $env:SOPS_AGE_KEY_FILE = ""
4. Commit and PR targeting main:
git checkout -b fix/rotate-discord-bot-token origin/main
git add secrets/monitoring.env.sops
git commit -m "chore: rotate DISCORD_BOT_TOKEN (scheduled 90d rotation)"
git push -u origin fix/rotate-discord-bot-token
gh pr create --base main --title "chore: rotate DISCORD_BOT_TOKEN"
gh pr merge --merge --delete-branch
5. Append to docs/secrets-rotation-log.md:
| <YYYY-MM-DD HH:MM UTC> | — | DISCORD_BOT_TOKEN | scheduled 90d rotation | AI-agent | SOPS (monitoring) |
Option B — Manual (fallback)
Step 1 — Log in to Discord Developer Portal
URL: https://discord.com/developers/applications
Email: radieu@gmail.com
Password: see discord_radieu_password in secrets/monitoring.env.sops
Complete 2FA if prompted
Step 2 — Reset bot token
→ Select the p24-infra bot application
→ Bot → Reset Token → confirm
→ Copy the new token (shown once; starts with long string containing two periods)
Step 3 — Store in .env.local then hand off to Claude:
Add to d:\code_2026\p24-infra\.env.local:
DISCORD_BOT_TOKEN_NEW=<value>
Tell Claude "Discord bot token updated in .env.local" — Claude handles SOPS + PR.
Verification
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$line = sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops |
Select-String "^DISCORD_BOT_TOKEN="
$val = $line.ToString().Split('=',2)[1]
Write-Host "DISCORD_BOT_TOKEN length: $($val.Length)" # expected: > 50
# Quick API check (bot identity — no messages sent)
$env:T = $val
$r = Invoke-RestMethod "https://discord.com/api/v10/users/@me" `
-Headers @{ Authorization = "Bot $env:T" }
Write-Host "Discord bot: $($r.username)#$($r.discriminator)"
$env:T = ""; $env:SOPS_AGE_KEY_FILE = ""Escalation
| Symptom | Action |
|---|---|
| Discord login requires 2FA code | Check .env.local for DISCORD_TOTP_SECRET; if absent, user must provide OTP (Option B) |
| Developer Portal shows no applications | Confirm you are logged in as radieu@gmail.com, not a different account |
| ”Reset Token” button is greyed out | Bot may require privileged intents to be confirmed — check Bot settings |
| vps-i1 automation still fails after merge | Check secrets-sync.yml ran; or SSH to vps-i1 and restart affected container |
Prevention
- Discord bot tokens do not expire but can be compromised; rotate on schedule.
- The
credential-rotation.ymlGH Actions workflow (Monday 06:00 UTC) checksnext_dueand opens ahuman-actionissue. - After rotation, update Last rotated and Next due at the top of this playbook.
Audit Log — Log to infra_operations
After this operation completes, log it to the infra_operations audit table.
Python (Linux server — bms-4, vps-i1, vps-h1, or similar):
import sys
sys.path.insert(0, '/opt/p24-infra')
from scripts.lib.log_op import log_op
log_op(
actor="claude", # "radieu" for manual human ops, "claude" for agent
op_type="credential_rotation",
resource="DISCORD_BOT_TOKEN",
result="success", # "success" | "failed" | "skipped"
detail="Scheduled rotation — new Discord bot token regenerated and SOPS updated",
env="bms-4",
gh_issue=2730,
)PowerShell (Windows dev machine):
$env:SUPABASE_URL = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_URL=").ToString().Split("=",2)[1].Trim()
$env:SUPABASE_SERVICE_KEY = (Get-Content "C:\code_2026\p24-infra\.env.local" | Select-String "^SUPABASE_SERVICE_KEY=").ToString().Split("=",2)[1].Trim()
python -c "
import os, sys
sys.path.insert(0, 'C:/code_2026/p24-infra')
from scripts.lib.log_op import log_op
log_op('claude', 'credential_rotation', 'DISCORD_BOT_TOKEN', 'success', 'Scheduled rotation — new Discord bot token regenerated and SOPS updated', 'bms-4')
"
$env:SUPABASE_URL = ''; $env:SUPABASE_SERVICE_KEY = ''