Xircuit Blog

Product updates, developer logs, and news from behind the scenes at Xircuit.

The Connector at the Door — and a Secure Auto-Update cover image

The Connector at the Door — and a Secure Auto-Update


The Connector at the Door — and a Secure Auto-Update

A circuit board as a symbol for the connector Photo: Rawpixel (CC0 1.0), via Openverse

Most of the software I work on lives in the cloud. Container Apps, a Key Vault, a handful of workers that pick up messages from a Service Bus in the background — all conveniently reachable, all updatable with an azd deploy. This week was different. This week was about a piece of software that doesn't run in Azure, but sits in a utility room next to a door, plugged into a power supply, talking to hardware I can't reach over SSH. As soon as you send code to a place where physically nobody from the team is sitting, the entire risk calculus changes.

The occasion is simply described: for access control in the studios, the platform needs an intermediary that drives the existing door hardware. That's the connector. And a connector that runs unattended on-site for months at a time has to be able to update itself — otherwise it ages, accumulates security gaps, and becomes a maintenance burden. But that very auto-update is also the most dangerous part of the whole setup. That's why it's the focus this week, and why, at the time of writing, it is not yet switched on.

Why an on-device connector at all

The door hardware in the studios comes from Gantner. It speaks its own protocols, wants to be reachable locally on the network, and has no notion that somewhere in the cloud a Blazor app is waiting for a membership card. Something has to translate between this physical world and the platform. That something is a small service that runs on-site — the on-device connector — which connects the Gantner door hardware to the platform.

One might ask why this can't be done directly from the cloud. The answer is latency and reachability: a door should open even when the studio's internet connection is being temperamental, and it should react to a presented reader signal in milliseconds, not in whatever a round trip through Azure costs. The connector keeps the connection to the hardware local, reports events upward, and accepts commands from above. It is deliberately kept narrow — the less it decides on its own, the less it can get wrong.

The model is thus clear: a thin, long-lived process at the edge that encapsulates proprietary hardware and leaves the rest of the logic to the platform. What's missing is the answer to the uncomfortable question of how this process gets maintained when nobody is on-site.

Auto-update is the real attack surface

An update mechanism is, by definition, a back door you build in yourself: a channel through which foreign code reaches a device and gets executed there. That's true of a cloud deployment pipeline too, but there the execution happens in a controlled environment with clear permissions. With the connector, the code runs on a machine in a studio, and a compromised update wouldn't just be an outage — it would potentially mean control over a door.

That's why I didn't simply switch on the auto-update this week, but hardened it first — before it runs live for the first time. Three vectors were the focus: where does the package come from, what is the package allowed to do, and who decides which package gets pulled at all. Each of these questions got its own countermeasure, and none of them is particularly spectacular. Security at the edge rarely comes from one clever trick, but from several unglamorous bolts that together form a door an attacker finds nothing to gain from.

A shield as a symbol for signed updates Photo: Rawpixel (CC0 1.0), via Openverse

Only signed packages: Authenticode verification of the MSI

The first question — where does the package come from — is answered by signature verification. The update is delivered as an MSI, and before the connector so much as touches a downloaded MSI, it verifies its Authenticode signature. If the check doesn't pass cleanly, the package is discarded. Only signed packages run; everything else is thrown out before the installer even starts.

The value of this check lies in the fact that it holds regardless of the transport path. Even if someone were to manipulate the download channel or plant a substituted file in the right place, they would lack the matching signature. The connector doesn't trust the source because it had the right URL — it trusts the package because it cryptographically originates from us. That is the foundation everything else builds on; without it, the other measures would be mere cosmetics.

Closing off root RCE: the connector only writes to its own data/

The second question — what the package, or rather the running connector, is allowed to do — leads to a risk that's easily overlooked. A process that is allowed to write files and apply updates is an attractive target: anyone who can get it to write to an arbitrary location on the filesystem can quickly turn that into remote code execution with elevated privileges. I closed off exactly this root-RCE risk by having the connector write exclusively into its own, writable data/ subdirectory.

Everything the connector puts down at runtime — intermediate state, downloaded packages, whatever it may be — stays fenced into this one directory. The program files themselves, and everything outside this subfolder, are not writable by it. The effect is that a manipulated write path runs into nothing: it lands in the sandbox, not in a system directory. That turns a potential privilege escalation, in the worst case, into a full data/ directory that can simply be emptied.

Pinning down the update source: a pinned manifest URL

That leaves the third question: who actually determines which package the connector pulls? An auto-update follows a manifest — a list that says which version is current and where it lives. Whoever can redirect that manifest can point the connector at an arbitrary update source of their own, and while that wouldn't defeat the signature check, it would at least be an attack on the ecosystem in which that check takes place.

That's why the manifest URL on the API is only ever set from CI, and is therefore pinned. It doesn't come from a configuration that can be changed at runtime — something someone could alter through an admin interface or a compromised record — but is set exclusively in the build and deployment pipeline. Nobody can redirect the update source while the system is running. Together with the signature check, that means: the connector only pulls from a source we defined, and only accepts from there what we signed.

A pause brake, until the hardware confirms it

As thorough as the three bolts are, I have no interest in releasing an auto-update into production before it has proven, on real hardware, that it behaves predictably. That's why there's a pause-and-pin brake that holds the auto-update back until an on-device hardware gate has been passed. Until then, the mechanism remains built and verified, but engaged. It pulls nothing, it installs nothing, it waits.

This brake is intentional, not an embarrassment. The gap between "it works in the test setup" and "it works on a real Gantner door in a real studio" is exactly the kind of gap where unpleasant surprises accumulate. There's a runbook for release that describes the process: which checks must run on the hardware, who signs off on them, and how the brake is subsequently released. As long as the gate hasn't been passed, the brake stays on — documented and deliberate.

It's important to me that this is an honest interim state: the security work is done, the trigger is not — yet. That's the right state for something attached to a door. Better a connector that doesn't update itself yet than one that does it too soon.

What's next?

Next week: quality as a system — a test coverage program, central package management, and a real production bug we found.


Comments

No comments yet. Be the first to share your thoughts!

Leave a Comment