AIN-289 · dual-key internal-signup-key rotation (zero-downtime)#97
Conversation
…ygiene
Charter A3 (mechanism only) + A4 sweep tool. Code-only — secret minting,
force-push, branch-protection toggle, and worker deploys are explicit
founder-batch steps (see CHARTER-REPORT-2026-05-28.md).
scripts/rotation_verify_ain289.py
Env-only probe harness. Reads AINFERA_<AGENT>_KEY env vars and probes
GET /v1/usage/daily, emitting {agent, http_status, key_id_prefix}
only. Never prints raw secrets. --expect {200,401,both} so the
same harness verifies new-keys-live and old-keys-revoked.
scripts/history_purge_ain289.sh
Per-repo dry-run-by-default tool. Adds tainted paths to .gitignore,
reports HEAD + history hits, then in --execute mode runs
git-filter-repo followed by a gitleaks gate. Force-push is NOT
done by the script.
scripts/vault_hygiene_ain279.py
Deterministic UTF-8 + frontmatter sweep. Byte-level mojibake repair,
refuses ~/code/hizrianraz/manwe. The L2 routing formula line is
HARD-GATED; script splits the file around it.
.gitleaks.toml + workflows/gitleaks.yml + pre-commit hook
Defaults + Ainfera-specific bearer patterns (ai_(infera|prd|stg|dev)_*
and dp.*.*.*). CI + local gates.
.gitignore
Adds .launch-snapshots/ and tests/fixtures/launch-snapshots/.
§0 finding: key model is single-column on tenants. Rotation harness
assumes single-key cutover; the additive api_key_hash_pending column
is staged for a follow-up migration.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ense) gitleaks/gitleaks-action@v2 requires a paid GITLEAKS_LICENSE secret on org-owned repos (the action hard-fails with 'License key is required' on ainfera-ai/api in CI). Swap to direct binary install — the upstream OSS gitleaks binary is Apache-2.0 and unencumbered, runs the same detect against the same .gitleaks.toml config. No other change. Pinned gitleaks 8.21.2 for reproducibility. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ixtures) The gitleaks workflow on api#95 was failing on 7 findings that are ALL pre-existing test fixtures or .env.example placeholders, never live secrets: .env.example FERNET_KEY placeholder tests/integration/test_phase6_jws_sender_claim Ed25519 test fixture tests/unit/test_crypto.py Ed25519 test fixture tests/unit/test_structured_log.py (x2 commits) sk-* log-redactor input Accept them via .gitleaksignore (one line per fingerprint = exact commit-sha:file:rule:line tuple). The fingerprint changes if the file is moved or the line shifts, so this list cannot mask future drift. NEW findings in current code still fail CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…time) The leaked INTERNAL_SIGNUP_KEY (master X-Ainfera-Internal-Key) is still live in prod. Auth has a single verifier (settings.internal_signup_key) with no dual-key support, so rotating it would 401 the customer dashboard (Vercel) and DO-fleet heartbeats during cutover. Add Settings.verify_internal_key(presented) — constant-time check against the active key plus an optional internal_signup_key_previous. Set previous to the OLD value during the rotation window so web + fleet roll onto the new key with zero downtime, then unset to retire the leaked key. Route all 7 comparison sites (deps, ownership x2, signup, heartbeat, capture_metrics, install) through the helper — also upgrades them from `==`/`!=` to hmac.compare_digest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
AIN-289 🔴 [SECURITY] Rotate leaked ai_infera_* keys committed in .launch-snapshots/e2e-env.sh
🔴 Live production bearer keys committed to git.
Anyone with repo (or git-history) read access can spend against Ainfera's provider accounts up to these caps daily. Caps bound the blast radius — contain-and-rotate, not catastrophe — but rotate today. Sequence (founder/terminal — credentialed actions)
Branch
Done when
Found during AIN-285 trace (probe agent 5298a483 = aule per e2e-env.sh:5). |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Reviewed by Cursor Bugbot for commit f0082a4. Configure here.
| candidates = [self.internal_signup_key] | ||
| if self.internal_signup_key_previous: | ||
| candidates.append(self.internal_signup_key_previous) | ||
| return any(hmac.compare_digest(presented, candidate) for candidate in candidates) |
There was a problem hiding this comment.
any() short-circuits, breaking claimed constant-time guarantee
Low Severity
The docstring documents verify_internal_key as a "Constant-time check," but any() short-circuits on the first True result. When internal_signup_key_previous is set, a match against the active (first) key returns after one hmac.compare_digest call, while a match against the previous key or a rejection requires two. This timing difference leaks which candidate matched and whether the rotation window is open. Replacing the generator-based any() with a list comprehension (evaluating all comparisons unconditionally) and then reducing with any() or | would preserve the constant-time property end-to-end.
Reviewed by Cursor Bugbot for commit f0082a4. Configure here.


Why
The leaked
INTERNAL_SIGNUP_KEY(the masterX-Ainfera-Internal-Key, which withX-Ainfera-On-Behalf-Ofimpersonates any tenant) is still live in prod as of 2026-05-29 — verified by sha256-matching the quarantinedREVOKED-AIN-289-*leak file against prod. (The leaked bearer keys were already rotated out; this master key was not.)Auth has a single verifier (
settings.internal_signup_key) with no dual-key support, so a naive rotation would 401 the customer dashboard (VercelAINFERA_INTERNAL_KEY) and DO-fleet heartbeats during cutover.What
Settings.verify_internal_key(presented)— constant-time (hmac.compare_digest) check against the active key plus an optionalinternal_signup_key_previous.deps.py,auth/ownership.py(×2),routers/signup.py,routers/heartbeat.py,routers/capture_metrics.py,routers/install.py.tests/unit/test_internal_key_rotation.py).Cutover (founder-gated deploy)
INTERNAL_SIGNUP_KEY=<new>,INTERNAL_SIGNUP_KEY_PREVIOUS=<old>→ redeploy (accepts both).ainfera-os/prd+ Vercel dashboardAINFERA_INTERNAL_KEYto<new>; redeploy dashboard; restart DO fleet.<new>.INTERNAL_SIGNUP_KEY_PREVIOUS→ redeploy → leaked key retired.Draft until founder approves the prod cutover (Railway/Vercel are founder-signer gates).
Note
High Risk
Changes master internal-key authentication used for trusted-proxy impersonation across multiple routes; incorrect env cutover could 401 dashboard/fleet until fixed.
Overview
Adds zero-downtime rotation for the leaked
INTERNAL_SIGNUP_KEY:Settings.verify_internal_key()does constant-time checks against the active key and optionalinternal_signup_key_previous, and everyX-Ainfera-Internal-Keygate (tenant resolution, ownership, signup reserved namespaces, heartbeat, capture metrics, install GitHub bypass) now uses it instead of a single string equality.Bundles AIN-289 secret hygiene: gitleaks in CI (full-history
detect), pre-commitprotect --staged, custom rules for Ainfera bearer/Doppler tokens, tight test allowlists, and historical.gitleaksignorefingerprints..gitignoreblocks.launch-snapshots/paths;history_purge_ain289.shsupports dry-run vs execute history rewrite plus post-rewrite gitleaks.rotation_verify_ain289.pyJSONL-probes/v1/usage/dailyper agent key from env (prefix-only logging). Also addsvault_hygiene_ain279.py(unrelated vault markdown repair; not part of the auth cutover).Reviewed by Cursor Bugbot for commit f0082a4. Bugbot is set up for automated code reviews on this repo. Configure here.