Fail startup on half-set metrics credentials (closes #205) #216
Reference in New Issue
Block a user
Delete Branch "issue-205-metrics-auth-fail-loud"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #205.
The defect
METRICS_USERNAMEalone mounted/metricsbehind a credential map whose only password was the empty string, socurl -u 'metrics:'returned 200 while the startup log reportedhasMetricsAuth:false. The route mount tested the username (internal/server/routes.go), the log tested both variables (internal/config/config.go), so the two could disagree about whether the endpoint existed.Approach
One value behind both.
Config.MetricsAuthEnabled()is now the only answer to "is/metricsserved": the route mount, the Prometheus recording middleware and the startup log'shasMetricsAuthfield all read it. It requires both credentials, not the username alone, so aConfigassembled in code (which bypasses environment validation) still cannot publish an endpoint that accepts an empty password.Fail loudly on half-set.
loadFromEnvrejects a half-set pair via a newErrIncompleteMetricsAuth, naming both variables in either direction. A set-but-invalid configuration aborts startup rather than degrading into something the operator did not ask for. Neither fallback was acceptable: mounting on the username alone publishes an endpoint with an empty password, and quietly leaving it unmounted withholds an endpoint that was asked for. The error carries neither credential value — the password is a secret.Both unset remains valid and leaves
/metricsunmounted.Definition of done
METRICS_USERNAMEset + password empty/unset is a startup error naming both variables, exit non-zeroresolveMetricsAuthMETRICS_PASSWORDset, username empty) is the same error/metricsunmountedMetricsAuthEnabled()falsehasMetricsAuthcannot disagree with the mountMetricsAuthEnabled()TestMetricsAuthConfigTestMetricsAuthConfigtreats "set to the empty string" and "not set at all" as separate inputs on each side (7 rows), since the reported bug arrived through the first.TestMetricsRouteUnmountedWithoutCredentials,TestMetricsRouteRequiresCredentialsandTestMetricsRouteUnmountedOnHalfSetConfigpin the same states against the real route tree; the middle one asserts an empty password gets 401.newTestEnvininternal/server/routes_test.gogained anewTestEnvWithConfigvariant so a test can drive the production route tree with a chosenConfig; the same pointer reaches the router and every middleware.TODO.mddeliberately untouched (#112).Gate evidence
make check— exit 0Lint ran in Docker (
Dockerfile.lint, golangci-lint v2.12.2 pinned by digest):Cache-defeated container build — exit 0
docker build --no-cache-filter=lint --no-cache-filter=builder --progress=plain ., run on the rebased tree. TheCACHEDlayers are only the two base-imageFROMresolutions and the dependency-download steps (go.mod/go.sumunchanged); the lint and test layers executed:All 15 packages ran with real durations, no
(cached)lines:New tests, in-container:
Build images and containers were removed afterwards;
docker ps -aanddocker imagesshow nothing of this run. No prune was used.Live verification against the issue's own reproduction
Half-set, both directions — the process refuses to start, exit 1:
Both set —
hasMetricsAuth:true, and the issue'scurl -u 'metrics:'now gets 401 where it previously got 200:Both unset —
hasMetricsAuth:falseand the route does not exist:Docs
README: both env-var rows note the pairing requirement, a new "Metrics credentials" subsection states the three outcomes, and the endpoint table plus the middleware list no longer describe the mount as keyed on
METRICS_USERNAMEalone.make fmtrun and included.PASS — satisfies every DoD item in #205; independently re-ran
docker build --no-cache-filter=lint --no-cache-filter=builderonc172dee: lint 78.6s / 0 issues,make test126.4s, all 15 packages with real durations and zero(cached)lines, exit 0.Anomalies that pass anyway, for the record:
metrics.NewRecorder(metrics.Config{})ininternal/middleware/middleware.go:444registers onprometheus.DefaultRegistererviaMustRegister, which panics on a duplicate.TestMetricsRouteRequiresCredentialsis the first test ininternal/serverto construct a metrics-enabled router, so exactly one registration happens today and it passes. A second metrics-enabled router in that package (orgo test -count=2) would panic. Relevant to #209: no conflict with registering new collectors —/metricsservespromhttp.Handler()over the default gatherer, so collectors registered elsewhere are picked up — but #209 must not add a second metrics-enabled router test without giving the recorder its own registry.Middleware.MetricsAuth()is unchanged and still buildsmap[string][]string{username: {password}}unconditionally, so it would accept an empty password if ever called outside theMetricsAuthEnabled()guard. Unreachable today (single call site, inside the guard) and the password compare is constant-time (subtle.ConstantTimeCompare, with the length-mismatch dummy), so-u 'metrics:'gets 401. Noted as hardening, not a defect.Disclosure — judgement call waived:
Config.MetricsAuthEnabled()returns false for a half-setConfigbuilt in code rather than panicking, i.e. auth-off without noise. That is not a fail-loudly violation because every productionConfigcomes fromconfig.New/loadFromEnv, which now aborts, and it is documented at the method. Verified there is no config-reload path and no second productionConfigprovider, sohasMetricsAuthand the mount read the same fields on the same never-mutated pointer.Also checked: base
next, fast-forward on10c8dd2; CI green (4m49s, real); title and commit both end(closes #205);TODO.mduntouched; no attribution trailers or vendor references;make fmt-checkclean in-container; the two half-set config rows assertErrIncompleteMetricsAuthand the absence of the password from the message, andTestMetricsRouteUnmountedOnHalfSetConfigis the assertion that actually regresses against pre-fix behaviour.