Skip to main content
TACAVAR
Build in Public

Run Commands on Another Server Without Sharing SSH Keys

One server runs commands on another. No shared SSH keys. Just a whitelist gate and an audit log.

That's not a thought experiment. It's the exact problem Tacavar hit when our dispatch server—we call it bailian—needed to trigger deploys, post Instagram reels, and write video briefs on the primary droplet. The obvious answer was to share an SSH key and call it a day. But obvious is often dangerous, and cross-server RPC done wrong becomes a security hole. Here's how we built a safer path.

The cross-server problem

Every multi-server setup eventually hits the same wall: one machine needs to make things happen on another. Maybe it's a CI server triggering a deploy. Maybe it's a scheduler kicking off a backup. In our case, bailian is the brain that coordinates jobs across droplets. It's not a human SSH session—it's an automated system sending commands over the network. That's cross-server RPC, and it's fundamentally different from interactive admin access.

The usual pattern is to drop a private key on the caller and add the corresponding public key to the target's authorized_keys file. That works, but it's a blunt instrument. You're granting direct shell access to a process, not to a controlled set of operations. The target server can't tell the difference between a benign command and a destructive one. And you have zero visibility into what actually happened unless you're already running a full command-logging suite.

Tacavar's operational model demands more structure. When bailian triggers a deploy, we want that action to be explicit, restricted, and recorded. We don't want a generic "run anything" channel.

Why shared SSH keys are a trap

Let me be direct: shared SSH keys are a devops security trap. They solve the connectivity problem, but they also create a massive blast radius. If the key on the caller is compromised, the attacker inherits every permission that key has—usually full user access.

And it's not just about intruders. It's about mistakes. A bug in the dispatching code can accidentally run rm -rf / on a production droplet. With a shared key, nothing stops it. No gate, no validation, no log (at least not one that's easy to correlate).

There's also the operational debt. Keys get copied. They get embedded in scripts, in CI configs, in someone's dotfiles. They never get rotated. That's the classic ssh security problem: the more endpoints that trust the key, the more trust becomes meaningless.

We needed a different approach—one that limits each caller to a specific set of commands and leaves behind a complete audit trail. That's what pushed us toward a whitelist dispatcher.

The whitelist dispatcher pattern

The core idea is simple: the target server does not accept raw bash from the caller. It accepts a command name—a symbolic identifier—and optional arguments. Then it checks that identifier against a whitelist before executing anything. If the command name is not in the list, the request is rejected. If it is, the server runs the corresponding handler under sudo and logs the event.

This is the whitelist dispatcher pattern. Instead of "here's a shell, do whatever you want," it's "here are five things you're allowed to do, pick one." The caller never sees a shell prompt. It just makes an RPC-style request and gets back a status.

The security model shifts from "who is this key?" to "what is this action allowed to do?" Even if someone steals the deploy key, they can only trigger the whitelisted operations. They can't write to arbitrary files, change configs, or pivot to a root shell. That's a dramatically smaller blast radius.

How deploy_gate.sh works

We built a script on the primary droplet that we call deploy_gate.sh. It's the gatekeeper for every cross-server command. The flow looks like this:

  1. Bailian connects to the primary via a dedicated deploy key—a key that can only run this one script, not open a full shell.
  2. The script reads the command name and arguments from the request.
  3. It checks the command name against allowed_commands, a hardcoded list of handlers.
  4. If the command is allowed, it runs the corresponding handler under sudo.
  5. Every call gets appended to an audit log: timestamp, command name, arguments, and exit code.

The whitelist is intentionally explicit. Right now it includes instagram_post, domain_pending, and a family of pip_install_* commands. Nothing else is allowed. If we need a new operation, we add it deliberately—not in the heat of a debugging session.

This pattern gives us most of the power of shared SSH with a fraction of the risk. The dedicated key is not a backdoor; it's a door with a bouncer.

Audit logging as documentation

The audit log is where the pattern really shines. Every command that runs through deploy_gate.sh is recorded. That means we can answer the question "what did bailian do to the primary kernel?" with a simple query, not a forensic investigation.

But there's a second benefit: the whitelist itself becomes documentation. When a new team member or a new agent joins the system, they can read the script and immediately know the supported operations. No need to ask "can I run this command?"—the answer is written in the gate. That's a huge win for operational clarity.

The log also serves as a source of truth for debugging. If a deploy fails, you can look at the exact command that ran, the arguments, and the exit code. No more guessing whether something was "close enough" to what you wanted.

Scaling to new commands safely

Adding a new command to the dispatcher is a deliberate act. You write a handler function, add it to the whitelist, and test it. That's it. The pattern doesn't break when you scale, because the mechanics stay the same: name in, check against list, execute, log.

We've used this for anything that needs to happen on the primary from an external trigger. domain_pending checks pending domain actions. pip_install_* installs packages with a controlled pattern. instagram_post handles social content distribution. Each one is a small function, not a raw command string. That's the key: we're exposing a small API surface, not a backdoor to the OS.

This also means you can safely delegate the caller to a less-trusted context. If bailian is compromised, the attacker's options are limited to whatever handlers exist. They can't escalate to arbitrary code execution, and they certainly can't wipe the box.

What this means for agent orchestration

Agent orchestration is the next frontier. When you have multiple agents coordinating across infrastructure—dispatching jobs, retrying failures, triggering releases—you need the same discipline we built here. Each agent should only be able to perform the exact actions it was designed to perform, and every action should be auditable.

Tacavar uses this pattern across our multi-droplet orchestration. It's not a one-off script; it's a reusable model for how automation should talk to infrastructure. The whitelist dispatcher isn't just a security tool—it's a design principle. It forces you to define the boundaries of your system before you wire it together.

If you're building agent-based workflows, don't hand your agents shell access. Give them a gate.


Need a secure command path between your agents and your infrastructure? Tacavar's deploy gate pattern is baked into multi-droplet orchestration. Explore tacavar.com/devops.