All rules Rule BC004 · PC
Credential rule · any database
critical
PC · Privileges & credentials
also runs on any file, bolvrk secrets
free in the CLI

What happens when an API key or private key is committed in SQL?

API key, access token or private key in a string literal

Critical: a working credential committed to the repository; rotate it, git history keeps it.

What happens

Vendors give their secrets recognisable prefixes precisely so that leaks can be caught: sk_live_ (Stripe), AKIA (AWS), ghp_ (GitHub), xoxb- (Slack), -----BEGIN PRIVATE KEY-----. One of those in an INSERT, an UPDATE or a column DEFAULT is a working credential committed to the repository. The rule matches only published formats, never entropy, a uuid, a bcrypt hash or a base64 blob is not a token, and the rule stays silent on all of them.

Why it is dangerous on a populated table

Table size does not change this one: a committed credential is live from the first row, and git history keeps it after the fix.

Fires on

INSERT INTO settings (key, value) VALUES ('stripe_secret', 'sk_live_51H8zK2eZvKYlo2CabcDEFghij');

The safe pattern

Never seed a live token through a migration. Store a reference (the secret's name in your vault) or a placeholder the deploy substitutes, and have the application read the value at runtime. Rotate the token that was already committed, git history keeps it.

INSERT INTO settings (key, value) VALUES ('stripe_secret', 'vault:stripe/live/secret_key');

Fixtures

The rule ships with these files and the test suite runs them on every change: the first set must fire, the second must stay silent.

Fires (6)

aws access key
UPDATE integrations SET config = '{"access_key_id": "AKIAIOSFODNN7EXAMPLE"}' WHERE name = 'exports';
github token
INSERT INTO integrations (name, token) VALUES ('github', 'ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdef012345');
jwt
INSERT INTO service_accounts (name, bearer) VALUES ('ingest', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJpbmdlc3QiLCJyb2xlIjoic2VydmljZSJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c');
private key
INSERT INTO signing_keys (kid, pem) VALUES ('2026-09', '-----BEGIN RSA PRIVATE KEY-----
MIIBOgIBAAJBAKj34GkxFhD90vcNLYLInFEX6Ppy1tPf9Cnzj4p4WGeKLs1Pt8Qu
KUpRKfFLfRYC9AIKjbJTWit+CqvjWYzvQwECAwEAAQJAIJLixBy2qpFoS4DSmoEm
-----END RSA PRIVATE KEY-----');
stripe secret key
INSERT INTO settings (key, value) VALUES ('stripe_secret', 'sk_test_51H8zK2eZvKYlo2CabcDEFghijKLMNopqrSTUv');
two tokens one literal
-- Two vendor tokens inside one JSON literal: both must be masked, not just the first.
INSERT INTO integrations (name, config)
VALUES ('billing', '{"stripe":"sk_live_51H8zK2eZvKYlo2CabcDEFghij","slack":"xoxb-1234567890-abcdefghijklmnop"}');

Stays silent (4)

prefix words only
INSERT INTO settings (key, value) VALUES ('provider', 'sk_live'), ('region_code', 'AKIA'), ('note', 'rotate the ghp_ token quarterly');
publishable key
INSERT INTO settings (key, value) VALUES ('stripe_publishable', 'pk_live_51H8zK2eZvKYlo2CabcDEFghijKLMNopqrSTUv');
uuid hash and base64
INSERT INTO api_clients (id, secret_hash, logo) VALUES ('6f1c2b3a-4d5e-4f60-8a7b-9c0d1e2f3a4b', '$2b$12$C6UzMDM.H6dfI/f/IKcEeO9rOsTbnR6Q3oFf9zGjHpMqU9ZAwK9Tu', 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==');
vault reference
INSERT INTO settings (key, value) VALUES ('stripe_secret', 'vault:stripe/live/secret_key');

How to check locally

Catch this before it ships

This rule runs locally in the free CLI, or with the full corpus through the hosted service. No install, nothing leaves your machine:

npx bolvrk check migration.sql