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); } }