diff --git a/aspnetcore/host-and-deploy/web-farm.md b/aspnetcore/host-and-deploy/web-farm.md index e1a2676f0b8c..8fb1b445cafa 100644 --- a/aspnetcore/host-and-deploy/web-farm.md +++ b/aspnetcore/host-and-deploy/web-farm.md @@ -5,7 +5,7 @@ description: Learn how to host multiple instances of an ASP.NET Core app with sh monikerRange: '>= aspnetcore-2.1' ms.author: tdykstra ms.custom: mvc -ms.date: 01/13/2020 +ms.date: 12/3/2024 uid: host-and-deploy/web-farm --- # Host ASP.NET Core in a web farm @@ -38,11 +38,17 @@ When an app is scaled to multiple instances, there might be app state that requi ## Required configuration -Data Protection and Caching require configuration for apps deployed to a web farm. +Data Protection and Caching may require configuration for apps deployed to a web farm. -### Data Protection +### Data Protection in distributed environments -The [ASP.NET Core Data Protection system](xref:security/data-protection/introduction) is used by apps to protect data. Data Protection relies upon a set of cryptographic keys stored in a *key ring*. When the Data Protection system is initialized, it applies [default settings](xref:security/data-protection/configuration/default-settings) that store the key ring locally. Under the default configuration, a unique key ring is stored on each node of the web farm. Consequently, each web farm node can't decrypt data that's encrypted by an app on any other node. The default configuration isn't generally appropriate for hosting apps in a web farm. An alternative to implementing a shared key ring is to always route user requests to the same node. For more information on Data Protection system configuration for web farm deployments, see . +The [ASP.NET Core Data Protection system](xref:security/data-protection/introduction) is used by apps to protect data. Data Protection relies upon a set of cryptographic keys stored in a *key ring*. When the Data Protection system is initialized, it applies [default settings](xref:security/data-protection/configuration/default-settings) that store the key ring locally. The default configuration is appropriate for apps that run in a single instance. + +Apps that are running in distributed environments that don't configure Data Protection automatically need to explicitly configure Data Protection. See for environments that require explicit Data Protection configuration and those that don't. + +Under the default configuration, a unique key ring is stored on each node of the web farm. Consequently, each web farm node can't decrypt data that's encrypted by an app on any other node. The default configuration isn't generally appropriate for hosting apps in a web farm. Sticky sessions using [ARR Affinity](/azure/app-service/manage-automatic-scaling?#how-does-arr-affinity-affect-automatic-scaling) is an alternative to implementing a shared key ring. However, ARR can reduce the scalability of a web farm. + +For more information on Data Protection system configuration for web farm deployments, see . ### Caching diff --git a/aspnetcore/security/data-protection/configuration/default-settings.md b/aspnetcore/security/data-protection/configuration/default-settings.md index 82efecdf5298..ccfd13a1da99 100644 --- a/aspnetcore/security/data-protection/configuration/default-settings.md +++ b/aspnetcore/security/data-protection/configuration/default-settings.md @@ -14,7 +14,7 @@ By [Rick Anderson](https://twitter.com/RickAndMSFT) The app attempts to detect its operational environment and handle key configuration on its own. -1. If the app is hosted in [Azure Apps](https://azure.microsoft.com/services/app-service/), keys are persisted to the *%HOME%\ASP.NET\DataProtection-Keys* folder. This folder is backed by network storage and is synchronized across all machines hosting the app. +1. If the app is hosted in [Azure Apps](/azure/app-service/overview), keys are persisted to the *%HOME%\ASP.NET\DataProtection-Keys* folder. This folder is backed by network storage and is synchronized across all machines hosting the app. * Keys aren't protected at rest. * The *DataProtection-Keys* folder supplies the key ring to all instances of an app in a single deployment slot. * Separate deployment slots, such as Staging and Production, don't share a key ring. When you swap between deployment slots, for example swapping Staging to Production or using A/B testing, any app using Data Protection won't be able to decrypt stored data using the key ring inside the previous slot. This leads to users being logged out of an app that uses the standard ASP.NET Core cookie authentication, as it uses Data Protection to protect its cookies. If you desire slot-independent key rings, use an external key ring provider, such as Azure Blob Storage, Azure Key Vault, a SQL store, or Redis cache. diff --git a/aspnetcore/security/data-protection/configuration/scaling.md b/aspnetcore/security/data-protection/configuration/scaling.md new file mode 100644 index 000000000000..1852f7bb5e46 --- /dev/null +++ b/aspnetcore/security/data-protection/configuration/scaling.md @@ -0,0 +1,55 @@ +--- +title: Configure ASP.NET Core Data Protection in distributed or load-balanced environments +author: acasey +description: Learn how to configure Data Protection in ASP.NET Core for multi-instance apps. +ms.author: acasey +ms.date: 7/18/2024 +content_well_notification: AI-contribution +uid: security/data-protection/configuration/scaling +--- + +# Configure ASP.NET Core Data Protection in distributed or load-balanced environments + +:::moniker range=">= aspnetcore-8.0" + +ASP.NET Core [Data Protection](xref:security/data-protection/introduction) is a library that provides a cryptographic API to protect data. Data Protection protects anti-forgery tokens, authentication cookies, and other sensitive data. However, in some distributed environments that don't put data protection keys in shared storage, when an app scales horizontally by adding more instances: + +* It's necessary to explicitly configure Data Protection to establish a shared storage location for Data Protection keys. +* There’s ***NO*** guarantee that the HTTP POST request, used to submit a form, will be routed to the same instance that served the initial page via an HTTP GET request. If the requests are handled by different instances, the anti-forgery tokens aren’t synchronized, and an exception occurs. Sticky sessions via [ARR Affinity](/azure/app-service/manage-automatic-scaling?#how-does-arr-affinity-affect-automatic-scaling) routes user requests to the same node. However, ARR can reduce the scalability of a web farm. + +The following distributed environments provide automatic key storage in a shared location: + +* [Azure apps](/aspnet/core/security/data-protection/configuration/default-settings). For more information see . +* Newly created Azure Container Apps built using ASP.NET Core. For more information see [Autoscaling considerations +](/azure/container-apps/dotnet-overview#autoscaling-considerations). + +The following scenarios do ***NOT*** provide automatic key storage in a shared location: + +* Separate [deployment slots](/azure/app-service/deploy-staging-slots), such as Staging and Production. +* Azure Container Apps built using ASP.NET Core Kestrel 7.0 or earlier. For more information see [Autoscaling considerations +](/azure/container-apps/dotnet-overview#autoscaling-considerations). +* Distributed apps that don't have a shared storage location or synchronization mechanism for Data Protection keys. + +## Managing Data Protection keys outside the app + +An app with multiple instances might encounter a [System.Security.Cryptography.CryptographicException](/dotnet/api/system.security.cryptography.cryptographicexception) with the message `The key {A6EF5BC2-FDCC-4C0C-A3A5-CDA9A1733D70}` `was not found in the key ring.` This error occurs when instances become out of sync, causing data protected on one instance, such as an anti-forgery token, to fail when unprotected on another instance. This can happen, for example, if a form is served by one instance but posted to another that has not yet updated its key ring. When this issue arises, users may need to resubmit a form or re-authenticate if the issue involves an authentication token. + +One common reason app instances end up with different sets of keys is that, in the absence of a usable key (e.g. due to expiration, lack of access to the backing repository, etc), an instance will generate a new key of its own. Until that key has propagated to all other instances (which can take up to two days), there's a risk that data protected with that new key will sent to an instance that doesn't know how to unprotect it. + +Generally, app instances don't know about each other, so coordinating the generation and distribution of new keys (e.g. when they are periodically rotating) requires explicit configuration. One way to avoid having instances generate and use keys that are unknown to other instances is to prevent them from generating keys at all. The details of how to accomplish this vary slightly from app to app, but the general approach is straightforward. + +First, app instances [disable key generation](xref:security/data-protection/configuration/overview#disableautomatickeygeneration). Next, a new component is introduced that connects to the same key repository and performs a dummy protect operation once a day or so. + +For example, with Azure blob storage as the key repository, the key manager could be a basic console app run on a schedule: + +:::code language="csharp" source="~/security/data-protection/configuration/scaling/samples/AzBlobKey/Program.cs"::: + +The `appsettings.json` file contains the URIs for the key repository and key vault: + +:::code language="json" source="~/security/data-protection/configuration/scaling/samples/AzBlobKey/appsettings.json" highlight="2-5"::: + +Note that app instances throw exceptions if they perform any `Protect` or `Unprotect` operations before the key manager has run for the first time. To prevent exceptions, start the key manager so it before creating app instances. In most scenarios, Azure Key Vault starts the key manager before the app instances. + +:::moniker-end + +[!INCLUDE[](~/security/data-protection/configuration/scaling/includes/scaling7.md)] diff --git a/aspnetcore/security/data-protection/configuration/scaling/includes/scaling7.md b/aspnetcore/security/data-protection/configuration/scaling/includes/scaling7.md new file mode 100644 index 000000000000..03c3a1659262 --- /dev/null +++ b/aspnetcore/security/data-protection/configuration/scaling/includes/scaling7.md @@ -0,0 +1,9 @@ + +:::moniker range="< aspnetcore-8.0" + + + +:::moniker-end diff --git a/aspnetcore/security/data-protection/configuration/scaling/samples/AzBlobKey/AzBlobKey.csproj b/aspnetcore/security/data-protection/configuration/scaling/samples/AzBlobKey/AzBlobKey.csproj new file mode 100644 index 000000000000..0140774e8538 --- /dev/null +++ b/aspnetcore/security/data-protection/configuration/scaling/samples/AzBlobKey/AzBlobKey.csproj @@ -0,0 +1,18 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + diff --git a/aspnetcore/security/data-protection/configuration/scaling/samples/AzBlobKey/Program.cs b/aspnetcore/security/data-protection/configuration/scaling/samples/AzBlobKey/Program.cs new file mode 100644 index 000000000000..ec3438ba30a0 --- /dev/null +++ b/aspnetcore/security/data-protection/configuration/scaling/samples/AzBlobKey/Program.cs @@ -0,0 +1,21 @@ +using Azure.Identity; +using Microsoft.AspNetCore.DataProtection; + +var hostBuilder = new HostApplicationBuilder(); + +// hostBuilder.Configuration.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false); + +var blobStorageUri = hostBuilder.Configuration["AzureURIs:BlobStorage"]!; +var keyVaultURI = hostBuilder.Configuration["AzureURIs:KeyVault"]!; + +// Use the same persistence and protection mechanisms as your app. +hostBuilder.Services + .AddDataProtection() + .PersistKeysToAzureBlobStorage(new Uri(blobStorageUri), new DefaultAzureCredential()) + .ProtectKeysWithAzureKeyVault(new Uri(keyVaultURI), new DefaultAzureCredential()); + +using var host = hostBuilder.Build(); + +// Perform a dummy operation to force key creation or rotation, if needed. +var dataProtector = host.Services.GetDataProtector("Default"); +dataProtector.Protect([]); diff --git a/aspnetcore/security/data-protection/configuration/scaling/samples/AzBlobKey/appsettings.json b/aspnetcore/security/data-protection/configuration/scaling/samples/AzBlobKey/appsettings.json new file mode 100644 index 000000000000..99e89d2cf1e5 --- /dev/null +++ b/aspnetcore/security/data-protection/configuration/scaling/samples/AzBlobKey/appsettings.json @@ -0,0 +1,13 @@ +{ + "AzureURIs": { + "BlobStorage": "https://.blob.core.windows.net//keys.xml", + "KeyVault": "https://.vault.azure.net/keys//" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +}