From 6a656a8aaf0f29594164093ded6482b7c6121b5d Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 21 Jul 2026 05:12:31 +0100 Subject: [PATCH 1/2] =?UTF-8?q?security:=20add=20gitleaks=20allowlist=20?= =?UTF-8?q?=E2=80=94=20greens=20the=20now-real=20gate=20on=20main?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to #511. That PR merged at its first commit, so the caller fix landed without this allowlist and main's Secret Scanner went straight from startup_failure (dead) to failure (real gate, 15 false positives). This completes the change. Net effect of #511 + this commit: the scanner goes from not executing at all to executing and passing honestly. All 15 findings were verified individually as false positives: * deno.lock x8 — SHA-256 integrity hashes of public registry modules * Project.toml — a Julia General-registry package UUID * otpiser audit — a 40-hex OpenPGP fingerprint (published by design) * haskell design md — the type name in `data Ed25519KeyPair = Ed25519KeyPair` * example.zig — `abc123def456...`, commented "// Would be real token" * NEXT-STEPS.md x2 — `1234567890_abc123...` in a fenced terminal mockup * examples.ndjson — `0a1b2c3d4e5f...`, an ascending-nibble filler walk This wall-of-noise is almost certainly what the original `continue-on-error: true` was papering over. So the allowlist is the *unblocker* for re-pinning callers onto the real gate estate-wide, not merely local cleanup. Design rule, documented in the file: allowlist by pattern class, anchored to the whole value; never allowlist a path because "that file is fine" and never disable a rule. `paths` is confined to formats that cannot hold a live credential (lockfile integrity hashes, dependency UUIDs). Every regex is anchored ^...$ deliberately. An earlier draft used unanchored `(?i)test|example`, and canary-testing caught that it silently allowlisted the real AWS key wJalrXUtnFEMI/K7MDENG/bPxRfiCY... purely because "EXAMPLE" appears inside it. Anchoring fixed it. Verified against origin/main: scan -> no leaks found (exit 0) planted realistic secrets -> still detected: github-pat, gitlab-pat, slack-bot-token, stripe-access-token, generic-api-key (AWS) Also clears all 8 of boj-server-cartridges' equivalent fixture findings with no repo-specific additions (see that repo's PR #99), which is the evidence it stands up as the estate baseline template rather than a standards-only patch. Co-Authored-By: Claude Opus 4.8 --- .gitleaks.toml | 98 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 .gitleaks.toml diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 00000000..f4a81c0e --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,98 @@ +# SPDX-License-Identifier: MPL-2.0 +# +# Gitleaks configuration — estate baseline. +# +# WHY THIS FILE EXISTS +# -------------------- +# Until #500 the estate's gitleaks step carried `continue-on-error: true`, so +# the primary secret scanner could not fail a PR. #500 made it gate for real +# (`gitleaks detect --exit-code 1` against a checksum-verified binary). The +# first honest run then surfaced 15 findings in this repo — every one a false +# positive: module integrity hashes, package UUIDs, a public GPG fingerprint, +# a Haskell type *name*, and synthetic `.example` fixtures. +# +# That is very likely the trap the `continue-on-error` was papering over: turn +# the gate on with no allowlist, get a wall of red that is entirely noise, +# someone reverts, and the estate is back to a scanner that cannot fail. This +# config is therefore the *unblocker* for re-pinning callers onto the real +# gate across the estate. +# +# DESIGN RULE — KEEP THIS WHEN EDITING +# ------------------------------------ +# Allowlist by PATTERN CLASS, anchored to the WHOLE value. Never allowlist a +# path wholesale because "that file is fine", and never disable a rule. +# +# Every `regexes` entry below is anchored `^...$` on purpose. An unanchored +# entry is a substring match, which silently blinds the scanner: a pattern +# like `(?i)test|example` would allowlist the real AWS key +# `wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY` purely because "EXAMPLE" appears +# inside it. That was caught by canary-testing an earlier draft of this file — +# if you add a pattern here, plant a realistic secret and confirm it is still +# detected before you commit. +# +# `paths` entries are limited to formats that cannot legitimately hold a live +# credential (lockfile integrity hashes, dependency UUIDs). If you want to +# allowlist any other path wholesale, that is a signal the secret should be +# removed from the tree instead. + +[extend] +useDefault = true + +[allowlist] +description = "Estate baseline: dependency metadata, anchored placeholder shapes, published test vectors" + +paths = [ + # Lockfiles: the "secrets" are SHA-256 *integrity hashes* of public registry + # modules. A lockfile has no legitimate reason to contain a credential. + '''(^|/)deno\.lock$''', + '''(^|/)package-lock\.json$''', + '''(^|/)yarn\.lock$''', + '''(^|/)Cargo\.lock$''', + # Julia project manifests: entries are public package UUIDs from the General + # registry, which trip the generic-api-key entropy heuristic. + '''(^|/)Project\.toml$''', + '''(^|/)Manifest\.toml$''', +] + +regexes = [ + # --- documented placeholder shapes (all anchored to the whole value) ------ + + # Ellipsis-terminated illustrative values: `abc123def456...` (carrying an + # explicit "// Would be real token" comment), `1234567890_abc123...` inside a + # fenced ASCII mockup of terminal output. A real credential is never elided. + '''^[A-Za-z0-9_\-]{4,}\.\.\.$''', + + # Repeated-character masks: `glpat-xxxxxxxxxxxxxxxxxxxx`, `ghp_xxxx…`, + # `sk-xxxx…`. A real credential never repeats a single character 8+ times. + '''^[A-Za-z0-9_\-]*?(x{8,}|X{8,}|y{8,}|z{8,}|0{12,}|\*{6,})[A-Za-z0-9_\-]*$''', + + # Monotonic hex walks used as filler: `0123456789abcdef` repeated, + # `ABCDEF0123456789ABCDEF0123456789`, `0a1b2c3d4e5f6a7b…` (ascending nibble + # pairs). These are keyboard-walk fillers, never generated key material. + '''^(?i:0123456789abcdef|abcdef0123456789|0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1|deadbeef|cafebabe)+$''', + + # Snake-case self-labelled fixtures, e.g. `sg_test_api_key_abc123`. Anchored + # and shape-constrained: an all-lowercase snake_case string with an explicit + # test/demo marker as a bounded segment. Real API keys carry mixed case, + # digits and symbols and will not match this. + '''^[a-z]{2,6}_(test|demo|fake|sample|example|dummy|placeholder)_[a-z0-9_]{2,40}$''', + + # --- published test vectors ---------------------------------------------- + # RFC 8032 §7.1 TEST 1 Ed25519 seed and the public key it derives. These + # exact values are printed in the RFC; checking that one yields the other is + # the entire point of the interoperability test. + '''^9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60$''', + '''^d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a$''', + + # --- public-by-definition / non-secret identifiers ------------------------ + # OpenPGP 40-hex fingerprints. A fingerprint *identifies* a key; it is + # published deliberately (SECURITY.md, audit records, keyservers) and + # discloses nothing. Secret key *material* is PEM-armoured and is caught by + # the dedicated private-key rules, which are NOT allowlisted here. + '''^[A-F0-9]{40}$''', + + # Cryptographic *type names* appearing in source/design docs, e.g. the + # Haskell declaration `data Ed25519KeyPair = Ed25519KeyPair`. A bare + # CamelCase identifier with no digits or separators is a type, not a key. + '''^(Ed25519|X25519|Curve25519|RSA|ECDSA|EdDSA|Secp256k1)[A-Za-z]*$''', +] From 71761050fe0d5802007227e17e261e0d5580b96f Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 21 Jul 2026 05:14:53 +0100 Subject: [PATCH 2/2] security: describe the masked-token shape instead of showing it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hypatia flagged .gitleaks.toml itself — "Secret found: GitLab PAT" — because the comment documenting the repeated-character mask contained a literal masked token. The allowlist file tripping the estate's own secret detector is a neat demonstration of the problem it exists to solve. Describe the shape in prose instead, and record the rule in the file so the next editor does not reintroduce a literal example token. Behaviour is unchanged: the regex is untouched. Re-verified — standards scans clean (exit 0) and planted github-pat / gitlab-pat / slack-bot-token / stripe-access-token values are all still detected. Co-Authored-By: Claude Opus 4.8 --- .gitleaks.toml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.gitleaks.toml b/.gitleaks.toml index f4a81c0e..db763a2c 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -62,8 +62,15 @@ regexes = [ # fenced ASCII mockup of terminal output. A real credential is never elided. '''^[A-Za-z0-9_\-]{4,}\.\.\.$''', - # Repeated-character masks: `glpat-xxxxxxxxxxxxxxxxxxxx`, `ghp_xxxx…`, - # `sk-xxxx…`. A real credential never repeats a single character 8+ times. + # Repeated-character masks: a provider prefix followed by a long run of a + # single filler character (the GitLab, GitHub and OpenAI token prefixes are + # all commonly masked this way in docs). A real credential never repeats one + # character 8+ times. + # + # Deliberately described rather than shown: writing the masked token out + # literally here makes THIS file trip the estate's Hypatia secret detector + # ("Secret found: GitLab PAT"), which is how that was found. Do not add + # literal example tokens to this file, even inside comments. '''^[A-Za-z0-9_\-]*?(x{8,}|X{8,}|y{8,}|z{8,}|0{12,}|\*{6,})[A-Za-z0-9_\-]*$''', # Monotonic hex walks used as filler: `0123456789abcdef` repeated,