Skip to content

Commit

Permalink
Add devops-pat and az website stores to secret rotation (#5591)
Browse files Browse the repository at this point in the history
* Add devops-pat and az website stores

* Update rotation logic to support post-primary stores and annotation

* Add enableWindowsTargeting to secret rotation cli proj

* Fix paths in unit tests

* Update tools/secret-rotation/Azure.Sdk.Tools.SecretRotation.Stores.AzureDevOps/ServiceAccountPersonalAccessTokenStore.cs

Co-authored-by: Ben Broderick Phillips <[email protected]>

---------

Co-authored-by: Ben Broderick Phillips <[email protected]>
  • Loading branch information
hallipr and benbp authored Mar 1, 2023
1 parent 6c95123 commit 9a11a8c
Show file tree
Hide file tree
Showing 18 changed files with 600 additions and 68 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="Azure.Core" Version="1.25.0" />
<PackageReference Include="Azure.Identity" Version="1.8.0" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.4.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using Azure.Core;

namespace Azure.Sdk.Tools.SecretRotation.Azure;

public interface ISecretProvider
{
Task<string> GetSecretValueAsync(TokenCredential credential, Uri secretUri);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
using System.Text.RegularExpressions;
using Azure.Core;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Extensions.Logging;

namespace Azure.Sdk.Tools.SecretRotation.Azure;

public class SecretProvider : ISecretProvider
{
private static readonly Regex secretRegex = new (@"(?<vault>https://.*?\.vault\.azure\.net)/secrets/(?<secret>.*)", RegexOptions.Compiled, TimeSpan.FromSeconds(5));
private readonly ILogger logger;

public SecretProvider(ILogger logger)
{
this.logger = logger;
}

public async Task<string> GetSecretValueAsync(TokenCredential credential, Uri secretUri)
{
Match match = secretRegex.Match(secretUri.AbsoluteUri);

if (!match.Success)
{
throw new ArgumentException("Unable to parse uri as a Key Vault secret uri");
}

string vaultUrl = match.Groups["vault"].Value;
string secretName = match.Groups["secret"].Value;

try
{
var secretClient = new SecretClient(new Uri(vaultUrl), credential);

this.logger.LogDebug("Retrieving value for secret '{SecretUrl}'", secretUri);

KeyVaultSecret secret = await secretClient.GetSecretAsync(secretName);

return secret.Value;
}
catch (Exception exception)
{
this.logger.LogError(exception, "Unable to read secret {SecretName} from vault {VaultUrl}", secretName, vaultUrl);
throw;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
<OutputType>Exe</OutputType>
<TargetFramework>net6.0-windows</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
<EnableWindowsTargeting>true</EnableWindowsTargeting>
<IsPackable>true</IsPackable>
<PackAsTool>true</PackAsTool>
<ToolCommandName>secrets</ToolCommandName>
Expand All @@ -20,6 +21,7 @@
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Configuration\Azure.Sdk.Tools.SecretRotation.Configuration.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Core\Azure.Sdk.Tools.SecretRotation.Core.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Stores.AzureActiveDirectory\Azure.Sdk.Tools.SecretRotation.Stores.AzureActiveDirectory.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Stores.AzureAppService\Azure.Sdk.Tools.SecretRotation.Stores.AzureAppService.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Stores.AzureDevOps\Azure.Sdk.Tools.SecretRotation.Stores.AzureDevOps.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Stores.Generic\Azure.Sdk.Tools.SecretRotation.Stores.Generic.csproj" />
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Stores.KeyVault\Azure.Sdk.Tools.SecretRotation.Stores.KeyVault.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using System.CommandLine;
using System.CommandLine;
using System.CommandLine.Invocation;
using Azure.Identity;
using Azure.Sdk.Tools.SecretRotation.Azure;
using Azure.Sdk.Tools.SecretRotation.Configuration;
using Azure.Sdk.Tools.SecretRotation.Core;
using Azure.Sdk.Tools.SecretRotation.Stores.AzureActiveDirectory;
using Azure.Sdk.Tools.SecretRotation.Stores.AzureAppService;
using Azure.Sdk.Tools.SecretRotation.Stores.AzureDevOps;
using Azure.Sdk.Tools.SecretRotation.Stores.Generic;
using Azure.Sdk.Tools.SecretRotation.Stores.KeyVault;
Expand Down Expand Up @@ -68,8 +70,11 @@ private static IDictionary<string, Func<StoreConfiguration, SecretStore>> GetDef
[KeyVaultSecretStore.MappingKey] = KeyVaultSecretStore.GetSecretStoreFactory(tokenCredential, logger),
[KeyVaultCertificateStore.MappingKey] = KeyVaultCertificateStore.GetSecretStoreFactory(tokenCredential, logger),
[ManualActionStore.MappingKey] = ManualActionStore.GetSecretStoreFactory(logger, new ConsoleValueProvider()),
[ServiceAccountPersonalAccessTokenStore.MappingKey] = ServiceAccountPersonalAccessTokenStore.GetSecretStoreFactory(tokenCredential, new SecretProvider(logger), logger),
[ServiceConnectionParameterStore.MappingKey] = ServiceConnectionParameterStore.GetSecretStoreFactory(tokenCredential, logger),
[AadApplicationSecretStore.MappingKey] = AadApplicationSecretStore.GetSecretStoreFactory(tokenCredential, logger)
[AadApplicationSecretStore.MappingKey] = AadApplicationSecretStore.GetSecretStoreFactory(tokenCredential, logger),
[AzureWebsiteStore.MappingKey] = AzureWebsiteStore.GetSecretStoreFactory(tokenCredential, logger),
[AadApplicationSecretStore.MappingKey] = AadApplicationSecretStore.GetSecretStoreFactory(tokenCredential, logger),
};
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using Azure.Sdk.Tools.SecretRotation.Core;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -219,21 +219,18 @@ private IList<SecretStore> GetSecondaryStores(PlanConfiguration planConfiguratio
AddCapabilityError(validationErrors, store, nameof(store.CanRead), "Primary");
}

if (storeConfiguration.IsOrigin)
bool mustAnnotateCompletion = storeConfiguration.IsOrigin ||
planConfiguration.StoreConfigurations.Any(x => x.UpdateAfterPrimary);

if (mustAnnotateCompletion && !store.CanAnnotate)
{
// An origin primary must support post-rotation annotation
if (!store.CanAnnotate)
{
AddCapabilityError(validationErrors, store, nameof(store.CanAnnotate), "Primary + Origin");
}
AddCapabilityError(validationErrors, store, nameof(store.CanAnnotate), "Primary");
}
else

if (!storeConfiguration.IsOrigin && !store.CanWrite)
{
// A non origin primary has to support Write because it doesn't originate values
if (!store.CanWrite)
{
AddCapabilityError(validationErrors, store, nameof(store.CanWrite), "Primary");
}
AddCapabilityError(validationErrors, store, nameof(store.CanWrite), "Primary");
}

return store;
Expand Down Expand Up @@ -271,6 +268,9 @@ private SecretStore ResolveStore(string configurationKey, StoreConfiguration sto
SecretStore store = factory.Invoke(storeConfiguration);

store.Name = storeName;
store.UpdateAfterPrimary = storeConfiguration.UpdateAfterPrimary;
store.IsOrigin = storeConfiguration.IsOrigin;
store.IsPrimary = storeConfiguration.IsPrimary;

return store;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ public class StoreConfiguration
[JsonPropertyName("name")]
public string? Name { get; set; }

[JsonPropertyName("updateAfterPrimary")]
public bool UpdateAfterPrimary { get; set; }

public string ResolveStoreName(string configurationKey)
{
return Name ?? $"{configurationKey} ({Type})";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using Azure.Sdk.Tools.SecretRotation.Core;
using Microsoft.Extensions.Logging;

Expand Down Expand Up @@ -134,51 +134,67 @@ private async Task RotateAsync(string operationId, SecretState currentState, boo
DateTimeOffset invocationTime = this.timeProvider.GetCurrentDateTimeOffset();

SecretValue newValue = await OriginateNewValueAsync(operationId, invocationTime, currentState, whatIf);
// TODO: some providers will issue secrets for longer than we requested. Should we propagate the real expiration date, or the desired expiration date?

await WriteValueToPrePrimaryStoresAsync(newValue, currentState, whatIf);

await WriteValueToSecondaryStoresAsync(newValue, currentState, whatIf);
if (OriginStore != PrimaryStore)
{
await WriteValueToPrimaryAsync(newValue, currentState, invocationTime, whatIf);
}

await WriteValueToPrimaryAndOriginAsync(newValue, invocationTime, currentState, whatIf);
await WriteValueToPostPrimaryStoresAsync(newValue, currentState, whatIf);

await MarkRotationCompleteAsync(newValue, currentState, invocationTime, whatIf);
}

private async Task WriteValueToPrimaryAndOriginAsync(SecretValue newValue,
DateTimeOffset invocationTime,
SecretState currentState,
private async Task WriteValueToPrimaryAsync(SecretValue newValue, SecretState currentState, DateTimeOffset invocationTime,
bool whatIf)
{
DateTimeOffset? revokeAfterDate = RevokeAfterPeriod.HasValue
? invocationTime.Add(RevokeAfterPeriod.Value)
: null;

// Complete rotation by either annotating the origin or writing to primary
if (OriginStore != PrimaryStore)
if (!PrimaryStore.CanWrite)
{
if (!PrimaryStore.CanWrite)
{
// Primary only has to support write when it's not also origin.
throw new RotationException(
$"Rotation plan '{Name}' uses separate Primary and Origin stores, but its primary store type '{OriginStore.GetType()}' does not support CanWrite");
}

// New value along with the datetime when old values should be revoked
await PrimaryStore.WriteSecretAsync(newValue, currentState, revokeAfterDate, whatIf);
// Primary only has to support write when it's not also origin.
throw new RotationException(
$"Rotation plan '{Name}' uses separate Primary and Origin stores, but its primary store type '{OriginStore.GetType()}' does not support CanWrite");
}
else
{
if (!OriginStore.CanAnnotate)
{
throw new RotationException(
$"Rotation plan '{Name}' uses a combined Origin and Primary store, but the store type '{OriginStore.GetType()}' does not support CanAnnotate");
}

await OriginStore.MarkRotationCompleteAsync(newValue, revokeAfterDate, whatIf);
// New value along with the datetime when old values should be revoked
await PrimaryStore.WriteSecretAsync(newValue, currentState, revokeAfterDate, whatIf);
}

private async Task MarkRotationCompleteAsync(SecretValue newValue, SecretState currentState, DateTimeOffset invocationTime,
bool whatIf)
{
DateTimeOffset? revokeAfterDate = RevokeAfterPeriod.HasValue
? invocationTime.Add(RevokeAfterPeriod.Value)
: null;

if (!PrimaryStore.CanAnnotate)
{
throw new RotationException(
$"Rotation plan '{Name}' uses the store type '{PrimaryStore.GetType()}' which does not support CanAnnotate");
}

await PrimaryStore.MarkRotationCompleteAsync(newValue, revokeAfterDate, whatIf);
}

private async Task WriteValueToSecondaryStoresAsync(SecretValue newValue, SecretState currentState, bool whatIf)

private async Task WriteValueToPrePrimaryStoresAsync(SecretValue newValue, SecretState currentState, bool whatIf)
{
// TODO: some providers will issue secrets for longer than we requested. Should we propagate the real expiration date, or the desired expiration date?
foreach (SecretStore secondaryStore in SecondaryStores.Where(store => !store.UpdateAfterPrimary))
{
// secondaries don't store revocation dates.
await secondaryStore.WriteSecretAsync(newValue, currentState, null, whatIf);
}
}

foreach (SecretStore secondaryStore in SecondaryStores)
private async Task WriteValueToPostPrimaryStoresAsync(SecretValue newValue, SecretState currentState, bool whatIf)
{
foreach (SecretStore secondaryStore in SecondaryStores.Where(store => store.UpdateAfterPrimary))
{
// secondaries don't store revocation dates.
await secondaryStore.WriteSecretAsync(newValue, currentState, null, whatIf);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Azure.Sdk.Tools.SecretRotation.Core;
namespace Azure.Sdk.Tools.SecretRotation.Core;

public abstract class SecretStore
{
Expand All @@ -12,7 +12,15 @@ public abstract class SecretStore

public virtual bool CanRevoke => false;

public string? Name { get; set; }
public virtual string? Name { get; set; }

public virtual bool UpdateAfterPrimary { get; set; }

public virtual bool IsPrimary { get; set; }

public virtual bool IsOrigin { get; set; }



// Read

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Azure.Sdk.Tools.SecretRotation.Core;
namespace Azure.Sdk.Tools.SecretRotation.Core;

public class SecretValue
{
Expand All @@ -10,13 +10,13 @@ public class SecretValue
public string Value { get; set; } = string.Empty;

/// <summary>
/// A state object created by origin stores that can be used in post-rotation annotation.
/// A state object created by primary stores that can be used in post-rotation annotation.
/// </summary>
/// <remarks>
/// This is a round-trip object that will be returned to the origin IStateStore once all other stores are updated.
/// For example, a Key Vault origin may store the original KeyVaultCertificate reference in OriginState.
/// This is a round-trip object that will be returned to the primary IStateStore once all other stores are updated.
/// For example, a Key Vault primary may store the original KeyVaultCertificate reference in PrimaryState.
/// </remarks>
public object? OriginState { get; set; }
public object? PrimaryState { get; set; }

// During propagation, origin and secondary stores can add tags to be written to the primary store during the completion/annotation phase.
// These tags are used during revocation to ensure the appropriate origin or downstream resource is revoked.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">

<ItemGroup>
<PackageReference Include="Azure.ResourceManager.AppService" Version="1.0.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Azure.Sdk.Tools.SecretRotation.Configuration\Azure.Sdk.Tools.SecretRotation.Configuration.csproj" />
</ItemGroup>

</Project>
Loading

0 comments on commit 9a11a8c

Please sign in to comment.