Guide · Security

18 things to fix before your first real user

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.


The week the checklist stops being optional

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.

The 18-point checklist

A quick-reference list first. Each one gets a short explanation below, with the actual commands.

  1. Never commit a secret, not even for five minutes.
  2. Treat .env.example as the only env file that belongs in git.
  3. Audit git history, not just the working tree, for anything that leaked.
  4. Rotate first, investigate second.
  5. Pin your lockfile and scan it for known CVEs on every install.
  6. Know your dependency licenses before you take money for the product.
  7. Scope every API key to the least it needs to do its job.
  8. Never ship a secret key inside client-side JavaScript.
  9. Use separate keys per environment, and kill old ones when people leave.
  10. Change every default credential before anything goes live.
  11. Review Dockerfiles, Terraform, and CI config like you review application code.
  12. Put cloud storage and databases behind a private network, not the open internet.
  13. Force HTTPS everywhere and turn on HSTS.
  14. Set the handful of security headers that actually matter.
  15. Get CORS right, especially once cookies or auth headers are involved.
  16. Hash passwords properly and rate-limit anything that accepts one.
  17. Log enough to debug a problem, never enough to leak a secret.
  18. Back up your data, and prove the restore works before you need it.

Secrets are the fastest way to lose the company

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.

1. Never commit a secret, not even for five minutes

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.

2. Treat .env.example as the only env file that belongs in git

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.

.env.example STRIPE_SECRET_KEY=sk_live_placeholder_never_a_real_value DATABASE_URL=postgres://user:pass@host:5432/db

3. Audit git history, not just the working tree

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.

4. Rotate first, investigate second

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.

Dependencies: someone else's bug becomes yours the moment you install it

5. Pin your lockfile and scan it for known CVEs on every install

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.

npm audit --omit=dev pip-audit -r requirements.txt osv-scanner -L package-lock.json

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.

6. Know your dependency licenses before you take money for the product

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.

Keys: the difference between a leak and a disaster is scope

7. Scope every API key to the least it needs to do its job

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.

8. Never ship a secret key inside client-side JavaScript

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.

9. Use separate keys per environment, and kill old ones when people leave

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.

Config and infrastructure: the part most tutorials skip

10. Change every default credential before anything goes live

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.

11. Review Dockerfiles, Terraform, and CI config like you review application code

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.

12. Put cloud storage and databases behind a private network, not the open internet

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.

What a browser or a scanner can see from the outside

13. Force HTTPS everywhere and turn on HSTS

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-security

14. Set the handful of security headers that actually matter

This 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.

curl -sI https://yourapp.com | grep -iE 'content-security-policy|x-frame-options|x-content-type-options'

15. Get CORS right, especially once cookies or auth headers are involved

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.

Application basics that get skipped under deadline pressure

16. Hash passwords properly and rate-limit anything that accepts one

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.

17. Log enough to debug a problem, never enough to leak a secret

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.

18. Back up your data, and prove the restore works before you need it

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.

Before your first real user

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.

Before your first security questionnaire or pentest

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.

What DIRA checks for you automatically

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 itemsWhat DIRA covers
1–4, secrets and git historySecret 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 licensesEvery 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 infrastructure38 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 headersA 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 restAn 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 →

When the checklist isn't enough on its own

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 point of a checklist

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.

Tools referenced in this guide

  • DIRA — the free scanner behind most of this checklist: secrets, dependency CVEs, config, TLS, and a startup readiness score.
  • Shield Audit — a DIRA-powered audit with a human walkthrough and a fix plan, for when the checklist needs a second set of eyes.
  • Store — templates and playbooks for the parts of shipping a product that aren't code.

FAQ

Quick answers

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.

Leaked key, rewrite history?

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.

Minimum setup before a paying customer?

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.

Does DIRA replace an audit?

No, it says so itself. It finds known bad patterns; MFA, access reviews, and real threat modeling stay manual attestations, not automated checks.

Before a security questionnaire?

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.

Cost to fix most of this list?

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.