From c5e894d9b3e9c9891a78fcb386bb6f4548b6e6bf Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 11 Aug 2026 09:04:02 -0400 Subject: [PATCH] =?UTF-8?q?feat(secrets):=200.6.2=20=E2=80=94=20the=20cont?= =?UTF-8?q?ent-root=20rejection=20message=20teaches=20the=20container=20ca?= =?UTF-8?q?se?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Suggested by the OtOpcUa session, the only real containerized deployment in this family and the one that hit the false positive. A store on a volume mounted inside the content root is durable across container replacement but is rejected anyway, because the rule cannot tell a mount from an ordinary directory. The message now says so, gives the fix, and forecloses the wrong one by stating that no override exists. A false positive's entire cost is the confusion in the moment, so an error that names its own fix costs nothing on the true-positive path and turns the false positive into a two-minute edit. Message only, no behaviour change. Their case for keeping the rule strict is recorded in the CHANGELOG because it is better than the trade-off I had written: the workaround is an improvement rather than a concession, an override flag has the lifecycle of a knob set once and never revisited, and mount detection would make the verdict depend on runtime platform state so the same path would pass on one host and fail on another. --- ZB.MOM.WW.Secrets/CHANGELOG.md | 26 +++++++++++++++++++ ZB.MOM.WW.Secrets/Directory.Build.props | 2 +- .../Configuration/SecretsOptionsValidator.cs | 7 ++++- .../SecretsStorePathRulesTests.cs | 23 ++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/ZB.MOM.WW.Secrets/CHANGELOG.md b/ZB.MOM.WW.Secrets/CHANGELOG.md index 3522f3a..00cbe84 100644 --- a/ZB.MOM.WW.Secrets/CHANGELOG.md +++ b/ZB.MOM.WW.Secrets/CHANGELOG.md @@ -5,6 +5,32 @@ follows [Keep a Changelog](https://keepachangelog.com/); versions are shared acr package in the family (see `Directory.Build.props`) even when a given release only touches one of them. +## 0.6.2 — 2026-08-11 + +### Changed + +- **The under-content-root rejection message now teaches the container case.** Suggested by the + OtOpcUa session, which is the only real containerized deployment in this family and the one that + hit the false positive. A store on a volume mounted inside the content root is durable across + container replacement but is rejected anyway, because the rule cannot tell a mount from an + ordinary directory. The message now says so, gives the fix (mount the same volume at a second + path outside the content root; the file sits at the volume root either way, so an existing store + carries over), and forecloses the wrong fix by stating that no override exists. + + A false positive's entire cost is the confusion in the moment, so a boot error that names its own + fix costs nothing on the true-positive path and converts the false positive into a two-minute + edit. **Message only — no behaviour change.** Existing 0.6.1 consumers can take this whenever they + next touch the pin; it does not warrant a re-pin round on its own. + + Their argument for keeping the rule strict is recorded because it is better than the trade-off I + had written down: the workaround is an improvement rather than a concession (it separates the + application from its durable state at the filesystem level — the exact distinction the incident + blurred); an `AllowStoreUnderContentRoot` flag has the lifecycle of a knob set once to clear a + symptom and never revisited, surviving the redeploy that changes the layout it was reasoned + against; and mount detection would make the verdict depend on runtime platform state, so the same + path would pass on one host and fail on another — strictly worse than a stable false positive + with a cheap documented fix. + ## 0.6.1 — 2026-08-11 ### Fixed diff --git a/ZB.MOM.WW.Secrets/Directory.Build.props b/ZB.MOM.WW.Secrets/Directory.Build.props index 67d33d2..de1e817 100644 --- a/ZB.MOM.WW.Secrets/Directory.Build.props +++ b/ZB.MOM.WW.Secrets/Directory.Build.props @@ -5,7 +5,7 @@ enable enable latest - 0.6.1 + 0.6.2 true README.md diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretsOptionsValidator.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretsOptionsValidator.cs index 2027495..2b37da4 100644 --- a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretsOptionsValidator.cs +++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretsOptionsValidator.cs @@ -80,7 +80,12 @@ public sealed class SecretsOptionsValidator : OptionsValidatorBase f.Contains("content root", StringComparison.Ordinal)); } + [Fact] + public void The_rejection_message_teaches_the_container_case() + { + // A false positive's entire cost is the confusion in the moment, and the known one is a + // store on a volume mounted inside the content root — durable in a container, rejected + // anyway, because the rule cannot tell a mount from an ordinary directory. A boot error + // that names its own fix costs nothing on the true-positive path and turns that false + // positive into a two-minute edit. Asserted rather than left to the doc comment, because + // the message is the only part of this a stuck operator will actually read. + var env = new StubHostEnvironment { ContentRootPath = Root("app") }; + var validator = new SecretsOptionsValidator(env); + + ValidateOptionsResult result = + validator.Validate(name: null, OptionsWith(Root("app", "data", "secrets.db"))); + + Assert.True(result.Failed); + string message = Assert.Single(result.Failures!); + Assert.Contains("mounted volume", message, StringComparison.OrdinalIgnoreCase); + Assert.Contains("OUTSIDE the content root", message, StringComparison.Ordinal); + // And that it forecloses the wrong fix, rather than leaving the reader to hunt for a flag. + Assert.Contains("not look for an override", message, StringComparison.OrdinalIgnoreCase); + } + [Fact] public void Validator_accepts_an_absolute_path_outside_the_content_root() {