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.json - verify 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.json - appsettings.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.