A Pragmatic Security Hardening Checklist for Early-Stage SaaS
Most early-stage security checklists are either too abstract to act on (“follow security best practices”) or too heavy to be realistic for a two-person engineering team (“stand up a SIEM”). This one is neither — it’s a set of specific, checkable items, ordered roughly by how often they show up as the actual finding in a security review, and how expensive they get if you retrofit them after you have real customer data instead of building them in from the start.
§ 1Encrypt sensitive columns, not just the disk
“We use encryption at rest” almost always means the cloud provider’s disk-level encryption is on. That’s necessary and also not what it sounds like it’s protecting against — disk encryption protects against someone stealing the physical drive. It does nothing for a SQL injection, an overly broad database read replica, a leaked connection string, or a backup file that ends up somewhere it shouldn’t.
What actually protects sensitive fields (financial account numbers, access tokens, PII beyond name/email) is application-level column encryption: encrypt the value before it’s written, decrypt it after it’s read, with the key material held separately from the database itself. A practical version of this:
- A root key (KEK) held in your secrets manager or environment configuration — never in the database, never in the repo.
- Per-tenant data encryption keys (DEKs), generated randomly, encrypted (“wrapped”) by the KEK, and stored (as ciphertext) in the database.
- Sensitive columns encrypted with AES-256-GCM using the tenant’s DEK, with authenticated data bound to the tenant and column so ciphertext can’t be silently moved between rows.
- A versioned KEK (e.g., a
kek_versionfield alongside each wrapped DEK) so you can rotate the root key without a flag day — old wraps stay decryptable under their recorded version while new wraps use the current one.
This is more work than turning on a managed database’s encryption toggle. It’s also the difference between “a database dump is a serious incident” and “a database dump is ciphertext someone would need the KMS keys to do anything with.” It’s the same envelope-encryption pattern behind a production Plaid integration — the column changes, the key model doesn’t.
§ 2Access control that’s actually checked per request
A common gap: authorization logic that trusts a role or permission baked into a session token or JWT at login time, and never re-checks it against current state. This means revoking a user’s access — firing an employee, downgrading a compromised account — doesn’t take effect until their token expires, which could be hours or days later depending on your session length.
The fix is to treat authorization as a per-request lookup against current state, not a claim trusted from the token: check current membership/role from the database on every access to sensitive resources, and maintain an explicit “access revoked at” timestamp you can check against the token’s issued-at time. A token issued before a revocation event is rejected, even if it hasn’t technically expired. This is more database load than trusting the JWT, and it’s the difference between “we revoked access” being true immediately versus eventually.
§ 3Multi-factor authentication that isn’t just SMS OTP
SMS-based one-time codes are better than a password alone, but SIM-swapping is a known, practiced attack against exactly this. If your product handles financial data or anything else attractive to a targeted attacker, offer (and where reasonable, require) WebAuthn/passkeys and/or TOTP authenticator apps as the primary MFA method, with SMS as a fallback rather than the default. Also implement recovery codesgenerated at enrollment — the alternative, “email support to prove your identity,” doesn’t scale and is itself a social-engineering target.
§ 4An audit log that’s actually useful during an incident
“We have logging” usually means application logs mixed in with debug output, rotated after a few days, not queryable by the dimensions you’d actually need during an incident (“show every read of this user’s data in the last 30 days”). A real audit log is a separate, append-onlytable: every sensitive read or write, with the actor, IP address, user agent, and timestamp, indexed by the dimensions you’d query during an investigation (typically tenant + time range). Append-only matters — if the audit log itself can be edited or deleted by the same access level that can read customer data, it’s not evidence, it’s a suggestion.
§ 5Secrets that never touch the git history
Plaintext secrets in a .envfile committed to git — even briefly, even in a private repo — are effectively permanent, because git history persists even after the file is deleted in a later commit. The practical fix is encrypting secrets files with a tool like SOPS (backed by age or a cloud KMS) so what’s committed is ciphertext, decrypted only at deploy time on the target infrastructure. Anyone without the decryption key sees encrypted YAML; anyone reviewing a PR sees a diff of encrypted bytes, not the secret itself.
§ 6CI/CD that fails loudly, not silently
A deploy pipeline that builds and ships a container image without scanning it for known-vulnerable dependencies is shipping unknown risk on every deploy. A practical minimum: lint and type-check gates before merge, a vulnerability scan (e.g., Trivy) on the built image that fails the pipeline on critical, fixable findings, and a documented (and ideally tested) rollback path — redeploying a previous image tag should be a one-line operation, not an emergency improvisation.
§ 7A disaster-recovery plan you’ve actually run
The most common gap here isn’t “no backups” — it’s backups nobody has restored from. A DR plan that exists only as a document, never executed, has an unknown success rate. The stronger pattern is a provisioning script that is the recovery runbook — the same script that sets up a fresh environment can restore one, which means it gets exercised (and therefore kept accurate) far more often than a document that’s updated once and never opened again.
§ 8Prioritizing when you can’t do all of it at once
If you have to sequence this work: encrypted storage for anything an attacker could monetize directly (tokens, financial account data) comes first, because it’s the highest-severity gap and the most expensive to retrofit once you have real customer data in unencrypted columns. Access control and audit logging come next, because they determine how bad an incident is when (not if) one happens. CI/CD and secrets hygiene are comparatively cheap to fix at any point and don’t get more expensive with scale, so they can reasonably come after the data-protection work — but don’t let “comparatively cheap to fix later” become “never fixed.”
None of this requires a dedicated security team. It requires treating these seven items as core engineering scope, on the same footing as the features on your roadmap — not as a compliance checkbox to backfill once an enterprise customer’s security questionnaire forces the issue. This is the same checklist I hold my own systems to — see how I build and operate them — and it’s the structure behind DevStrike’s Security & Infrastructure Hardening engagements.