Hey Rod — this one's a design question rather than a bug, so I split it into its own thread.
I noticed Cimian.Infrastructure pulls in AWSSDK.S3 and Azure.Storage.Blobs, and Configuration has cloud.aws / cloud.azure sections, but the live download path still goes through plain HttpClient to {repo}/pkgs/… and doesn't instantiate either SDK yet. So I wanted to ask where you're headed with cloud repo support before assuming anything — and, if it's still open, float a couple of directions that might keep the client lean. Totally possible you've already thought past all of this; if so, ignore me. 🙂
The thing I keep coming back to is how Munki deliberately keeps its core transport-agnostic — the client only ever does a plain HTTPS GET, and anything vendor-specific (S3 signing, CloudFront/GCS/Azure auth) lives outside it, either in the deployment in front of the bucket or in drop-in middleware. Embedding the full vendor SDKs is the opposite trade — the client learns to speak each cloud's object API natively, which couples it to specific vendors, adds a lot of weight to an already-large trimmed binary, and quietly signs the project up to maintain an S3 client, an Azure client, and whatever comes next. Two directions I could imagine that stay closer to the Munki model, lightest first:
Rung 1 — recommend the pattern, don't absorb it (a serverless signer in front of the bucket)
The highest-leverage move might be to add no cloud code at all and instead publish a short "recommended repo architectures" doc. Cimian's client already does everything the good pattern needs — plain HTTPS, follows redirects, and mTLS/client-cert auth (which you even use for the client identity) — so an admin can host their repo in the cloud today, with zero client changes. "Here are the blessed ways to put your repo in a bucket" is a page you write once; two embedded SDKs are code you own forever, and the vendor coupling stays on the deployment side where it belongs and can differ per-org.
The concrete pattern to recommend: a tiny serverless endpoint in front of the storage bucket that authenticates the client and 302-redirects to a short-lived signed URL; the client just follows the redirect (which HttpClient already does) and downloads straight from storage. It stays completely dumb — no vendor SDK, no cloud credentials on the device — and you rotate/revoke centrally. jc0b wrote this up nicely for Munki (GCP Cloud Run + GCS, Munki-native Basic Auth, a 302 — deliberately not a 301, so expired tokens don't get cached client-side): https://jc0b.computer/posts/move-munki-middleware-to-cloud/ . The same shape maps directly to AWS API Gateway + Lambda + S3 or an Azure Function + Blob SAS.
Why this fits Cimian specifically: you already support client-certificate / mTLS in HttpClientFactory, which is a stronger front door for the signer than Basic Auth — the gateway authenticates each device by its cert, hands back a signed URL scoped to what that device may fetch, and no long-lived cloud keys ever touch the fleet. It reuses identity you're already establishing rather than distributing a new secret, and it's almost entirely your users' infrastructure (at most a documented reference Lambda/Function they deploy) rather than Cimian's code.
One small enabler worth flagging, because it's what makes the mTLS side deployable at scale: right now the client can select a store certificate only by exact thumbprint (or a file path on disk), and a thumbprint is unique per device — so a shared Config.yaml can't point every machine at its own cert. Selecting by a stable pattern instead — issuer = your SCEP/enrollment CA, or a subject/template match — would let one config resolve each device's own certificate, with the private key never leaving the machine store. It's a small change in LoadClientCertificate (X509FindType.FindByIssuerName / FindBySubjectName, preferring a match that has a private key and the furthest-out expiry), and it's the piece that turns "we support client certs" into "one config, whole fleet." Happy to draft it.
Rung 2 — if you want it in the client, do it the Munki way: drop-in middleware
Munki's middleware is operator extensibility — drop a plugin into /usr/local/munki/middleware/ and it's picked up with no rebuild (Munki 7 makes it a compiled dylib; munki/DemoMiddleware is the template). A compiled-in, PR-only signer wouldn't really be that — Munki doesn't work that way — so if you want in-client middleware I'd aim straight for the faithful version: runtime-loaded plugins, which .NET supports natively through AssemblyLoadContext (its official "app with plugins" pattern — the dlopen equivalent).
- A small contract package —
Cimian.Middleware.Abstractions — that plugins implement:
public interface ICimianMiddleware
{
// Mutate the outgoing request before it is sent:
// rewrite the URL, add/replace headers, sign it, redirect it, etc.
ValueTask ProcessRequestAsync(HttpRequestMessage request, CancellationToken ct);
}
- At startup, load every
.dll in a middleware directory (e.g. %ProgramFiles%\Cimian\middleware\) in its own collectible AssemblyLoadContext (with an AssemblyDependencyResolver so a plugin's own dependencies resolve), instantiate the ICimianMiddleware types, and run them in the download request pipeline (an HttpClient DelegatingHandler is the natural place to invoke them). Adding a backend becomes dropping a DLL in a folder — no Cimian rebuild.
- Ship a
Cimian.Middleware contract plus a reference DemoMiddleware — a sample that just logs the URL and headers, with a build.ps1 that packages it — one-to-one with munki/DemoMiddleware, so third parties have something concrete to write against (and your Mac-admin users get a familiar shape).
Two things to keep in mind, neither a blocker:
- That plugin directory is loaded into a process that runs as SYSTEM, so it has to be admin-only — the installer can set that ACL when it creates the directory (SYSTEM + Admins full, users read/execute), so a standard user can't drop in code that would then run as SYSTEM.
- Runtime plugin loading trades against the trimmed single-file publish — the trimmer has to preserve the contract and the BCL surface plugins touch, and single-file complicates dependency resolution. This is the same wall Munki hit going from drop-in Python to compiled dylibs, so it's a known tradeoff rather than a Cimian-specific snag; in practice it probably means not trimming the host (or carving the contract out of the trim).
Where I might be wrong
The one thing that flips all of this is if you specifically need the native cloud API surface for something a signed GET can't do — bucket enumeration for makecatalogs-style tooling, multipart for very large payloads, server-side filtering, etc. If that's the driver, the SDKs earn their keep and I'd love to understand the use case so I'm not suggesting you throw away something load-bearing.
If any of this is useful, I'm happy to help however fits: draft the "recommended cloud architectures" doc, sketch a reference Lambda/Function signer to pair with your mTLS (plus the small client-cert pattern-selection change that unlocks it fleet-wide), or stand up the Cimian.Middleware contract + a DemoMiddleware if you want the drop-in route. And if cloud backends are further down the road than I assumed, no worries at all — mostly I wanted to check before anyone "cleans up" those SDK references in case they're scaffolding you're mid-way through.
Hey Rod — this one's a design question rather than a bug, so I split it into its own thread.
I noticed
Cimian.Infrastructurepulls inAWSSDK.S3andAzure.Storage.Blobs, andConfigurationhascloud.aws/cloud.azuresections, but the live download path still goes through plainHttpClientto{repo}/pkgs/…and doesn't instantiate either SDK yet. So I wanted to ask where you're headed with cloud repo support before assuming anything — and, if it's still open, float a couple of directions that might keep the client lean. Totally possible you've already thought past all of this; if so, ignore me. 🙂The thing I keep coming back to is how Munki deliberately keeps its core transport-agnostic — the client only ever does a plain HTTPS GET, and anything vendor-specific (S3 signing, CloudFront/GCS/Azure auth) lives outside it, either in the deployment in front of the bucket or in drop-in middleware. Embedding the full vendor SDKs is the opposite trade — the client learns to speak each cloud's object API natively, which couples it to specific vendors, adds a lot of weight to an already-large trimmed binary, and quietly signs the project up to maintain an S3 client, an Azure client, and whatever comes next. Two directions I could imagine that stay closer to the Munki model, lightest first:
Rung 1 — recommend the pattern, don't absorb it (a serverless signer in front of the bucket)
The highest-leverage move might be to add no cloud code at all and instead publish a short "recommended repo architectures" doc. Cimian's client already does everything the good pattern needs — plain HTTPS, follows redirects, and mTLS/client-cert auth (which you even use for the client identity) — so an admin can host their repo in the cloud today, with zero client changes. "Here are the blessed ways to put your repo in a bucket" is a page you write once; two embedded SDKs are code you own forever, and the vendor coupling stays on the deployment side where it belongs and can differ per-org.
The concrete pattern to recommend: a tiny serverless endpoint in front of the storage bucket that authenticates the client and 302-redirects to a short-lived signed URL; the client just follows the redirect (which
HttpClientalready does) and downloads straight from storage. It stays completely dumb — no vendor SDK, no cloud credentials on the device — and you rotate/revoke centrally. jc0b wrote this up nicely for Munki (GCP Cloud Run + GCS, Munki-native Basic Auth, a 302 — deliberately not a 301, so expired tokens don't get cached client-side): https://jc0b.computer/posts/move-munki-middleware-to-cloud/ . The same shape maps directly to AWS API Gateway + Lambda + S3 or an Azure Function + Blob SAS.Why this fits Cimian specifically: you already support client-certificate / mTLS in
HttpClientFactory, which is a stronger front door for the signer than Basic Auth — the gateway authenticates each device by its cert, hands back a signed URL scoped to what that device may fetch, and no long-lived cloud keys ever touch the fleet. It reuses identity you're already establishing rather than distributing a new secret, and it's almost entirely your users' infrastructure (at most a documented reference Lambda/Function they deploy) rather than Cimian's code.One small enabler worth flagging, because it's what makes the mTLS side deployable at scale: right now the client can select a store certificate only by exact thumbprint (or a file path on disk), and a thumbprint is unique per device — so a shared
Config.yamlcan't point every machine at its own cert. Selecting by a stable pattern instead — issuer = your SCEP/enrollment CA, or a subject/template match — would let one config resolve each device's own certificate, with the private key never leaving the machine store. It's a small change inLoadClientCertificate(X509FindType.FindByIssuerName/FindBySubjectName, preferring a match that has a private key and the furthest-out expiry), and it's the piece that turns "we support client certs" into "one config, whole fleet." Happy to draft it.Rung 2 — if you want it in the client, do it the Munki way: drop-in middleware
Munki's middleware is operator extensibility — drop a plugin into
/usr/local/munki/middleware/and it's picked up with no rebuild (Munki 7 makes it a compiled dylib;munki/DemoMiddlewareis the template). A compiled-in, PR-only signer wouldn't really be that — Munki doesn't work that way — so if you want in-client middleware I'd aim straight for the faithful version: runtime-loaded plugins, which .NET supports natively throughAssemblyLoadContext(its official "app with plugins" pattern — thedlopenequivalent).Cimian.Middleware.Abstractions— that plugins implement:.dllin a middleware directory (e.g.%ProgramFiles%\Cimian\middleware\) in its own collectibleAssemblyLoadContext(with anAssemblyDependencyResolverso a plugin's own dependencies resolve), instantiate theICimianMiddlewaretypes, and run them in the download request pipeline (anHttpClientDelegatingHandleris the natural place to invoke them). Adding a backend becomes dropping a DLL in a folder — no Cimian rebuild.Cimian.Middlewarecontract plus a referenceDemoMiddleware— a sample that just logs the URL and headers, with abuild.ps1that packages it — one-to-one withmunki/DemoMiddleware, so third parties have something concrete to write against (and your Mac-admin users get a familiar shape).Two things to keep in mind, neither a blocker:
Where I might be wrong
The one thing that flips all of this is if you specifically need the native cloud API surface for something a signed GET can't do — bucket enumeration for
makecatalogs-style tooling, multipart for very large payloads, server-side filtering, etc. If that's the driver, the SDKs earn their keep and I'd love to understand the use case so I'm not suggesting you throw away something load-bearing.If any of this is useful, I'm happy to help however fits: draft the "recommended cloud architectures" doc, sketch a reference Lambda/Function signer to pair with your mTLS (plus the small client-cert pattern-selection change that unlocks it fleet-wide), or stand up the
Cimian.Middlewarecontract + aDemoMiddlewareif you want the drop-in route. And if cloud backends are further down the road than I assumed, no worries at all — mostly I wanted to check before anyone "cleans up" those SDK references in case they're scaffolding you're mid-way through.