RCA — gitleaks false-positive fires nightly on Cloudflare Account ID
Incident ID: 2026-05-01-gitleaks-cf-account-id-false-positive Date: 2026-05-01 Severity: SEV-4 (infrastructure drift / toil accumulating; no secrets exposed) Duration: ~24h between first filing (2026-04-30 nightly scan) and resolution (2026-05-01) Blast radius: nightly security scan noise; #682 filed; would continue filing duplicate issues on every subsequent nightly run without remediation Author: sre-agent
Summary
The nightly gitleaks scan filed #682 (CRITICAL: generic-api-key in terraform/freescout/terraform.tfvars:31) daily. The flagged value is the Cloudflare Account ID — a public identifier, not a secret. An allowlist entry for this pattern already existed in .gitleaks.toml (added in PR #633), but the allowlist was silently not working because regexTarget = "match" was not set, causing gitleaks to test the regex against the bare hex Secret value rather than the full variable-assignment Match string. PR #839 adds regexTarget = "match" and two additional SOP path suppressions; full-history scan drops from 19 findings to 0.
Timeline (all times UTC)
- 2026-04-30 08:07 — Nightly scan runs. gitleaks flags
cf_access_account_idinterraform/freescout/terraform.tfvars:31andterraform/cf-access/terraform.tfvars:13. #682 filed bysecurity_file_issues.py. - 2026-05-01 ~13:00 — sre-agent assigned to fully remediate #682.
- 2026-05-01 13:05 — Identified flagged value as
cf_access_account_id(Cloudflare Account ID). Confirmed it is a public, non-secret identifier. - 2026-05-01 13:10 — Read
.gitleaks.toml: allowlist regex forcf_access_account_idalready present from PR #633. Ran gitleaks--no-git=falselocally; findings still appeared. - 2026-05-01 13:15 — Read gitleaks v8.18.4 source (
config/allowlist.go): withoutregexTarget, regexes test against the extracted Secret field, not the Match field. Regexcf_access_account_id\s*=\s*"[0-9a-f]{32}"matches Match but not Secret. Root cause confirmed. - 2026-05-01 13:20 — Added
regexTarget = "match"and SOP path suppressions to.gitleaks.toml. - 2026-05-01 13:22 — Verified locally: 914 commits scanned, 0 findings.
- 2026-05-01 13:25 — Committed fix, opened PR #839, posted comment on #682.
Impact
- Users affected: none (internal scan noise only)
- User-visible symptoms: none
- Data integrity: ok
- Revenue / billing: ok
- Secret exposure: none —
cf_access_account_idis the Cloudflare Account ID, a public identifier not a credential. No rotation required.
What went well
- The PR #633 allowlist entry showed correct intent — someone wrote the right regex, just without the required
regexTargetfield. - gitleaks allowlist.go source was available and readable; root cause was determined from source code, not guessing.
- Local scan tooling (gitleaks v8.30.1) was compatible enough with v8.18.4 (CI version) to reproduce and verify the fix.
What didn't go well
- The allowlist regex was added in PR #633 without being tested end-to-end against the full history scan. A local test would have caught the
regexTargetgap immediately. - The nightly scanner filed a CRITICAL severity issue for what is demonstrably a public identifier. The
generic-api-keyrule fires on entropy alone; the severity label amplified toil. - No runbook existed for gitleaks false-positive remediation, so each recurrence required ad-hoc investigation.
Root cause analysis
-
Contributing factor 1:
regexTargetnot set in allowlist — gitleaks[allowlist]regexesdefault to matching against the extracted Secret (the pattern group capture), not the full Match string. The allowlist entry was written as a full-assignment regex (cf_access_account_id\s*=\s*"[0-9a-f]{32}"). WithoutregexTarget = "match", gitleaks tested this regex against the raw hex value22b5c35090724fbf05db6d4f501ac821, which did not match. The allowlist silently did nothing. -
Contributing factor 2: Allowlist entry not validated against full-history scan — the entry was added in PR #633 targeting the shallow file scan. The nightly CI scan uses
--no-git=false(full git history). No test was run in full-history mode to confirm suppression. -
Contributing factor 3: No gitleaks runbook — when the finding recurred in #682, there was no documented procedure for diagnosing why an allowlist entry wasn't working. The investigation had to start from scratch.
Detection
- What alerted us: #682 filed automatically by
security_file_issues.pyfrom the nightly scan (2026-04-30 08:07 UTC) - How long between cause and detection: ~1 day (the root cause was the missing
regexTargetfield since PR #633 merged 2026-04-30; #682 was filed the same night) - How to detect faster next time: add a CI step that runs
gitleaks detect --no-git=false --config=.gitleaks.tomlon the PR branch and fails if new findings appear beyond the baseline (see action items)
Resolution
- What was changed: Added
regexTarget = "match"to[allowlist]in.gitleaks.toml. Added path suppressions^console/app/sops/rotation/and^app/sops/rotation/forcurl-auth-userfalse positives on SOP documentation copies. - Validation:
gitleaks detect --source=. --config=.gitleaks.toml --no-git=false— 914 commits, 0 findings (was 19). - Key rotation: Not required.
cf_access_account_idis a public Cloudflare Account ID visible in the dashboard URL. - History rewrite: Not required. The value is not a secret; no exposure to remediate. Command documented in PR #839 if ever needed with authorization.
Action items
| # | Action | Owner | Due | Issue |
|---|---|---|---|---|
| 1 | Merge PR #839 — gitleaks allowlist fix | Kristerpher | 2026-05-02 | #839 |
| 2 | Add CI job that runs gitleaks full-history scan on PRs touching .gitleaks.toml and fails on new findings |
sre-agent | 2026-05-08 | (new) |
| 3 | Add note to .gitleaks.toml header: "when adding a regexes entry, test with gitleaks detect --no-git=false" |
sre-agent | 2026-05-08 | (in PR #839 comment) |
References
- Runbook:
docs/ops/runbooks/gitleaks.md(new, created with this RCA) - Related incidents: PR #633 (allowlist entry first added), #682 (this incident)
- gitleaks allowlist.go source:
https://github.com/gitleaks/gitleaks/blob/v8.18.4/config/allowlist.go