public override async Task RunAsync(CancellationToken cancellationToken) { var now = _clock.UtcNow; var manifest = SecretManifest.Read(_manifestFile); using var storage = _storageLocationTypeRegistry.Create(manifest.StorageLocation.Type, manifest.StorageLocation.Parameters); var existingSecrets = (await storage.ListSecretsAsync()).ToDictionary(p => p.Name); foreach (var(name, secret) in manifest.Secrets) { var secretType = _secretTypeRegistry.Create(secret.Type, secret.Parameters); var names = secretType.GetCompositeSecretSuffixes().Select(suffix => name + suffix).ToList(); var existing = new List <SecretProperties>(); foreach (var n in names) { existingSecrets.TryGetValue(n, out var e); existing.Add(e); // here we intentionally ignore the result of TryGetValue because we want to add null to the list to represent "this isn't in the store" } bool regenerate = false; if (existing.Any(e => e == null)) { // secret is missing from storage (either completely missing or partially missing) regenerate = true; } else { // If these fields aren't the same for every part of a composite secrets, assume the soonest value is right var nextRotation = existing.Select(e => e.NextRotationOn).Min(); var expires = existing.Select(e => e.ExpiresOn).Min(); if (nextRotation <= now) { // we have hit the rotation time, rotate regenerate = true; } else if (expires <= now) { // the secret has expired, this shouldn't happen in normal operation but we should rotate regenerate = true; } } if (regenerate) { var primary = existing.FirstOrDefault(p => p != null); var currentTags = primary?.Tags ?? ImmutableDictionary.Create <string, string>(); var context = new RotationContext(currentTags); var newValues = await secretType.RotateValues(context, cancellationToken); var newTags = context.GetValues(); foreach (var(n, value) in names.Zip(newValues)) { await storage.SetSecretValueAsync(n, new SecretValue(value.Value, newTags, value.NextRotationOn, value.ExpiresOn)); } } } }
public override async Task RunAsync(CancellationToken cancellationToken) { try { _console.WriteLine($"Synchronizing secrets contained in {_manifestFile}"); if (_force || _forcedSecrets.Any()) { bool confirmed = await _console.ConfirmAsync( "--force or --force-secret is set, this will rotate one or more secrets ahead of schedule, possibly causing service disruption. Continue? "); if (!confirmed) { return; } } DateTimeOffset now = _clock.UtcNow; SecretManifest manifest = SecretManifest.Read(_manifestFile); using StorageLocationType.Bound storage = _storageLocationTypeRegistry .Get(manifest.StorageLocation.Type).BindParameters(manifest.StorageLocation.Parameters); using var disposables = new DisposableList(); var references = new Dictionary <string, StorageLocationType.Bound>(); foreach (var(name, storageReference) in manifest.References) { var bound = _storageLocationTypeRegistry.Get(storageReference.Type) .BindParameters(storageReference.Parameters); disposables.Add(bound); references.Add(name, bound); } Dictionary <string, SecretProperties> existingSecrets = (await storage.ListSecretsAsync()).ToDictionary(p => p.Name); List <(string name, SecretManifest.Secret secret, SecretType.Bound bound, HashSet <string> references)> orderedSecretTypes = GetTopologicallyOrderedSecrets(manifest.Secrets); var regeneratedSecrets = new HashSet <string>(); foreach (var(name, secret, secretType, secretReferences) in orderedSecretTypes) { _console.WriteLine($"Synchronizing secret {name}, type {secret.Type}"); List <string> names = secretType.GetCompositeSecretSuffixes().Select(suffix => name + suffix).ToList(); var existing = new List <SecretProperties>(); foreach (string n in names) { existingSecrets.Remove(n, out SecretProperties e); existing.Add(e); // here we intentionally ignore the result of Remove because we want to add null to the list to represent "this isn't in the store" } bool regenerate = false; if (_force) { _console.WriteLine("--force is set, will rotate."); regenerate = true; } else if (_forcedSecrets.Contains(name)) { _console.WriteLine($"--force-secret={name} is set, will rotate."); regenerate = true; } else if (existing.Any(e => e == null)) { _console.WriteLine("Secret not found in storage, will create."); // secret is missing from storage (either completely missing or partially missing) regenerate = true; } else if (regeneratedSecrets.Overlaps(secretReferences)) { _console.WriteLine("Referenced secret was rotated, will rotate."); regenerate = true; } else { // If these fields aren't the same for every part of a composite secrets, assume the soonest value is right DateTimeOffset nextRotation = existing.Select(e => e.NextRotationOn).Min(); DateTimeOffset expires = existing.Select(e => e.ExpiresOn).Min(); if (nextRotation <= now) { _console.WriteLine($"Secret scheduled for rotation on {nextRotation}, will rotate."); // we have hit the rotation time, rotate regenerate = true; // since the rotation runs weekly, we need a 1 week grace period // where verification runs will not fail, but rotation will happen. // otherwise a secret scheduled for rotation on tuesday, will cause // a build failure on wednesday, before it gets rotated normally on the following monday // the verification mode is to catch the "the rotation hasn't happened in months" case if (_verifyOnly && nextRotation > now.AddDays(-7)) { _console.WriteLine("Secret is within verification grace period."); regenerate = false; } } if (expires <= now) { _console.WriteLine($"Secret expired on {expires}, will rotate."); // the secret has expired, this shouldn't happen in normal operation but we should rotate regenerate = true; } } if (!regenerate) { _console.WriteLine("Secret is fine."); } if (regenerate && _verifyOnly) { _console.LogError($"Secret {name} requires rotation."); } else if (regenerate) { _console.WriteLine($"Generating new value(s) for secret {name}..."); SecretProperties primary = existing.FirstOrDefault(p => p != null); IImmutableDictionary <string, string> currentTags = primary?.Tags ?? ImmutableDictionary.Create <string, string>(); var context = new RotationContext(name, currentTags, storage, references); List <SecretData> newValues = await secretType.RotateValues(context, cancellationToken); IImmutableDictionary <string, string> newTags = context.GetValues(); regeneratedSecrets.Add(name); _console.WriteLine("Done."); _console.WriteLine($"Storing new value(s) in storage for secret {name}..."); foreach (var(n, value) in names.Zip(newValues)) { await storage.SetSecretValueAsync(n, new SecretValue(value.Value, newTags, value.NextRotationOn, value.ExpiresOn)); } _console.WriteLine("Done."); } } if (!_verifyOnly) { foreach (var(name, key) in manifest.Keys) { await storage.EnsureKeyAsync(name, key); } foreach (var(name, value) in existingSecrets) { _console.LogWarning($"Extra secret '{name}' consider deleting it."); } } } catch (FailWithExitCodeException) { throw; } catch (HumanInterventionRequiredException hire) { _console.LogError(hire.Message); throw new FailWithExitCodeException(42); } catch (Exception ex) { _console.LogError($"Unhandled Exception: {ex.Message}"); throw new FailWithExitCodeException(-1); } }
public override async Task RunAsync(CancellationToken cancellationToken) { bool haveErrors = false; var manifestFiles = new Dictionary <string, string>(StringComparer.OrdinalIgnoreCase); foreach (string manifestFile in _manifestFiles) { SecretManifest manifest = SecretManifest.Read(manifestFile); StorageLocationType storageType = _storageLocationTypeRegistry.Get(manifest.StorageLocation.Type); if (!(storageType is AzureKeyVault azureKeyVaultStorageType)) { _console.WriteImportant($"Skipping non-azure-key-vault manifest {manifestFile}", ConsoleColor.Yellow); continue; } string vaultUri = azureKeyVaultStorageType.GetAzureKeyVaultUri(ParameterConverter.ConvertParameters <AzureKeyVaultParameters>(manifest.StorageLocation.Parameters)); manifestFiles.Add(vaultUri, manifestFile); } var settingsFiles = new List <(FileInfo environmentFile, FileInfo baseFile)>(); foreach (string jsonFile in Directory.EnumerateFiles(_basePath, "settings.json", SearchOption.AllDirectories)) { var baseFile = new FileInfo(jsonFile); foreach (string envFile in Directory.EnumerateFiles(baseFile.DirectoryName, "settings.*.json")) { settingsFiles.Add((new FileInfo(envFile), baseFile)); } } foreach (var(envFile, baseFile) in settingsFiles) { string specifiedVaultUri = await ReadVaultUriFromSettingsFile(envFile.FullName) ?? await ReadVaultUriFromSettingsFile(baseFile.FullName); if (string.IsNullOrEmpty(specifiedVaultUri)) { _console.LogError($"Settings file pair ({envFile}, {baseFile}) has no vault uri.", envFile.FullName, 0, 0); haveErrors = true; continue; } _console.WriteLine($"Settings file pair ({envFile}, {baseFile}) has vault uri {specifiedVaultUri}"); if (!manifestFiles.TryGetValue(specifiedVaultUri, out string manifestFile)) { _console.LogError($"Vault Uri {specifiedVaultUri} does not have a matching manifest.", envFile.FullName, 0, 0); haveErrors = true; continue; } haveErrors |= !await _settingsFileValidator.ValidateFileAsync(envFile.FullName, baseFile.FullName, manifestFile, cancellationToken); } if (haveErrors) { throw new FailWithExitCodeException(77); } }
public void CanDeserialize() { var subscription = "007ae47e-f491-4706-a4ad-288c235dd30e"; var vaultName = "pizza"; var testManifest = $@" storageLocation: type: key-vault parameters: name: {vaultName} subscription: {subscription} keys: key1: type: one size: 1 key2: type: two size: 2 secrets: secret1: type: three owner: sally description: the first secret parameters: one: 1 two: ni three: san secret2: type: four owner: bob description: the second secret parameters: a: yon b: cinco c: six "; var parsed = SecretManifest.ParseWithoutImports(new StringReader(testManifest)); parsed.Should().BeEquivalentTo(new { StorageLocation = new { Type = "key-vault", Parameters = new Dictionary <string, string> { ["subscription"] = subscription, ["name"] = vaultName, }, }, Keys = new Dictionary <string, object> { ["key1"] = new { Type = "one", Size = 1, }, ["key2"] = new { Type = "two", Size = 2, }, }, Secrets = new Dictionary <string, object> { ["secret1"] = new { Type = "three", Owner = "sally", Description = "the first secret", Parameters = new Dictionary <string, string> { ["one"] = "1", ["two"] = "ni", ["three"] = "san" }, }, ["secret2"] = new { Type = "four", Owner = "bob", Description = "the second secret", Parameters = new Dictionary <string, string> { ["a"] = "yon", ["b"] = "cinco", ["c"] = "six" }, }, }, }); }