How SSH destroys your robot

This one is an argument, not a tutorial. If you'd rather just try the alternative, install Wendy below and run wendy run.
It's 2am. A robot in the field stops reporting. You open a laptop, type ssh pi@robot-07, and start poking around. A service is wedged, so you restart it. A config file has the wrong value, so you fix it by hand. A dependency is missing, so you apt install it. The robot comes back. You close the laptop.
You have just made robot-07 unique. It is now the only device in your fleet with that fix, that config, that package. Nobody wrote it down. Next month, when you flash a fresh image or add robot-08, none of it carries over. And the next time robot-07 misbehaves, you will SSH back in and do it all again, because that is the only interface you have to it.
You have felt the other side of this too. You are on call, something is down, and you ping the person who owns the service. They poke at it for a minute and back comes the sentence every on-call engineer dreads: "Huh. Works on my machine." Your heart sinks, because you have just learned that the two of you are looking at two different machines and neither of you can say when or how they drifted apart. There is no artifact to compare. There is only their box, your box, and a shrug.
In a robot fleet that sentence gets a hardware edition: "works on my robot." Same dread, except now the machine that works is a physical object in another building, or another state, with its own calibration and its own private history of 2am fixes. You cannot even read the difference, let alone close it.

This is what it looks like at fleet scale. Seventy are flying. Thirty are not. Every one of them was supposedly deployed the same way, and the only tool you have for finding out how the thirty differ is to start logging into them one at a time.
This is the trap. And server operations climbed out of it more than a decade ago.
The idea we borrowed from server ops
Around 2012, infrastructure engineers started describing their servers with a now-famous metaphor: pets versus cattle.
A pet is a server you name. You know its quirks. When it gets sick you nurse it back to health at 2am, because it is yours and losing it would hurt. Think mail-server-gandalf running the one config nobody dares touch.
Cattle are numbered, not named. web-0042 is identical to web-0043. When one gets sick you do not nurse it - you shoot it and provision a fresh one from the same template. No single animal is special, because the template is the source of truth, not any running machine.
The insight was never that pets are bad. It was that pets do not scale and cannot be trusted, because the real state of the system lives in a place you cannot see: the accumulated, undocumented history of every hand-fix ever typed into that box. Move to cattle and the source of truth moves into version control, where you can read it, review it, and reproduce it.
Servers made this jump. Robot fleets, by and large, have not. Most physical AI is still run as a barn full of pets, and SSH is how we pet them.
Why SSH feels right (and is a trap)
SSH is a wonderful tool and an honest first instinct. On device one, on the bench, it is exactly right. The problem is that it quietly becomes your deployment system, your configuration manager, and your incident-response runbook all at once - and it is terrible at all three.
Here is what SSH-as-a-strategy actually costs you:
- Snowflake drift. Every manual fix makes one device diverge from the rest. After a few months no two robots are the same, and "works on robot-03" tells you nothing about robot-04.
- No source of truth. The real configuration of the fleet is the union of every command anyone ever ran. It exists only in shell history, if that. You cannot diff it, review it, or roll it back.
- It does not survive a reflash. The moment a device needs a fresh image (failed SD card, warranty swap, new unit), every hand-fix evaporates. You are rebuilding tribal knowledge from memory.
- It does not scale past your fingers. SSHing into 3 robots is a Tuesday. SSHing into 30 is a bad week. SSHing into 300 is impossible, and you will not even try - you will build a
forloop that runs a script over a list of IPs, which is just a worse, more dangerous version of the fleet tooling you were avoiding. - It needs a way in. To reach a device you need an open port, a reachable address, a key on the box. Multiply that across a fleet on customer networks and behind NATs and the attack surface gets large fast, which is worth a section of its own below.
The deepest problem is the last mental one: when SSH is your interface, the running device is your source of truth. You debug the machine, not a description of the machine. That is the exact inversion the cattle model exists to fix.
The part nobody likes to talk about
There is a second cost, and it is the one that tends to get discovered by someone else.
Nobody sets out to ship an insecure robot. What actually happens is a deadline. The demo is Thursday, the device is on a bench, key auth is fighting you for reasons you do not have time to understand, and there is a line in sshd_config that makes the problem go away right now. So you set PermitRootLogin yes. You turn PasswordAuthentication back on and use the password from the setup guide, which is the vendor default, which is in a PDF on the internet. You copy one private key onto every device in the fleet because generating per-device keys means writing a provisioning tool and you are writing a perception pipeline. You commit that key so CI can deploy. You ask the customer's IT person to forward port 22 because you need to reach the thing from your hotel.
Every one of those is, in the moment, the fastest path to a working robot. We have worked with a lot of teams, and we genuinely do not think less of any of them for it. Under that kind of pressure we would be tempted too. The problem is that all of it is meant to be temporary, and nothing on a shipped device is more permanent than a temporary auth bypass. It survives the demo. It survives the pilot. It is still there when there are forty units in the field, because by then nobody remembers it is there.
And robots make each of these worse than a server would:
- The device lives on someone else's network. You do not control the firewall, the segmentation, or who else is on that VLAN. Your device's security posture is now a guest in a building you have never visited.
- A fleet-wide key is a fleet-wide breach. One recovered device, one repo leak, one laptop, and the attacker has every robot you have shipped. Rotating that key means reaching all of them, which is the thing you already could not reliably do.
- Nobody is reading robot-07's auth log. On a server fleet there is at least a SIEM and someone on call for it. On a robot fleet, failed-login noise is usually seen by no one, ever.
- The keys outlive the people. An engineer sets up a fleet, leaves two years later, and their key is still in
authorized_keyson every device, because removing it would mean logging into every device. - The blast radius has motors. A compromised web server leaks data. A compromised robot has actuators, cameras, and a physical position in a real room. The worst case is a different category of bad.
Here is the part that matters, though: this is not a discipline problem. Telling developers to be more careful has never once fixed it, because the incentive that produced the shortcut is still there next quarter.
It is an architecture problem. If the only way to operate your fleet is to log into it, then you have to keep a way in open, permanently, to every device you have ever shipped. The inbound path is not an accident of sloppy setup; it is a requirement of the pets model. Every hardening step you take is you fighting your own operating model, and the operating model wins on the week of a deadline.
Change the model and the surface mostly disappears. If a device converges to a declared state on its own, there is nothing to log into in the normal case: no listening port to expose, no shared password to leave enabled, no fleet key to leak. The device opens an authenticated outbound connection and pulls what it should be running. Access, when a human truly needs it, is brokered through the device's own identity, granted deliberately, revocable from one place, and logged. That is a control you can actually enforce at 2am on a Thursday, because it does not depend on anyone remembering to be careful.
But robots aren't cattle. Not quite.
Here is where the borrowed metaphor needs honesty, because a robot is not a stateless web server, and pretending otherwise is how you get burned.
A physical device has things a cattle server never does. It has a location. It has sensors with per-unit calibration. It has a specific serial number bolted to a specific chassis with a specific camera that was focused by hand. You genuinely cannot treat robot-07 as perfectly interchangeable with robot-08, because robot-07 is the one in the northeast corner with the slightly-off IMU.
So the useful distinction for physical AI is not pets versus cattle. It is fleet-of-pets versus cattle-with-identity.
The pet parts of a robot - its physical placement, its calibration, its identity - are real and must be respected. The cattle insight still applies to everything else: the OS, the application, the dependencies, the config. Those should come from a template you can read and version, not from your fingers at 2am. The per-device specifics get modeled as data attached to a durable identity, not as hand-edits to a running box.
Put plainly: you stop treating the device as the source of truth, and start treating a declared description of the fleet as the source of truth - one that knows each device by a stable cryptographic identity and carries that device's unique parameters as reviewable configuration. You never SSH in to change state. You change the description, and the fleet converges to it.
That is the whole shift. Everything below is what it looks like in practice.
The two models, side by side
| Concern | Fleet of pets (SSH each) | Cattle with identity (declare state) |
|---|---|---|
| Source of truth | The running device | A versioned description in git |
| Applying a change | SSH in, edit by hand | Change the declaration, fleet converges |
| Adding device #31 | Repeat the setup from memory | Enroll it; it pulls the same known state |
| A device dies | Rebuild its snowflake by hand | Reflash, re-enroll, identity + config restored |
| OS / driver updates | Manual apt, hope it boots | Signed A/B image, health-checked, auto-rollback |
| Per-device calibration | A hand-edit nobody logged | Config bound to the device's identity |
| Knowing fleet state | SSH in and look | Read telemetry; the description already tells you |
| Getting in at 2am | Open port, key, reachable IP | No inbound port; identity-brokered access |
| Scales to | Roughly your patience | Thousands |
The 2am bug, run both ways
Say a bad config value is crashing a service across the fleet.
The pets way. You SSH into robot-07, find the value, fix it, restart the service. It works. Now you do robot-01 through robot-30, either by hand or with a shell loop over a list of IPs that you hope is current. Three of them are unreachable tonight, so you write those down to do later, and forget. Two have a slightly different path because of an old hand-fix, so your loop errors out halfway and you are not sure which ones took. The fix exists nowhere except in thirty-ish shell sessions. Next quarter someone reflashes robot-07 and the bug comes back on exactly one device, and nobody can explain why.
The cattle way. You change the value in the fleet's declared configuration and commit it. The change is reviewed like any other diff. The fleet rolls forward to the new description: devices that are online converge now, devices that are asleep or in a faraday-caged lab converge the moment they reappear, and the update lands on an inactive slot that health-checks and rolls back on its own if the new config won't boot clean. When robot-07 gets reflashed next quarter, it pulls the current description and comes up correct, because the fix was never in robot-07. It was in the source of truth, and robot-07 is just an expression of it.
One of these you can do at 2am half-asleep. The other one you are doing at 2am, half-asleep, and getting wrong.
People have tried to fix this, mostly at the OS layer
We are not the first to notice any of this, and the existing answers are serious, good work.
NixOS is probably the purest expression of the idea. The entire machine is described in a configuration file, builds are reproducible, and rollbacks are atomic. If you want a Linux box whose full state is a reviewable artifact, Nix genuinely delivers it.
rpm-ostree and OSTree (the machinery under Fedora IoT and a lot of image-based edge Linux) ship the operating system as a versioned tree that gets swapped atomically, with rollback if the new one doesn't come up. Same instinct, different mechanism.
Yocto and Buildroot let you build precisely the image you want, down to which bytes are on the device. Every serious embedded product eventually meets one of them.
All of these move the source of truth off the running machine, which is the part that matters. But notice where they put the declaration: at the bare metal. To use them you are describing a Linux distribution. You are picking kernel options, writing recipes and layers, resolving cross-compile toolchains, and learning a build system whose full-time job is embedded Linux.
That is a real tax, and it lands hardest on exactly the teams that need reproducibility most. A startup that wants a perception model running on a Jetson has to become a distro maintainer first. Most early teams we talk to have no Yocto skills, no one who wants to acquire them, and no business reason to spend their first two months there. Nix asks for a language and a mental model before it gives anything back. None of this makes those tools wrong. It means the declaration is sitting at a layer most product teams should never have to open.
So we took a different approach at Wendy: the declaration belongs at the application level.
The bare metal is our problem, not yours. WendyOS is the Yocto-built, immutable, A/B-updated base, and we maintain it so your team doesn't have to. What you describe is your application, in one or two files you already know how to write:
- A Dockerfile for what your app is built from: your language runtime, your libraries, your build. Nothing new to learn, and it is the same file that already works on your laptop.
- A
wendy.jsonfor what your app needs from the device:gpu,network,audio,persistentVolumes, and the rest of the entitlements. This is the part a Dockerfile can't express, because it is about the hardware the container is allowed to reach.
That is the whole declaration. Two files, in your repo, reviewed in a pull request like any other change. A new engineer can clone the repo and read the complete truth of what runs on the robot without ever learning a build system for embedded Linux, and wendy run is what turns those two files into a running app on a device.
The trade we are making is deliberate: you should get reproducibility without having to become an embedded Linux team to earn it.
What this looks like with WendyOS
This is the model WendyOS is built around, so the "cattle with identity" column above is not aspirational - it's the default workflow.
- You declare, you don't log in. The two files above are the interface. The description is the thing you version and review; the device is downstream of it.
- Every device has a real identity. Each WendyOS device is issued a certificate at enrollment and speaks mutual TLS by default. The CLI and cloud reach it by name, with no open inbound ports to babysit - that is what replaces the SSH tunnel.
- The OS is cattle too. Updates ship as signed A/B images: the whole root filesystem is replaced on an inactive slot, health-checked on first boot, and rolled back automatically if it doesn't come up clean. Updating a fleet behaves like updating phones, not like SSHing into servers.
- Per-device reality is respected. The pet parts - calibration, placement, unit-specific parameters - live as configuration bound to each device's identity, not as hand-edits you'll lose on the next reflash.
- You still get to look inside. Giving up SSH does not mean giving up visibility. Streamed logs, metrics, and traces (
wendy device logs, telemetry) tell you what a device is doing, and remote access is brokered through the device's identity when you truly need a shell - as the exception, not the interface.
None of this means you never touch a device. It means touching a device is the rare, deliberate act it should be, instead of your entire operating model.
The honest caveat
If you have exactly one robot on your desk that you administer directly and enjoy, SSH is fine. It is a pet, you named it, and nobody is asking you to stop. The pets model is not wrong at the scale of one.
It starts costing you the moment there is a second device, a device you cannot physically reach, or a teammate who needs to know what state the fleet is in without asking you. That is the threshold where "just SSH in" stops being a convenience and starts being the thing quietly preventing you from ever having a fleet you can trust.
The fix is not a better SSH habit or a cleverer deploy script. It is moving the source of truth off the device and into something you can read, review, and reproduce - and then letting the fleet converge to it.
So: stop SSHing into your robots. Start describing them.
Get started today
The fastest way to feel the difference is to stop reading and deploy something. Head to docs.wendy.dev for the install guide, the wendy.json entitlement reference, and the flashing steps for Jetson Orin Nano, Jetson Thor, and Raspberry Pi 5.
Install the CLI, point it at a device, and run wendy run. Your first deploy is a Dockerfile and a wendy.json, with no SSH session anywhere in it.
Keep reading: WendyOS vs. Docker + ROS 2 on why containers alone don't ship robots, or how an MIT roboticist went from 3.5 hours of Jetson setup to a 3-minute deploy. Or install the CLI and try wendy run on a device you have.
Related post
Expand your knowledge with these hand-picked posts.

We Open-Sourced our One-Command Vision Stack
The RealSense and YOLO template apps that power computer vision on WendyOS are now fully open source. Capture, inference, streaming, and a browser UI - deployed to a Jetson or Pi with a single `wendy run`.
Wendy Labs - Wendy Labs Team

Recommended LLMs for Jetson Orin Nano and Jetson Thor on WendyOS
Which local LLMs to run on the NVIDIA Jetson Orin Nano 8GB and Jetson AGX Thor with the Wendy llm template, plus how to install, scaffold, and deploy them with the Wendy CLI.
Wendy Labs - Wendy Labs Team


Ready to build on WendyOS?
WendyOS is the open-source operating system for Physical AI — deploy your apps to NVIDIA Jetson, Raspberry Pi, and more in seconds, over USB-C, wireless, or the cloud.