If users lose sessions, auth cookies fail validation, or antiforgery tokens start throwing after an IIS recycle, the usual culprit is ephemeral Data Protection keys. ASP.NET Core encrypts cookies, tokens, and related payloads with a key ring. When that ring lives only in process memory—or under a disposable publish folder—every worker restart mints new keys and invalidates what browsers still hold.

On Windows hosting this shows up constantly: default app pool idle timeouts, overlapping recycle, Web Deploy publish that replaces the app directory, or a second worker process under a web garden. Fixing it is a short configuration change plus correct NTFS permissions for the app pool identity—not a rewrite of your auth stack.

#What breaks when keys are not durable

Data Protection is used by cookie authentication, antiforgery, session (when configured to protect payloads), and several middleware components. Keys are versioned and can roll forward, but decryption still requires the previous key material. If the ring disappears, old cookies look forged. Symptoms look like random logouts, 400s on form posts, or “The antiforgery token could not be decrypted” right after a deploy or recycle—not a steady failure that is easy to catch in a smoke test against a warm process.

Shared and reseller Windows plans make this sharper: you often cannot control recycle timing the way you would on a dedicated box, and publish tools may wipe wwwroot-adjacent folders you thought were permanent. Treat the key ring like connection strings and TLS material—store it outside the content root you overwrite.

#Persist the key ring on the file system

For a single IIS site or a small set of servers that can share a path, file-system persistence is the straightforward fix. Create a directory outside the site root (for example under a host-provided data path or a sibling folder that Web Deploy does not delete), grant the app pool identity Modify, and point Data Protection at it at startup.

csharp
using Microsoft.AspNetCore.DataProtection;

var builder = WebApplication.CreateBuilder(args);

var keysPath = builder.Configuration["DataProtection:KeysPath"]
    ?? @"D:\app-data\dp-keys\my-site";

Directory.CreateDirectory(keysPath);

builder.Services.AddDataProtection()
    .PersistKeysToFileSystem(new DirectoryInfo(keysPath))
    .SetApplicationName("MyAspNetApp"); // must match across nodes

// Cookie auth, antiforgery, etc. register as usual
builder.Services.AddAuthentication(/* ... */);
builder.Services.AddAntiforgery();

var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.Run();

SetApplicationName matters when several apps share a machine-level key store or when you promote the same binaries across slots. Without a stable application name, two sites can read each other’s key files and still fail to unwrap payloads. Keep the name identical everywhere that should accept the same cookies.

#App pool identity and folder ACLs

IIS runs your app as the app pool identity (ApplicationPoolIdentity, a custom domain account, or a least-privilege local user). That identity needs create/read/write on the keys directory—not on the whole drive. Avoid granting Users or IIS_IUSRS broad write access. On Windows Server 2025 you can set this once with icacls or your control panel’s file manager, then leave the folder out of publish profiles.

powershell
# Run elevated on the server; adjust pool name and path
$keys = 'D:\app-data\dp-keys\my-site'
$pool = 'IIS AppPool\MyAspNetAppPool'

New-Item -ItemType Directory -Force -Path $keys | Out-Null
icacls $keys /grant "${pool}:(OI)(CI)M" /T

If you deploy with Web Deploy, exclude the keys path from sync rules so a full site publish cannot delete key-*.xml files. Prefer an absolute path from configuration or an environment variable set on the app pool rather than a relative path under ContentRoot—relative paths move when the physical path changes during folder-swap style deploys.

#SQL storage and multi-worker notes

When two IIS nodes must accept each other’s cookies, or your host gives you SQL Server but not a shared disk, persist keys to a database table instead. The Microsoft.AspNetCore.DataProtection.EntityFrameworkCore package (or XML repository patterns against SQL) keeps a single ring both workers read. Use the same application name, protect the connection string like any other secret, and restrict table permissions to the app identity.

  • Web garden (multiple worker processes): file or SQL persistence is required; in-memory keys diverge per worker.
  • Overlapping recycle: old and new workers must decrypt the same cookies during the handoff window.
  • Load-balanced Windows VMs: shared path or shared SQL repository; sticky sessions alone do not fix key mismatch.
  • Key lifetime defaults are fine for most sites; only shorten rolling periods if you have a documented rotation policy and dual-node coverage.

Do not commit key XML to source control. Treat the directory like a secrets volume. On shared hosting, confirm the path survives account moves and that only your app pool can read it. If you ever rotate by deleting keys deliberately, expect a one-time logout wave—communicate that the same way you would a forced password reset.

#Quick verification after deploy

After the first start you should see key-*.xml files appear in the configured directory. Sign in, note the auth cookie, recycle the app pool from IIS Manager or by touching web.config, and confirm the session still validates. Post a form that requires antiforgery before and after recycle. If either fails, check ACL failures in the event log, a wrong application name, or a publish step that wiped the folder.

Practical takeaway: on any IIS-hosted ASP.NET Core app that uses cookies or antiforgery, call PersistKeysToFileSystem (or a shared SQL repository), set a stable application name, grant the app pool Modify on a path outside the publish root, and exclude that path from Web Deploy. Do this once per app and recycles stop looking like auth outages.