Repository navigation
feat(self-update): the bundle client presents its key only to a declared artifact host (#4123, core half) - #6378
Conversation
… presents its key only to a declared artifact host (#4123) The key a consumer holds for registry mount M now goes to an artifact's OCI host H only when H is M's own host or M's bundle index declares it (index-level `artifactRegistry`, the registry's own `registry.host`, rendered by the chart as PluginBundles__ArtifactRegistry). An undeclared host is never contacted; the bytes come over the registry's HTTP route, so installs keep adopting. Previously the key followed whatever host a publisher-written bundle entry named. Policy `registry-trust-one-declaration` in the register; SelfUpdateRegistryCredential and SelfUpdateControlLane describe the rule. The self-update half (HelmValues derives selfUpdate.registryValidationUrl on the control instance) lands in MeshWeaver.Plugins. Refs #4123 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
The current host validation still permits credential disclosure through hostile OCI challenge realms and userinfo-bearing host values.
1 open finding
What changed in this PR
Introduces registry-owned artifact-host trust for plugin bundle credentials, with safe HTTP fallback.
Changes:
- Adds
artifactRegistryto bundle indexes and chart configuration. - Restricts OCI artifact fetching to declared hosts.
- Adds tests and updates architecture policy documentation.
| File | Description |
|---|---|
src/MeshWeaver.PluginCatalog/PluginBundleClient.cs |
Implements artifact-host authorization and fallback. |
memex/Memex.Portal.Shared/Api/PluginBundleEndpoints.cs |
Publishes the configured artifact registry. |
deploy/helm/templates/memex-portal/config.yaml |
Renders the registry-owned declaration. |
test/Memex.Portal.Shared.Test/PluginBundleArtifactFetchTest.cs |
Tests declared and undeclared artifact hosts. |
test/Memex.Portal.Shared.Test/PluginBundleIndexArtifactTest.cs |
Tests index declaration output. |
src/MeshWeaver.Documentation/Data/Architecture/SelfUpdateRegistryCredential.md |
Documents the unified trust model. |
src/MeshWeaver.Documentation/Data/Architecture/SelfUpdateControlLane.md |
Records the implemented design. |
src/MeshWeaver.Documentation/Data/Architecture/PolicyNotProse.md |
Registers the new policy. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| pluginId, bundle.Version, refusal, _registryUrl); | ||
| return DownloadOverHttp(pluginId, bundle.Version); | ||
| } | ||
| return DownloadArtifact(pluginId, bundle.Version, artifact); |
There was a problem hiding this comment.
Real defect, fixed at the root in f50faaf (pushed with a merge of main as 17d8bca). OciRegistryClient.ExchangeAsync now presents Basic instance:<key> at the token realm only when the realm's authority AND scheme equal the registry's own. Otherwise it raises RegistryRefusedException before anything is sent there. The approved artifact host is the client's registry, so this is the same check the thread asks for, and it also covers the self-updater's OciTagLister. Every fleet realm is same-host: cr.meshweaver.cloud/auth and meshweaver.azurecr.io/oauth2/token, read live from the /v2/ challenges.
Regression: OciRegistryClientTest.ATokenRealmOffTheRegistrysOwnAuthority_NeverReceivesTheKey, three cases: another host, a suffix-extended host, and an http downgrade. In each, the hostile fake gets zero requests and no presented secret. With the authority check disabled, both host cases go red. OciRegistryClientTest, OciTagListerTest and PluginBundleArtifactFetchTest: 60/60. Release -warnaserror clean.
…'s own authority and scheme (#4123) The Bearer challenge's realm is named by the registry's 401, and ExchangeAsync sent Basic instance:<key> to an absolute realm on any host. A registry approved for the key could redirect it. The realm must now match the registry's own authority and scheme, else RegistryRefusedException is raised before anything is sent. Every fleet realm is same-host (cr.meshweaver.cloud/auth, meshweaver.azurecr.io/oauth2/token). Hostile-realm regression test, three cases. Refs #4123 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…fact-trust # Conflicts: # src/MeshWeaver.Documentation/Data/Architecture/PolicyNotProse.md
Test Results 24 files ±0 24 suites ±0 50m 27s ⏱️ +17s Results for commit bea4786. ± Comparison against base commit f7853c5. This pull request removes 40 and adds 24 tests. Note that renamed tests count towards both.♻️ This comment has been updated with latest results. |

Summary
Maintainer decision on #4123: "derive the self-update registry/validator pairing on control at render time from one declaration." Recorded as policy
registry-trust-one-declarationin the register.This is the core half: the second trust rule the issue names. The self-update half (control instance derives
selfUpdate.registryValidationUrl) is in MeshWeaver.Plugins (paired PR below). Neither half depends on the other to compile, and the chart already rendersselfUpdate.registryValidationUrl(#4094).Before:
PluginBundleClient.DownloadArtifactsent the instance key to whatever OCI host a bundle entry'sartifactnamed. A publisher writes that entry.After: the key for registry mount M goes to artifact host H only when H is M's own host, or M's bundle index declares H (
artifactRegistry).artifactRegistryis index-level and comes from the registry's own config (PluginBundles:ArtifactRegistry). The chart renders that key from the hosted registry's ownregistry.host(PluginBundles__ArtifactRegistry, only whenregistry.enabled). A bundle entry cannot earn the key.SelfUpdate:Registry. The control instance pulls its image from ACR but adopts bundles sealed oncr.meshweaver.cloud.BundleIndex.ArtifactRegistryis an init property, so the record's primary constructor is unchanged (no binary break).Tests (Release,
-warnaserrorclean:Memex.Portal.Shared.Test,MeshWeaver.Documentation)PluginBundleArtifactFetchTest: 14/14.AnUndeclaredArtifactHost_NeverReceivesTheKey_AndTheHttpRouteServes, three cases: no declaration, another host, a suffix-extended host. In each, the OCI fake receives zero requests and no presented secret, and the bundle lands over HTTP.AnAdvertisedArtifact_IsFetchedFromTheOciRegistry…is the positive control (declared host gets the key).TheArtifactKeyTarget_…: both directions.if (false && …)), the new theory goes 3/3 red. Restored.PluginBundleIndexArtifactTest: 4/4. NewTheIndexStatesTheRegistrysOwnArtifactRegistry_AndNullWhenUndeclaredcovers both arms.check-chart-invariants.sh: 20 combinations and 10 refusal controls hold.Rollout / what goes live when
memex-cloud(hostscr.meshweaver.cloud) needs a re-render on a chart and image carrying this change: a governedReconcileafter the roll. Then its index carriesartifactRegistry: cr.meshweaver.cloud.Bundle for X@v: N byte(s) fetched from cr.meshweaver.cloud/…, digest-verifiedonce the registry declares the host. Before that, the line is… declares no artifact registry … taking …'s HTTP bundle route instead.Pairs-with: none — no public type or member is removed (
BundleIndexgains an init property;Downloadis private)Implementers: none — no interface member added
Mirror-sync: none — no i18n catalog change
Refs #4123
🤖 Generated with Claude Code