Skip to content

Commit d8b6a29

Browse files
amcaseygithub-actions
authored and
github-actions
committed
Introduce a read-only mode for data protection keyring consumers
When multiple app instances consume the same keyring, they all try to rotate it, leading to races. This change introduces an IConfiguration property (usually set as an env var) that puts data protection in a read-only mode. The expectation is that writing will be done by a separate (i.e. non-app-instance) component. Part of #52915
1 parent 476e2aa commit d8b6a29

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

src/DataProtection/DataProtection/src/DataProtectionServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ private static void AddDataProtectionServices(IServiceCollection services)
6767

6868
services.TryAddEnumerable(
6969
ServiceDescriptor.Singleton<IConfigureOptions<KeyManagementOptions>, KeyManagementOptionsSetup>());
70+
services.TryAddEnumerable(
71+
ServiceDescriptor.Singleton<IPostConfigureOptions<KeyManagementOptions>, KeyManagementOptionsPostSetup>());
7072
services.TryAddEnumerable(
7173
ServiceDescriptor.Transient<IConfigureOptions<DataProtectionOptions>, DataProtectionOptionsSetup>());
7274

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.IO;
6+
using System.Xml.Linq;
7+
using Microsoft.AspNetCore.DataProtection.KeyManagement;
8+
using Microsoft.AspNetCore.DataProtection.Repositories;
9+
using Microsoft.AspNetCore.DataProtection.XmlEncryption;
10+
using Microsoft.Extensions.Configuration;
11+
using Microsoft.Extensions.Logging;
12+
using Microsoft.Extensions.Options;
13+
14+
namespace Microsoft.AspNetCore.DataProtection.Internal;
15+
16+
/// <summary>
17+
/// Performs additional <see cref="KeyManagementOptions" /> configuration, after the user's configuration has been applied.
18+
/// </summary>
19+
/// <remarks>
20+
/// In practice, this type is used to set key management to readonly mode if an environment variable is set and the user
21+
/// has not explicitly configured data protection.
22+
/// </remarks>
23+
internal sealed class KeyManagementOptionsPostSetup : IPostConfigureOptions<KeyManagementOptions>
24+
{
25+
/// <remarks>
26+
/// Settable as `ReadOnlyDataProtectionKeyDirectory`, `DOTNET_ReadOnlyDataProtectionKeyDirectory`,
27+
/// or `ASPNETCORE_ReadOnlyDataProtectionKeyDirectory`, in descending order of precedence.
28+
/// </remarks>
29+
internal const string ReadOnlyDataProtectionKeyDirectoryKey = "ReadOnlyDataProtectionKeyDirectory";
30+
31+
private readonly string? _keyDirectoryPath;
32+
private readonly ILoggerFactory? _loggerFactory; // Null iff _keyDirectoryPath is null
33+
34+
public KeyManagementOptionsPostSetup()
35+
{
36+
// If there's no IConfiguration, there's no _keyDirectoryPath and this type will do nothing.
37+
// This is mostly a convenience for tests since ASP.NET Core apps will have an IConfiguration.
38+
}
39+
40+
public KeyManagementOptionsPostSetup(IConfiguration configuration, ILoggerFactory loggerFactory)
41+
{
42+
_keyDirectoryPath = configuration[ReadOnlyDataProtectionKeyDirectoryKey];
43+
_loggerFactory = loggerFactory;
44+
}
45+
46+
void IPostConfigureOptions<KeyManagementOptions>.PostConfigure(string? name, KeyManagementOptions options)
47+
{
48+
if (_keyDirectoryPath is null || name != Options.DefaultName)
49+
{
50+
return;
51+
}
52+
53+
// If Data Protection has not been configured, then set it up according to the environment variable
54+
if (options is { XmlRepository: null, XmlEncryptor: null })
55+
{
56+
var keyDirectory = new DirectoryInfo(_keyDirectoryPath);
57+
58+
options.AutoGenerateKeys = false;
59+
options.XmlEncryptor = InvalidEncryptor.Instance;
60+
options.XmlRepository = new ReadOnlyFileSystemXmlRepository(keyDirectory, _loggerFactory!);
61+
}
62+
}
63+
64+
private sealed class InvalidEncryptor : IXmlEncryptor
65+
{
66+
public static readonly IXmlEncryptor Instance = new InvalidEncryptor();
67+
68+
private InvalidEncryptor()
69+
{
70+
}
71+
72+
EncryptedXmlInfo IXmlEncryptor.Encrypt(XElement plaintextElement)
73+
{
74+
throw new InvalidOperationException("Keys access is set up as read-only, so nothing should be encrypting");
75+
}
76+
}
77+
78+
private sealed class ReadOnlyFileSystemXmlRepository : FileSystemXmlRepository
79+
{
80+
public ReadOnlyFileSystemXmlRepository(DirectoryInfo directory, ILoggerFactory loggerFactory)
81+
: base(directory, loggerFactory)
82+
{
83+
}
84+
85+
public override void StoreElement(XElement element, string friendlyName)
86+
{
87+
throw new InvalidOperationException("Keys access is set up as read-only, so nothing should be storing keys");
88+
}
89+
}
90+
}

0 commit comments

Comments
 (0)