From b9686c6d7bd46cdf845a33a589a9e6e215e64122 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Tue, 13 Oct 2026 19:10:58 +0200 Subject: [PATCH] config: a metrics username without a password stops startup (closes #268) DNSWATCHER_METRICS_USERNAME turns on /metrics, and its Basic Auth used DNSWATCHER_METRICS_PASSWORD as it was, empty included, so the username with an empty password got in. config.New now refuses a username without a password with ErrNoMetricsPassword, whose message names both variables. The README's variable table says the password is required with the username. Judgement call: the check is in New, after buildConfig, since it compares two settings buildConfig has already read; buildConfig is also at the linter's length limit. Model: opus-5-5 --- README.md | 2 +- TODO.md | 2 ++ internal/config/config.go | 11 +++++++++++ internal/config/config_test.go | 18 ++++++++++++++++++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8a7e98e..f1a4108 100644 --- a/README.md +++ b/README.md @@ -437,7 +437,7 @@ following precedence (highest to lowest): | `DNSWATCHER_SENTRY_DSN` | Sentry DSN for error reporting | `""` | | `DNSWATCHER_MAINTENANCE_MODE` | Only sets `maintenanceMode` in the health check response; changes nothing else | `false` | | `DNSWATCHER_METRICS_USERNAME` | Basic auth username for /metrics, which is served only when this is set | `""` | -| `DNSWATCHER_METRICS_PASSWORD` | Basic auth password for /metrics | `""` | +| `DNSWATCHER_METRICS_PASSWORD` | Basic auth password for /metrics; required when `DNSWATCHER_METRICS_USERNAME` is set, and startup stops without it | `""` | | `DNSWATCHER_SEND_TEST_NOTIFICATION` | Send a test notification after first scan completes | `false` | **`DNSWATCHER_TARGETS` is required.** dnswatcher will refuse to start if no diff --git a/TODO.md b/TODO.md index c212724..96cbcee 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149 # Completed Steps +- 2026-10-13: `DNSWATCHER_METRICS_USERNAME` set without + `DNSWATCHER_METRICS_PASSWORD` stops startup (closes #268). - 2026-10-13: a save syncs the state file to disk before renaming it into place, so a crash cannot leave it empty and stop the next start (closes #264). - 2026-10-13: a `PORT` that cannot be bound stops startup with an error naming diff --git a/internal/config/config.go b/internal/config/config.go index 5e7b30b..4f1d6b7 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -61,6 +61,13 @@ var ErrInvalidTLSExpiryWarning = errors.New( // unset and means the default. var ErrInvalidBool = errors.New("value must be true or false") +// ErrNoMetricsPassword is returned when DNSWATCHER_METRICS_USERNAME is set and +// DNSWATCHER_METRICS_PASSWORD is empty. Basic Auth on /metrics would otherwise +// let in anyone who sends the username with an empty password. +var ErrNoMetricsPassword = errors.New( + "DNSWATCHER_METRICS_PASSWORD must be set when DNSWATCHER_METRICS_USERNAME is set", +) + // Params contains dependencies for Config. type Params struct { fx.In @@ -108,6 +115,10 @@ func New(_ fx.Lifecycle, params Params) (*Config, error) { return nil, err } + if cfg.MetricsUsername != "" && cfg.MetricsPassword == "" { + return nil, ErrNoMetricsPassword + } + configureDebugLogging(cfg, params) return cfg, nil diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 0c44726..35b4307 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -354,6 +354,24 @@ func TestNew_SkipRecordNotificationsNotInTargetsStopsStartup(t *testing.T) { require.ErrorContains(t, err, `"www.example.net"`) } +// TestNew_MetricsUsernameWithoutPasswordStopsStartup checks an unset and an +// empty password; TestNew_EnvironmentOverrides checks a username with one. +func TestNew_MetricsUsernameWithoutPasswordStopsStartup(t *testing.T) { + viper.Reset() + t.Setenv("DNSWATCHER_TARGETS", "example.com") + t.Setenv("DNSWATCHER_METRICS_USERNAME", "prom") + + _, err := config.New(nil, newTestParams(t)) + require.ErrorIs(t, err, config.ErrNoMetricsPassword, "password unset") + require.ErrorContains(t, err, "DNSWATCHER_METRICS_PASSWORD") + + viper.Reset() + t.Setenv("DNSWATCHER_METRICS_PASSWORD", "") + + _, err = config.New(nil, newTestParams(t)) + require.ErrorIs(t, err, config.ErrNoMetricsPassword, "password empty") +} + func TestNew_CustomDNSIntervalDuration(t *testing.T) { viper.Reset() t.Setenv("DNSWATCHER_TARGETS", "example.com")