Check for a committed secret?
git log --all --full-history on the file, or run a real scanner like dira scan . or gitleaks against full history, since deleting a file in a new commit doesn't remove it from old ones.
Guide · Security
Nobody teaches this in the intro CS sequence, and by the time it matters, it usually matters all at once. None of these 18 items need a security team, a budget, or an expert. Most take under an hour. A few take five minutes.
A hackathon project ships over a weekend. A domain, a signup form, a Stripe test key that quietly becomes a live key somewhere around week two. Three weeks later there is a first paying customer, or a mention somewhere that sends real traffic, or a procurement team sends a one-page security questionnaire asking about encryption at rest, key rotation, and the last time someone tested a restore. Nobody covered this in class, and the honest answer to most of those questions right now is "I don't know", which is a worse answer than admitting a real, specific gap.
The good news is that this is not a 200-item enterprise framework. It is 18 things, all of them fixable by a single founder, most of them free. Do them before the questionnaire arrives, before the pentest is scheduled, or before an automated scanner finds the gap for you, and the first serious security conversation about the product goes very differently.
A quick-reference list first. Each one gets a short explanation below, with the actual commands.
.env.example as the only env file that belongs in git.A leaked secret is not a hypothetical. GitHub's own secret-scanning partner program means a real subset of provider keys pushed to a public repo can be flagged and revoked by the provider within minutes of the push, not at the next audit. Attackers run the same kind of scan continuously and for free. The fix costs nothing and takes less time than this paragraph did to write.
Every key, token, password, and connection string belongs in an environment variable loaded from a file git never sees. The habit that actually holds: create .env before the first route, and put .env in .gitignore in that same first commit. "I'll add it to .gitignore before I push" is the sentence that precedes almost every leaked-key story.
Commit a .env.example with variable names and placeholder values, never the real file. That gives the next person, or you on a new laptop in six months, the exact shape of what's required without a single real credential ever touching version control.
Deleting a file in a new commit does not remove it from history. Anyone who clones the repo can still read every version that ever existed, including the one from three commits ago before anyone noticed.
git log --all --full-history -- .env git log -p --all -S "sk_live" -- .If something real turns up, do not just delete it in a new commit. Purge it from history with git filter-repo (the maintained tool for this) or BFG Repo-Cleaner, then force-push to every remote, and treat the key as burned regardless, per the next point.
The moment a leaked secret turns up, the order is: revoke it at the provider, issue a new one, update everywhere it's used, then figure out how it got there and for how long. Rewriting history removes it from future clones, but does nothing about copies already sitting in forks, CI logs, or a bot that already scraped it. A key that has ever been public is compromised, full stop, whether or not there's evidence of misuse yet.
package.json and requirements.txt describe ranges; package-lock.json, yarn.lock, and uv.lock pin exact versions. Commit the lockfile, always. Then check the pinned versions against a vulnerability database, not just at launch but on a cadence: a dependency that was clean the day it was installed can pick up a CVE the following month with nothing on your side having changed.
Most findings resolve with a version bump. The ones that don't, usually a transitive dependency stuck behind an unmaintained package, are worth a one-line comment explaining why it's accepted, so the next audit doesn't rediscover the same dead end.
A permissive license (MIT, BSD, Apache-2.0) imposes almost no obligation. A copyleft license, especially AGPL, can require you to open-source your own server code if you distribute a modified version, a fact that has killed real acquisitions when it surfaces late in diligence. Check this once, before charging anyone, not during a term sheet.
Stripe, AWS, and most serious providers let you create a restricted key limited to specific actions or a specific resource, instead of the full-account key. Use the scoped version everywhere except the one place that genuinely needs full access. A leaked read-only key is an inconvenience. A leaked full-account key is the whole business.
Anything prefixed for the browser (NEXT_PUBLIC_, VITE_, REACT_APP_) gets compiled directly into the bundle every visitor downloads. A "secret" key carrying one of those prefixes is not secret. It is public, indexed, and readable from dev tools by anyone who looks. Server-only secrets stay server-only; if the browser needs to reach a paid API, it calls your server, which holds the real key.
Dev, staging, and production should never share a key. A staging key exposed in a public demo repo should not be able to touch a real customer's data. And a key issued to a cofounder or contractor who leaves the project is a live credential until someone actively revokes it, so put that one step in whatever passes for an offboarding process, even if the process is a single line in a note.
Default database passwords, default admin panels, a Redis instance with no auth at all: these get found by automated scanners within hours of a port opening to the internet, not by an attacker who picked the project specifically. Change every default on day one, before the first deploy, not after the first incident.
Infrastructure as code is still code, and it fails the same way application code does when nobody reviews it: containers running as root, security groups open to the entire internet on a port with no business being public, state files with plaintext secrets committed alongside everything else. These files get written once, work, and never get looked at again until something goes wrong.
A storage bucket or database that only needs to talk to your own application server has no reason to accept connections from anywhere else. Network-scoped access and security groups restricted to known sources turn "the whole internet can reach it" into "only my application can reach it," at effectively zero cost.
Most serious hosts do this by default now, which is exactly why it's easy to forget to verify. Confirm plain HTTP actually redirects rather than serving content, and confirm the HSTS header is present so a browser refuses to downgrade even if something tries to force it.
curl -sI http://yourapp.com # should redirect, not serve content curl -sI https://yourapp.com | grep -i strict-transport-securityThis does not need a security team, it needs about six headers: a Content-Security-Policy, X-Content-Type-Options: nosniff, a frame-ancestors or X-Frame-Options rule, a sane cookie policy (Secure, HttpOnly, SameSite), and a referrer policy. Most frameworks ship a one-line helper for all of it. Check what's actually being sent, not what you think you configured.
A wildcard Access-Control-Allow-Origin: * next to Access-Control-Allow-Credentials: true is a misconfiguration most browsers refuse to honor, but the version that does work, an explicit origin reflected back for every request, is just as bad in practice. Allow only the origins that genuinely need access, and treat the list like a firewall rule, not a formality.
bcrypt or argon2, never a home-rolled scheme, never plain SHA-256 with no salt. Put a rate limit on login, signup, and password-reset endpoints specifically: these are what automated credential-stuffing tools hit first, and they're cheap to protect because legitimate traffic (a human typing a password) looks nothing like the attack pattern of thousands of attempts a minute.
Request IDs, timestamps, status codes, and sanitized error messages are genuinely useful at 2am. Full request bodies, tokens, and passwords in the same log are a second, less-protected copy of every secret already being protected elsewhere. Redact before logging, not after something goes wrong.
A backup that has never been restored from is a hypothesis, not a backup. The old rule of thumb (3 copies, 2 media, 1 offsite) is overkill for a two-person side project, but the restore test is not optional: schedule one actual restore onto a clean environment before there is a customer whose data cannot be lost, so the day the backup turns out to be silently broken isn't the day it's needed.
Not all 18 matter equally on day one. If time is short, the ones that bite fastest are the secrets group (1 through 4), default credentials (10), forced HTTPS (13), and a tested backup (18). Everything else matters, but those are the ones an opportunistic scanner finds within hours of a public launch, not the ones that need a targeted attacker.
One more thing worth doing that isn't on the numbered list, because it's a process rather than a control: write a one-paragraph incident response note before it's needed: who gets notified if a key leaks, what gets rotated first, where the postmortem gets written down. A plan written calmly in five minutes beats one improvised during the incident.
This is the point where "no known bad patterns" stops being enough and a customer wants evidence: a disclosure policy (a security.txt file is the lightest version of this), branch protection with required reviews, CI that runs tests and a secret scan on every pull request, dependency updates on a schedule rather than never, and a documented, even lightweight, incident response process. None of it is exotic. All of it is exactly what shows up on the enterprise security questionnaires and SOC 2 readiness checklists that eventually land on a growing founder's desk, usually with a two-week deadline attached.
A meaningful share of this checklist is mechanical enough to automate, which is the reason I built DIRA: a free, MIT-licensed command-line scanner that runs the secrets, dependency, config, and TLS checks above against your own codebase in under a second on a warm run, with zero telemetry and zero dependencies of its own.
| Checklist items | What DIRA covers |
|---|---|
| 1–4, secrets and git history | Secret scanning across two dozen provider patterns plus an entropy-gated generic rule, redacted before they reach a report, plus a dedicated pass over git history for anything since removed from the working tree. |
| 5–6, dependencies and licenses | Every lockfile entry resolved against OSV.dev for known CVEs, and every dependency's license classified, so copyleft risk shows up before a diligence call instead of during one. |
| 10–12, config and infrastructure | 38 misconfiguration rules across Docker, Kubernetes, Terraform, GitHub Actions, cloud IAM, frontend code, and LLM-specific patterns like a provider key shipped to the browser. |
| 13–15, TLS and headers | A live check of the domain's TLS validity and expiry, HSTS, and the same security headers covered above, plus whether /.env or /.git/config is being served to the public internet. |
| 16–18 and the rest | An 18-point security-readiness score modeled on what SOC 2 and enterprise questionnaires actually ask for: CI, tests, secret scanning, CODEOWNERS, IaC, backups, incident response, rolled into one percentage, with checks no scanner can verify, like MFA and access reviews, listed as manual attestations instead of silently skipped. |
dira scan . for a first pass, dira scan . -t yourapp.com to add the live TLS and header checks, dira fix --apply to write the policy files it can generate safely. It will not replace a real pentest or a SOC 2 audit, and it says so in its own report, but it closes most of this checklist in about the time it takes to read this sentence twice. See the full scanner and install instructions →
DIRA is free because most of this list is mechanical, and mechanical problems don't need a person attached to them. What it can't do is sit on a call, read the actual architecture, and tell you which of the manual attestations in its own report are real for your specific setup. That's the gap Shield Audit fills: DIRA runs against the repo and the live domain, then the findings get walked through together into a scored report with a fix plan attached, not just a list.
And if the immediate need is smaller than a full audit, a chunk of what's above is also written up as templates and playbooks in the store, the kind of thing that's faster to buy than to rebuild from scratch at 11pm before a demo.
The founder from the opening story didn't need a security team. They needed about ninety minutes, spread across a weekend, going down this exact list before the questionnaire arrived instead of after. That's the whole job a checklist does: not turning anyone into an expert, just making sure the easy, expensive mistakes never get the chance to become the story.
FAQ
git log --all --full-history on the file, or run a real scanner like dira scan . or gitleaks against full history, since deleting a file in a new commit doesn't remove it from old ones.
Revoke and reissue the key first, always. History rewriting (filter-repo/BFG + force-push) only stops future clones; forks and caches can still hold the old commits.
Secrets out of git and history, default creds changed, HTTPS + HSTS forced, one CVE scan run, one tested backup restore. Those five get found fastest by bots.
No, it says so itself. It finds known bad patterns; MFA, access reviews, and real threat modeling stay manual attestations, not automated checks.
A security.txt, branch protection with required reviews, CI running tests + a secret scan per PR, a dependency-update cadence, and a one-paragraph incident response plan.
Close to nothing: git, free-tier CVE scanners, a framework's headers middleware, and a free scanner for the mechanical checks. The real cost is a weekend, not money.