Repository navigation
Description
Cloud providers other than GCS should be able to ship their own object-store plugin from their own repository. This issue lists what works today and what is missing.
Without per-actor credentials: works today
A provider can build a plugin out of tree now, as long as it transfers with its own credentials.
- The plugin API,
objectstore.v1, is public inpkg/proto/objectstorepb/v1. A plugin servesNodeProviderandAssetProviderbeside atelet, andControlProviderbeside ate-api-server, over a Unix socket. - atelet sends
actor_jwtwith every snapshot transfer. A plugin that does not authorize per actor ignores it. Our S3 backend does the same today.
The gap is installation. The manifests name our image, ko://go.ate.dev/substrate/cmd/objectstore-plugin, and its flags in manifests/ate-install/atelet.yaml and ate-api-server.yaml, and ate-setup has no setting for another plugin image. A provider has to patch those manifests.
With per-actor credentials: needs packages moved to pkg/
GCS scopes each transfer to the actor's own snapshots. A provider that wants the same writes its own exchange and issuer for its cloud, but it should reuse the checks that decide what an actor may access. Those are under internal/, so a binary outside this repository cannot import them.
These would move to pkg/:
| Package | What it does |
|---|---|
internal/oidcjwt |
verifies the actor JWT against the jwks file |
internal/objectstoreplugin/actorlookup |
fetches the actor from ate-api-server, refuses a recreated one |
internal/objectstoreplugin/actorscope |
turns the actor's status into grants, read-write on its own prefix, read-only on its tag |
checks in internal/objectstoreplugin/exchange |
refuse a token that is not the worker-for-actor kind, refuse empty identity claims, and refuse a token whose node is not the actor's assignedNode or an actor outside resources.TransferStates. Every provider must apply these, so they belong beside actorscope |
ActorJWTAudience, in internal/objectstoreplugin/exchange |
the audience atelet mints every worker-for-actor JWT with, which every provider's verifier must accept |
A package under pkg/ cannot import internal/, so these come along:
internal/actoridjwt, the claim types.oidcjwtandactorlookupimport it.internal/ateapiauth, the mTLS dial optionsactorlookupuses, with its importsinternal/credbundleandinternal/k8sresolver.- The part of
internal/resourcesthatactorscopeand the checks use: the snapshot URI parsing,StoragePrefix, the actor prefix helper, andTransferStates.
None of these imports a cloud SDK or anything GCS-specific.
The manifest wiring for our exchange also applies to a provider's: the network policy that lets atelet reach port 8445 on ate-api-server, and the serving certificate, client certificate and jwks mounts on the control sidecar. A provider's sidecar needs the same, through the same manifest patches as above.
Stays ours
Our exchange proto (internal/proto/objectstoreexchangepb), the GCS issuer, the access boundary code and the node-side token cache are specific to GCS. A provider writes its own for its cloud.
Activity
aramase commentedon Oct 22, 2026
One correction first: a package under
pkg/can importinternal/. Go's internal rule checks the importer's path, so anything under the module root,pkg/...included, may importgo.ate.dev/substrate/internal/..., and an external module can import thatpkg/package. I built a probe from an external module to confirm it. So nothing in the table has to move, and oidcjwt, actorlookup, ateapiauth, credbundle, k8sresolver and part of resources don't become public API.Instead I'd add one small public package,
pkg/objectstore/actorauthz, that runs the same checks the GCS exchange runs today. A provider then writes only its issuer and wire format. Sketch:I'd land it in this order:
exchange.Serverinto an internal actorauthz package, no behavior change, and have the GCS exchange call it. That's objectstore-plugin: move the credential exchange's actor checks into actorauthz #3138.pkg/wrapper above.The install side comes after: an ate-setup setting for another plugin's component, and moving the in-tree-only sidecar env and args out of the base manifests. The naming there should line up with #2378's storage settings.
ILLUM1N0X two questions. Is one wrapper package OK instead of moving the packages? And is a URI string the right public form for a grant's prefix, or would you rather see bucket and path exposed separately?
ILLUM1N0X commentedon Oct 23, 2026
Answers to the above questions:
pkg/package importsinternal/today, so please call that out in the PR so maintainers can agree to it.--snapshot-location,snapshot_uri). One note: The prefix has no trailing slash so a plain string prefix match also matches a different actor whose UID starts the same way. The GCS boundary adds the/for this reason.Other notes:
actorauthz.Audienceand update atelet and the other callers in the same PR instead of keepingexchange.ActorJWTAudienceas an alias.AGENTS.mdasks us not to keep old names.Covers: is this meant to replace the cache check ingcsissuer, or to check a single object URI? Ours compares whole grant sets by exact prefix, which works because both sides come from the same helper. If Azure needs the URI form, I'd like to understand what calls it before it goes intopkg/.aramase commentedon Oct 23, 2026
ILLUM1N0X I rebased #3138 on main and it now moves the audience to
actorauthz.Audience, updates atelet, the objectstore plugin and the snapshotcredentials e2e suite, and deletesexchange.ActorJWTAudience. I applied the same rule to theexchange.Verifierandexchange.Lookupaliases it had added, soexchange.Newtakes the actorauthz types and the aliases are gone.The rest is for the
pkg/PR.Grant.Prefixwill end in/, built the same way the GCS boundary gets it (boundary.go#L38-L39, throughobjectstore.BucketPrefix), so a plain prefix match can't reach another actor whose UID starts the same way. That PR's description will also call out that it's the firstpkg/package importinginternal/.On
Covers: it doesn't replace the cache check ingcsissuer, and the GCS exchange won't call it. That check asks whether a token minted for one grant set can serve another, and exact prefixes work there because both sets come fromactorscope.FromActor. The GCS exchange never sees an object URI: the request carries onlyactor_jwtandforce, and the access boundary rules do the scoping inside GCS.It was meant for the Azure exchange, which isn't written yet. That exchange may need to check that one blob falls under one of the actor's grants before it signs a SAS for that blob. Nothing in the repo would call
Coverstoday, so I'll leave it out of thepkg/PR. If the Azure exchange needs it, I'll propose it together with that exchange.ILLUM1N0X commentedon Oct 23, 2026
Sounds good, thanks.
One more question for the
pkg/PR: what reads Decision.Expiry? The GCS exchange doesn't use the JWT's expiryaramase commentedon Oct 23, 2026
ILLUM1N0X nothing reads it, so the
pkg/PR leavesExpiryout. Following your #3138 review, itsDecisionwill also dropAtespace,ActorNameandNodeand carry onlyActorUIDandGrants, andNewbuilds the verifier fromAudienceitself.objectstore-plugin: move the credential exchange's actor checks into …
aramase commentedon Oct 29, 2026
Scoping the installation half of this issue. #3138 has merged. #3358 adds
pkg/objectstore/actorauthzand is back in draft until the exchange moves to OIDC discovery in its own PR, which removes thejwksmount from the control sidecar. This covers the rest: how a provider gets its own plugin image into an install.What a provider has to replace today
ko://go.ate.dev/substrate/cmd/objectstore-plugin, in two places: theobjectstore-plugininit container inatelet.yaml(nodemode) and inate-api-server.yaml(controlmode).ATE_STORAGE_BACKENDand the GCS/S3 settings are ours./ko-app/objectstore-plugin healthcheck --socket=.... That path only exists in a ko-built image.What a provider can keep as it is
/run/objectstore-plugin/{node,control}.sock, which atelet and ate-api-server dial.ate-api-serverService port, and theate-systemnetwork policy that admits atelet on it.servicednsandpodidentitycertificate mounts on the control sidecar, and theservicedns-catrust bundle on the node sidecar.objectstore-pluginand the same mounts.Proposal
--component DIR, composed into every control plane apply after ours. ate-setup already composes components this way for--e2e-tuning,--fake-data-planeand others (Env.componentsincmd/ate-setup/internal/steps/overlay.go). A provider ships one component that patches the image, args, env and probes of bothobjectstore-plugincontainers. This is generic rather than an image-only flag, because a provider has to change args, env and probes too.healthcheckexpectation, port 8445 and what calls it, and the mounts above. The rest of the manifests stay ours to change.docs/(or a test fixture) that swaps in a placeholder image, with a render test so the contract breaks loudly in our CI rather than in a provider's install.Not proposing
Open questions:
--image-repopass a provider's non-ko://image through untouched?Prebuilt.rewriteonly matchesko://references, so I expect so, but I haven't run it.healthchecksubcommand at a fixed path), or should the component replace it?Happy to send 1–3 as one PR after #3358 if this shape works.
Verify actor JWTs in the credential exchange through OIDC discovery (a…
Verify actor JWTs in the credential exchange through OIDC discovery (a…
Verify actor JWTs in the credential exchange through OIDC discovery (#…
BenTheElder commentedon Nov 5, 2026
This is done?
Add pkg/objectstore/actorauthz so out-of-tree object-store providers …
Add pkg/objectstore/actorauthz so out-of-tree object-store providers …