One Server, One Whitelist: Cross-Droplet Commands Without Root SSH Keys
Tacavar's deploy_gate.sh pattern uses a whitelist dispatcher, cross-server RPC, and audit logging to replace root SSH keys with least privilege automation.
I needed cross-server commands. Sharing root SSH keys was obvious. I built a whitelist instead. At Tacavar, the bailian droplet handles scheduled work: triggering deploys on the primary droplet, posting Instagram reels, writing video briefs, checking pending domains. The primary droplet runs the production app and the scripts that mutate it. The first version of this handoff was going to be a root SSH key copied from primary to bailian. That would have worked for about an hour. Then the ssh security review would have killed it. So Tacavar got deploy_gate.sh: a whitelist dispatcher for cross-server rpc that accepts command names, not raw bash, runs each under sudo, and writes every call to an audit log.
Why bailian needed to trigger deploys on the primary droplet
Bailian is not production. It is the scheduler and content worker. It wakes up, checks queues, builds video briefs, schedules Instagram posts, validates domain states, and decides when a deploy is safe. The primary droplet is where the repo lives, where builds are promoted, and where Nginx, the app, and the deploy scripts actually run. Bailian cannot pull that off by itself. It needs to trigger a deploy at the right moment, not just send a notification to a human.
The pattern is simple: bailian finishes a batch, validates the content or build artifact, and then needs to run a fixed action on the primary. That action might be deploy_app, instagram_post, domain_pending, or pip_install_requirements. The important part is not that bailian can run bash on primary. The important part is that bailian can ask primary to perform a known operation. That is a cross-server rpc problem, not a shell problem.
Once you frame it that way, the shared-root-key solution looks like a sledgehammer. You do not need a shell. You need a narrow API with names. Tacavar already had the services. The missing piece was a dispatcher that could accept those names and enforce the list.
The blast radius problem with shared root SSH keys
Shared root SSH keys are the default shortcut because they are easy to reason about for five minutes. Copy the private key to bailian. Add the public key to primary's root authorized_keys. Now bailian can SSH in and run anything. That is exactly the problem.
If bailian is compromised, the attacker gets root on primary. If a cron job on bailian is hijacked, the attacker gets root on primary. If a dependency on bailian has a malicious postinstall script, the attacker gets root on primary. The blast radius is not bailian. The blast radius is every droplet, every database credential, every deploy secret, and every customer-facing endpoint that primary can reach. That is not ssh security. That is lateral movement with extra steps.
Rotation is no better. A shared root key lives in multiple places, often in environment variables, CI secrets, and config files. You cannot revoke it for one caller without breaking the other. You cannot scope it to one command. You cannot log intent. You only know that root logged in, not what it was supposed to do. For a founder/operator, that is an unacceptable trade: you save an hour of setup and accept permanent uncertainty.
Tacavar rejected the shared root key. The rule became: bailian gets a dedicated deploy key, and that key can only invoke one script on primary. That script decides what is allowed. This is least privilege automation in practice, not a slide in a security deck.
Building deploy_gate.sh around command names, not raw bash
deploy_gate.sh lives on the primary droplet. It is the only command the bailian deploy key is allowed to run. When bailian connects over SSH, it does not get an interactive shell. It runs deploy_gate.sh <command-name>. If the command name is not in the whitelist, the script exits non-zero and logs the attempt.
The whitelist is a case statement or associative array that maps names to fixed scripts. deploy_app calls the deploy script with preset arguments. instagram_post calls the Instagram publishing helper. domain_pending calls the DNS/TLS checker. pip_install_requirements calls a wrapper that reads a known requirements file. The dispatcher never passes raw bash from bailian. It never evaluates a string as shell. It never accepts arbitrary flags. If you need a new action, you add a new named entry, review it, and redeploy the dispatcher.
That constraint is the feature. A whitelist dispatcher turns a dangerous remote execution channel into an explicit API. You can read the whitelist and know exactly what bailian can do to primary. You can test each command. You can version the dispatcher. You can reject anything that is not on the list. And because the interface is command names, you can log something meaningful: not ssh root@primary, but deploy_gate.sh domain_pending.
Adding instagram_post, domain_pending, and pip_install_* to the whitelist
The first three additions to Tacavar's whitelist were practical, not theoretical. instagram_post lets bailian publish scheduled reels through the primary's authenticated social integration. domain_pending lets bailian check whether a customer domain has finished DNS and TLS provisioning before it notifies anyone. pip_install_* lets bailian trigger dependency installs on primary after a lockfile update, but only through fixed wrappers. The star does not mean arbitrary package names. It means a small set of known install profiles, each mapped to a specific requirements file and a specific virtualenv.
This is where a whitelist beats a general-purpose SSH key. With a root key, pip_install_* would mean bailian can run pip install anything as root. With the dispatcher, it means bailian can run pip_install_requirements or pip_install_dev. The install script still executes on primary, but the caller cannot choose the package. The caller cannot inject --index-url or --target or a malicious wheel. The command name is the contract.
Adding a command is a code change. That sounds slower. It is slower in the same way that not leaving your database port open is slower. You review the new command, decide which user runs it, decide which arguments are fixed, and add it to the whitelist. For Tacavar, that review is the point. It keeps the cross-server RPC surface small enough to understand.
Audit logging every dispatched command under sudo
Every call to deploy_gate.sh writes to an audit log before and after execution. The log records the caller key fingerprint, the command name, the resolved script, the exit code, the duration, and a hash of stdout/stderr. If the command runs under sudo, the sudo log records the same invocation. The audit log is append-only. It is not a dashboard. It is evidence.
This is where audit logging DevOps practices become operational. When bailian triggers deploy_app, you can see which key asked, what it asked for, and whether it succeeded. When someone tries deploy_gate.sh rm -rf /, the dispatcher rejects it and logs the attempt. When a scheduled job fails at 3 a.m., you can tell whether bailian called the command, whether primary rejected it, and where the failure actually happened. You do not have to guess from a root shell history that may not even exist.
Audit logging also changes behavior. A shared root key has no accountability. A whitelist dispatcher with an audit log makes every cross-server action attributable. If bailian is compromised, the attacker can only run the listed commands, and every one of those runs is logged. That does not make compromise harmless. It makes compromise bounded and visible.
What the whitelist gets you that SSH keys never will
A whitelist gives you enumeration. You can list every action bailian can take on primary. A root SSH key gives you a blank check.
A whitelist gives you revocation. You can remove one command name without rotating every secret in your infrastructure. A root SSH key forces you to rotate the key everywhere or live with the risk.
A whitelist gives you least privilege automation. Bailian gets exactly the capabilities it needs to trigger deploys, post reels, check domains, and install known dependencies. It does not get a shell as root. The primary droplet keeps its authority.
A whitelist gives you audit logging DevOps teams can use. You can answer who did what, when, and with what result. You can alert on rejected commands. You can review new whitelist entries like code.
A whitelist also gives you documentation. The file that enforces access is the same file that describes access. New engineers read deploy_gate.sh and understand the cross-server RPC contract in minutes. There is no hidden root key floating in a password manager. There is one dispatcher, one whitelist, and one audit log.
Tacavar did not build this because it was clever. It built it because sharing root SSH keys between droplets is a one-way door to a larger incident. The whitelist is not a perfect security boundary. It is a much smaller one. For a founder/operator, smaller boundaries are how you keep moving without gambling the whole stack.
Harden cross-droplet automation with Tacavar's deploy-gate pattern at tacavar.com.