fix(redfix): STATUS/BACKLOG — TWO exposed live credentials, sweep regenerates them WEEKLY (both corrections conceded to Adversary)

Adversary wake #55 conceded my insteadOf falsification but corrected me twice; verified both first-hand:

1. "Production CI does not regenerate / manual-* = hand-run" WRONG. run_id()=manual-<pid> for any non-Drone
   run (run_recipe_ci.py:318-319); nightly-sweep runs run_recipe_ci.py outside Drone with CCCI_SKIP_FETCH=1
   (nightly_sweep.py:88). Sweep regenerates the exposed copies WEEKLY (freshest 07-05 03:37-59 = sweep fire).

2. Census missed a 2nd credential (grep keyed on autonomic-bot: cannot see oauth2:). Full per-file census:
   78 files carry the password, 117 a live oauth2 token, 62 both, 133 distinct under /var/lib. Token is LIVE
   + PUSH-capable (api/v1/user->200 autonomic-bot/64) and is what recipe-mirror-sync.sh:39 pushes with —
   falsifies my own B-redfix-8 "small blast radius" note.

STATUS steps 3-4 rewritten (two creds, weekly regen, combined remedy + chmod 0750); B-redfix-8/9 corrected.
DONE stands; no VETO; no DoD item touched; rotation of BOTH secrets remains operator-only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Y9GyBPF1EgTTh6277Xjj7k
This commit is contained in:
2026-07-09 10:38:09 +00:00
parent ad1abcb792
commit 28529e7508
4 changed files with 128 additions and 17 deletions

View File

@ -190,12 +190,16 @@ hold). Concrete fix designs from M1 evidence:
- **B-redfix-9 — harden `CCCI_SKIP_FETCH` staging: it copies credentialed `.git/config` into a world-readable
run tree.** *(DEFERRED — post-merge; outside redfix DoD; no gate impact. Filed wake #54.)*
`fetch_recipe`'s staging branch (`runner/run_recipe_ci.py:348-353`) does
`shutil.copytree(~/.abra/recipes/<recipe>, <run-dir>)`. The canonical clones carry the bot password in
`remote.origin.url` but are shielded by `/root` = `0700`; the run tree `/var/lib/cc-ci-runs` is `0755`, so
the copy **loses the protection of its source**68 world-readable copies today. Note the normal fetch path
is already correct: it clones a clean URL and authenticates with a per-command
`http.extraHeader` token (landed `9b33fdf`, 2026-05-27), which is why **0** production (numeric) runs carry
the credential and all 68 are `manual-*`.
`shutil.copytree(~/.abra/recipes/<recipe>, <run-dir>)`. The canonical clones carry the bot password AND a
live oauth2 token in their remotes but are shielded by `/root` = `0700`; the run tree `/var/lib/cc-ci-runs`
is `0755`, so the copy **loses the protection of its source**133 world-readable cred-bearing copies today
(78 password, 117 token, overlapping). **CORRECTED (wake #55): this regenerates WEEKLY, not just on hand-runs.**
The `manual-*` prefix does not mean hand-run: the autonomous `nightly-sweep.timer` runs `run_recipe_ci.py`
outside Drone (`nightly_sweep.py:88` sets `CCCI_SKIP_FETCH="1"`), and `run_id()` labels every non-Drone run
`manual-<pid>` (`run_recipe_ci.py:318-319`). So the sweep itself is the generator and re-arms the exposure
each fire. (My earlier "0 production runs carry it, all `manual-*` = leftovers" mis-read `manual-*`; the
normal Drone fetch path IS clean — clean URL + per-command `http.extraHeader` token, landed `9b33fdf` — but
the sweep does not take that path.)
**Fix:** after `copytree`, strip userinfo from the copied remote (`git -C <dest> remote set-url origin <clean>`),
or exclude `.git/config` from the copy and re-init the remote clean; plus `chmod 0750 /var/lib/cc-ci-runs`.
**Do not** rely on rotating the password: rotation without this re-exposes the *new* value on the next staged run.
@ -768,6 +772,15 @@ token file with `0600`, or at minimum `chmod 0600 /etc/cc-ci/.git/config`. Do no
only by `scripts/bootstrap-drone-oauth.sh` (a one-off bootstrap). `scripts/recipe-mirror-sync.sh` pushes with
an **OAuth token**, not the password, so the weekly sweep's mirror sync does **not** depend on it.
**CORRECTION (wake #55): the "small blast radius" reasoning is HALF WRONG, and the OAuth token is ALSO
exposed.** The above argues rotation is safe because mirror-sync uses a token, not the password — but that
same `oauth2` token (`recipe-mirror-sync.sh:39`, `https://oauth2:${TOKEN}@…`, PUSH-capable) is itself leaked
in **117** world-readable `.git/config` copies under `/var/lib/cc-ci-runs` (sentinel `9c44a1aea2ecb389`;
verified live: `GET /api/v1/user` → 200 `autonomic-bot`/id 64). So there are **two** live push-capable
credentials to rotate, and rotating `GITEA_PASSWORD` alone leaves the token exposed. The token rotation DOES
have a real dependent (mirror-sync) — so it must be re-minted and the new value delivered to mirror-sync's
secret source, not merely revoked. See STATUS-redfix.md steps 34 (updated) for the combined remedy.
### A-redfix-1 — ADDENDUM (wake #53, 2026-07-09T10:15Z): the "sole copy" claim is FALSIFIED — **78** world-readable copies, root-caused to a git `insteadOf` rewrite that regenerates them on every CI run
The original finding asserted (see above, line ~47): "`/etc/cc-ci` is the **sole** copy whose parents are