Why our AI agents deploy over SFTP, not a cloud API

· ~9 minute read

The post you are reading was drafted, formatted, and published to our website by an AI agent. Every Tuesday morning, Windows Task Scheduler fires a small PowerShell script. That script invokes a coding agent (in our case Claude Code), hands it our editorial calendar, and steps back. Ninety seconds later there's a new blog post on production. We didn't push a button. We didn't review the deploy. We didn't, in fact, do anything.

That setup raises an obvious operational question: what does the agent have access to, exactly? In most agentic-deployment recipes you find online, the answer is “quite a lot.” A long-lived cloud IAM role with write access to an S3 bucket. A GitHub Personal Access Token with the repo scope. An OAuth token bound to a CI/CD service account that itself has the keys to half the production estate. The agent shells out to aws s3 cp or gh pr merge and the deploy works, and everyone moves on.

It works. It also produces a blast radius that does not match the job. The agent's task is “put one HTML file in one directory on one website.” The credentials it's holding could empty a bucket, push to main on every repo in the organisation, or invoke a Lambda. If the agent's prompt is hijacked, its tool calls go awry, or its model just hallucinates an unexpected command, the consequences scale to whatever the credential covers, not to what the task required.

So when we built our own agent-driven publishing pipeline, we asked the inverse question. What is the smallest credential the agent could possibly need? And how do we make sure that nothing else is reachable when something goes wrong?

The answer turned out to be: one SFTP login.

What the agent actually has

The agent runs on a developer workstation behind our firewall. It is given exactly four environment variables: an SFTP host, a port, a username, and a password. The username corresponds to a single account on our production SFTP listener, which serves the home directory that is the document root of stylussftpserver.com. That account can read and write inside its own home. It cannot escape it. It cannot reach any other account. It cannot reach the operating system. It cannot reach the database. It cannot reach any other site on the same host (and there are several — our SFTP user has a deliberately scoped chroot).

That's the whole credential.

The agent uses it to do exactly one thing: push HTML files into its home directory using SFTP. The publishing script we wrote (publish-blog.ps1) wraps WinSCP from PowerShell, opens an SFTP session, and uploads the new post plus the updated index page and sitemap. Total wire time per deploy is a few seconds. Then the session ends and the credential goes back into idle. There is no API. There is no shell. There is no “deploy webhook.” There is one TCP port (22) on one host, and inside it, one user with one home.

Why SFTP turns out to be the right boundary

This isn't an “old protocols are cool” argument. The reason SFTP works as the security boundary for AI agents is that the SFTP server — specifically, our SFTP server — gives us a set of operational guarantees that map cleanly onto everything you'd want when the client on the other end is a process you don't fully trust.

The agent can't corrupt a partial deploy

Every upload is staged. The SFTP client writes to filename.UUID.tmp inside the user's home, and only after the client cleanly closes the channel does Stylus SFTP Server atomically rename the staged file into its final canonical name. The rename uses Files.move(... StandardCopyOption.ATOMIC_MOVE), which means it either succeeds completely or it doesn't happen. Downstream consumers — in this case, the IIS process serving the file — never see a half-written HTML page. If the agent crashes mid-deploy, the orphan .tmp file gets cleaned up on the next server restart by an age-thresholded sweeper, and the canonical file is untouched.

Every action is recorded

The SFTP server's audit subsystem records every connection, every authentication outcome, every file operation. The audit sinks run in parallel: a local log file, a row per event in our JDBC audit table, a syslog feed, and a webhook for the few events we want to react to in real time. When the agent connects from its workstation, we get a record. When it uploads a file, we get a record. When it disconnects, we get a record. If we ever wanted to know “what exactly did the agent do at 9:07 last Tuesday,” the answer is one SELECT away.

The agent can't fill the disk

The account has a per-user quota. The agent can't, even by mistake, push gigabytes of garbage into the home directory and exhaust the filesystem. The quota is enforced at the SFTP layer — the moment the cumulative bytes in the account's home exceed the threshold, the next write fails with a clean storage-exceeded response, and the agent's deployment fails fast rather than half-succeeding.

The agent can only connect from one place

The SFTP listener is fronted by our IP block list and GeoIP filter. Only the workstation IP the agent runs on is allowed to authenticate as the publishing account. If a leaked password ended up in the wrong hands, the attacker would still need to be on our network — or, more precisely, on the specific IP we've allowlisted — to use it. Rate limiting and account lockout sit underneath that as a second tripwire.

Nothing else is exposed

The firewall has exactly one inbound port open for this listener: TCP 22 (SFTP). There is no admin API, no REST endpoint, no SSH shell access for this account, no port forwarding, no X11. The SFTP subsystem is the only thing the agent can speak to, and the only thing it can ask for is “read this file” or “write this file” inside the chroot. The control plane that would let someone do something genuinely scary — the Web Admin console, the JMX endpoint, the database itself — lives behind the firewall, on different ports, completely unreachable from outside.

The flow in one diagram (in words)

Every Tuesday at 09:07 ET, Windows Task Scheduler triggers weekly-blog.ps1. That script verifies the four environment variables are present, runs git pull --rebase on the working repo so it has the latest editorial calendar, and then invokes the AI agent with our weekly prompt. The agent reads EDITORIAL_CALENDAR.md, finds today's row, drafts an HTML post that mirrors our existing template, updates blog/index.html and sitemap.xml, marks the calendar row as shipped, and runs publish-blog.ps1. That script opens an SFTP session using the env-var credentials, uploads the new HTML file, the index, and the sitemap, and disconnects. Stylus SFTP Server processes each upload through its staging+rename pipeline, audits the session, and the new post is live within a minute or two of the original task fire. Then the agent commits the calendar update and the new post to git, pushes, and exits.

Nothing during that flow ever opens the firewall further. Nothing ever calls into a cloud control plane. Nothing ever holds a credential that could do more than write one file into one directory.

What this changes about agent security

The standard advice for AI agents is the same advice that's always applied to any automated process: principle of least privilege, narrow scopes, short-lived credentials, audit everything. None of that is new. What's new is how badly you'd like that advice to actually be true when the agent gets things wrong — not maliciously, just plausibly. Prompts get injected. Tools get called with arguments the developer didn't anticipate. Models hallucinate file paths. The whole AI-agent threat model is one in which the most likely failure isn't a sophisticated attack, it's a confidently-wrong instruction.

The way you make “confidently wrong” cheap is to make the credential cheap. A credential that authorises “put files in this directory” and nothing else means the worst-case mistake is “a wrong file got published” — recoverable in seconds with a re-deploy, fully visible in the audit log, contained inside one chroot. A credential that authorises “administer the production AWS account” means the worst case is a 2 AM page and a public apology.

SFTP got popular thirty years ago as a way for humans to move files between hosts they had accounts on. It turns out to be exactly the right size for a robot doing the same job: one identity, one home directory, one protocol port, one audit trail. The protocol does the minimum and refuses to do more. That is precisely what you want when the client on the other end is something you have to trust slightly less than you'd like.

The take-home

If you're standing up agent-driven automation for any kind of deployment — content, configuration, downloads, partner exchange — consider whether the deploy could go through SFTP instead of a cloud API. The credential you'd issue is smaller. The blast radius is bounded by the chroot. The audit trail comes for free. The firewall doesn't open. And the protocol has been doing exactly this job, reliably, since before AI agents were a thing anyone worried about.

We use Stylus SFTP Server for ours because we wrote it, but the architectural point doesn't require the brand. Any decent SFTP server with audit, quotas, and IP filtering can play the role. The point is that the right boundary between an agent and a production system might not be a clever new permission model. It might be a thirty-year-old protocol that already knows how to say no.

Want SFTP as your agent security boundary?

Free evaluation key. Install on Windows or Linux, give your automation a tightly-scoped account, and let SSS handle the audit, quotas, and firewall posture for you.

Request Evaluation Key More articles ›