Skip to content

Secrets Inventory and Rotation Guide

1. Secrets Inventory

1.1 AI / ML Provider Keys

VariableProviderStorage LocationUsed ByRotation URL
ANTHROPIC_API_KEYAnthropic (Claude)Cloudflare Worker secrets, AWS SSMworkers/api (image enhancement, PDF conversion)https://console.anthropic.com/settings/keys
GEMINI_API_KEYGoogle AI StudioCloudflare Worker secretsworkers/api (PDF conversion, vision)https://aistudio.google.com/apikey
OPENAI_API_KEYOpenAICloudflare Worker secretsworkers/api (GPT-4o mini vision)https://platform.openai.com/api-keys
MISTRAL_API_KEYMistral AICloudflare Worker secretsworkers/api (OCR extraction, optional)https://console.mistral.ai/api-keys
MARKER_API_KEYDatalabCloudflare Worker secretsworkers/api (PDF-to-Markdown)Datalab dashboard

1.2 Document Processing Keys

VariableProviderStorage LocationUsed ByRotation URL
MATHPIX_APP_IDMathpixCloudflare Worker secretsworkers/api (math OCR)https://accounts.mathpix.com
MATHPIX_APP_KEYMathpixCloudflare Worker secretsworkers/api (math OCR)https://accounts.mathpix.com

1.3 Payment Processing

VariableProviderStorage LocationUsed ByRotation URL
STRIPE_SECRET_KEYStripeCloudflare Worker secretsworkers/api, workers/org-chart-api (billing)https://dashboard.stripe.com/apikeys
STRIPE_WEBHOOK_SECRETStripeCloudflare Worker secretsworkers/api (webhook verification)https://dashboard.stripe.com/webhooks
STRIPE_PUBLISHABLE_KEYStripeCloudflare Worker secretsFrontend apps (client-side Stripe.js)https://dashboard.stripe.com/apikeys
STRIPE_LINKS_PRO_MONTHLY_PRICE_IDStripeCloudflare Worker secretsworkers/api (pricing)Stripe Products dashboard
STRIPE_LINKS_PRO_YEARLY_PRICE_IDStripeCloudflare Worker secretsworkers/api (pricing)Stripe Products dashboard

1.4 Database & Auth

VariableProviderStorage LocationUsed ByRotation URL
SUPABASE_URLSupabase.env files, Cloudflare secretsAll appshttps://supabase.com/dashboard/project/vuvwmfxssjosfphzpzim/settings/api
SUPABASE_ANON_KEYSupabase.env files, Cloudflare secretsAll apps (public, safe to expose)Same as above
SUPABASE_SERVICE_ROLE_KEYSupabaseCloudflare secrets, AWS SSMworkers/api, server-side operationsSame as above
JWT_SECRETSelf-managedCloudflare Worker secretsworkers/api (token signing)Generate new: openssl rand -base64 48

1.5 Email & Notifications

VariableProviderStorage LocationUsed ByRotation URL
RESEND_API_KEYResendCloudflare Worker secrets, AWS SSMworkers/api (transactional email)https://resend.com/api-keys
AWS_ACCESS_KEY_IDAWS IAMCloudflare Worker secrets, GitHub Actions secretsworkers/api (SES), deploy pipelinehttps://console.aws.amazon.com/iam
AWS_SECRET_ACCESS_KEYAWS IAMCloudflare Worker secrets, GitHub Actions secretsworkers/api (SES), deploy pipelineSame as above
SES_WEBHOOK_SECRETSelf-managedCloudflare Worker secretsworkers/api (EventBridge auth)Generate new: openssl rand -hex 32
TELEGRAM_BOT_TOKENTelegramCloudflare Worker secretsworkers/api (admin alerts, optional)https://t.me/BotFather

1.6 Infrastructure & Deployment

VariableProviderStorage LocationUsed ByRotation URL
CLOUDFLARE_API_TOKENCloudflareGitHub Actions secretsCI/CD deploy pipelinehttps://dash.cloudflare.com/profile/api-tokens
PACKAGES_TOKENGitHubGitHub Actions secretsCI/CD (private npm registry)https://github.com/settings/tokens
GITHUB_TOKENGitHubCloudflare Worker secretsworkers/api (issue creation)https://github.com/settings/tokens

1.7 Web Push (Optional)

VariableProviderStorage LocationUsed By
VAPID_PUBLIC_KEYSelf-generatedCloudflare Worker secretsPush notifications
VAPID_PRIVATE_KEYSelf-generatedCloudflare Worker secretsPush notifications

1.8 Search / Enrichment (Optional)

VariableProviderStorage LocationUsed By
SERPER_API_KEYSerper.devCloudflare Worker secretsProspect enrichment

1.9 Non-Secret Configuration (reference only)

These are not secrets but are deployed alongside them:

  • CLOUDFLARE_ACCOUNT_ID: c6cce84d1636ec85ec946a19edef0103
  • SUPABASE_PROJECT_REF (prod): vuvwmfxssjosfphzpzim
  • SUPABASE_PROJECT_REF (dev): rmwscaavdmzoerqyppbf
  • KV namespace IDs (in wrangler.toml)
  • R2 bucket names (in wrangler.toml)
  • SES_FROM_EMAIL, SES_CONFIGURATION_SET, PUBLIC_BASE_URL
  • Stripe price IDs

2. Where Secrets Are Stored

StoreWhat Lives ThereAccess
Cloudflare Worker SecretsAll runtime secrets for Workerswrangler secret put <KEY> or Cloudflare dashboard
AWS SSM Parameter StoreSupabase, S3, Resend keys for LambdaAWS Console or aws ssm put-parameter
GitHub Actions SecretsCLOUDFLARE_API_TOKEN, PACKAGES_TOKEN, AWS_*GitHub repo Settings β†’ Secrets
Local .env filesDev-only values, never committed.gitignore covers .env, .env.*
tools/benchmark-cli/.envLocal dev keys for benchmarking.gitignore covers it β€” not in git

3. Rotation Procedures

General Rotation Protocol

For every key rotation, follow this sequence:

  1. Generate a new key in the provider’s dashboard (do NOT revoke the old key yet)
  2. Deploy the new key to all locations that consume it (see table below)
  3. Verify the service works with the new key (hit a test endpoint, run a smoke test)
  4. Revoke the old key in the provider’s dashboard
  5. Log the rotation date (update the table in Section 5)

3.1 Cloudflare Worker Secrets

Most runtime secrets live here. To rotate:

Terminal window
# Set the new secret on each worker that uses it
wrangler secret put ANTHROPIC_API_KEY --name accessible-pdf-api
wrangler secret put ANTHROPIC_API_KEY --name accessible-org-chart-api
# ... repeat for each worker listed in workers/ and apps/
# Verify by hitting a test endpoint or checking logs
curl -s https://api.theaccessible.org/health | jq .

Workers that consume secrets (check each wrangler.toml for the authoritative list):

WorkerKey Secrets
accessible-pdf-apiANTHROPIC, GEMINI, OPENAI, MISTRAL, MATHPIX, MARKER, STRIPE, RESEND, JWT_SECRET, SUPABASE, AWS, SES_WEBHOOK_SECRET
accessible-org-chart-apiSTRIPE, SUPABASE
accessible-convert-emailSUPABASE, RESEND
accessible-gatewayJWT_SECRET
accessible-phoneSUPABASE
Other app workersVaries β€” check individual wrangler.toml

3.2 AWS SSM Parameter Store

Terminal window
# Update a parameter (SecureString)
aws ssm put-parameter \
--name "/accessible-pdf/production/RESEND_API_KEY" \
--value "re_NEW_KEY_HERE" \
--type SecureString \
--overwrite
# Lambda functions pick up new SSM values on next cold start.
# Force a refresh by redeploying or updating the function config:
aws lambda update-function-configuration \
--function-name accessible-pdf-api \
--description "rotated RESEND_API_KEY $(date +%Y-%m-%d)"

3.3 GitHub Actions Secrets

  1. Go to GitHub repo β†’ Settings β†’ Secrets and variables β†’ Actions
  2. Click the secret name β†’ Update
  3. Paste the new value β†’ Save
  4. The next CI/CD run will use the new secret

3.4 Provider-Specific Notes

Supabase Keys

  • Supabase keys cannot be independently rotated β€” they are derived from the JWT secret
  • To rotate: Dashboard β†’ Settings β†’ API β†’ β€œGenerate new keys”
  • This invalidates the anon key, service_role key, and JWT secret simultaneously
  • Impact: Every app and worker that uses Supabase will need the new keys deployed
  • Downtime risk: HIGH β€” plan for a brief maintenance window

Stripe Keys

  • Stripe supports rolling keys β€” create a new key, both work during the overlap
  • Rotate the webhook secret by creating a new webhook endpoint, testing it, then deleting the old one
  • Test mode keys (sk_test_*) and live keys (sk_live_*) are rotated independently

AWS IAM Credentials

  • Create a second access key on the IAM user (AWS allows 2 active keys)
  • Deploy the new key to all consumers
  • Verify, then deactivate and delete the old key
  • Better long-term: migrate to IAM roles (no long-lived keys)

Self-Managed Secrets (JWT_SECRET, SES_WEBHOOK_SECRET)

Terminal window
# Generate a new secret
NEW_SECRET=$(openssl rand -base64 48)
# Deploy to Cloudflare
wrangler secret put JWT_SECRET --name accessible-pdf-api <<< "$NEW_SECRET"
wrangler secret put JWT_SECRET --name accessible-gateway <<< "$NEW_SECRET"
# Deploy to AWS SSM
aws ssm put-parameter \
--name "/accessible-pdf/production/JWT_SECRET" \
--value "$NEW_SECRET" \
--type SecureString \
--overwrite

Warning: Rotating JWT_SECRET invalidates all active user sessions. Do this during low-traffic hours.


4. Automation

4.1 GitHub Action: Full End-to-End Rotation

Workflow: .github/workflows/rotate-secrets.yml

Automates the complete rotation lifecycle: cloud secrets β†’ test server β†’ rolling production deploy.

How it works:

  1. Updates Cloudflare Worker secrets and AWS SSM Parameter Store
  2. SSHes to 10.1.1.17 (test server), updates .env.node-server, restarts pptx-remediate, runs health check
  3. If test passes, SSHes to 10.1.1.4 (production), updates .env.node-server, rolling-restarts api-node-1 then api-node-2, then sidecars
  4. If any step fails, .env.node-server is rolled back from .bak and containers are restarted with old values

Usage:

Terminal window
# Rotate a single key
gh workflow run rotate-secrets.yml -f anthropic_api_key=sk-ant-api03-newkeyhere
# Rotate multiple keys at once
gh workflow run rotate-secrets.yml \
-f stripe_secret_key=sk_live_newkey \
-f resend_api_key=re_newkey
# Rotate JWT (caution: invalidates all sessions)
gh workflow run rotate-secrets.yml -f jwt_secret=$(openssl rand -base64 48)

Prerequisites β€” add SERVER_SSH_KEY to GitHub Actions secrets (the ~/.ssh/nightly-audit private key). CLOUDFLARE_API_TOKEN, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY already exist.

4.2 Helper Scripts

  • scripts/update-env-secret.sh β€” Safely updates a single key in .env.node-server with backup
  • scripts/smoke-test.sh β€” Lightweight health check (HTTP 200 + status field)

4.3 Quarterly Reminders

Workflow: .github/workflows/rotation-reminder.yml

Runs quarterly (Jan, Apr, Jul, Oct) and opens a GitHub issue with a rotation checklist.

4.4 Manual Rotation (Cloudflare Workers only)

For quick cloud-only rotation without touching on-prem servers:

Terminal window
# scripts/rotate-secrets.sh β€” Deploy a rotated secret to Cloudflare Workers
# Usage: ./scripts/rotate-secrets.sh <SECRET_NAME> <NEW_VALUE>
# Map secret names to the workers that consume them
declare -A SECRET_WORKERS
SECRET_WORKERS=(
["ANTHROPIC_API_KEY"]="accessible-pdf-api"
["GEMINI_API_KEY"]="accessible-pdf-api"
["OPENAI_API_KEY"]="accessible-pdf-api"
["MISTRAL_API_KEY"]="accessible-pdf-api"
["MARKER_API_KEY"]="accessible-pdf-api"
["MATHPIX_APP_KEY"]="accessible-pdf-api"
["MATHPIX_APP_ID"]="accessible-pdf-api"
["RESEND_API_KEY"]="accessible-pdf-api accessible-convert-email"
["STRIPE_SECRET_KEY"]="accessible-pdf-api accessible-org-chart-api"
["STRIPE_WEBHOOK_SECRET"]="accessible-pdf-api"
["JWT_SECRET"]="accessible-pdf-api accessible-gateway"
["SES_WEBHOOK_SECRET"]="accessible-pdf-api"
)
# Also update AWS SSM if applicable
SSM_SECRETS=("SUPABASE_SERVICE_ROLE_KEY" "SUPABASE_JWT_SECRET" "RESEND_API_KEY" "JWT_SECRET" "S3_ACCESS_KEY_ID" "S3_SECRET_ACCESS_KEY")
for SSM_KEY in "${SSM_SECRETS[@]}"; do
if [[ "$SECRET_NAME" == "$SSM_KEY" ]]; then
echo " β†’ Updating AWS SSM /accessible-pdf/production/$SECRET_NAME"
aws ssm put-parameter \
--name "/accessible-pdf/production/$SECRET_NAME" \
--value "$NEW_VALUE" \
--type SecureString \
--overwrite
fi
done
echo ""
echo "Verifying health..."
sleep 3
HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://api.theaccessible.org/health)
if [[ "$HTTP_STATUS" == "200" ]]; then
echo "Health check passed (HTTP $HTTP_STATUS)."
else
echo "WARNING: Health check returned HTTP $HTTP_STATUS β€” investigate before revoking old key."
exit 1
fi
echo ""
echo "Done. Now revoke the old key in the provider dashboard."
echo "Log the rotation date in docs/admin/secrets-inventory-and-rotation.md Section 5."

4.2 Scheduled Rotation Reminders

Add a GitHub Actions workflow that opens an issue every 90 days:

.github/workflows/rotation-reminder.yml
name: Secret Rotation Reminder
on:
schedule:
- cron: '0 9 1 */3 *' # 9 AM on the 1st of Jan, Apr, Jul, Oct
jobs:
remind:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Create rotation reminder issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh issue create \
--title "Quarterly secret rotation due ($(date +%B\ %Y))" \
--body "$(cat <<'BODY'
## Quarterly Secret Rotation Checklist
Review and rotate the following keys per
`docs/admin/secrets-inventory-and-rotation.md`:
### High-Priority (rotate every 90 days)
- [ ] `AWS_ACCESS_KEY_ID` / `AWS_SECRET_ACCESS_KEY`
- [ ] `CLOUDFLARE_API_TOKEN`
- [ ] `PACKAGES_TOKEN` / `GITHUB_TOKEN`
- [ ] `JWT_SECRET`
### Standard (rotate every 6 months, check quarterly)
- [ ] `ANTHROPIC_API_KEY`
- [ ] `GEMINI_API_KEY`
- [ ] `OPENAI_API_KEY`
- [ ] `STRIPE_SECRET_KEY` + `STRIPE_WEBHOOK_SECRET`
- [ ] `RESEND_API_KEY`
### Low-Priority (rotate annually or on suspicion)
- [ ] `MISTRAL_API_KEY`
- [ ] `MARKER_API_KEY`
- [ ] `MATHPIX_APP_KEY`
- [ ] `VAPID_PRIVATE_KEY`
- [ ] `TELEGRAM_BOT_TOKEN`
- [ ] `SERPER_API_KEY`
### Post-Rotation
- [ ] Update rotation log in `docs/admin/secrets-inventory-and-rotation.md` Section 5
- [ ] Run `scripts/rotate-secrets.sh` for each rotated key
- [ ] Verify all health checks pass
- [ ] Revoke old keys in provider dashboards
BODY
)" \
--label "security,chore"

4.3 Fully Automated Rotation (Future State)

For keys that support programmatic rotation, the flow can be fully automated:

ProviderAutomation PathFeasibility
AWS IAMAWS Secrets Manager auto-rotation with a Lambda rotatorReady β€” AWS provides built-in rotation for IAM keys
StripeStripe API POST /v1/api_keys/roll (rolling key)Ready β€” Stripe supports API-driven key rolling
GitHub PATGitHub API to create fine-grained tokens, revoke oldReady β€” use fine-grained PATs with expiry dates
CloudflareCloudflare API to create/revoke API tokensReady β€” API supports token lifecycle management
AnthropicNo rotation API β€” manual dashboard onlyManual
OpenAINo rotation API β€” manual dashboard onlyManual
Google AIGoogle Cloud API to manage API keysReady (if using Google Cloud API keys)
ResendNo rotation API β€” manual dashboard onlyManual
SupabaseNo independent key rotation β€” regenerates all keysManual, high-impact

Recommended next step: Start with AWS IAM key rotation via Secrets Manager (highest risk, best automation support), then Stripe and GitHub tokens.


5. Rotation Log

SecretLast RotatedRotated ByNext DueNotes
Fill in after first rotation cycle

6. Emergency Rotation (Key Compromise)

If a key is suspected compromised:

  1. Immediately revoke the old key in the provider dashboard (do not wait for overlap)
  2. Generate a new key
  3. Run scripts/rotate-secrets.sh <SECRET_NAME> <NEW_VALUE>
  4. Check application logs for unauthorized usage during the exposure window
  5. If AWS keys: check CloudTrail for unauthorized API calls
  6. If Stripe keys: check Stripe dashboard for unauthorized charges
  7. File a security incident in GitHub Issues with label security
  8. Notify larry@anglin.com via ~/.claude/scripts/send-email.sh