Skip to content

Cloud repo backends — recommend deployment patterns instead of absorbing vendor SDKs? #67

Description

@hurricanehrndz

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions