diff --git a/POC/bwsh.Test/ConfigTests.cs b/POC/bwsh.Test/ConfigTests.cs index 9d143bb..ebfd630 100644 --- a/POC/bwsh.Test/ConfigTests.cs +++ b/POC/bwsh.Test/ConfigTests.cs @@ -1,4 +1,5 @@ -using Bit.SelfHost.Deployments; +using Bit.SelfHost.Commands; +using Bit.SelfHost.Deployments; using Bit.SelfHost.Setup; using Xunit; @@ -69,6 +70,26 @@ public class StandardTopologyTests => Assert.Equal(13, new StandardDeployment().BuildTopology(CtxWith(true, true)).Count); } +public class ConfigRedactionTests +{ + [Theory] + [InlineData("SA_PASSWORD")] + [InlineData("globalSettings__sqlServer__connectionString")] + [InlineData("globalSettings__identityServer__certificatePassword")] + [InlineData("globalSettings__internalIdentityKey")] + [InlineData("globalSettings__mail__smtp__password")] + [InlineData("BW_INSTALLATION_KEY")] + public void Secrets_are_redacted(string key) => Assert.True(ConfigCommand.IsSecret(key)); + + [Theory] + [InlineData("globalSettings__mail__smtp__host")] + [InlineData("globalSettings__mail__smtp__username")] + [InlineData("globalSettings__installation__id")] + [InlineData("BW_DOMAIN")] + [InlineData("DATABASE")] + public void Non_secrets_are_shown(string key) => Assert.False(ConfigCommand.IsSecret(key)); +} + public class StandardAssetBuilderTests { private static StandardConfig Config() => new() { Url = "http://localhost", GenerateNginxConfig = false }; diff --git a/POC/bwsh/Commands/ConfigCommand.cs b/POC/bwsh/Commands/ConfigCommand.cs index d30b38a..bcbd0a6 100644 --- a/POC/bwsh/Commands/ConfigCommand.cs +++ b/POC/bwsh/Commands/ConfigCommand.cs @@ -7,19 +7,17 @@ public static class ConfigCommand { public static Command Build() { - var cmd = new Command("config", "Get or set deployment configuration (e.g. config key=value)."); + var cmd = new Command("config", "Show the current config, or get/set a key (config key=value)."); var assignment = new Argument("assignment") - { Description = "key=value to set, or key to get. Omit with --show to list.", Arity = ArgumentArity.ZeroOrOne }; + { Description = "key=value to set, or key to get. Omit to print the current config.", Arity = ArgumentArity.ZeroOrOne }; var deployment = Cli.DeploymentOption(); var root = new Option("--root") { Description = "Data directory (bwdata).", DefaultValueFactory = _ => "./bwdata" }; - var show = new Option("--show") { Description = "Show resolved config files." }; cmd.Arguments.Add(assignment); cmd.Options.Add(deployment); cmd.Options.Add(root); - cmd.Options.Add(show); cmd.SetAction(parseResult => { @@ -28,15 +26,8 @@ public static class ConfigCommand var rootDir = parseResult.GetValue(root)!; var arg = parseResult.GetValue(assignment); - if (parseResult.GetValue(show) || arg is null) - { - Console.WriteLine($"{kind} deployment config files (under {rootDir}):"); - Console.WriteLine(kind == DeploymentKind.Standard - ? " config.yml (structural; run `bwsh apply`)\n" + - " env/global.override.env (SMTP / admin / globalSettings; run `bwsh apply`)" - : " settings.env (all BW_* / globalSettings__*; run `bwsh apply`)"); - return 0; - } + if (arg is null) + return PrintConfig(dep, kind, rootDir); var eq = arg.IndexOf('='); var key = eq >= 0 ? arg[..eq] : arg; @@ -74,4 +65,47 @@ public static class ConfigCommand return cmd; } + + /// Prints the deployment's on-disk config files, redacting secret values. + private static int PrintConfig(IDeployment dep, DeploymentKind kind, string rootDir) + { + var printed = false; + foreach (var rel in dep.ConfigFiles) + { + var path = Path.Combine(rootDir, rel); + if (!File.Exists(path)) continue; + printed = true; + + Console.WriteLine($"# {rel}"); + foreach (var raw in File.ReadLines(path)) + { + var line = raw.TrimEnd(); + var eq = line.IndexOf('='); + if (eq > 0 && !line.TrimStart().StartsWith('#')) + { + var key = line[..eq]; + Console.WriteLine(IsSecret(key.Trim()) ? $"{key}=" : line); + } + else + { + Console.WriteLine(line); // YAML (config.yml), comments, blanks + } + } + Console.WriteLine(); + } + + if (!printed) + { + Console.Error.WriteLine($"No {kind} deployment config found under {rootDir}. Run `install` first."); + return 4; + } + return 0; + } + + /// Values worth hiding: anything that ends in a key, or names a password/connection string. + internal static bool IsSecret(string key) + { + var k = key.ToLowerInvariant(); + return k.Contains("password") || k.Contains("connectionstring") || k.EndsWith("key"); + } } diff --git a/POC/bwsh/Deployments/IDeployment.cs b/POC/bwsh/Deployments/IDeployment.cs index 36b757b..551d7f2 100644 --- a/POC/bwsh/Deployments/IDeployment.cs +++ b/POC/bwsh/Deployments/IDeployment.cs @@ -38,7 +38,10 @@ public interface IDeployment /// The container graph the Orchestrator brings up. IReadOnlyList BuildTopology(InstallContext ctx); - /// Resolve a `config set key=value` key to the file it lives in + the action to apply it. + /// Relative paths of the on-disk config files, for `config` to print (secrets redacted). + IReadOnlyList ConfigFiles { get; } + + /// Resolve a `config key=value` key to the file it lives in + the action to apply it. bool TryResolveConfigKey(string key, out ConfigBinding binding); } diff --git a/POC/bwsh/Deployments/LiteDeployment.cs b/POC/bwsh/Deployments/LiteDeployment.cs index 6a829d4..75b2e16 100644 --- a/POC/bwsh/Deployments/LiteDeployment.cs +++ b/POC/bwsh/Deployments/LiteDeployment.cs @@ -20,6 +20,8 @@ public sealed class LiteDeployment : IDeployment public string InstalledMarker => SettingsFile; + public IReadOnlyList ConfigFiles { get; } = [SettingsFile]; + public string ResolveUrl(string root) { var env = new Dictionary(); diff --git a/POC/bwsh/Deployments/StandardDeployment.cs b/POC/bwsh/Deployments/StandardDeployment.cs index 2105da8..f14bd7e 100644 --- a/POC/bwsh/Deployments/StandardDeployment.cs +++ b/POC/bwsh/Deployments/StandardDeployment.cs @@ -12,6 +12,14 @@ public sealed class StandardDeployment : IDeployment public string InstalledMarker => "config.yml"; + public IReadOnlyList ConfigFiles { get; } = + [ + "config.yml", + "env/global.override.env", + "env/mssql.override.env", + "env/key-connector.override.env", + ]; + public string ResolveUrl(string root) => Setup.StandardConfig.Load(root).Url; public IReadOnlyList Networks { get; } = diff --git a/POC/bwsh/README.md b/POC/bwsh/README.md index 8e2e506..b5a7811 100644 --- a/POC/bwsh/README.md +++ b/POC/bwsh/README.md @@ -66,6 +66,7 @@ This is how you change config (e.g. add SMTP under the manifest's `config:` bloc ```bash dotnet run -- status # health, versions, and vault URL +dotnet run -- config # print the current config (secrets redacted); config key=value to set dotnet run -- logs identity # a service's logs; --export bundles all to a zip dotnet run -- update # pull latest images and recreate changed services dotnet run -- backup # snapshot config + secrets + database to a .tar.gz