Skip to content

Security

Secret scanning

Keep APP_KEY, API tokens and Stripe keys out of git with Betterleaks - the starter .betterleaks.toml, the pre-commit hook, the secrets:scan script, the CI job pinned by version and checksum, and vukem doctor.

Last updated:

A vukem app holds secrets that must never reach git: APP_KEY and APP_PREVIOUS_KEYS, API tokens, database URLs, Stripe, mail and OAuth keys. They belong in .env (ignored by git) or in your host's secret store. A secret pushed once stays in the history and in every clone, so the only fix is to rotate it.

Projects created with vukem new --standards or --ci scan for leaked secrets with Betterleaks, an open-source scanner by the Gitleaks authors. It is a separate binary: nothing is added to your go.mod.

You will learn:

  • what the starter writes and when each scan runs;
  • which secret formats the rules catch and how placeholders stay quiet;
  • how to install Betterleaks and check it with vukem doctor;
  • what to do when a scan finds something.

What the starter writes#

FileFlagWhat it does
.betterleaks.toml--standards or --ciThe Betterleaks default rules plus vukem's secret formats and a placeholder allowlist. Every scan below picks it up by name.
.githooks/pre-commit--standardsScans the staged changes before each commit, then runs gofmt and go vet.
package.json script secrets:scan--standardsScans the whole git history: npm run secrets:scan (or bun run, pnpm run).
.github/workflows/ci.yml job secrets--ciDownloads Betterleaks by exact version and sha256 checksum and scans the whole history on every push and pull request.

When Betterleaks is not installed, the pre-commit hook prints a message and lets the commit through. The CI job always runs the scan, so a leak never passes silently.

Rules#

.betterleaks.toml extends the default rule set with:

RuleMatches
vukem-app-keybase64: followed by 32 base64-encoded bytes: APP_KEY and each APP_PREVIOUS_KEYS entry
vukem-api-tokenAPI tokens from auth.Tokens with the default vk_ prefix
stripe-secret-keyStripe secret and restricted keys: sk_test_, sk_live_, rk_test_, rk_live_
stripe-webhook-secretStripe webhook signing secrets: whsec_

The default rule stripe-access-token is turned off because stripe-secret-key replaces it, so a Stripe key is reported once.

The allowlist matches placeholder values, never file paths: a value containing _FAKE_, placeholder, changeme, example, eight or more x, or ending in ... is ignored. .env.example with empty values or placeholders such as sk_test_... or sk_test_ followed by a run of x passes. A real key pasted into .env.example still fails the scan.

Add your own formats as more [[rules]] entries. The Betterleaks README describes the format.

Install and check#

Download a release for your platform from the Betterleaks releases page and put the binary on your PATH. Use a stable release. The CI job pins v1.9.0.

Terminal
vukem doctor
Text
betterleaks: 1.9.0 (/usr/local/bin/betterleaks)

vukem doctor reports whether Betterleaks is installed and its version. It always exits 0.

Scan by hand:

Terminal
betterleaks git --pre-commit --staged --no-banner --redact   # staged changes, what the hook runs
betterleaks git --no-banner --redact                          # the whole history, what CI runs

--redact keeps the secret out of the output and CI logs.

Validation stays off#

Betterleaks can check a finding against the provider's API to see whether the key is live. That sends your secret to a third party, so the starter never enables it. To opt in, run betterleaks git --validation yourself, and read the Betterleaks docs on the --validation-* flags first.

When a scan finds a secret#

  1. Rotate the secret at its source. For APP_KEY, run vukem key:rotate. For an API token, revoke it. For Stripe, roll the key in the dashboard.
  2. Remove it from the code and load it from the environment.
  3. If the commit was already pushed, treat the secret as public even after you rewrite the history.

A false positive in a test fixture: change the value so it contains _FAKE_ or placeholder. Do not add the file path to the allowlist, or a real secret in that file would also pass.