Skip to content
fullstackhero

Guide

Production security checklist

Ten configuration items you must check before shipping fullstackhero to production. Skip none.

views 0 Last updated

The kit ships with secure defaults wherever possible. Some things, by their nature, you must configure for your environment — there’s no “secure default” for a JWT signing key or an allowed-origins list. This page is the ten-item checklist; skip none of them.

1. Set JwtOptions:SigningKey from a secrets manager

The repo’s dev-only key lives in appsettings.Development.json; base and production config leave the key empty, and startup validation refuses to boot with an empty key, a key shorter than 32 characters, or the sample replace-with-… placeholder. So the failure mode isn’t “silently forgeable tokens” — it’s “won’t start until you set one”. Set a real one:

Terminal window
openssl rand -base64 32

Inject via secrets manager / environment variable:

Terminal window
JWTOPTIONS__SIGNINGKEY=...

Never check the production key into git, and never reuse the dev key. See data protection for the secrets-management patterns.

2. Configure password policy to your compliance needs

The kit defaults:

  • 10-character minimum with digit + uppercase + lowercase required (IdentityOptions in IdentityModule)
  • 5-password history (prevent reuse)
  • 90-day expiry with 14-day warning

Adjust for your industry. Healthcare (HIPAA) and finance (PCI-DSS) tend to require longer histories and shorter expiries. Consumer products can be looser (longer minimum + no expiry, with strong 2FA).

{
"PasswordPolicy": {
"PasswordHistoryCount": 12,
"PasswordExpiryDays": 60,
"PasswordExpiryWarningDays": 7,
"EnforcePasswordExpiry": true
}
}

3. Allowlist CORS origins

CorsOptions:AllowAll = true (and the SetIsOriginAllowed(_ => true) policy it enables) is dev only. Production needs the explicit lists — and note that appsettings.Production.json ships AllowedOrigins empty, which means no CORS middleware mounts at all until you fill it in; your front-ends on other origins will be blocked by the browser. See CORS & security headers.

{
"CorsOptions": {
"AllowAll": false,
"AllowedOrigins": [
"https://app.example.com",
"https://admin.example.com"
],
"AllowedHeaders": [ "content-type", "authorization" ],
"AllowedMethods": [ "GET", "POST", "PUT", "DELETE" ]
}
}

4. Review the security headers

The kit emits CSP, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, and HSTS (on HTTPS responses) by default — verify they survive your reverse proxy, and tune the knobs:

{
"SecurityHeadersOptions": {
"Enabled": true,
"ExcludedPaths": [ "/scalar", "/openapi" ],
"AllowInlineStyles": true,
"ScriptSources": [], // add your analytics/widget origins here
"StyleSources": []
}
}

If your production posture disables the API docs UI (item 7), you can drop the ExcludedPaths entries too. Test with browser dev tools open — CSP violations log to the console.

5. Turn rate limiting on and tune the values

RateLimitingOptions:Enabled is false in the base config (so local dev never trips it) and true in appsettings.Production.jsonverify your deployed config actually enables it. The auth policy defaults to 10 requests per minute per user-or-IP; the global chained limiters cover tenant (1000/min), user (200/min), and IP (300/min):

{
"RateLimitingOptions": {
"Enabled": true,
"Auth": { "PermitLimit": 10, "WindowSeconds": 60, "QueueLimit": 0 },
"Tenant": { "PermitLimit": 1000, "WindowSeconds": 60, "QueueLimit": 0 },
"User": { "PermitLimit": 200, "WindowSeconds": 60, "QueueLimit": 0 },
"Ip": { "PermitLimit": 300, "WindowSeconds": 60, "QueueLimit": 0 }
}
}

Adjust Auth to your traffic profile: relax for high-volume consumer apps on shared NAT, tighten for internal admin surfaces.

6. HTTPS everywhere

UseHttpsRedirection is on by default. Verify:

  • Reverse proxy / load balancer terminates TLS with a valid certificate.
  • X-Forwarded-Proto: https forwards to the kit so the redirect middleware doesn’t double-redirect — and so the HSTS header (emitted only on HTTPS requests) actually fires.
  • HTTP/2 or HTTP/3 enabled at the LB for performance.

If you’re behind Cloudflare / a CDN, also enable “Always Use HTTPS” + “HSTS” at the CDN.

7. Lock down or remove debug endpoints

In production, decide:

  • /scalar (the OpenAPI browser) and /openapi/v1.jsonappsettings.Production.json ships with OpenApiOptions:Enabled = false, which removes both. If you re-enable them, consider whether revealing your API surface to the world is what you want. Note these paths also bypass the security-headers middleware and authorization (so the docs UI works) — another reason to keep them off in production.
  • /health — expose to your platform’s probes; firewall externally.
  • OpenTelemetry collector endpoints — these are inbound to your collector, not the kit, but verify they’re not Internet-exposed.

8. Lock down the Hangfire dashboard

The jobs dashboard (default route /jobs, configurable via HangfireOptions:Route) is gated by basic auth. Startup validation enforces a username of 3+ chars and a password of 12+ chars — empty values won’t boot. Verify:

  • Username is not admin (rename to something less guessable).
  • Password is long (16+ random chars) and not the dev value from appsettings.Development.json.
  • Behind HTTPS (basic auth is plaintext otherwise).
  • IP-allowlist at the reverse proxy (Cloudflare WAF rule, Nginx allow/deny, etc.) — only your ops IPs.
{
"HangfireOptions": {
"UserName": "ops-team",
"Password": "set-via-secrets-manager",
"Route": "/jobs"
}
}

9. Configure Data Protection key persistence

Set CachingOptions:Redis. Without it, each instance maintains its own Data Protection key ring locally, and email-confirmation links, password reset links, and webhook signing secrets stop working across rolling deploys.

See data protection.

10. Enable audit retention

The kit’s AuditRetentionJob is opt-in (Enabled defaults to false). Without it, the audit table grows forever. Configure:

{
"Auditing": {
"Retention": {
"Enabled": true,
"ActivityRetentionDays": 30,
"EntityChangeRetentionDays": 90,
"SecurityRetentionDays": 365,
"ExceptionRetentionDays": 180,
"DeleteBatchSize": 5000,
"Cron": "30 3 * * *"
}
}
}

Adjust retention windows per your compliance regime. Most teams want security events kept much longer than activity events (a year vs a month is typical).

Bonus: items worth doing within the first month

These aren’t blockers, but the sooner the better:

  • Make 2FA mandatory for admin roles. A leaked admin password compromises everything; require TOTP.
  • Add monitoring + alerts on auth-failure spikes, impersonation events, and 5xx error rates.
  • Run a vulnerability scan of the deployed image (Snyk, Trivy, Dependabot) to catch transitive CVEs early.
  • Set up backup + restore tests for Postgres + MinIO. Backups you haven’t tested restoring are not backups.
  • Document an incident-response runbook — who’s on-call, where the alerts go, how to revoke every session (admin revoke-all is built in), how to rotate the JWT signing key in an emergency.