DEV Community

Geminate Solutions
Geminate Solutions

Posted on

A GitHub Actions Security Gate for Node.js: Block Leaked Keys

A GitHub Actions security gate for Node.js is a single workflow that runs on every pull request. It scans the new commits for leaked API keys, checks new dependencies for known vulnerabilities and confirms the lockfile is honest. If any of those checks finds something real, it blocks the merge. You can build it in an afternoon with free tools, and most of that afternoon goes into tuning, not setup.

You may have found a live Stripe key in a commit from three weeks ago. Or you may have a Dependabot tab full of alerts that nobody has opened since spring. Either way, this walkthrough is for you.

Why do leaked keys and vulnerable packages get past code review?

Code review happens on diffs, and people reviewing a diff read the logic, not the config. A key pasted into a test fixture looks like noise. A risky transitive dependency hardly shows up at all. It sits somewhere in a 2,000-line lockfile change that nobody scrolls through.

AI coding assistants make both problems worse. To get an example working, they hardcode credentials, and they add packages without a second thought. The code runs, the reviewer approves, and the key goes into history.

What does it cost when one slips through?

Deleting a leaked key in a later commit does not remove it. Git history keeps it, and so does every clone and fork. Bots scan public pushes for credential patterns all the time. The only real fix is rotation: you find every service and environment that uses the key, issue a new one and redeploy without breaking production.

A vulnerable dependency costs you differently. You end up doing an emergency upgrade across major versions, under pressure, on code you didn't write.

The hidden cost is a noisy gate. If the check fails on things that don't matter, people stop reading it. They re-run it, or an admin turns on bypass. Then the one real finding goes straight through.

Do you actually need a custom gate?

Maybe not. A solo side project with no paying users and no production credentials may only need what GitHub already gives you: secret scanning with push protection and Dependabot alerts, both free on public repositories. Turn those on and stop there.

You need the gate when several people merge code, when the app holds customer data or payment keys, or when part of the codebase came from an AI builder and nobody has reviewed it line by line.

The workflow

Save this as .github/workflows/security-gate.yml:

name: security-gate
on:
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  secrets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Scan PR commits for secrets
        uses: trufflesecurity/trufflehog@main # pin a release tag or SHA
        with:
          extra_args: --results=verified,unknown

  dependencies:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/dependency-review-action@v4
        with:
          fail-on-severity: high
          fail-on-scopes: runtime

  lockfile:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - name: Lockfile matches package.json
        run: npm ci --ignore-scripts
      - name: Lockfile only pulls from the npm registry over https
        run: npx --yes lockfile-lint --path package-lock.json --type npm --allowed-hosts npm --validate-https
Enter fullscreen mode Exit fullscreen mode

Here is what each job does and why it is set up this way.

Secrets. TruffleHog scans only the commits between the PR base and head, so it stays fast. fetch-depth: 0 gives it that history. --results=verified,unknown means it fails on keys it confirmed are live, plus keys it couldn't check. It ignores ones it proved are dead.

Dependencies. Dependency review looks only at packages this PR adds or changes. That one choice keeps old debt from blocking unrelated work. fail-on-scopes: runtime ignores dev-only tooling. On private repositories this action needs GitHub's paid code security add-on.

Lockfile. npm ci fails when package.json and package-lock.json disagree, which catches the classic "I edited package.json by hand" PR. --ignore-scripts stops install scripts from running inside your gate. lockfile-lint fails if a resolved URL points to an unknown host or plain http, which is a common way to sneak in a tampered package.

If dependency review isn't available to you, swap that job for npx --yes audit-ci@7 --config audit-ci.jsonc with a config like this:

{
  "high": true,
  "skip-dev": true,
  "allowlist": [
    // Only reachable from an internal admin script. Review by 2026-12-01. Owner: backend lead
    "GHSA-xxxx-xxxx-xxxx"
  ]
}
Enter fullscreen mode Exit fullscreen mode

How do you tune it so the team doesn't learn to ignore it?

Only fail on findings someone would actually fix today. Everything else goes into a report, not a red X.

  • Start with verified secrets only. Run TruffleHog with --results=verified for the first two weeks, then add unknown. The team's first experience should be a real catch, not ten false alarms.
  • Gate on high and critical in runtime code. Moderate issues in a test runner are real, but they are not merge blockers. Handle them in a scheduled job.
  • Run a nightly full audit separately. A schedule: workflow running npm audit --omit=dev across the whole tree catches newly disclosed issues in old packages. It opens an issue instead of blocking someone's unrelated PR.
  • Every exception needs a reason, an owner and a review date. An allowlist entry with no comment becomes permanent. The allow-ghsas input on dependency review and the audit-ci allowlist both work for this. Read through them once a month.
  • Keep the gate under three minutes. Slow checks get skipped. The three jobs run in parallel for that reason.
  • Make the failure tell people what to do. Add a short note to your PR template: if the secrets job fails, rotate the key first, then remove it from the code. Removing it alone fixes nothing.

How do you make sure nobody can skip it?

In your repository settings, add a branch protection rule (or ruleset) on main and mark secrets, dependencies and lockfile as required status checks. Turn on "Do not allow bypassing" for admins too. A gate that the most senior person can quietly skip only trains everyone else to ask that person.

One honest limit: this gate stops new problems. It doesn't find the key that was committed eight months ago. Run a full history scan once (trufflehog git file://. --results=verified) and rotate whatever it finds before you trust the clean green checks.

If your app started life in Lovable, Bolt, Cursor or a similar tool, the exposure is usually bigger than one pipeline. The vibe coding security guide covers what to check beyond CI. Or send your repo or pipeline to Geminate Solutions for a free review, under NDA, with a reply within 24 hours.

Top comments (1)

Collapse
 
kreout profile image
Andrei Ovcharenko •

The lockfile check has one dependency outside the lockfile: npx --yes lockfile-lint. If the tool isn't already a project dependency, npx fetches it into its cache, so the checker itself isn't fixed by npm ci.

I'd add the checker to devDependencies, commit the resulting lockfile, and run it through an npm script after npm ci --ignore-scripts. That makes changes to the gate's own dependency tree reviewable too. npm documents the local-versus-remote resolution here: docs.npmjs.com/cli/v11/commands/npx/