Skip to main content
A .env.bak file containing real secrets was committed to the facilitator repo. Removing it from HEAD did not un-leak the values - they live in git history and in every clone. The only remediation is rotation. This runbook enumerates every burned secret and the exact procedure to rotate it. Enforced by scripts/check-no-committed-env.sh.
Status: active, Incident date: 2026-06, Owner: buckspay security

Incident

A .env.bak carrying production-grade secrets was committed to github.com/bucks-pay/facilitator (present in commits 25bd513 and 4e8c83b). It was later removed from tracking with git rm --cached .env.bak and .env.bak is now in .gitignore. Removal from HEAD is NOT remediation: the values remain in git history and in every existing clone/fork, in any CI log that printed them, and with every current/former collaborator who pulled. Anyone with repo history has every value below. The blast radius is the full set of secrets listed in the Burned secrets section below. The only fix is to rotate each to brand-new material and abandon the old.

Burned secrets (rotate ALL)

Exact keys present in the leaked .env.bak (verified from the file; values intentionally not reproduced here). Non-secret values also present (no rotation needed): PORT, SWAP_DEFAULT_SLIPPAGE_BPS, SWAP_MAX_SLIPPAGE_BPS, SWAP_QUOTE_TTL_SECONDS.
The mainnet sponsor STELLAR_SPONSOR_SECRET_PUBNET was NOT in .env.bak, but it MUST be generated fresh on a clean host post-incident and MUST differ from the leaked address above - enforced by scripts/check-no-committed-env.sh Guard 2.

Rotation procedure

Apply these four steps to every row above:
  1. Generate new material on a clean host. Never paste an old value into a new file; never reuse a leaked key on a “fresh” account.
  2. Deploy to the secret manager / deploy target - the facilitator’s SecretProvider: KmsSecretProvider in prod, a gitignored .env in dev. Never into a tracked file.
  3. Revoke / abandon the old value at its source: gateway revoke for API keys (0x/1inch/ API_KEY); for on-chain keys, sweep all funds out and never reuse the address.
  4. Verify the new value works and the old value fails: old API_KEY -> 401; leaked sponsor G -> zero balance and no longer referenced anywhere.
Drain the leaked sponsor (specific): build a payment of the full XLM balance (minus the base reserve) from the leaked GDKACWHU...4RPI to the new sponsor (or treasury), submit it once, then record the leaked G as abandoned in this doc. Never re-fund it.

Never commit a secret file again (controls)

  1. .gitignore in both repos ignores .env and .env.* and explicitly .env.bak / .env.local, allowing ONLY .env.example. (SDK: .env, .env.*, !.env.example; facilitator: .env, .env.bak, .env.local, .env.*.local.)
  2. scripts/check-no-committed-env.sh runs in CI and the release gate - fails if any .env* (≠ .env.example) is tracked, and fails if the configured pubnet sponsor equals the leaked address.
  3. Pre-commit / CI secret scanner (gitleaks or trufflehog) blocks secret-shaped strings before they land. This extends scripts/check-no-secrets-in-src.sh (blocks S... seeds / PEM markers / apiKey literals in packages/*/src) to tracked files anywhere, not just src.

Post-rotation verification

  • Leaked sponsor GDKACWHU...4RPI balance == 0 (drained + abandoned).
  • New sponsor funded and referenced via the secret manager (never a tracked file).
  • Old API_KEY returns 401 at the gateway; new key distributed to the BFF.
  • bash scripts/check-no-committed-env.sh exits 0.
  • No .env* (except .env.example) tracked in either repo: git ls-files | grep -E '(^|/)\.env' shows only *.env.example.
  • 0x / 1inch / Calibur / EVM relayer + deployer keys rotated and old ones revoked/emptied.