ASP.NET Core DataProtection for Service Fabric with Kestrel & WebListener
In ASP.NET 1.x - 4.x, if you deployed your application to a Web farm, you had to ensure that the configuration files on each server shared the same value for validationKey and decryptionKey, which were used for hashing and decryption respectively. In ASP.NET Core this is accomplished via the data protection stack which was designed to address many of the shortcomings of the old cryptographic stack. The new API provides a simple, easy to use mechanism for data encryption, decryption, key management and rotation. The data protection system ships with several in-box key storage providers; File system, Registry, AzureStorage and Redis.
Since we are working with low-latency microservices at massive scale via Azure Service Fabric, in this blog post we’ll describe an approach to create a custom ASP.NET Core data protection key repository using Service Fabric’s built in Reliable Collections, which are Replicated, Persisted, Asynchronous and Transactional.
Previous readers will note we’ve covered how to integrate ASP.Net Core and Kestrel into Service Fabric, moreover how to create Service Fabric microservices in the new .Net Core xproj structure (soon to be superseded with VS 2017), therefore we’ll jump straight into building the AspNetCore.DataProtection.ServiceFabric microservice (warning this post is code heavy). To test everything out we’ll create a sample ASP.Net Core Web API microservice and finally for completeness integrate WebListener, a Windows only web server.
To begin, we create a new stateful Service Fabric microservice called DataProtectionService:
using Microsoft.ServiceFabric.Data; using Microsoft.ServiceFabric.Data.Collections; using Microsoft.ServiceFabric.Services.Communication.Runtime; using Microsoft.ServiceFabric.Services.Remoting.Runtime; using Microsoft.ServiceFabric.Services.Runtime; using System; using System.Collections.Generic; using System.Fabric; using System.Threading; using System.Threading.Tasks; using System.Xml.Linq;
publicasync Task<List<XElement>> GetAllDataProtectionElements() { var elements = new List<XElement>();
var dictionary = awaitthis.StateManager.GetOrAddAsync<IReliableDictionary<Guid, XElement>>("AspNetCore.DataProtection"); using (var tx = this.StateManager.CreateTransaction()) { var enumerable = await dictionary.CreateEnumerableAsync(tx); var enumerator = enumerable.GetAsyncEnumerator(); var token = new CancellationToken();
while (await enumerator.MoveNextAsync(token)) { elements.Add(enumerator.Current.Value); } }
return elements; }
publicasync Task<XElement> AddDataProtectionElement(XElement element) { Guid id = Guid.Parse(element.Attribute("id").Value);
var dictionary = awaitthis.StateManager.GetOrAddAsync<IReliableDictionary<Guid, XElement>>("AspNetCore.DataProtection"); using (var tx = this.StateManager.CreateTransaction()) { var result = await dictionary.GetOrAddAsync(tx, id, element); await tx.CommitAsync();
return result; } } } }
Congratulations you’ve just implemented a custom key storage provider using a Service Fabric Reliable Dictionary! To integrate with ASP.Net Core Data Protection API we need to also create a ServiceFabricXmlRepository class which implements IXmlRepository. In a new stateless microservice called ServiceFabric.DataProtection.Web create ServiceFabricXmlRepository:
using AspNetCore.DataProtection.ServiceFabric; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.ServiceFabric.Services.Client; using Microsoft.ServiceFabric.Services.Remoting.Client; using System; using System.Collections.Generic; using System.Xml.Linq;
namespaceServiceFabric.DataProtection.Web { publicclassServiceFabricXmlRepository : IXmlRepository { public IReadOnlyCollection<XElement> GetAllElements() { var proxy = ServiceProxy.Create<IDataProtectionService>(new Uri("fabric:/ServiceFabric.DataProtection/DataProtectionService"), new ServicePartitionKey()); return proxy.GetAllDataProtectionElements().Result.AsReadOnly(); }
using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.DataProtection.Repositories; using Microsoft.Extensions.DependencyInjection; using System;
if (descriptor == null) { thrownew ArgumentNullException(nameof(descriptor)); }
for (int i = builder.Services.Count - 1; i >= 0; i--) { if (builder.Services[i]?.ServiceType == descriptor.ServiceType) { builder.Services.RemoveAt(i); } }
builder.Services.Add(descriptor);
return builder; } } }
Building upon previous articles detailing how to integrate Kestrel and Service Fabric, we extend WebHostBuilderHelper to also support the WebListener webserver:
using Microsoft.ServiceFabric.Services.Communication.AspNetCore; using Microsoft.ServiceFabric.Services.Communication.Runtime; using Microsoft.ServiceFabric.Services.Runtime; using System.Collections.Generic; using System.Fabric;
[Option(Default = "localhost", HelpText = "IP Address or Uri - Example [localhost] or [127.0.0.1]")] publicstring IpAddressOrFQDN { get; set; }
[Option(Default = "5000", HelpText = "Port - Example [80] or [5000]")] publicstring Port { get; set; } } }
And finally PersistKeysToServiceFabric needs to be added to Startup.cs as this will instruct the ASP.NET Core data protection stack to use our custom AspNetCore.DataProtection.ServiceFabric key repository:
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Swashbuckle.AspNetCore.Swagger;
// This method gets called by the runtime. Use this method to add services to the container. publicvoidConfigureServices(IServiceCollection services) { // Add framework services. services.AddMvc();
// Add Service Fabric DataProtection services.AddDataProtection() .SetApplicationName("ServiceFabric-DataProtection-Web") .PersistKeysToServiceFabric();
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Title = "AspNetCore.DataProtection.ServiceFabric API", Version = "v1" }); }); }
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. publicvoidConfigure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { app.UseMvc(); app.UseSwaggerUi(c => { c.SwaggerEndpoint("/swagger/v1/swagger.json", "AspNetCore.DataProtection.ServiceFabric API v1"); }); app.UseSwagger(); } } }
All that is now left to do is within your .Net Core Web Application PackageRoot, edit the ServiceManifest.xml CodePackage so that we tell Web.exe to “host” within Service Fabric using WebListener:
At an administrative command prompt you’ll need to issue the below command to create the correct Url ACL for port 80 (please refer to the WebListener references section below for detailed instructions):
netsh http add urlacl url=http://+:80/ user=Users
Upon successful deployment to a multi-node cluster, use Swagger and the Protect/Unprotect APIs to test that all nodes have access to the same data protection keys:
Note, as we've created a custom ASP.NET Core data protection key repository, the data protection system will deregister the default key encryption at rest mechanism that the heuristic provided, so keys will no longer be encrypted at rest. It is strongly recommended that you additionally specify an explicit key encryption mechanism for production applications.