ci: guard against committed credentials#18
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds defense-in-depth measures to prevent committed credentials in a public repository, combining CI secret scanning, expanded .gitignore coverage for common credential filenames, and updated security guidance for incident response.
Changes:
- Add a new GitHub Actions workflow that runs
gitleakson every PR (and on pushes tomain) with full history and redacted findings. - Expand
.gitignoreto ignore common environment file variants and typical cloud credential / key file names. - Update
SECURITY.mdto document the new credential hygiene layers and emphasize “rotate first” if a secret is ever committed.
Reviewed changes
Copilot reviewed 2 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
SECURITY.md |
Documents the new credential hygiene approach and references the new gitleaks workflow. |
.gitignore |
Adds ignore patterns for common env/credential/key filenames to reduce accidental commits. |
.github/workflows/gitleaks.yml |
Introduces PR-time secret scanning with gitleaks (full history, checksum-verified install, redacted output). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - **CI checks** on every PR: `ruff` (lint + format), `bandit` (static security lint), `pip-audit` | ||
| (dependency CVEs), `gitleaks` (committed credentials), and the `tests` suite. |
There was a problem hiding this comment.
Thanks — half right, and the half that's right is a real doc bug. Both claims checked rather than taken on faith:
"CI is path-filtered" — correct, and now fixed. ci.yml filters pull_request on src/idc_api/**, tests/**, pyproject.toml, and uv.lock. This PR is its own proof: it touches only .gitignore, SECURITY.md, and a workflow, so it ran actionlint and gitleaks only — no pytest, no bandit, no pip-audit. PR #17, which touched src/idc_api/__init__.py, did get the test matrix. The bullet said "CI checks on every PR" and that was simply false. Reworded in 2a662d6 to name what runs unconditionally (gitleaks — deliberately unfiltered, since a credential can land in any file) versus what's gated on paths.
"no ruff format" — was true of this branch's base, not of main. ruff format --check was added to ci.yml in #15 / #16. This branch had been cut from a stale local main that predated them, so the file you reviewed genuinely lacked the step. Good catch on the symptom. I've rebased onto current main (8100fbf), and ci.yml now shows Lint (ruff), Format (ruff), Security lint (bandit), Dependency vulnerability scan (pip-audit), Test (pytest) — so "lint + format" is accurate and stays.
Net: the bullet now reads
gitleaksruns on every PR — deliberately not path-filtered […]ci.ymlrunsruff(lint + format),bandit,pip-audit, and thetestssuite on Python 3.11/3.12 — but only for PRs touchingsrc/idc_api/**,tests/**,pyproject.toml, oruv.lock. […] A docs-only PR therefore runsgitleaksand nothing else.
Re-verified after the rebase: actionlint clean, gitleaks finds no leaks across history, credential filenames still ignored, and no tracked file is swallowed by the new patterns.
Drafted with Claude Code.
The repo is public and nothing stopped a service-account key from being committed. History is clean (verified with gitleaks over all 70 commits), so this is prevention, not cleanup. Three layers, weakest last: - GitHub secret-scanning push protection, enabled on the repo (settings, not in this diff). Rejects the push before the secret reaches GitHub -- the only guard that prevents rather than detects. - gitleaks on every PR, not path-filtered: a secret can land in any file, and fork PRs are not covered by push protection. Pinned + checksum- verified, matching actionlint.yml. --redact keeps key material out of the Actions log, which is public here. - .gitignore for the names deployer keys land under. dev/deployment.md tells operators to fetch per-tier SA JSON keys, so `gcp-sa-key.json` and friends routinely sit in a working tree one `git add -A` from a commit. Previously only `.env` and `*.env` were covered -- not `.env.local`, and no JSON/PEM shape at all. SECURITY.md documents the layers and the response: rotate first, purge second. Deleting the commit does not un-leak a pushed key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Enabled on the repository (Settings -> Code security), alongside the secret-scanning work in this PR: - Dependabot alerts + automated security updates. pip-audit only fails a PR whose deps already carry a CVE; a CVE disclosed after merge went unnoticed until someone looked. Security updates are the repo setting, not dependabot.yml -- that file schedules weekly *version* updates. These are server-side settings, invisible in any diff, so they can be switched off without review. SECURITY.md is now the record that they are meant to be on, with the gh command to verify. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot review, PR #18: the bullet claimed "CI checks on every PR", but ci.yml is path-filtered on pull_request (src/idc_api/**, tests/**, pyproject.toml, uv.lock). A docs-only PR runs neither the tests nor bandit/pip-audit -- this PR is itself the proof: it triggered only actionlint and gitleaks. gitleaks is deliberately unfiltered, so it is the one check that really does run on every PR. Say so, and stop implying the rest do. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2506e67 to
2a662d6
Compare
| [ci.yml](.github/workflows/ci.yml) runs `ruff` (lint + format), `bandit` (static security lint), | ||
| `pip-audit` (dependency CVEs), and the `tests` suite on Python 3.11/3.12 — but **only** for PRs | ||
| touching `src/idc_api/**`, `tests/**`, `pyproject.toml`, or `uv.lock`. `actionlint` likewise runs | ||
| only when a workflow changes. A docs-only PR therefore runs `gitleaks` and nothing else. |
There was a problem hiding this comment.
Correct — fixed in the latest commit.
ci.yml's pull_request.paths has five entries and I documented four, dropping .github/workflows/ci.yml itself. As you say, that implies a PR editing only ci.yml would skip the Python suite, when in fact it triggers it — which is exactly the case where you'd most want the suite to run, since the change is to the CI definition.
The bullet now reads "…src/idc_api/**, tests/**, pyproject.toml, uv.lock, or ci.yml itself."
I added a mechanical check rather than re-eyeballing: parse pull_request.paths out of ci.yml and assert every entry appears in the SECURITY.md bullet. It passes. Noting for the record that this sentence duplicates the workflow's filter list, so it can drift again — you've now caught two separate bugs in it. If it drifts a third time, the right move is to stop enumerating and just link to the workflow.
Drafted with Claude Code.
Copilot review, PR #18: ci.yml's pull_request.paths also lists .github/workflows/ci.yml, so a PR editing only that file does run the Python suite. The bullet named four of the five paths, implying it would not. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The repo is public and had essentially no safeguard against a service-account key being
committed.
.gitignorecovered.envand*.env; GitHub secret scanning and push protection weredisabled; there was no
gitleaks/trufflehoganywhere. (banditis a code linter — it doesnot detect committed credentials.)
The existing history is clean. I scanned all 70 commits across every ref with
gitleaks, andseparately grepped every blob for private-key headers, GCP/GitHub/AWS key patterns, and
service-account JSON. Nothing. So this is prevention, not cleanup.
Why this repo specifically
dev/deployment.mdinstructs operators to fetch each tier's deployer service-account JSON keyfrom that tier's deployment bucket. Those keys routinely sit in a working tree. Before this PR,
every name they realistically land under was trackable:
Note
*.envmatchesprod.envbut not.env.prod. Onegit add -Aaway from publishing adeployer key for a GCP project.
Three layers, weakest last
it is not in this diff. Rejects the push before the secret reaches GitHub. The only layer that
prevents rather than detects. Does not apply to pushes on forks.
gitleakson every PR (.github/workflows/gitleaks.yml) — covers fork PRs, and flagsgeneric private-key blocks and service-account JSON that provider pattern-matching misses.
Deliberately not path-filtered, unlike
ci.yml: a secret can land in any file.Pinned and checksum-verified, mirroring
actionlint.yml.fetch-depth: 0because a secret is aleak even if a later commit "removes" it — the blob stays reachable.
.gitignore— the filename shapes above. Weakest layer, since no list is exhaustive; itexists to catch the common
git add -A.--redactkeeps matched key material out of the Actions log, which is world-readable on this repo.Verification
gitleaks git --redactover this repo: no leaks found, 70 commits scanned.service_accountJSON in a scratch repo: gitleaksdetected it and exited 1 (so the check fails), and the key material did not appear in the
output — redaction works.
becomes ignored by the new patterns, and that
.env.exampleremains committable.actionlintclean on the new workflow.If a key ever does get committed
SECURITY.mdnow says it plainly: rotate first. Deleting the commit does not un-leak it — theblob stays reachable and this repo is public. Purge from history afterwards, not instead.
Repo settings enabled alongside this PR
These are server-side settings, not files, so they cannot appear in a diff. All three are now on,
and
SECURITY.mdrecords that they are meant to be on — otherwise nothing stops them beingswitched off without review:
pip-auditfails a PR whose deps carry a known CVE; it does nothing about a CVE disclosed after merge. Security updates open a fix PR againstmaininstead of waiting for someone to notice.Dependabot security updates are the repo setting, not
.github/dependabot.yml— that fileschedules weekly grouped version updates. Security fixes stay ungrouped so they ship on their own.
Verify any time:
gh api repos/ImagingDataCommons/IDC-REST-MCP \ --jq '.security_and_analysis | {secret_scanning, secret_scanning_push_protection, dependabot_security_updates}'Still not fixed
The durable fix for the underlying exposure is keyless auth — with Workload Identity Federation
there is no
GCP_SA_KEYJSON to leak at all. That is tracked as future work.Drafted with Claude Code.
🤖 Generated with Claude Code