On Windows shared and VPS hosting, the incidents that actually land are rarely exotic zero-days. They are missing security headers, session cookies without Secure/HttpOnly/SameSite, SQL credentials sitting in plain Web.config, and app pools still running months behind the current .NET LTS patch level. If you ship ASP.NET on IIS, those four controls are the floor—not a nice-to-have checklist.
This post is a production hardening pass you can apply on IIS 10.0 under Windows Server 2025 with ASP.NET Core / .NET 10 (or Framework apps still on the same box). No invented advisories—just settings that close the common paths attackers and scanners still hit every week.
#Ship security headers from IIS or middleware
Browsers only protect users if your responses tell them how. Prefer setting baseline headers once at the site or application level so every endpoint inherits them—static files included. For classic ASP.NET and many mixed sites, web.config customHeaders is still the reliable path on shared IIS.
<system.webServer>
<httpProtocol>
<customHeaders>
<remove name="X-Powered-By" />
<add name="X-Content-Type-Options" value="nosniff" />
<add name="X-Frame-Options" value="DENY" />
<add name="Referrer-Policy" value="strict-origin-when-cross-origin" />
<add name="Permissions-Policy" value="geolocation=(), microphone=(), camera=()" />
<add name="Strict-Transport-Security" value="max-age=31536000; includeSubDomains" />
</customHeaders>
</httpProtocol>
</system.webServer>
Notes that matter in hosting: only enable HSTS after the site is fully on HTTPS with no mixed-content holdouts. X-Frame-Options DENY is fine for apps that never frame themselves; use SAMEORIGIN or a tight CSP frame-ancestors if you embed admin UI. For ASP.NET Core 10, the same headers belong in middleware (UseSecurityHeaders or explicit middleware) so Kestrel- and IIS-hosted pipelines stay aligned when you move between hosts.
Add a Content-Security-Policy when you can. Start report-only in staging, lock script-src and style-src to your CDNs and self, and avoid unsafe-inline once bundles are hashed. A partial CSP still beats none for XSS blast radius.
#Lock down auth and session cookies
Cookie flags are still one of the highest-ROI fixes on login-heavy ASP.NET apps. Every auth cookie must be Secure, HttpOnly, and SameSite (Lax is the usual default; Strict when your UX allows). Name cookies generically—avoid advertising stack or role in the cookie name.
// Program.cs — ASP.NET Core 10 cookie auth
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.Cookie.Name = ".AspNet.SharedAuth";
options.Cookie.HttpOnly = true;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.SameSite = SameSiteMode.Lax;
options.SlidingExpiration = true;
options.ExpireTimeSpan = TimeSpan.FromHours(8);
options.Cookie.Path = "/";
});
For ASP.NET Framework forms auth or OWIN, set requireSSL="true" and httpOnlyCookies="true" under <system.web><httpCookies> and the forms element. On IIS, confirm the site binding is HTTPS-only (or HTTP→HTTPS redirect at the site level) so Secure cookies are not silently dropped for half your users.
Data-protection keys matter on web farms and when app pools recycle onto new machines. If tickets suddenly invalidate after a recycle or scale-out, persist keys to a shared location or a protected repository—do not leave ephemeral keys as the only store for production auth cookies.
#Get secrets out of Web.config and publish packages
Connection strings and API keys in source-controlled Web.config remain a top finding on Windows hosts. Prefer environment variables or IIS app settings injected at deploy time, and keep production SQL credentials out of the repo entirely. Rotate any secret that has ever lived in git history.
// Prefer env / host injection over checked-in secrets
var cs = builder.Configuration.GetConnectionString("AppDb")
?? throw new InvalidOperationException("AppDb connection string missing");
// Example host env name: ConnectionStrings__AppDb
builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlServer(cs));
SQL side: use a dedicated login with least privilege—db_datareader/db_datawriter or explicit grants, not sysadmin. Disable or rename leftover sa-style accounts on instances you control, require encrypted connections where the host supports it, and never reuse the same SQL password across staging and production. On shared SQL, treat the connection string like a password: unique per app, rotatable, and never embedded in client-side code or Web Deploy parameter files committed to the repo.
- Strip secrets from Web Deploy packages and parameter files before archive or CI artifacts leave the build agent.
- Restrict app pool identity: dedicated pool per site, ApplicationPoolIdentity or a locked-down custom identity—no interactive admin accounts.
- Turn off directory browsing, remove unused handlers/modules, and delete sample apps and leftover publish folders under the site root.
#Patch discipline beats one-off hardening
Headers and cookies do not help if the runtime is stale. Track the current .NET 10 LTS servicing train and apply host/runtime updates on a calendar, not only after an incident. On IIS, recycle app pools cleanly after runtime installs, confirm the site’s bitness and module versions, and verify health endpoints before you walk away. Keep Windows Server and IIS cumulative updates in the same rhythm—shared hosts and VPS images drift faster than teams expect.
After each change, smoke-test login, antiforgery, file upload paths, and any webhook or payment callback. A five-minute checklist after patching prevents the classic “security update broke SameSite/HTTPS redirect” outage.
#Practical takeaway
Before your next production push, do four things: add the baseline security headers (and HSTS only when HTTPS is solid), force Secure + HttpOnly + SameSite on every auth cookie, move SQL and API secrets out of Web.config into host-injected settings with a least-privilege database login, and confirm the app pool is on a current .NET 10 patch with a dedicated identity. Those steps are boring on purpose—they close the holes scanners and opportunistic attackers still use against ASP.NET sites on IIS. Revisit the list after every major runtime upgrade and whenever you add a new login or admin surface.
Comments
No comments yet