flybrain/infra/docs/secrets-history-scan.md
acamilo 660c3cf00d
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
flybrain v0.4.0: public tree (history retained privately)
2026-09-21 15:09:46 +00:00

101 lines
6.5 KiB
Markdown

# Secrets scan over the whole git history
Run 2026-09-17 against the local checkout at `main` = `c9858cc`, before any public push.
Scope: **every commit on every ref**, not just the working tree.
No secret value appears anywhere in this document. Findings are described by *shape* and
*location* only.
## What was run
| tool | version | command | coverage |
| --- | --- | --- | --- |
| gitleaks | 8.30.0 | `gitleaks git . --log-opts="--all" --redact` | 460 non-merge commits, ~8.27 MB of diffs |
| gitleaks | 8.30.0 | `gitleaks dir .` | the checked-out tree, 2.02 GB incl. `target/` and `node_modules/` |
| trufflehog | 3.90.10 | `trufflehog git file:///<checkout>` | 322,604 chunks, 4.23 GB, all branches |
| grep (own pattern set) | — | every blob in the object DB, see below | 5,791 objects incl. dangling blobs |
Both binaries were installed as prebuilt releases under `~/.local/bin` (no root, no package
manager). Repository size for reference: 563 commits, 87 refs, one author.
The grep pass exists because gitleaks and trufflehog know generic provider shapes but not this
project's own ones. It enumerates `git rev-list --objects --all` plus the dangling blobs from
`git fsck --lost-found`, pipes each blob through `git cat-file blob`, and matches:
- `-----BEGIN [A-Z ]*PRIVATE KEY-----`
- Twitch IRC tokens: `oauth:[A-Za-z0-9]{20,}`
- Twitch/RTMP stream keys: `live_[0-9]{6,}_[A-Za-z0-9]{20,}`
- `Authorization: (Bearer|Basic|OAuth) <12+ chars>`
- `client_secret` / `clientSecret` / `CLIENT_SECRET` assigned a 20+ char value
- `stream_key` / `STREAM_KEY` / `streamKey` assigned a 12+ char value
- `access_token` / `refresh_token` / `accessToken` / `refreshToken` assigned a 16+ char value
- `api_key` / `API_KEY` / `apiKey` assigned a 16+ char value
- `password` / `passwd` / `PASSWORD` assigned an 8+ char literal
- `pass show` / `pass ls` (this repo's credential store is `pass`, so captured output would
look like a shell pipeline in a script or a log)
- AWS `AKIA…`, GitHub `ghp_`/`gho_`/`ghu_`/`ghs_`/`ghr_`, Slack `xox[baprs]-`, OpenAI `sk-`
- a separate broad sweep for any 32+ char high-entropy token not already explained by a
checksum, `integrity` field or base64 blob
## Findings
gitleaks: **no leaks found**, on history and on the tree.
trufflehog: **0 verified, 0 unverified secrets** (one non-critical decode error on
`data/fafb-v783/weights.binz`, which is a gzip stream trufflehog tried to read as brotli — not a
finding).
The grep pass matched 14 blobs across 5 paths. Every one is a *reference to* a credential, never
a credential. Full list:
| # | shape matched | path | commits (first → last touching) | verdict |
| --- | --- | --- | --- | --- |
| 1 | `pass show "$PASS_KEY"` shell pipelines, 4 blobs | `infra/06-secrets.sh` | `0d1a762` → `69816e6` | **False positive.** The script's whole job is to pipe `pass show` into `systemd-creds` on the container; the value never lands in a file and never in git. `PASS_KEY` holds a pass *entry name* (`twitch/fly-pokemon-key`), not a secret. |
| 2 | `pass show … \| head -c8` in a verify helper, 6 blobs | `infra/verify.sh` | `0d1a762` → `57276d4` | **False positive.** Reads the first 8 bytes of the stored key to compare a prefix against what the container holds. The prefix is computed at run time and printed nowhere that is committed. |
| 3 | `accessToken:` / `refreshToken:` struct fields, 3 blobs | `services/bridge/src/auth.ts` | `d9f00b6` → `48cd40a` | **False positive.** Twurple `AccessToken` field names in code (`accessToken: token.accessToken`). No literal values. |
| 4 | `accessToken:` / `refreshToken:` struct fields, 1 blob | `services/bridge/tools/authorize.mts` | `77f5892` | **False positive.** Same: the interactive authorize tool writes the token it just fetched to a path outside the repo. |
| 5 | the words `pass show \| systemd-creds` in prose | `infra/docs/release-ct-provision.md` | `6598ad0` | **False positive.** A provisioning log describing the mechanism. |
Broad high-entropy sweep: 1,101 blobs carried a 32+ char token line, all accounted for by
`package-lock.json` integrity hashes, the FlyWire `meta.json` / `tools/artifact-checksums.txt`
sha256 digests, and vendored `binjgb` tables. Nothing credential-shaped.
## Negative results worth recording
- **No `.env` was ever committed.** The only `*.env` paths in history are
`infra/env/fly-pokemon.env`, `infra/env/fly-platformer.env` and `infra/env/spike.env`, which
are *deployment profiles*, not credential files. Dumping every `KEY=value` line those three
files have ever carried, across every commit, yields only CT sizing (`CORES`, `CPUSET`,
`MEMORY_MB`), network (`IP`, `HOSTNAME`), stream geometry (`STREAM_WIDTH`, `STREAM_KBPS`),
feature flags, `PASS_KEY` (an entry *name*), `TWITCH_CHANNEL`/`TWITCH_BOT_USER` (public
handles) and `PUSH_TARGET`. `.gitignore` covers `.env` and `.env.*` with a
`!.env.example` exception.
- **No key material of any kind.** Zero matches for `BEGIN … PRIVATE KEY`, `id_rsa`,
`id_ed25519`, `*.pem`, `*.key`, `*.p12`, `*.pfx` as paths or as content.
- **No stream key, ever.** Zero matches for the `live_…` RTMP shape or for a `STREAM_KEY=`
assignment. Stream keys reach the containers only through `pass` → `systemd-creds`.
- **The remote carries no credential.** the operator's git remote — SSH, no
token in the URL.
- **One advisory, not a secret:** `ROM_SHA256=0e85…219f` is committed in
`infra/env/spike.env` and `infra/env/fly-pokemon.env`. It is a cartridge fingerprint, so it
is not a credential and needs no rotation, but it does identify a specific commercial ROM
dump. Flagging it for whoever is handling the licence/redaction pass, not for this scan.
## Verdict
**Clean. Nothing to rotate, nothing to rewrite history for.** Three independent engines
(gitleaks, trufflehog, a project-specific grep set over every blob including dangling objects)
found zero real credentials across all 563 commits. All 14 grep hits are code and prose that
*name* credentials handled by `pass` at run time. The hard rule in `CLAUDE.md` — "Stream keys and
tokens live in `pass`, never in git" — holds in the history as well as in the tree.
## Re-running this
```sh
gitleaks git . --log-opts="--all" --redact --no-banner
trufflehog git "file://$PWD" --no-update
```
Neither is wired into `.forgejo/workflows/ci.yml` or `.github/workflows/ci.yml`: CI runs no
secret-bearing step, and a full-history scan is a pre-publish gate rather than a per-push one.
Run it again before any new public push, and after any commit that touches
`infra/06-secrets.sh`, `infra/env/*.env` or `services/bridge/src/auth.ts`.