The difference between a secure ASP.NET Core deployment and a vulnerable one is usually a dozen configuration lines, not a rewrite. The framework has excellent security primitives, but many are opt-in or environment-sensitive, and it's easy to ship a service that works perfectly in a demo while leaking stack traces, accepting cleartext HTTP, or missing antiforgery protection. Use this checklist as a gate before anything reaches production. It's organized by concern, and every item includes the concrete configuration so you're not left guessing.
Transport: is every byte encrypted and non-downgradeable?
Start at the wire. HTTPS redirection and HSTS together stop passive eavesdropping and active downgrade.
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseHsts(); // adds Strict-Transport-Security
app.UseHttpsRedirection(); // 307/308 to https
}
Configure Kestrel or your reverse proxy to a TLS 1.2 minimum and let the OS negotiate the cipher suite rather than hardcoding one. If you terminate TLS at a proxy, add app.UseForwardedHeaders() so the app sees the real scheme and client IP — otherwise Request.IsHttps lies and your redirects loop.
-
UseHstsandUseHttpsRedirectionenabled outside Development - TLS 1.2+ enforced; no hardcoded protocol pin
- Forwarded headers configured when behind a proxy
Headers: are the browser-side defenses on?
ASP.NET Core doesn't send a Content-Security-Policy by default. Add a small middleware to set the security headers that mitigate XSS, clickjacking, and MIME sniffing.
app.Use(async (ctx, next) =>
{
var h = ctx.Response.Headers;
h["Content-Security-Policy"] = "default-src 'self'; object-src 'none'; frame-ancestors 'none'";
h["X-Content-Type-Options"] = "nosniff";
h["Referrer-Policy"] = "no-referrer";
await next();
});
Prefer a nonce-based CSP over 'unsafe-inline'; the latter defeats most of CSP's XSS value. Verify the headers land in production, since a misordered middleware pipeline can silently drop them.
- CSP set (nonce-based, no
unsafe-inline) -
X-Content-Type-Options: nosniff -
frame-ancestors 'none'(or an explicit allow-list)
Authentication and authorization: is the fallback deny?
The safest posture is default-deny with explicit permits layered on top. In minimal APIs and MVC alike, apply a fallback policy so a forgotten [Authorize] fails closed instead of open.
builder.Services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
Then mark public endpoints with [AllowAnonymous] deliberately. Use ASP.NET Core Identity or an external identity provider rather than hand-rolled auth, store cookies with Secure, HttpOnly, and SameSite=Strict, and keep access-token lifetimes short with refresh rotation.
- Fallback authorization policy requires authentication
- Public routes explicitly
[AllowAnonymous] - Cookies:
Secure+HttpOnly+SameSite - Short-lived tokens; rotation on refresh
CSRF and antiforgery: are state-changing requests protected?
Any endpoint that relies on a session cookie needs antiforgery protection. In .NET 8+, wire the antiforgery middleware and validate tokens on unsafe methods.
app.UseAntiforgery();
// Razor forms emit the token automatically; validate on POST:
[ValidateAntiForgeryToken]
public IActionResult UpdateProfile(ProfileModel m) { /* ... */ }
Pure token-authenticated APIs (Authorization header, no cookie) are not CSRF-susceptible in the same way, but the moment you introduce cookie auth the requirement returns.
-
UseAntiforgeryenabled -
[ValidateAntiForgeryToken](or[AutoValidateAntiforgeryToken]) on cookie flows
Abuse resistance: can one client exhaust the service?
Since .NET 7, rate limiting is built in. Add a policy and apply it to the endpoints that are expensive or attractive to brute-force (login, search, export).
builder.Services.AddRateLimiter(o =>
o.AddFixedWindowLimiter("login", opt => {
opt.PermitLimit = 5;
opt.Window = TimeSpan.FromMinutes(1);
}));
app.UseRateLimiter();
- Rate limiting on auth and expensive endpoints
- Request body size limits set (
MaxRequestBodySize)
Data protection and errors: no leaks, no shared key mistakes
The Data Protection API backs antiforgery tokens, cookie encryption, and more. In a multi-instance deployment, persist keys to shared storage and protect them, or every instance generates its own and users get logged out unpredictably — and a container that regenerates keys on restart can invalidate tokens mid-session.
builder.Services.AddDataProtection()
.PersistKeysToAzureBlobStorage(blobUri, cred)
.ProtectKeysWithAzureKeyVault(keyId, cred);
Finally, hide internals: app.UseExceptionHandler("/error") in production and never the developer exception page. Set server.error details off so class names and paths don't leak.
- Data Protection keys persisted and protected in shared storage
- Production exception handler; no dev error page in prod
- Runs as a non-root user in the container
Verifying the checklist actually holds
Configuration drifts. A profile override, a merged branch, or a base image change can quietly undo any of these. Confirm the deployed state with a dynamic scan of the running application that checks headers, cookie flags, and TLS from the outside, and cover the framework and NuGet layer with software composition analysis so an outdated ASP.NET Core or third-party package doesn't reintroduce a patched flaw. If you deploy to Kubernetes or a cloud runtime, add infrastructure-as-code scanning so the container and manifest match the hardening you set in code.
How Safeguard Helps
Safeguard treats this checklist as continuously verifiable rather than a one-time review. It scans your ASP.NET Core dependency graph, flags CVEs in the packages you actually call using reachability analysis, and pairs that with dynamic testing that probes the deployed endpoints for the header, cookie, and CSRF gaps this checklist targets. Griffin AI explains each finding in context — why a missing SameSite flag or an exposed error page matters for your specific routing — and the auto-fix workflow can open a pull request that adds the missing middleware or bumps a vulnerable package. Teams migrating off heavyweight scanners often compare the experience on the Safeguard vs Checkmarx page.
Spin up a free project at app.safeguard.sh/register and follow the ASP.NET Core setup at docs.safeguard.sh.